Skip to content

Commit 40d342b

Browse files
Merge pull request #9515 from BitGo/CSHLD-1185-transfer-hook
feat(sdk-coin-sol): resolve Token-2022 transfer hook accounts live
2 parents 1c2f84b + 3509f03 commit 40d342b

11 files changed

Lines changed: 536 additions & 146 deletions

File tree

modules/sdk-coin-sol/src/config/token2022StaticConfig.ts

Lines changed: 0 additions & 48 deletions
This file was deleted.

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,21 @@ export interface Transfer {
7878
};
7979
}
8080

81+
/**
82+
* Extra account metadata required by a Token-2022 Transfer Hook.
83+
*
84+
* These are resolved live (in the order the hook's ExtraAccountMetaList requires)
85+
* and supplied to the instruction factory. See {@link TokenTransfer}.
86+
*/
87+
export interface ExtraAccountMeta {
88+
/** The base58-encoded public key of the account */
89+
pubkey: string;
90+
/** Whether the account must sign the transaction */
91+
isSigner: boolean;
92+
/** Whether the account is writable */
93+
isWritable: boolean;
94+
}
95+
8196
export interface TokenTransfer {
8297
type: InstructionBuilderTypes.TokenTransfer;
8398
params: {
@@ -91,6 +106,12 @@ export interface TokenTransfer {
91106
programId?: string;
92107
/** Withheld transfer fee in raw base units */
93108
fee?: string;
109+
/**
110+
* Resolved Transfer Hook extra account metas, in the exact order the hook
111+
* requires. Only used for Token-2022 transfers whose mint has a Transfer
112+
* Hook extension; resolved live by the caller (offline builders never fetch).
113+
*/
114+
transferHookAccounts?: ExtraAccountMeta[];
94115
};
95116
}
96117

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
AtaInit,
3838
AtaRecoverNested,
3939
Burn,
40+
ExtraAccountMeta,
4041
InstructionParams,
4142
Memo,
4243
MintTo,
@@ -237,6 +238,12 @@ function parseSendInstructions(
237238
if (instruction.programId) {
238239
programIDForTokenTransfer = instruction.programId.toString();
239240
}
241+
const transferHookAccounts = findTransferHookAccounts(
242+
ttKeys.owner.pubkey.toString(),
243+
ttKeys.destination.pubkey.toString(),
244+
tokenAddress,
245+
instructionMetadata
246+
);
240247
const tokenTransfer: TokenTransfer = {
241248
type: InstructionBuilderTypes.TokenTransfer,
242249
params: {
@@ -249,6 +256,7 @@ function parseSendInstructions(
249256
programId: programIDForTokenTransfer,
250257
decimalPlaces: ttDecimals,
251258
...(ttFee !== undefined ? { fee: ttFee } : {}),
259+
...(transferHookAccounts ? { transferHookAccounts } : {}),
252260
},
253261
};
254262
instructionData.push(tokenTransfer);
@@ -1334,3 +1342,25 @@ export function findTokenName(
13341342

13351343
return token;
13361344
}
1345+
1346+
export function findTransferHookAccounts(
1347+
fromAddress: string,
1348+
toAddress: string,
1349+
tokenAddress: string,
1350+
instructionMetadata?: InstructionParams[]
1351+
): ExtraAccountMeta[] | undefined {
1352+
let transferHookAccounts: ExtraAccountMeta[] | undefined;
1353+
1354+
instructionMetadata?.forEach((instruction) => {
1355+
if (
1356+
instruction.type === InstructionBuilderTypes.TokenTransfer &&
1357+
instruction.params.tokenAddress === tokenAddress &&
1358+
instruction.params.fromAddress === fromAddress &&
1359+
instruction.params.toAddress === toAddress
1360+
) {
1361+
transferHookAccounts = instruction.params.transferHookAccounts;
1362+
}
1363+
});
1364+
1365+
return transferHookAccounts;
1366+
}

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

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
AtaClose,
3131
AtaInit,
3232
AtaRecoverNested,
33+
ExtraAccountMeta,
3334
InstructionParams,
3435
Memo,
3536
MintTo,
@@ -50,7 +51,6 @@ import {
5051
} from './iface';
5152
import { computeTransferFee, getSolTokenFromTokenName, isValidBase64, isValidHex } from './utils';
5253
import { depositSolInstructions, withdrawStakeInstructions } from './jitoStakePoolOperations';
53-
import { getToken2022Config, TransferHookConfig } from './token2022Config';
5454

5555
/**
5656
* Construct Solana instructions from instructions params
@@ -245,10 +245,10 @@ function tokenTransferInstruction(data: TokenTransfer): TransactionInstruction[]
245245
TOKEN_2022_PROGRAM_ID
246246
);
247247
}
248-
// Check if this token has a transfer hook configuration
249-
const tokenConfig = getToken2022Config(tokenAddress);
250-
if (tokenConfig?.transferHook) {
251-
addTransferHookAccounts(transferInstruction, tokenConfig.transferHook);
248+
// Append any resolved Transfer Hook extra accounts. These are resolved live by
249+
// the caller (offline builders never fetch) and supplied in the required order.
250+
if (data.params.transferHookAccounts?.length) {
251+
addTransferHookAccounts(transferInstruction, data.params.transferHookAccounts);
252252
}
253253
} else {
254254
transferInstruction = createTransferCheckedInstruction(
@@ -787,22 +787,16 @@ function upsertAccountMeta(keys: AccountMeta[], meta: AccountMeta): void {
787787
}
788788
}
789789

790-
function buildStaticTransferHookAccounts(transferHook: TransferHookConfig): AccountMeta[] {
791-
const metas: AccountMeta[] = [];
792-
if (transferHook.extraAccountMetas?.length) {
793-
for (const meta of transferHook.extraAccountMetas) {
794-
metas.push({
795-
pubkey: new PublicKey(meta.pubkey),
796-
isSigner: meta.isSigner,
797-
isWritable: meta.isWritable,
798-
});
799-
}
800-
}
801-
return metas;
790+
function buildTransferHookAccountMetas(extraAccountMetas: ExtraAccountMeta[]): AccountMeta[] {
791+
return extraAccountMetas.map((meta) => ({
792+
pubkey: new PublicKey(meta.pubkey),
793+
isSigner: meta.isSigner,
794+
isWritable: meta.isWritable,
795+
}));
802796
}
803797

804-
function addTransferHookAccounts(instruction: TransactionInstruction, transferHook: TransferHookConfig): void {
805-
const extraMetas = buildStaticTransferHookAccounts(transferHook);
798+
function addTransferHookAccounts(instruction: TransactionInstruction, extraAccountMetas: ExtraAccountMeta[]): void {
799+
const extraMetas = buildTransferHookAccountMetas(extraAccountMetas);
806800
for (const meta of extraMetas) {
807801
upsertAccountMeta(instruction.keys, meta);
808802
}

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

Lines changed: 0 additions & 56 deletions
This file was deleted.

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
validateOwnerAddress,
1111
} from './utils';
1212
import { InstructionBuilderTypes } from './constants';
13-
import { AtaInit, TokenAssociateRecipient, TokenTransfer, SetPriorityFee } from './iface';
13+
import { AtaInit, ExtraAccountMeta, TokenAssociateRecipient, TokenTransfer, SetPriorityFee } from './iface';
1414
import assert from 'assert';
1515
import { TransactionBuilder } from './transactionBuilder';
1616
import _ from 'lodash';
@@ -29,12 +29,28 @@ const UNSIGNED_BIGINT_MAX = BigInt('18446744073709551615');
2929
export class TokenTransferBuilder extends TransactionBuilder {
3030
private _sendParams: SendParams[] = [];
3131
private _createAtaParams: TokenAssociateRecipient[];
32+
private _transferHookAccounts?: ExtraAccountMeta[];
3233

3334
constructor(_coinConfig: Readonly<CoinConfig>) {
3435
super(_coinConfig);
3536
this._createAtaParams = [];
3637
}
3738

39+
/**
40+
* Set the resolved Token-2022 Transfer Hook extra account metas for this transfer.
41+
*
42+
* These must be resolved live by the caller (e.g. via `Sol.resolveTransferHookAccounts`)
43+
* since builders remain offline and never perform RPC. The order is significant and
44+
* must match the hook's ExtraAccountMetaList.
45+
*
46+
* @param {ExtraAccountMeta[]} metas - resolved extra account metas, in hook order
47+
* @returns {TokenTransferBuilder} This transaction builder
48+
*/
49+
transferHookAccounts(metas: ExtraAccountMeta[]): this {
50+
this._transferHookAccounts = metas;
51+
return this;
52+
}
53+
3854
protected get transactionType(): TransactionType {
3955
return TransactionType.Send;
4056
}
@@ -153,6 +169,7 @@ export class TokenTransferBuilder extends TransactionBuilder {
153169
tokenAddress: tokenAddress,
154170
programId: programId,
155171
decimalPlaces: decimals,
172+
...(this._transferHookAccounts ? { transferHookAccounts: this._transferHookAccounts } : {}),
156173
},
157174
};
158175
})

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from './utils';
1212
import { BaseCoin as CoinConfig } from '@bitgo/statics';
1313
import assert from 'assert';
14-
import { AtaInit, TokenAssociateRecipient, TokenTransfer, Transfer, SetPriorityFee } from './iface';
14+
import { AtaInit, ExtraAccountMeta, TokenAssociateRecipient, TokenTransfer, Transfer, SetPriorityFee } from './iface';
1515
import { InstructionBuilderTypes } from './constants';
1616
import _ from 'lodash';
1717

