diff --git a/modules/sdk-coin-sol/src/lib/constants.ts b/modules/sdk-coin-sol/src/lib/constants.ts index 33026c874e..7e56554eaf 100644 --- a/modules/sdk-coin-sol/src/lib/constants.ts +++ b/modules/sdk-coin-sol/src/lib/constants.ts @@ -49,6 +49,7 @@ export enum ValidInstructionTypesEnum { Memo = 'Memo', InitializeAssociatedTokenAccount = 'InitializeAssociatedTokenAccount', CloseAssociatedTokenAccount = 'CloseAssociatedTokenAccount', + RecoverNestedAssociatedTokenAccount = 'RecoverNestedAssociatedTokenAccount', Allocate = 'Allocate', Assign = 'Assign', Split = 'Split', @@ -74,6 +75,7 @@ export enum InstructionBuilderTypes { NonceAdvance = 'NonceAdvance', CreateAssociatedTokenAccount = 'CreateAssociatedTokenAccount', CloseAssociatedTokenAccount = 'CloseAssociatedTokenAccount', + RecoverNestedAssociatedTokenAccount = 'RecoverNestedAssociatedTokenAccount', TokenTransfer = 'TokenTransfer', StakingAuthorize = 'Authorize', StakingDelegate = 'Delegate', @@ -99,6 +101,7 @@ export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [ ValidInstructionTypesEnum.Memo, ValidInstructionTypesEnum.InitializeAssociatedTokenAccount, ValidInstructionTypesEnum.CloseAssociatedTokenAccount, + ValidInstructionTypesEnum.RecoverNestedAssociatedTokenAccount, ValidInstructionTypesEnum.TokenTransfer, ValidInstructionTypesEnum.Allocate, ValidInstructionTypesEnum.Assign, @@ -203,6 +206,11 @@ export const ataCloseInstructionIndexes = { CloseAssociatedTokenAccount: 0, } as const; +/** Const to check the order of the recover nested ATA instructions when decode */ +export const ataRecoverNestedInstructionIndexes = { + RecoverNestedAssociatedTokenAccount: 0, +} as const; + export const nonceAdvanceInstruction = 'AdvanceNonceAccount'; export const validInstructionData = '0a00000001000000'; export const validInstructionData2 = '0a00000000000000'; diff --git a/modules/sdk-coin-sol/src/lib/iface.ts b/modules/sdk-coin-sol/src/lib/iface.ts index 951b0e9da5..e494116abd 100644 --- a/modules/sdk-coin-sol/src/lib/iface.ts +++ b/modules/sdk-coin-sol/src/lib/iface.ts @@ -44,6 +44,7 @@ export type InstructionParams = | StakingWithdraw | AtaInit | AtaClose + | AtaRecoverNested | TokenTransfer | StakingAuthorize | StakingDelegate @@ -220,6 +221,18 @@ export interface AtaClose { params: { accountAddress: string; destinationAddress: string; authorityAddress: string }; } +export interface AtaRecoverNested { + type: InstructionBuilderTypes.RecoverNestedAssociatedTokenAccount; + params: { + nestedAccountAddress: string; + nestedMintAddress: string; + destinationAccountAddress: string; + ownerAccountAddress: string; + ownerMintAddress: string; + walletAddress: string; + }; +} + export type ValidInstructionTypes = | SystemInstructionType | StakeInstructionType @@ -227,6 +240,7 @@ export type ValidInstructionTypes = | 'Memo' | 'InitializeAssociatedTokenAccount' | 'CloseAssociatedTokenAccount' + | 'RecoverNestedAssociatedTokenAccount' | DecodedCloseAccountInstruction | 'TokenTransfer' | 'SetComputeUnitLimit' diff --git a/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts b/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts index ddadd0b1b1..65168f9723 100644 --- a/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts +++ b/modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts @@ -32,6 +32,7 @@ import { InstructionBuilderTypes, ValidInstructionTypesEnum, walletInitInstructi import { AtaClose, AtaInit, + AtaRecoverNested, Burn, InstructionParams, Memo, @@ -1071,14 +1072,23 @@ const ataCloseInstructionKeysIndexes = { AuthorityAddress: 2, }; +const ataRecoverNestedInstructionKeysIndexes = { + NestedAccountAddress: 0, + NestedMintAddress: 1, + DestinationAccountAddress: 2, + OwnerAccountAddress: 3, + OwnerMintAddress: 4, + WalletAddress: 5, +}; + /** * Parses Solana instructions to close associated token account tx instructions params * * @param {TransactionInstruction[]} instructions - an array of supported Solana instructions * @returns {InstructionParams[]} An array containing instruction params for Send tx */ -function parseAtaCloseInstructions(instructions: TransactionInstruction[]): Array { - const instructionData: Array = []; +function parseAtaCloseInstructions(instructions: TransactionInstruction[]): Array { + const instructionData: Array = []; for (const instruction of instructions) { const type = getInstructionType(instruction); switch (type) { @@ -1104,6 +1114,25 @@ function parseAtaCloseInstructions(instructions: TransactionInstruction[]): Arra }; instructionData.push(ataClose); break; + case ValidInstructionTypesEnum.RecoverNestedAssociatedTokenAccount: + const ataRecoverNested: AtaRecoverNested = { + type: InstructionBuilderTypes.RecoverNestedAssociatedTokenAccount, + params: { + nestedAccountAddress: + instruction.keys[ataRecoverNestedInstructionKeysIndexes.NestedAccountAddress].pubkey.toString(), + nestedMintAddress: + instruction.keys[ataRecoverNestedInstructionKeysIndexes.NestedMintAddress].pubkey.toString(), + destinationAccountAddress: + instruction.keys[ataRecoverNestedInstructionKeysIndexes.DestinationAccountAddress].pubkey.toString(), + ownerAccountAddress: + instruction.keys[ataRecoverNestedInstructionKeysIndexes.OwnerAccountAddress].pubkey.toString(), + ownerMintAddress: + instruction.keys[ataRecoverNestedInstructionKeysIndexes.OwnerMintAddress].pubkey.toString(), + walletAddress: instruction.keys[ataRecoverNestedInstructionKeysIndexes.WalletAddress].pubkey.toString(), + }, + }; + instructionData.push(ataRecoverNested); + break; default: throw new NotSupported( 'Invalid transaction, instruction type not supported: ' + getInstructionType(instruction) diff --git a/modules/sdk-coin-sol/src/lib/recoverNestedAtaBuilder.ts b/modules/sdk-coin-sol/src/lib/recoverNestedAtaBuilder.ts new file mode 100644 index 0000000000..0bd31cb23d --- /dev/null +++ b/modules/sdk-coin-sol/src/lib/recoverNestedAtaBuilder.ts @@ -0,0 +1,104 @@ +import { TransactionType } from '@bitgo/sdk-core'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import assert from 'assert'; +import { InstructionBuilderTypes } from './constants'; +import { AtaRecoverNested } from './iface'; +import { Transaction } from './transaction'; +import { TransactionBuilder } from './transactionBuilder'; +import { validateAddress } from './utils'; + +export class RecoverNestedAtaBuilder extends TransactionBuilder { + protected _nestedAccountAddress: string; + protected _nestedMintAddress: string; + protected _destinationAccountAddress: string; + protected _ownerAccountAddress: string; + protected _ownerMintAddress: string; + protected _walletAddress: string; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this._transaction = new Transaction(_coinConfig); + } + + protected get transactionType(): TransactionType { + return TransactionType.CloseAssociatedTokenAccount; + } + + nestedAccountAddress(nestedAccountAddress: string): this { + validateAddress(nestedAccountAddress, 'nestedAccountAddress'); + this._nestedAccountAddress = nestedAccountAddress; + return this; + } + + nestedMintAddress(nestedMintAddress: string): this { + validateAddress(nestedMintAddress, 'nestedMintAddress'); + this._nestedMintAddress = nestedMintAddress; + return this; + } + + destinationAccountAddress(destinationAccountAddress: string): this { + validateAddress(destinationAccountAddress, 'destinationAccountAddress'); + this._destinationAccountAddress = destinationAccountAddress; + return this; + } + + ownerAccountAddress(ownerAccountAddress: string): this { + validateAddress(ownerAccountAddress, 'ownerAccountAddress'); + this._ownerAccountAddress = ownerAccountAddress; + return this; + } + + ownerMintAddress(ownerMintAddress: string): this { + validateAddress(ownerMintAddress, 'ownerMintAddress'); + this._ownerMintAddress = ownerMintAddress; + return this; + } + + walletAddress(walletAddress: string): this { + validateAddress(walletAddress, 'walletAddress'); + this._walletAddress = walletAddress; + return this; + } + + /** @inheritDoc */ + initBuilder(tx: Transaction): void { + super.initBuilder(tx); + for (const instruction of this._instructionsData) { + if (instruction.type === InstructionBuilderTypes.RecoverNestedAssociatedTokenAccount) { + const recoverNestedInstruction: AtaRecoverNested = instruction; + this.nestedAccountAddress(recoverNestedInstruction.params.nestedAccountAddress); + this.nestedMintAddress(recoverNestedInstruction.params.nestedMintAddress); + this.destinationAccountAddress(recoverNestedInstruction.params.destinationAccountAddress); + this.ownerAccountAddress(recoverNestedInstruction.params.ownerAccountAddress); + this.ownerMintAddress(recoverNestedInstruction.params.ownerMintAddress); + this.walletAddress(recoverNestedInstruction.params.walletAddress); + } + } + } + + /** @inheritdoc */ + protected async buildImplementation(): Promise { + assert(this._nestedAccountAddress, 'nestedAccountAddress must be set before building the transaction'); + assert(this._nestedMintAddress, 'nestedMintAddress must be set before building the transaction'); + assert(this._destinationAccountAddress, 'destinationAccountAddress must be set before building the transaction'); + assert(this._ownerAccountAddress, 'ownerAccountAddress must be set before building the transaction'); + assert(this._ownerMintAddress, 'ownerMintAddress must be set before building the transaction'); + assert(this._walletAddress, 'walletAddress must be set before building the transaction'); + + const recoverNestedData: AtaRecoverNested = { + type: InstructionBuilderTypes.RecoverNestedAssociatedTokenAccount, + params: { + nestedAccountAddress: this._nestedAccountAddress, + nestedMintAddress: this._nestedMintAddress, + destinationAccountAddress: this._destinationAccountAddress, + ownerAccountAddress: this._ownerAccountAddress, + ownerMintAddress: this._ownerMintAddress, + walletAddress: this._walletAddress, + }, + }; + + this._instructionsData = [recoverNestedData]; + + return await super.buildImplementation(); + } +} diff --git a/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts b/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts index 52aea30157..2f7972d73b 100644 --- a/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts +++ b/modules/sdk-coin-sol/src/lib/solInstructionFactory.ts @@ -5,6 +5,7 @@ import { createCloseAccountInstruction, createMintToInstruction, createBurnInstruction, + createRecoverNestedInstruction, createTransferCheckedInstruction, TOKEN_2022_PROGRAM_ID, createApproveInstruction, @@ -27,6 +28,7 @@ import { InstructionBuilderTypes, MEMO_PROGRAM_PK } from './constants'; import { AtaClose, AtaInit, + AtaRecoverNested, InstructionParams, Memo, MintTo, @@ -79,6 +81,8 @@ export function solInstructionFactory(instructionToBuild: InstructionParams): Tr return createATAInstruction(instructionToBuild); case InstructionBuilderTypes.CloseAssociatedTokenAccount: return closeATAInstruction(instructionToBuild); + case InstructionBuilderTypes.RecoverNestedAssociatedTokenAccount: + return recoverNestedATAInstruction(instructionToBuild); case InstructionBuilderTypes.StakingAuthorize: return stakingAuthorizeInstruction(instructionToBuild); case InstructionBuilderTypes.StakingDelegate: @@ -551,6 +555,45 @@ function closeATAInstruction(data: AtaClose): TransactionInstruction[] { return [closeAssociatedTokenAccountInstruction]; } +/** + * Construct RecoverNested ATA Solana instruction + * + * Recovers tokens from a nested ATA (an ATA whose owner is another ATA rather than a wallet address). + * This uses the Associated Token Account program's RecoverNested instruction, which allows the root + * wallet owner to sign and recover tokens without needing the intermediate ATA to sign. + * + * @param {AtaRecoverNested} data - the data to build the instruction + * @returns {TransactionInstruction[]} An array containing the RecoverNested instruction + */ +function recoverNestedATAInstruction(data: AtaRecoverNested): TransactionInstruction[] { + const { + params: { + nestedAccountAddress, + nestedMintAddress, + destinationAccountAddress, + ownerAccountAddress, + ownerMintAddress, + walletAddress, + }, + } = data; + assert(nestedAccountAddress, 'Missing nestedAccountAddress param'); + assert(nestedMintAddress, 'Missing nestedMintAddress param'); + assert(destinationAccountAddress, 'Missing destinationAccountAddress param'); + assert(ownerAccountAddress, 'Missing ownerAccountAddress param'); + assert(ownerMintAddress, 'Missing ownerMintAddress param'); + assert(walletAddress, 'Missing walletAddress param'); + + const recoverNestedInstruction = createRecoverNestedInstruction( + new PublicKey(nestedAccountAddress), + new PublicKey(nestedMintAddress), + new PublicKey(destinationAccountAddress), + new PublicKey(ownerAccountAddress), + new PublicKey(ownerMintAddress), + new PublicKey(walletAddress) + ); + return [recoverNestedInstruction]; +} + /** * Construct Staking Account Authorize Solana instructions * diff --git a/modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts index ee7260cebc..d53d641564 100644 --- a/modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts @@ -2,6 +2,7 @@ import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { AtaInitializationBuilder } from './ataInitializationBuilder'; import { CloseAtaBuilder } from './closeAtaBuilder'; +import { RecoverNestedAtaBuilder } from './recoverNestedAtaBuilder'; import { CustomInstructionBuilder } from './customInstructionBuilder'; import { StakingActivateBuilder } from './stakingActivateBuilder'; import { StakingAuthorizeBuilder } from './stakingAuthorizeBuilder'; @@ -178,6 +179,13 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.initializeBuilder(tx, new CloseAtaBuilder(this._coinConfig)); } + /** + * Returns the builder to recover tokens from a nested ATA (an ATA owned by another ATA). + */ + getRecoverNestedAtaBuilder(tx?: Transaction): RecoverNestedAtaBuilder { + return this.initializeBuilder(tx, new RecoverNestedAtaBuilder(this._coinConfig)); + } + /** * Returns the builder to create transactions with custom Solana instructions. */ diff --git a/modules/sdk-coin-sol/src/lib/utils.ts b/modules/sdk-coin-sol/src/lib/utils.ts index 963938c192..a5d2494d5d 100644 --- a/modules/sdk-coin-sol/src/lib/utils.ts +++ b/modules/sdk-coin-sol/src/lib/utils.ts @@ -36,6 +36,7 @@ import nacl from 'tweetnacl'; import { ataCloseInstructionIndexes, ataInitInstructionIndexes, + ataRecoverNestedInstructionIndexes, MAX_MEMO_LENGTH, MEMO_PROGRAM_PK, nonceAdvanceInstruction, @@ -370,6 +371,8 @@ export function getTransactionType(transaction: SolTransaction): TransactionType return TransactionType.AssociatedTokenAccountInitialization; } else if (matchTransactionTypeByInstructionsOrder(instructions, ataCloseInstructionIndexes)) { return TransactionType.CloseAssociatedTokenAccount; + } else if (matchTransactionTypeByInstructionsOrder(instructions, ataRecoverNestedInstructionIndexes)) { + return TransactionType.CloseAssociatedTokenAccount; } else { return TransactionType.CustomTx; } @@ -418,6 +421,8 @@ export function getInstructionType(instruction: TransactionInstruction): ValidIn // Both instruction types are treated as 'InitializeAssociatedTokenAccount' for compatibility if (instruction.data.length === 0 || isIdempotentAtaInstruction(instruction)) { return 'InitializeAssociatedTokenAccount'; + } else if (instruction.data.length === 1 && instruction.data[0] === 2) { + return 'RecoverNestedAssociatedTokenAccount'; } else { throw new NotSupported( 'Invalid transaction, instruction program id not supported: ' + instruction.programId.toString() diff --git a/modules/sdk-coin-sol/src/sol.ts b/modules/sdk-coin-sol/src/sol.ts index 4199ddf76b..e0e23f449f 100644 --- a/modules/sdk-coin-sol/src/sol.ts +++ b/modules/sdk-coin-sol/src/sol.ts @@ -172,6 +172,12 @@ export interface SolRecoveryOptions extends MPCRecoveryOptions { closeAtaAddress?: string; // destination address where token should be sent before closing the ATA address recoveryDestinationAtaAddress?: string; + // nested ATA address (ATA whose owner is another ATA) to recover tokens from + nestedAtaAddress?: string; + // the ATA that owns the nested ATA (and where recovered tokens will be sent) + ownerAtaAddress?: string; + // the token mint address for both ATAs (required when recovering from nested ATA) + tokenMintAddress?: string; programId?: string; // programId of the token apiKey?: string; // API key for node requests } @@ -1528,6 +1534,81 @@ export class Sol extends BaseCoin { return recovertTxns; } + /** + * Recovers tokens from a nested ATA — an ATA whose owner is another ATA rather than a wallet address. + * + * This situation occurs when an external sender mistakenly calls createAssociatedTokenAccount with + * an ATA address as the owner instead of the root wallet address. The result is a "nested ATA" + * (ATA-2) owned by the wallet's normal ATA (ATA-1). Because ATA-1 is a PDA with no private key, + * the standard recoverCloseATA flow cannot sign for ATA-2. + * + * This method uses the Associated Token Account program's RecoverNested instruction, which allows + * the root wallet owner to sign and atomically move tokens from ATA-2 → ATA-1 and close ATA-2, + * returning the rent-exempt SOL to the wallet address. + * + * @param {SolRecoveryOptions} params - recovery params, requires nestedAtaAddress, ownerAtaAddress, + * and tokenMintAddress in addition to the standard keychain fields + */ + async recoverNestedAta(params: SolRecoveryOptions): Promise { + if (!params.bitgoKey) { + throw new Error('missing bitgoKey'); + } + + if (!params.recoveryDestination || !this.isValidAddress(params.recoveryDestination)) { + throw new Error('invalid recoveryDestination'); + } + + if (!params.nestedAtaAddress || !this.isValidAddress(params.nestedAtaAddress)) { + throw new Error('invalid nestedAtaAddress'); + } + + if (!params.ownerAtaAddress || !this.isValidAddress(params.ownerAtaAddress)) { + throw new Error('invalid ownerAtaAddress'); + } + + if (!params.tokenMintAddress || !this.isValidAddress(params.tokenMintAddress)) { + throw new Error('invalid tokenMintAddress'); + } + + const bitgoKey = params.bitgoKey.replace(/\s/g, ''); + const MPC = await EDDSAMethods.getInitializedMpcInstance(); + + const index = params.index || 0; + const currPath = params.seed ? getDerivationPath(params.seed) + `/${index}` : `m/${index}`; + const accountId = MPC.deriveUnhardened(bitgoKey, currPath).slice(0, 64); + const bs58EncodedPublicKey = new SolKeyPair({ pub: accountId }).getAddress(); + + const blockhash = await this.getBlockhash(params.apiKey); + const rentExemptAmount = await this.getRentExemptAmount(); + + const factory = this.getBuilder(); + const txBuilder = factory.getRecoverNestedAtaBuilder(); + txBuilder.nonce(blockhash); + txBuilder.sender(bs58EncodedPublicKey); + txBuilder.feePayer(bs58EncodedPublicKey); + txBuilder.associatedTokenAccountRent(rentExemptAmount.toString()); + txBuilder.nestedAccountAddress(params.nestedAtaAddress); + txBuilder.nestedMintAddress(params.tokenMintAddress); + txBuilder.destinationAccountAddress(params.ownerAtaAddress); + txBuilder.ownerAccountAddress(params.ownerAtaAddress); + txBuilder.ownerMintAddress(params.tokenMintAddress); + txBuilder.walletAddress(bs58EncodedPublicKey); + + const recoverNestedTxn = await this.signAndGenerateBroadcastableTransaction( + params, + txBuilder, + bs58EncodedPublicKey + ); + + const serializedTxn = (await recoverNestedTxn).serializedTx; + const broadcastResult = await this.broadcastTransaction({ + serializedSignedTransaction: serializedTxn, + }); + logger.log(broadcastResult); + + return broadcastResult; + } + async signAndGenerateBroadcastableTransaction( params: SolRecoveryOptions, txBuilder: any, diff --git a/modules/sdk-coin-sol/test/unit/sol.ts b/modules/sdk-coin-sol/test/unit/sol.ts index 446f1ebc54..a24719803c 100644 --- a/modules/sdk-coin-sol/test/unit/sol.ts +++ b/modules/sdk-coin-sol/test/unit/sol.ts @@ -2519,6 +2519,67 @@ describe('SOL:', function () { '5oUBgXX4enGmFEspG64goy3PRysjfrekZGg3rZNkBHUCQFd482vrVWbfDcRYMBEJt65JXymfEPm8M6d89X4xV79n' ); }); + + it('should recover tokens from a nested ATA (ATA whose owner is another ATA)', async function () { + const result = await basecoin.recoverNestedAta({ + userKey: testData.closeATAkeys.userKey, + backupKey: testData.closeATAkeys.backupKey, + bitgoKey: testData.closeATAkeys.bitgoKey, + recoveryDestination: testData.closeATAkeys.destinationPubKey, + walletPassphrase: testData.closeATAkeys.walletPassword, + nestedAtaAddress: 'FGuZSBhtreqSUsE86xokyjKz2i8VBtJzy6uMXXKyGHug', + ownerAtaAddress: 'Zfm98ZpVafydhFTYcsY6bHgubhB4cFgWFvbdEJxYhTA', + tokenMintAddress: 'ZBCNpuD7YMXzTHB2fhGkGi78MNsHGLRXUhRewNRm9RU', + }); + + result.should.not.be.empty(); + should.equal( + result.txId, + '2id3YC2jK9G5Wo2phDx4gJVAew8DcY5NAojnVuao8rkxwPYPe8cSwE5GzhEgJA2y8fVjDEo6iR6ykBvDxrTQrtpb' + ); + }); + + it('should throw when nestedAtaAddress is missing for recoverNestedAta', async function () { + await basecoin + .recoverNestedAta({ + userKey: testData.closeATAkeys.userKey, + backupKey: testData.closeATAkeys.backupKey, + bitgoKey: testData.closeATAkeys.bitgoKey, + recoveryDestination: testData.closeATAkeys.destinationPubKey, + walletPassphrase: testData.closeATAkeys.walletPassword, + ownerAtaAddress: 'Zfm98ZpVafydhFTYcsY6bHgubhB4cFgWFvbdEJxYhTA', + tokenMintAddress: 'ZBCNpuD7YMXzTHB2fhGkGi78MNsHGLRXUhRewNRm9RU', + }) + .should.be.rejectedWith('invalid nestedAtaAddress'); + }); + + it('should throw when ownerAtaAddress is missing for recoverNestedAta', async function () { + await basecoin + .recoverNestedAta({ + userKey: testData.closeATAkeys.userKey, + backupKey: testData.closeATAkeys.backupKey, + bitgoKey: testData.closeATAkeys.bitgoKey, + recoveryDestination: testData.closeATAkeys.destinationPubKey, + walletPassphrase: testData.closeATAkeys.walletPassword, + nestedAtaAddress: 'FGuZSBhtreqSUsE86xokyjKz2i8VBtJzy6uMXXKyGHug', + tokenMintAddress: 'ZBCNpuD7YMXzTHB2fhGkGi78MNsHGLRXUhRewNRm9RU', + }) + .should.be.rejectedWith('invalid ownerAtaAddress'); + }); + + it('should throw when tokenMintAddress is missing for recoverNestedAta', async function () { + await basecoin + .recoverNestedAta({ + userKey: testData.closeATAkeys.userKey, + backupKey: testData.closeATAkeys.backupKey, + bitgoKey: testData.closeATAkeys.bitgoKey, + recoveryDestination: testData.closeATAkeys.destinationPubKey, + walletPassphrase: testData.closeATAkeys.walletPassword, + nestedAtaAddress: 'FGuZSBhtreqSUsE86xokyjKz2i8VBtJzy6uMXXKyGHug', + ownerAtaAddress: 'Zfm98ZpVafydhFTYcsY6bHgubhB4cFgWFvbdEJxYhTA', + }) + .should.be.rejectedWith('invalid tokenMintAddress'); + }); }); describe('Build Consolidation Recoveries:', () => {