Skip to content

Commit 76a84bf

Browse files
Merge pull request #8306 from BitGo/coin-7674
feat: add nested sol recovery
2 parents 99df148 + 44b46f0 commit 76a84bf

9 files changed

Lines changed: 355 additions & 2 deletions

File tree

modules/sdk-coin-sol/src/lib/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export enum ValidInstructionTypesEnum {
4949
Memo = 'Memo',
5050
InitializeAssociatedTokenAccount = 'InitializeAssociatedTokenAccount',
5151
CloseAssociatedTokenAccount = 'CloseAssociatedTokenAccount',
52+
RecoverNestedAssociatedTokenAccount = 'RecoverNestedAssociatedTokenAccount',
5253
Allocate = 'Allocate',
5354
Assign = 'Assign',
5455
Split = 'Split',
@@ -74,6 +75,7 @@ export enum InstructionBuilderTypes {
7475
NonceAdvance = 'NonceAdvance',
7576
CreateAssociatedTokenAccount = 'CreateAssociatedTokenAccount',
7677
CloseAssociatedTokenAccount = 'CloseAssociatedTokenAccount',
78+
RecoverNestedAssociatedTokenAccount = 'RecoverNestedAssociatedTokenAccount',
7779
TokenTransfer = 'TokenTransfer',
7880
StakingAuthorize = 'Authorize',
7981
StakingDelegate = 'Delegate',
@@ -99,6 +101,7 @@ export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [
99101
ValidInstructionTypesEnum.Memo,
100102
ValidInstructionTypesEnum.InitializeAssociatedTokenAccount,
101103
ValidInstructionTypesEnum.CloseAssociatedTokenAccount,
104+
ValidInstructionTypesEnum.RecoverNestedAssociatedTokenAccount,
102105
ValidInstructionTypesEnum.TokenTransfer,
103106
ValidInstructionTypesEnum.Allocate,
104107
ValidInstructionTypesEnum.Assign,
@@ -203,6 +206,11 @@ export const ataCloseInstructionIndexes = {
203206
CloseAssociatedTokenAccount: 0,
204207
} as const;
205208

209+
/** Const to check the order of the recover nested ATA instructions when decode */
210+
export const ataRecoverNestedInstructionIndexes = {
211+
RecoverNestedAssociatedTokenAccount: 0,
212+
} as const;
213+
206214
export const nonceAdvanceInstruction = 'AdvanceNonceAccount';
207215
export const validInstructionData = '0a00000001000000';
208216
export const validInstructionData2 = '0a00000000000000';

modules/sdk-coin-sol/src/lib/iface.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export type InstructionParams =
4444
| StakingWithdraw
4545
| AtaInit
4646
| AtaClose
47+
| AtaRecoverNested
4748
| TokenTransfer
4849
| StakingAuthorize
4950
| StakingDelegate
@@ -220,13 +221,26 @@ export interface AtaClose {
220221
params: { accountAddress: string; destinationAddress: string; authorityAddress: string };
221222
}
222223

224+
export interface AtaRecoverNested {
225+
type: InstructionBuilderTypes.RecoverNestedAssociatedTokenAccount;
226+
params: {
227+
nestedAccountAddress: string;
228+
nestedMintAddress: string;
229+
destinationAccountAddress: string;
230+
ownerAccountAddress: string;
231+
ownerMintAddress: string;
232+
walletAddress: string;
233+
};
234+
}
235+
223236
export type ValidInstructionTypes =
224237
| SystemInstructionType
225238
| StakeInstructionType
226239
| StakePoolInstructionType
227240
| 'Memo'
228241
| 'InitializeAssociatedTokenAccount'
229242
| 'CloseAssociatedTokenAccount'
243+
| 'RecoverNestedAssociatedTokenAccount'
230244
| DecodedCloseAccountInstruction
231245
| 'TokenTransfer'
232246
| 'SetComputeUnitLimit'

modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { InstructionBuilderTypes, ValidInstructionTypesEnum, walletInitInstructi
3232
import {
3333
AtaClose,
3434
AtaInit,
35+
AtaRecoverNested,
3536
Burn,
3637
InstructionParams,
3738
Memo,
@@ -1071,14 +1072,23 @@ const ataCloseInstructionKeysIndexes = {
10711072
AuthorityAddress: 2,
10721073
};
10731074

1075+
const ataRecoverNestedInstructionKeysIndexes = {
1076+
NestedAccountAddress: 0,
1077+
NestedMintAddress: 1,
1078+
DestinationAccountAddress: 2,
1079+
OwnerAccountAddress: 3,
1080+
OwnerMintAddress: 4,
1081+
WalletAddress: 5,
1082+
};
1083+
10741084
/**
10751085
* Parses Solana instructions to close associated token account tx instructions params
10761086
*
10771087
* @param {TransactionInstruction[]} instructions - an array of supported Solana instructions
10781088
* @returns {InstructionParams[]} An array containing instruction params for Send tx
10791089
*/
1080-
function parseAtaCloseInstructions(instructions: TransactionInstruction[]): Array<AtaClose | Nonce> {
1081-
const instructionData: Array<AtaClose | Nonce> = [];
1090+
function parseAtaCloseInstructions(instructions: TransactionInstruction[]): Array<AtaClose | AtaRecoverNested | Nonce> {
1091+
const instructionData: Array<AtaClose | AtaRecoverNested | Nonce> = [];
10821092
for (const instruction of instructions) {
10831093
const type = getInstructionType(instruction);
10841094
switch (type) {
@@ -1104,6 +1114,25 @@ function parseAtaCloseInstructions(instructions: TransactionInstruction[]): Arra
11041114
};
11051115
instructionData.push(ataClose);
11061116
break;
1117+
case ValidInstructionTypesEnum.RecoverNestedAssociatedTokenAccount:
1118+
const ataRecoverNested: AtaRecoverNested = {
1119+
type: InstructionBuilderTypes.RecoverNestedAssociatedTokenAccount,
1120+
params: {
1121+
nestedAccountAddress:
1122+
instruction.keys[ataRecoverNestedInstructionKeysIndexes.NestedAccountAddress].pubkey.toString(),
1123+
nestedMintAddress:
1124+
instruction.keys[ataRecoverNestedInstructionKeysIndexes.NestedMintAddress].pubkey.toString(),
1125+
destinationAccountAddress:
1126+
instruction.keys[ataRecoverNestedInstructionKeysIndexes.DestinationAccountAddress].pubkey.toString(),
1127+
ownerAccountAddress:
1128+
instruction.keys[ataRecoverNestedInstructionKeysIndexes.OwnerAccountAddress].pubkey.toString(),
1129+
ownerMintAddress:
1130+
instruction.keys[ataRecoverNestedInstructionKeysIndexes.OwnerMintAddress].pubkey.toString(),
1131+
walletAddress: instruction.keys[ataRecoverNestedInstructionKeysIndexes.WalletAddress].pubkey.toString(),
1132+
},
1133+
};
1134+
instructionData.push(ataRecoverNested);
1135+
break;
11071136
default:
11081137
throw new NotSupported(
11091138
'Invalid transaction, instruction type not supported: ' + getInstructionType(instruction)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { TransactionType } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import assert from 'assert';
4+
import { InstructionBuilderTypes } from './constants';
5+
import { AtaRecoverNested } from './iface';
6+
import { Transaction } from './transaction';
7+
import { TransactionBuilder } from './transactionBuilder';
8+
import { validateAddress } from './utils';
9+
10+
export class RecoverNestedAtaBuilder extends TransactionBuilder {
11+
protected _nestedAccountAddress: string;
12+
protected _nestedMintAddress: string;
13+
protected _destinationAccountAddress: string;
14+
protected _ownerAccountAddress: string;
15+
protected _ownerMintAddress: string;
16+
protected _walletAddress: string;
17+
18+
constructor(_coinConfig: Readonly<CoinConfig>) {
19+
super(_coinConfig);
20+
this._transaction = new Transaction(_coinConfig);
21+
}
22+
23+
protected get transactionType(): TransactionType {
24+
return TransactionType.CloseAssociatedTokenAccount;
25+
}
26+
27+
nestedAccountAddress(nestedAccountAddress: string): this {
28+
validateAddress(nestedAccountAddress, 'nestedAccountAddress');
29+
this._nestedAccountAddress = nestedAccountAddress;
30+
return this;
31+
}
32+
33+
nestedMintAddress(nestedMintAddress: string): this {
34+
validateAddress(nestedMintAddress, 'nestedMintAddress');
35+
this._nestedMintAddress = nestedMintAddress;
36+
return this;
37+
}
38+
39+
destinationAccountAddress(destinationAccountAddress: string): this {
40+
validateAddress(destinationAccountAddress, 'destinationAccountAddress');
41+
this._destinationAccountAddress = destinationAccountAddress;
42+
return this;
43+
}
44+
45+
ownerAccountAddress(ownerAccountAddress: string): this {
46+
validateAddress(ownerAccountAddress, 'ownerAccountAddress');
47+
this._ownerAccountAddress = ownerAccountAddress;
48+
return this;
49+
}
50+
51+
ownerMintAddress(ownerMintAddress: string): this {
52+
validateAddress(ownerMintAddress, 'ownerMintAddress');
53+
this._ownerMintAddress = ownerMintAddress;
54+
return this;
55+
}
56+
57+
walletAddress(walletAddress: string): this {
58+
validateAddress(walletAddress, 'walletAddress');
59+
this._walletAddress = walletAddress;
60+
return this;
61+
}
62+
63+
/** @inheritDoc */
64+
initBuilder(tx: Transaction): void {
65+
super.initBuilder(tx);
66+
for (const instruction of this._instructionsData) {
67+
if (instruction.type === InstructionBuilderTypes.RecoverNestedAssociatedTokenAccount) {
68+
const recoverNestedInstruction: AtaRecoverNested = instruction;
69+
this.nestedAccountAddress(recoverNestedInstruction.params.nestedAccountAddress);
70+
this.nestedMintAddress(recoverNestedInstruction.params.nestedMintAddress);
71+
this.destinationAccountAddress(recoverNestedInstruction.params.destinationAccountAddress);
72+
this.ownerAccountAddress(recoverNestedInstruction.params.ownerAccountAddress);
73+
this.ownerMintAddress(recoverNestedInstruction.params.ownerMintAddress);
74+
this.walletAddress(recoverNestedInstruction.params.walletAddress);
75+
}
76+
}
77+
}
78+
79+
/** @inheritdoc */
80+
protected async buildImplementation(): Promise<Transaction> {
81+
assert(this._nestedAccountAddress, 'nestedAccountAddress must be set before building the transaction');
82+
assert(this._nestedMintAddress, 'nestedMintAddress must be set before building the transaction');
83+
assert(this._destinationAccountAddress, 'destinationAccountAddress must be set before building the transaction');
84+
assert(this._ownerAccountAddress, 'ownerAccountAddress must be set before building the transaction');
85+
assert(this._ownerMintAddress, 'ownerMintAddress must be set before building the transaction');
86+
assert(this._walletAddress, 'walletAddress must be set before building the transaction');
87+
88+
const recoverNestedData: AtaRecoverNested = {
89+
type: InstructionBuilderTypes.RecoverNestedAssociatedTokenAccount,
90+
params: {
91+
nestedAccountAddress: this._nestedAccountAddress,
92+
nestedMintAddress: this._nestedMintAddress,
93+
destinationAccountAddress: this._destinationAccountAddress,
94+
ownerAccountAddress: this._ownerAccountAddress,
95+
ownerMintAddress: this._ownerMintAddress,
96+
walletAddress: this._walletAddress,
97+
},
98+
};
99+
100+
this._instructionsData = [recoverNestedData];
101+
102+
return await super.buildImplementation();
103+
}
104+
}

modules/sdk-coin-sol/src/lib/solInstructionFactory.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
createCloseAccountInstruction,
66
createMintToInstruction,
77
createBurnInstruction,
8+
createRecoverNestedInstruction,
89
createTransferCheckedInstruction,
910
TOKEN_2022_PROGRAM_ID,
1011
createApproveInstruction,
@@ -27,6 +28,7 @@ import { InstructionBuilderTypes, MEMO_PROGRAM_PK } from './constants';
2728
import {
2829
AtaClose,
2930
AtaInit,
31+
AtaRecoverNested,
3032
InstructionParams,
3133
Memo,
3234
MintTo,
@@ -79,6 +81,8 @@ export function solInstructionFactory(instructionToBuild: InstructionParams): Tr
7981
return createATAInstruction(instructionToBuild);
8082
case InstructionBuilderTypes.CloseAssociatedTokenAccount:
8183
return closeATAInstruction(instructionToBuild);
84+
case InstructionBuilderTypes.RecoverNestedAssociatedTokenAccount:
85+
return recoverNestedATAInstruction(instructionToBuild);
8286
case InstructionBuilderTypes.StakingAuthorize:
8387
return stakingAuthorizeInstruction(instructionToBuild);
8488
case InstructionBuilderTypes.StakingDelegate:
@@ -551,6 +555,45 @@ function closeATAInstruction(data: AtaClose): TransactionInstruction[] {
551555
return [closeAssociatedTokenAccountInstruction];
552556
}
553557

558+
/**
559+
* Construct RecoverNested ATA Solana instruction
560+
*
561+
* Recovers tokens from a nested ATA (an ATA whose owner is another ATA rather than a wallet address).
562+
* This uses the Associated Token Account program's RecoverNested instruction, which allows the root
563+
* wallet owner to sign and recover tokens without needing the intermediate ATA to sign.
564+
*
565+
* @param {AtaRecoverNested} data - the data to build the instruction
566+
* @returns {TransactionInstruction[]} An array containing the RecoverNested instruction
567+
*/
568+
function recoverNestedATAInstruction(data: AtaRecoverNested): TransactionInstruction[] {
569+
const {
570+
params: {
571+
nestedAccountAddress,
572+
nestedMintAddress,
573+
destinationAccountAddress,
574+
ownerAccountAddress,
575+
ownerMintAddress,
576+
walletAddress,
577+
},
578+
} = data;
579+
assert(nestedAccountAddress, 'Missing nestedAccountAddress param');
580+
assert(nestedMintAddress, 'Missing nestedMintAddress param');
581+
assert(destinationAccountAddress, 'Missing destinationAccountAddress param');
582+
assert(ownerAccountAddress, 'Missing ownerAccountAddress param');
583+
assert(ownerMintAddress, 'Missing ownerMintAddress param');
584+
assert(walletAddress, 'Missing walletAddress param');
585+
586+
const recoverNestedInstruction = createRecoverNestedInstruction(
587+
new PublicKey(nestedAccountAddress),
588+
new PublicKey(nestedMintAddress),
589+
new PublicKey(destinationAccountAddress),
590+
new PublicKey(ownerAccountAddress),
591+
new PublicKey(ownerMintAddress),
592+
new PublicKey(walletAddress)
593+
);
594+
return [recoverNestedInstruction];
595+
}
596+
554597
/**
555598
* Construct Staking Account Authorize Solana instructions
556599
*

modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
33
import { AtaInitializationBuilder } from './ataInitializationBuilder';
44
import { CloseAtaBuilder } from './closeAtaBuilder';
5+
import { RecoverNestedAtaBuilder } from './recoverNestedAtaBuilder';
56
import { CustomInstructionBuilder } from './customInstructionBuilder';
67
import { StakingActivateBuilder } from './stakingActivateBuilder';
78
import { StakingAuthorizeBuilder } from './stakingAuthorizeBuilder';
@@ -178,6 +179,13 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
178179
return this.initializeBuilder(tx, new CloseAtaBuilder(this._coinConfig));
179180
}
180181

182+
/**
183+
* Returns the builder to recover tokens from a nested ATA (an ATA owned by another ATA).
184+
*/
185+
getRecoverNestedAtaBuilder(tx?: Transaction): RecoverNestedAtaBuilder {
186+
return this.initializeBuilder(tx, new RecoverNestedAtaBuilder(this._coinConfig));
187+
}
188+
181189
/**
182190
* Returns the builder to create transactions with custom Solana instructions.
183191
*/

modules/sdk-coin-sol/src/lib/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import nacl from 'tweetnacl';
3636
import {
3737
ataCloseInstructionIndexes,
3838
ataInitInstructionIndexes,
39+
ataRecoverNestedInstructionIndexes,
3940
MAX_MEMO_LENGTH,
4041
MEMO_PROGRAM_PK,
4142
nonceAdvanceInstruction,
@@ -370,6 +371,8 @@ export function getTransactionType(transaction: SolTransaction): TransactionType
370371
return TransactionType.AssociatedTokenAccountInitialization;
371372
} else if (matchTransactionTypeByInstructionsOrder(instructions, ataCloseInstructionIndexes)) {
372373
return TransactionType.CloseAssociatedTokenAccount;
374+
} else if (matchTransactionTypeByInstructionsOrder(instructions, ataRecoverNestedInstructionIndexes)) {
375+
return TransactionType.CloseAssociatedTokenAccount;
373376
} else {
374377
return TransactionType.CustomTx;
375378
}
@@ -418,6 +421,8 @@ export function getInstructionType(instruction: TransactionInstruction): ValidIn
418421
// Both instruction types are treated as 'InitializeAssociatedTokenAccount' for compatibility
419422
if (instruction.data.length === 0 || isIdempotentAtaInstruction(instruction)) {
420423
return 'InitializeAssociatedTokenAccount';
424+
} else if (instruction.data.length === 1 && instruction.data[0] === 2) {
425+
return 'RecoverNestedAssociatedTokenAccount';
421426
} else {
422427
throw new NotSupported(
423428
'Invalid transaction, instruction program id not supported: ' + instruction.programId.toString()

0 commit comments

Comments
 (0)