Skip to content

Commit 7d2819d

Browse files
feat(sdk-coin-sol): token 2022 transfer hook implementation
transfer hook implementation for tbill token, added extra account metas Ticket: WIN-7258
1 parent 50f69f6 commit 7d2819d

File tree

2 files changed

+223
-1
lines changed

2 files changed

+223
-1
lines changed

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

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
createApproveInstruction,
1111
} from '@solana/spl-token';
1212
import {
13+
AccountMeta,
1314
Authorized,
1415
Lockup,
1516
PublicKey,
@@ -45,6 +46,7 @@ import {
4546
} from './iface';
4647
import { getSolTokenFromTokenName, isValidBase64, isValidHex } from './utils';
4748
import { depositSolInstructions, withdrawStakeInstructions } from './jitoStakePoolOperations';
49+
import { getToken2022Config, TransferHookConfig } from './token2022Config';
4850

4951
/**
5052
* Construct Solana instructions from instructions params
@@ -193,7 +195,10 @@ function tokenTransferInstruction(data: TokenTransfer): TransactionInstruction[]
193195
}
194196

195197
let transferInstruction: TransactionInstruction;
198+
const instructions: TransactionInstruction[] = [];
199+
196200
if (programId === TOKEN_2022_PROGRAM_ID.toString()) {
201+
// Create the base transfer instruction
197202
transferInstruction = createTransferCheckedInstruction(
198203
new PublicKey(sourceAddress),
199204
new PublicKey(tokenAddress),
@@ -204,6 +209,12 @@ function tokenTransferInstruction(data: TokenTransfer): TransactionInstruction[]
204209
[],
205210
TOKEN_2022_PROGRAM_ID
206211
);
212+
// Check if this token has a transfer hook configuration
213+
const tokenConfig = getToken2022Config(tokenAddress);
214+
if (tokenConfig?.transferHook) {
215+
addTransferHookAccounts(transferInstruction, tokenConfig.transferHook);
216+
}
217+
instructions.push(transferInstruction);
207218
} else {
208219
transferInstruction = createTransferCheckedInstruction(
209220
new PublicKey(sourceAddress),
@@ -213,8 +224,9 @@ function tokenTransferInstruction(data: TokenTransfer): TransactionInstruction[]
213224
BigInt(amount),
214225
decimalPlaces
215226
);
227+
instructions.push(transferInstruction);
216228
}
217-
return [transferInstruction];
229+
return instructions;
218230
}
219231

220232
/**
@@ -686,3 +698,39 @@ function customInstruction(data: InstructionParams): TransactionInstruction[] {
686698

687699
return [convertedInstruction];
688700
}
701+
function upsertAccountMeta(keys: AccountMeta[], meta: AccountMeta): void {
702+
const existing = keys.find((account) => account.pubkey.equals(meta.pubkey));
703+
if (existing) {
704+
existing.isWritable = existing.isWritable || meta.isWritable;
705+
existing.isSigner = existing.isSigner || meta.isSigner;
706+
} else {
707+
keys.push(meta);
708+
}
709+
}
710+
711+
function buildStaticTransferHookAccounts(transferHook: TransferHookConfig): AccountMeta[] {
712+
const metas: AccountMeta[] = [];
713+
if (transferHook.extraAccountMetas?.length) {
714+
for (const meta of transferHook.extraAccountMetas) {
715+
metas.push({
716+
pubkey: new PublicKey(meta.pubkey),
717+
isSigner: meta.isSigner,
718+
isWritable: meta.isWritable,
719+
});
720+
}
721+
}
722+
metas.push({ pubkey: new PublicKey(transferHook.programId), isSigner: false, isWritable: false });
723+
724+
if (transferHook.extraAccountMetasPDA) {
725+
metas.push({ pubkey: new PublicKey(transferHook.extraAccountMetasPDA), isSigner: false, isWritable: false });
726+
}
727+
728+
return metas;
729+
}
730+
731+
function addTransferHookAccounts(instruction: TransactionInstruction, transferHook: TransferHookConfig): void {
732+
const extraMetas = buildStaticTransferHookAccounts(transferHook);
733+
for (const meta of extraMetas) {
734+
upsertAccountMeta(instruction.keys, meta);
735+
}
736+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* Token-2022 Configuration for Solana tokens with transfer hooks
3+
* This file contains static configurations for Token-2022 tokens to avoid RPC calls
4+
* when building transfer transactions with transfer hooks.
5+
*/
6+
7+
import { PublicKey } from '@solana/web3.js';
8+
9+
/**
10+
* Interface for extra account metadata needed by transfer hooks
11+
*/
12+
export interface ExtraAccountMeta {
13+
/** The public key of the account */
14+
pubkey: string;
15+
/** Whether the account is a signer */
16+
isSigner: boolean;
17+
/** Whether the account is writable */
18+
isWritable: boolean;
19+
/** Optional seed for PDA derivation */
20+
seeds?: Array<{
21+
/** Literal seed value or instruction account index reference */
22+
value: string | number;
23+
/** Type of seed: 'literal' for string/buffer, 'accountKey' for instruction account index */
24+
type: 'literal' | 'accountKey';
25+
}>;
26+
}
27+
28+
/**
29+
* Interface for transfer hook configuration
30+
*/
31+
export interface TransferHookConfig {
32+
/** The transfer hook program ID */
33+
programId: string;
34+
/** The transfer hook authority */
35+
authority: string;
36+
/** Extra account metas required by the transfer hook */
37+
extraAccountMetas: ExtraAccountMeta[];
38+
/** The PDA address for extra account metas (cached) */
39+
extraAccountMetasPDA?: string;
40+
}
41+
42+
/**
43+
* Interface for Token-2022 configuration
44+
*/
45+
export interface Token2022Config {
46+
/** The mint address of the token */
47+
mintAddress: string;
48+
/** Token symbol */
49+
symbol: string;
50+
/** Token name */
51+
name: string;
52+
/** Number of decimal places */
53+
decimals: number;
54+
/** Program ID (TOKEN_2022_PROGRAM_ID) */
55+
programId: string;
56+
/** Transfer hook configuration if applicable */
57+
transferHook?: TransferHookConfig;
58+
/** Whether the token has transfer fees */
59+
hasTransferFees?: boolean;
60+
}
61+
62+
/**
63+
* Token configurations map
64+
* Key: mintAddress or symbol
65+
*/
66+
export const TOKEN_2022_CONFIGS: Record<string, Token2022Config> = {
67+
CXk2AMBfi3TwaEL2468s6zP8xq9NxTXjp9gjMgzeUynM: {
68+
mintAddress: 'CXk2AMBfi3TwaEL2468s6zP8xq9NxTXjp9gjMgzeUynM',
69+
symbol: 'PYUSD',
70+
name: 'PayPal USD (Devnet)',
71+
decimals: 6,
72+
programId: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb', // TOKEN_2022_PROGRAM_ID
73+
transferHook: {
74+
programId: 'BLZvvaQgPUvL2RWoJeovudbHMhqH4S3kdenN5eg1juDr',
75+
authority: 'BLZvvaQgPUvL2RWoJeovudbHMhqH4S3kdenN5eg1juDr',
76+
extraAccountMetas: [
77+
{
78+
pubkey: '6WV5e6vxBcerPxQtCGXHrVTB1xxqtrnL5hdBVfaRQJqh',
79+
isSigner: false,
80+
isWritable: false,
81+
},
82+
],
83+
extraAccountMetasPDA: '6WV5e6vxBcerPxQtCGXHrVTB1xxqtrnL5hdBVfaRQJqh',
84+
},
85+
},
86+
87+
'2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo': {
88+
mintAddress: '2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo',
89+
symbol: 'PYUSD',
90+
name: 'PayPal USD',
91+
decimals: 6,
92+
programId: 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb', // TOKEN_2022_PROGRAM_ID
93+
transferHook: {
94+
programId: 'pciTpX5NsLBSMmueV1rnekMBq6wJaJt55fMDPPj2vCG',
95+
authority: 'pciTpX5NsLBSMmueV1rnekMBq6wJaJt55fMDPPj2vCG',
96+
extraAccountMetas: [],
97+
},
98+
},
99+
100+
// Add more configurations as needed
101+
};
102+
103+
// Create symbol mappings for convenience
104+
Object.values(TOKEN_2022_CONFIGS).forEach((config) => {
105+
TOKEN_2022_CONFIGS[config.symbol] = config;
106+
});
107+
108+
/**
109+
* Get token configuration by mint address
110+
* @param mintAddress - The mint address of the token
111+
* @returns Token configuration or undefined if not found
112+
*/
113+
export function getToken2022Config(mintAddress: string): Token2022Config | undefined {
114+
return TOKEN_2022_CONFIGS[mintAddress];
115+
}
116+
117+
/**
118+
* Get token configuration by symbol
119+
* @param symbol - The token symbol
120+
* @returns Token configuration or undefined if not found
121+
*/
122+
export function getToken2022ConfigBySymbol(symbol: string): Token2022Config | undefined {
123+
return TOKEN_2022_CONFIGS[symbol];
124+
}
125+
126+
/**
127+
* Check if a token has a transfer hook
128+
* @param mintAddress - The mint address of the token
129+
* @returns true if the token has a transfer hook
130+
*/
131+
export function hasTransferHook(mintAddress: string): boolean {
132+
const config = getToken2022Config(mintAddress);
133+
return config?.transferHook !== undefined;
134+
}
135+
136+
/**
137+
* Get the extra account metas for a token's transfer hook
138+
* @param mintAddress - The mint address of the token
139+
* @returns Array of extra account metas or empty array if no transfer hook
140+
*/
141+
export function getTransferHookExtraAccounts(mintAddress: string): ExtraAccountMeta[] {
142+
const config = getToken2022Config(mintAddress);
143+
return config?.transferHook?.extraAccountMetas || [];
144+
}
145+
146+
/**
147+
* Calculate the PDA for extra account metas
148+
* This is typically [Buffer.from('extra-account-metas'), mintPubkey.toBuffer()]
149+
* @param mintAddress - The mint address of the token
150+
* @param programId - The transfer hook program ID
151+
* @returns The PDA public key as a string
152+
*/
153+
export function calculateExtraAccountMetasPDA(mintAddress: string, programId: string): string {
154+
const mintPubkey = new PublicKey(mintAddress);
155+
const programPubkey = new PublicKey(programId);
156+
157+
const [pda] = PublicKey.findProgramAddressSync(
158+
[Buffer.from('extra-account-metas'), mintPubkey.toBuffer()],
159+
programPubkey
160+
);
161+
162+
return pda.toBase58();
163+
}
164+
165+
/**
166+
* Add or update a token configuration at runtime
167+
* Useful for testing or adding new tokens dynamically
168+
* @param config - The token configuration to add
169+
*/
170+
export function addToken2022Config(config: Token2022Config): void {
171+
// Add both mint address and symbol keys for easy lookup
172+
TOKEN_2022_CONFIGS[config.mintAddress] = config;
173+
TOKEN_2022_CONFIGS[config.symbol] = config;
174+
}

0 commit comments

Comments
 (0)