@@ -29,11 +29,27 @@ const UNSIGNED_BIGINT_MAX = BigInt('18446744073709551615');
2929
export class TransferBuilderV2 extends TransactionBuilder {
3030
private _sendParams: SendParams[] = [];
3131
private _createAtaParams: TokenAssociateRecipient[];
32+
private _transferHookAccounts?: ExtraAccountMeta[];
3233
constructor(_coinConfig: Readonly<CoinConfig>) {
3334
super(_coinConfig);
3435
this._createAtaParams = [];
3536
}
3637

38+
/**
39+
* Set the resolved Token-2022 Transfer Hook extra account metas for this transfer.
40+
*
41+
* These must be resolved live by the caller (e.g. via `Sol.resolveTransferHookAccounts`)
42+
* since builders remain offline and never perform RPC. The order is significant and
43+
* must match the hook's ExtraAccountMetaList.
44+
*
45+
* @param {ExtraAccountMeta[]} metas - resolved extra account metas, in hook order
46+
* @returns {TransferBuilderV2} This transaction builder
47+
*/
48+
transferHookAccounts(metas: ExtraAccountMeta[]): this {
49+
this._transferHookAccounts = metas;
50+
return this;
51+
}
52+
3753
protected get transactionType(): TransactionType {
3854
return TransactionType.Send;
3955
}
@@ -164,6 +180,7 @@ export class TransferBuilderV2 extends TransactionBuilder {
164180
tokenAddress: tokenAddress,
165181
programId: programId,
166182
decimalPlaces: decimals,
183+
...(this._transferHookAccounts ? { transferHookAccounts: this._transferHookAccounts } : {}),
167184
},
168185
};
169186
} else {

0 commit comments

Comments
 (0)