Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0e48c45

Browse files
committedApr 17, 2024
solana/sdk: support transfer hook
1 parent 00834d1 commit 0e48c45

File tree

2 files changed

+253
-33
lines changed

2 files changed

+253
-33
lines changed
 

‎solana/tests/example-native-token-transfer.ts

+89-19
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@ import {
1616
serializePayload,
1717
deserializePayload,
1818
} from "@wormhole-foundation/sdk-definitions";
19-
import { NttMessage, postVaa, NTT, nttMessageLayout } from "../ts/sdk";
19+
import { postVaa, NTT, nttMessageLayout } from "../ts/sdk";
20+
import { WormholeTransceiverMessage } from "../ts/sdk/nttLayout";
21+
2022
import {
21-
NativeTokenTransfer,
22-
TransceiverMessage,
23-
WormholeTransceiverMessage,
24-
nativeTokenTransferLayout,
25-
nttManagerMessageLayout,
26-
} from "../ts/sdk/nttLayout";
23+
PublicKey,
24+
SystemProgram,
25+
Transaction,
26+
sendAndConfirmTransaction,
27+
} from "@solana/web3.js";
28+
29+
import { DummyTransferHook } from "../target/types/dummy_transfer_hook";
2730

2831
export const GUARDIAN_KEY =
2932
"cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0";
@@ -48,25 +51,68 @@ describe("example-native-token-transfers", () => {
4851
const user = anchor.web3.Keypair.generate();
4952
let tokenAccount: anchor.web3.PublicKey;
5053

51-
let mint: anchor.web3.PublicKey;
54+
const mint = anchor.web3.Keypair.generate();
55+
56+
const dummyTransferHook = anchor.workspace
57+
.DummyTransferHook as anchor.Program<DummyTransferHook>;
58+
59+
const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync(
60+
[Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()],
61+
dummyTransferHook.programId
62+
);
63+
64+
it("Initialize mint", async () => {
65+
const extensions = [spl.ExtensionType.TransferHook];
66+
const mintLen = spl.getMintLen(extensions);
67+
const lamports = await connection.getMinimumBalanceForRentExemption(
68+
mintLen
69+
);
70+
71+
const transaction = new Transaction().add(
72+
SystemProgram.createAccount({
73+
fromPubkey: payer.publicKey,
74+
newAccountPubkey: mint.publicKey,
75+
space: mintLen,
76+
lamports,
77+
programId: spl.TOKEN_2022_PROGRAM_ID,
78+
}),
79+
spl.createInitializeTransferHookInstruction(
80+
mint.publicKey,
81+
owner.publicKey,
82+
dummyTransferHook.programId,
83+
spl.TOKEN_2022_PROGRAM_ID
84+
),
85+
spl.createInitializeMintInstruction(
86+
mint.publicKey,
87+
9,
88+
owner.publicKey,
89+
null,
90+
spl.TOKEN_2022_PROGRAM_ID
91+
)
92+
);
5293

53-
before(async () => {
54-
// airdrop some tokens to payer
55-
mint = await spl.createMint(connection, payer, owner.publicKey, null, 9);
94+
await sendAndConfirmTransaction(connection, transaction, [payer, mint]);
5695

5796
tokenAccount = await spl.createAssociatedTokenAccount(
5897
connection,
5998
payer,
60-
mint,
61-
user.publicKey
99+
mint.publicKey,
100+
user.publicKey,
101+
undefined,
102+
spl.TOKEN_2022_PROGRAM_ID,
103+
spl.ASSOCIATED_TOKEN_PROGRAM_ID
62104
);
105+
63106
await spl.mintTo(
64107
connection,
65108
payer,
66-
mint,
109+
mint.publicKey,
67110
tokenAccount,
68111
owner,
69-
BigInt(10000000)
112+
BigInt(10000000),
113+
undefined,
114+
undefined,
115+
spl.TOKEN_2022_PROGRAM_ID
70116
);
71117
});
72118

@@ -75,22 +121,46 @@ describe("example-native-token-transfers", () => {
75121
expect(version).to.equal("1.0.0");
76122
});
77123

124+
it("Create ExtraAccountMetaList Account", async () => {
125+
const initializeExtraAccountMetaListInstruction =
126+
await dummyTransferHook.methods
127+
.initializeExtraAccountMetaList()
128+
.accountsStrict({
129+
payer: payer.publicKey,
130+
mint: mint.publicKey,
131+
extraAccountMetaList: extraAccountMetaListPDA,
132+
tokenProgram: spl.TOKEN_2022_PROGRAM_ID,
133+
associatedTokenProgram: spl.ASSOCIATED_TOKEN_PROGRAM_ID,
134+
systemProgram: SystemProgram.programId,
135+
})
136+
.instruction();
137+
138+
const transaction = new Transaction().add(
139+
initializeExtraAccountMetaListInstruction
140+
);
141+
142+
await sendAndConfirmTransaction(connection, transaction, [payer]);
143+
});
144+
78145
describe("Locking", () => {
79146
before(async () => {
80147
await spl.setAuthority(
81148
connection,
82149
payer,
83-
mint,
150+
mint.publicKey,
84151
owner,
85-
0, // mint
86-
ntt.tokenAuthorityAddress()
152+
spl.AuthorityType.MintTokens,
153+
ntt.tokenAuthorityAddress(),
154+
[],
155+
undefined,
156+
spl.TOKEN_2022_PROGRAM_ID
87157
);
88158

89159
await ntt.initialize({
90160
payer,
91161
owner: payer,
92162
chain: "solana",
93-
mint,
163+
mint: mint.publicKey,
94164
outboundLimit: new BN(1000000),
95165
mode: "locking",
96166
});

‎solana/ts/sdk/ntt.ts

+164-14
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
sendAndConfirmTransaction,
2121
type TransactionSignature,
2222
type Connection,
23+
SystemProgram,
2324
TransactionMessage,
2425
VersionedTransaction
2526
} from '@solana/web3.js'
@@ -232,7 +233,7 @@ export class NTT {
232233
const tokenProgram = mintInfo.owner
233234
const ix = await this.program.methods
234235
.initialize({ chainId, limit: args.outboundLimit, mode })
235-
.accounts({
236+
.accountsStrict({
236237
payer: args.payer.publicKey,
237238
deployer: args.owner.publicKey,
238239
programData: programDataAddress(this.program.programId),
@@ -241,8 +242,10 @@ export class NTT {
241242
rateLimit: this.outboxRateLimitAccountAddress(),
242243
tokenProgram,
243244
tokenAuthority: this.tokenAuthorityAddress(),
244-
custody: await this.custodyAccountAddress(args.mint),
245+
custody: await this.custodyAccountAddress(args.mint, tokenProgram),
245246
bpfLoaderUpgradeableProgram: BPF_LOADER_UPGRADEABLE_PROGRAM_ID,
247+
associatedTokenProgram: splToken.ASSOCIATED_TOKEN_PROGRAM_ID,
248+
systemProgram: SystemProgram.programId,
246249
}).instruction();
247250
return sendAndConfirmTransaction(this.program.provider.connection, new Transaction().add(ix), [args.payer, args.owner]);
248251
}
@@ -298,7 +301,9 @@ export class NTT {
298301
args.from,
299302
this.sessionAuthorityAddress(args.fromAuthority.publicKey, transferArgs),
300303
args.fromAuthority.publicKey,
301-
BigInt(args.amount.toString())
304+
BigInt(args.amount.toString()),
305+
[],
306+
config.tokenProgram
302307
);
303308
const tx = new Transaction()
304309
tx.add(approveIx, transferIx, releaseIx)
@@ -398,7 +403,7 @@ export class NTT {
398403
shouldQueue: args.shouldQueue
399404
}
400405

401-
return await this.program.methods
406+
const transferIx = await this.program.methods
402407
.transferLock(transferArgs)
403408
.accounts({
404409
common: {
@@ -416,6 +421,39 @@ export class NTT {
416421
sessionAuthority: this.sessionAuthorityAddress(args.fromAuthority, transferArgs)
417422
})
418423
.instruction()
424+
425+
const mintInfo = await splToken.getMint(
426+
this.program.provider.connection,
427+
config.mint,
428+
undefined,
429+
config.tokenProgram
430+
)
431+
const transferHook = splToken.getTransferHook(mintInfo)
432+
433+
if (transferHook) {
434+
const source = args.from
435+
const mint = config.mint
436+
const destination = await this.custodyAccountAddress(config)
437+
const owner = this.sessionAuthorityAddress(args.fromAuthority, transferArgs)
438+
await addExtraAccountMetasForExecute(
439+
this.program.provider.connection,
440+
transferIx,
441+
transferHook.programId,
442+
source,
443+
mint,
444+
destination,
445+
owner,
446+
// TODO(csongor): compute the amount that's passed into transfer.
447+
// Leaving this 0 is fine unless the transfer hook accounts addresses
448+
// depend on the amount (which is unlikely).
449+
// If this turns out to be the case, the amount to put here is the
450+
// untrimmed amount after removing dust.
451+
0,
452+
);
453+
}
454+
455+
return transferIx
456+
419457
}
420458

421459
/**
@@ -496,14 +534,15 @@ export class NTT {
496534
.releaseInboundMint({
497535
revertOnDelay: args.revertOnDelay
498536
})
499-
.accounts({
537+
.accountsStrict({
500538
common: {
501539
payer: args.payer,
502540
config: { config: this.configAccountAddress() },
503541
inboxItem: this.inboxItemAccountAddress(args.chain, args.nttMessage),
504-
recipient: getAssociatedTokenAddressSync(mint, recipientAddress),
542+
recipient: getAssociatedTokenAddressSync(mint, recipientAddress, true, config.tokenProgram),
505543
mint,
506-
tokenAuthority: this.tokenAuthorityAddress()
544+
tokenAuthority: this.tokenAuthorityAddress(),
545+
tokenProgram: config.tokenProgram
507546
}
508547
})
509548
.instruction()
@@ -551,22 +590,50 @@ export class NTT {
551590

552591
const mint = await this.mintAccountAddress(config)
553592

554-
return await this.program.methods
593+
const transferIx = await this.program.methods
555594
.releaseInboundUnlock({
556595
revertOnDelay: args.revertOnDelay
557596
})
558-
.accounts({
597+
.accountsStrict({
559598
common: {
560599
payer: args.payer,
561600
config: { config: this.configAccountAddress() },
562601
inboxItem: this.inboxItemAccountAddress(args.chain, args.nttMessage),
563-
recipient: getAssociatedTokenAddressSync(mint, recipientAddress),
602+
recipient: getAssociatedTokenAddressSync(mint, recipientAddress, true, config.tokenProgram),
564603
mint,
565-
tokenAuthority: this.tokenAuthorityAddress()
604+
tokenAuthority: this.tokenAuthorityAddress(),
605+
tokenProgram: config.tokenProgram
566606
},
567607
custody: await this.custodyAccountAddress(config)
568608
})
569609
.instruction()
610+
611+
const mintInfo = await splToken.getMint(this.program.provider.connection, config.mint, undefined, config.tokenProgram)
612+
const transferHook = splToken.getTransferHook(mintInfo)
613+
614+
if (transferHook) {
615+
const source = await this.custodyAccountAddress(config)
616+
const mint = config.mint
617+
const destination = getAssociatedTokenAddressSync(mint, recipientAddress, true, config.tokenProgram)
618+
const owner = this.tokenAuthorityAddress()
619+
await addExtraAccountMetasForExecute(
620+
this.program.provider.connection,
621+
transferIx,
622+
transferHook.programId,
623+
source,
624+
mint,
625+
destination,
626+
owner,
627+
// TODO(csongor): compute the amount that's passed into transfer.
628+
// Leaving this 0 is fine unless the transfer hook accounts addresses
629+
// depend on the amount (which is unlikely).
630+
// If this turns out to be the case, the amount to put here is the
631+
// untrimmed amount after removing dust.
632+
0,
633+
);
634+
}
635+
636+
return transferIx
570637
}
571638

572639
async releaseInboundUnlock(args: {
@@ -891,15 +958,98 @@ export class NTT {
891958
* (i.e. the program is initialised), the mint is derived from the config.
892959
* Otherwise, the mint must be provided.
893960
*/
894-
async custodyAccountAddress(configOrMint: Config | PublicKey): Promise<PublicKey> {
961+
async custodyAccountAddress(configOrMint: Config | PublicKey, tokenProgram = splToken.TOKEN_PROGRAM_ID): Promise<PublicKey> {
895962
if (configOrMint instanceof PublicKey) {
896-
return associatedAddress({ mint: configOrMint, owner: this.tokenAuthorityAddress() })
963+
return splToken.getAssociatedTokenAddress(configOrMint, this.tokenAuthorityAddress(), true, tokenProgram)
897964
} else {
898-
return associatedAddress({ mint: await this.mintAccountAddress(configOrMint), owner: this.tokenAuthorityAddress() })
965+
return splToken.getAssociatedTokenAddress(configOrMint.mint, this.tokenAuthorityAddress(), true, configOrMint.tokenProgram)
899966
}
900967
}
901968
}
902969

903970
function exhaustive<A>(_: never): A {
904971
throw new Error('Impossible')
905972
}
973+
974+
/**
975+
* TODO: this is copied from @solana/spl-token, because the most recent released
976+
* version (0.4.3) is broken (does object equality instead of structural on the pubkey)
977+
*
978+
* this version fixes that error, looks like it's also fixed on main:
979+
* https://github.com/solana-labs/solana-program-library/blob/ad4eb6914c5e4288ad845f29f0003cd3b16243e7/token/js/src/extensions/transferHook/instructions.ts#L208
980+
*/
981+
async function addExtraAccountMetasForExecute(
982+
connection: Connection,
983+
instruction: TransactionInstruction,
984+
programId: PublicKey,
985+
source: PublicKey,
986+
mint: PublicKey,
987+
destination: PublicKey,
988+
owner: PublicKey,
989+
amount: number | bigint,
990+
commitment?: Commitment
991+
) {
992+
const validateStatePubkey = splToken.getExtraAccountMetaAddress(mint, programId);
993+
const validateStateAccount = await connection.getAccountInfo(validateStatePubkey, commitment);
994+
if (validateStateAccount == null) {
995+
return instruction;
996+
}
997+
const validateStateData = splToken.getExtraAccountMetas(validateStateAccount);
998+
999+
// Check to make sure the provided keys are in the instruction
1000+
if (![source, mint, destination, owner].every((key) => instruction.keys.some((meta) => meta.pubkey.equals(key)))) {
1001+
throw new Error('Missing required account in instruction');
1002+
}
1003+
1004+
const executeInstruction = splToken.createExecuteInstruction(
1005+
programId,
1006+
source,
1007+
mint,
1008+
destination,
1009+
owner,
1010+
validateStatePubkey,
1011+
BigInt(amount)
1012+
);
1013+
1014+
for (const extraAccountMeta of validateStateData) {
1015+
executeInstruction.keys.push(
1016+
deEscalateAccountMeta(
1017+
await splToken.resolveExtraAccountMeta(
1018+
connection,
1019+
extraAccountMeta,
1020+
executeInstruction.keys,
1021+
executeInstruction.data,
1022+
executeInstruction.programId
1023+
),
1024+
executeInstruction.keys
1025+
)
1026+
);
1027+
}
1028+
1029+
// Add only the extra accounts resolved from the validation state
1030+
instruction.keys.push(...executeInstruction.keys.slice(5));
1031+
1032+
// Add the transfer hook program ID and the validation state account
1033+
instruction.keys.push({ pubkey: programId, isSigner: false, isWritable: false });
1034+
instruction.keys.push({ pubkey: validateStatePubkey, isSigner: false, isWritable: false });
1035+
}
1036+
1037+
// TODO: delete (see above)
1038+
function deEscalateAccountMeta(accountMeta: AccountMeta, accountMetas: AccountMeta[]): AccountMeta {
1039+
const maybeHighestPrivileges = accountMetas
1040+
.filter((x) => x.pubkey.equals(accountMeta.pubkey))
1041+
.reduce<{ isSigner: boolean; isWritable: boolean } | undefined>((acc, x) => {
1042+
if (!acc) return { isSigner: x.isSigner, isWritable: x.isWritable };
1043+
return { isSigner: acc.isSigner || x.isSigner, isWritable: acc.isWritable || x.isWritable };
1044+
}, undefined);
1045+
if (maybeHighestPrivileges) {
1046+
const { isSigner, isWritable } = maybeHighestPrivileges;
1047+
if (!isSigner && isSigner !== accountMeta.isSigner) {
1048+
accountMeta.isSigner = false;
1049+
}
1050+
if (!isWritable && isWritable !== accountMeta.isWritable) {
1051+
accountMeta.isWritable = false;
1052+
}
1053+
}
1054+
return accountMeta;
1055+
}

0 commit comments

Comments
 (0)
Please sign in to comment.