Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions modules/sdk-coin-sol/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export enum ValidInstructionTypesEnum {
Memo = 'Memo',
InitializeAssociatedTokenAccount = 'InitializeAssociatedTokenAccount',
CloseAssociatedTokenAccount = 'CloseAssociatedTokenAccount',
RecoverNestedAssociatedTokenAccount = 'RecoverNestedAssociatedTokenAccount',
Allocate = 'Allocate',
Assign = 'Assign',
Split = 'Split',
Expand All @@ -74,6 +75,7 @@ export enum InstructionBuilderTypes {
NonceAdvance = 'NonceAdvance',
CreateAssociatedTokenAccount = 'CreateAssociatedTokenAccount',
CloseAssociatedTokenAccount = 'CloseAssociatedTokenAccount',
RecoverNestedAssociatedTokenAccount = 'RecoverNestedAssociatedTokenAccount',
TokenTransfer = 'TokenTransfer',
StakingAuthorize = 'Authorize',
StakingDelegate = 'Delegate',
Expand All @@ -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,
Expand Down Expand Up @@ -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';
14 changes: 14 additions & 0 deletions modules/sdk-coin-sol/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type InstructionParams =
| StakingWithdraw
| AtaInit
| AtaClose
| AtaRecoverNested
| TokenTransfer
| StakingAuthorize
| StakingDelegate
Expand Down Expand Up @@ -220,13 +221,26 @@ 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
| StakePoolInstructionType
| 'Memo'
| 'InitializeAssociatedTokenAccount'
| 'CloseAssociatedTokenAccount'
| 'RecoverNestedAssociatedTokenAccount'
| DecodedCloseAccountInstruction
| 'TokenTransfer'
| 'SetComputeUnitLimit'
Expand Down
33 changes: 31 additions & 2 deletions modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { InstructionBuilderTypes, ValidInstructionTypesEnum, walletInitInstructi
import {
AtaClose,
AtaInit,
AtaRecoverNested,
Burn,
InstructionParams,
Memo,
Expand Down Expand Up @@ -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<AtaClose | Nonce> {
const instructionData: Array<AtaClose | Nonce> = [];
function parseAtaCloseInstructions(instructions: TransactionInstruction[]): Array<AtaClose | AtaRecoverNested | Nonce> {
const instructionData: Array<AtaClose | AtaRecoverNested | Nonce> = [];
for (const instruction of instructions) {
const type = getInstructionType(instruction);
switch (type) {
Expand All @@ -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)
Expand Down
104 changes: 104 additions & 0 deletions modules/sdk-coin-sol/src/lib/recoverNestedAtaBuilder.ts
Original file line number Diff line number Diff line change
@@ -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<CoinConfig>) {
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<Transaction> {
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();
}
}
43 changes: 43 additions & 0 deletions modules/sdk-coin-sol/src/lib/solInstructionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createCloseAccountInstruction,
createMintToInstruction,
createBurnInstruction,
createRecoverNestedInstruction,
createTransferCheckedInstruction,
TOKEN_2022_PROGRAM_ID,
createApproveInstruction,
Expand All @@ -27,6 +28,7 @@ import { InstructionBuilderTypes, MEMO_PROGRAM_PK } from './constants';
import {
AtaClose,
AtaInit,
AtaRecoverNested,
InstructionParams,
Memo,
MintTo,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
*
Expand Down
8 changes: 8 additions & 0 deletions modules/sdk-coin-sol/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
*/
Expand Down
5 changes: 5 additions & 0 deletions modules/sdk-coin-sol/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import nacl from 'tweetnacl';
import {
ataCloseInstructionIndexes,
ataInitInstructionIndexes,
ataRecoverNestedInstructionIndexes,
MAX_MEMO_LENGTH,
MEMO_PROGRAM_PK,
nonceAdvanceInstruction,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading