Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 7b3fef1

Browse files
author
Joe C
authored
[token js]: transfer-hook: add support for account data seeds
This PR adds support for the `AccountData` seed from the `spl-tlv-account-resolution` library and Token2022. This addresses point number 2 in #5685.
1 parent 7a4af0a commit 7b3fef1

File tree

6 files changed

+95
-17
lines changed

6 files changed

+95
-17
lines changed

token/js/src/errors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,8 @@ export class TokenTransferHookAccountNotFound extends TokenError {
7979
export class TokenTransferHookInvalidSeed extends TokenError {
8080
name = 'TokenTransferHookInvalidSeed';
8181
}
82+
83+
/** Thrown if account data required by an extra account meta seed config could not be fetched */
84+
export class TokenTransferHookAccountDataNotFound extends TokenError {
85+
name = 'TokenTransferHookAccountDataNotFound';
86+
}

token/js/src/extensions/transferHook/instructions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ export async function addExtraAccountsToInstruction(
156156
accountMetas.push({ pubkey: extraAccountsAccount, isSigner: false, isWritable: false });
157157

158158
for (const extraAccountMeta of extraAccountMetas) {
159-
const accountMeta = resolveExtraAccountMeta(
159+
const accountMeta = await resolveExtraAccountMeta(
160+
connection,
160161
extraAccountMeta,
161162
accountMetas,
162163
instruction.data,

token/js/src/extensions/transferHook/seeds.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { AccountMeta } from '@solana/web3.js';
2-
import { TokenTransferHookInvalidSeed } from '../../errors.js';
1+
import type { AccountMeta, Connection } from '@solana/web3.js';
2+
import { TokenTransferHookAccountDataNotFound, TokenTransferHookInvalidSeed } from '../../errors.js';
33

44
interface Seed {
55
data: Buffer;
@@ -11,6 +11,9 @@ const LITERAL_LENGTH_SPAN = 1;
1111
const INSTRUCTION_ARG_OFFSET_SPAN = 1;
1212
const INSTRUCTION_ARG_LENGTH_SPAN = 1;
1313
const ACCOUNT_KEY_INDEX_SPAN = 1;
14+
const ACCOUNT_DATA_ACCOUNT_INDEX_SPAN = 1;
15+
const ACCOUNT_DATA_OFFSET_SPAN = 1;
16+
const ACCOUNT_DATA_LENGTH_SPAN = 1;
1417

1518
function unpackSeedLiteral(seeds: Uint8Array): Seed {
1619
if (seeds.length < 1) {
@@ -54,7 +57,38 @@ function unpackSeedAccountKey(seeds: Uint8Array, previousMetas: AccountMeta[]):
5457
};
5558
}
5659

57-
function unpackFirstSeed(seeds: Uint8Array, previousMetas: AccountMeta[], instructionData: Buffer): Seed | null {
60+
async function unpackSeedAccountData(
61+
seeds: Uint8Array,
62+
previousMetas: AccountMeta[],
63+
connection: Connection
64+
): Promise<Seed> {
65+
if (seeds.length < 3) {
66+
throw new TokenTransferHookInvalidSeed();
67+
}
68+
const [accountIndex, dataIndex, length] = seeds;
69+
if (previousMetas.length <= accountIndex) {
70+
throw new TokenTransferHookInvalidSeed();
71+
}
72+
const accountInfo = await connection.getAccountInfo(previousMetas[accountIndex].pubkey);
73+
if (accountInfo == null) {
74+
throw new TokenTransferHookAccountDataNotFound();
75+
}
76+
if (accountInfo.data.length < dataIndex + length) {
77+
throw new TokenTransferHookInvalidSeed();
78+
}
79+
return {
80+
data: accountInfo.data.subarray(dataIndex, dataIndex + length),
81+
packedLength:
82+
DISCRIMINATOR_SPAN + ACCOUNT_DATA_ACCOUNT_INDEX_SPAN + ACCOUNT_DATA_OFFSET_SPAN + ACCOUNT_DATA_LENGTH_SPAN,
83+
};
84+
}
85+
86+
async function unpackFirstSeed(
87+
seeds: Uint8Array,
88+
previousMetas: AccountMeta[],
89+
instructionData: Buffer,
90+
connection: Connection
91+
): Promise<Seed | null> {
5892
const [discriminator, ...rest] = seeds;
5993
const remaining = new Uint8Array(rest);
6094
switch (discriminator) {
@@ -66,16 +100,23 @@ function unpackFirstSeed(seeds: Uint8Array, previousMetas: AccountMeta[], instru
66100
return unpackSeedInstructionArg(remaining, instructionData);
67101
case 3:
68102
return unpackSeedAccountKey(remaining, previousMetas);
103+
case 4:
104+
return unpackSeedAccountData(remaining, previousMetas, connection);
69105
default:
70106
throw new TokenTransferHookInvalidSeed();
71107
}
72108
}
73109

74-
export function unpackSeeds(seeds: Uint8Array, previousMetas: AccountMeta[], instructionData: Buffer): Buffer[] {
110+
export async function unpackSeeds(
111+
seeds: Uint8Array,
112+
previousMetas: AccountMeta[],
113+
instructionData: Buffer,
114+
connection: Connection
115+
): Promise<Buffer[]> {
75116
const unpackedSeeds: Buffer[] = [];
76117
let i = 0;
77118
while (i < 32) {
78-
const seed = unpackFirstSeed(seeds.slice(i), previousMetas, instructionData);
119+
const seed = await unpackFirstSeed(seeds.slice(i), previousMetas, instructionData, connection);
79120
if (seed == null) {
80121
break;
81122
}

token/js/src/extensions/transferHook/state.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { blob, greedy, seq, struct, u32, u8 } from '@solana/buffer-layout';
22
import type { Mint } from '../../state/mint.js';
33
import { ExtensionType, getExtensionData } from '../extensionType.js';
4-
import type { AccountInfo, AccountMeta } from '@solana/web3.js';
4+
import type { AccountInfo, AccountMeta, Connection } from '@solana/web3.js';
55
import { PublicKey } from '@solana/web3.js';
66
import { bool, publicKey, u64 } from '@solana/buffer-layout-utils';
77
import type { Account } from '../../state/account.js';
@@ -106,12 +106,13 @@ export function getExtraAccountMetas(account: AccountInfo<Buffer>): ExtraAccount
106106
}
107107

108108
/** Take an ExtraAccountMeta and construct that into an acutal AccountMeta */
109-
export function resolveExtraAccountMeta(
109+
export async function resolveExtraAccountMeta(
110+
connection: Connection,
110111
extraMeta: ExtraAccountMeta,
111112
previousMetas: AccountMeta[],
112113
instructionData: Buffer,
113114
transferHookProgramId: PublicKey
114-
): AccountMeta {
115+
): Promise<AccountMeta> {
115116
if (extraMeta.discriminator === 0) {
116117
return {
117118
pubkey: new PublicKey(extraMeta.addressConfig),
@@ -132,7 +133,7 @@ export function resolveExtraAccountMeta(
132133
programId = previousMetas[accountIndex].pubkey;
133134
}
134135

135-
const seeds = unpackSeeds(extraMeta.addressConfig, previousMetas, instructionData);
136+
const seeds = await unpackSeeds(extraMeta.addressConfig, previousMetas, instructionData, connection);
136137
const pubkey = PublicKey.findProgramAddressSync(seeds, programId)[0];
137138

138139
return { pubkey, isSigner: extraMeta.isSigner, isWritable: extraMeta.isWritable };

token/js/test/common.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export async function newAccountWithLamports(connection: Connection, lamports =
1212
export async function getConnection(): Promise<Connection> {
1313
const url = 'http://127.0.0.1:8899';
1414
const connection = new Connection(url, 'confirmed');
15-
await connection.getVersion();
1615
return connection;
1716
}
1817

token/js/test/unit/transferHook.test.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { getExtraAccountMetas, resolveExtraAccountMeta } from '../../src';
22
import { expect } from 'chai';
3+
import type { Connection } from '@solana/web3.js';
34
import { PublicKey } from '@solana/web3.js';
5+
import { getConnection } from '../common';
46

57
describe('transferHookExtraAccounts', () => {
8+
let connection: Connection;
69
const testProgramId = new PublicKey('7N4HggYEJAtCLJdnHGCtFqfxcB5rhQCsQTze3ftYstVj');
710
const instructionData = Buffer.from(Array.from(Array(32).keys()));
811
const plainAccount = new PublicKey('6c5q79ccBTWvZTEx3JkdHThtMa2eALba5bfvHGf8kA2c');
9-
const seeds = [Buffer.from('seed'), Buffer.from([4, 5, 6, 7]), plainAccount.toBuffer()];
12+
const seeds = [Buffer.from('seed'), Buffer.from([4, 5, 6, 7]), plainAccount.toBuffer(), Buffer.from([2, 2, 2, 2])];
1013
const pdaPublicKey = PublicKey.findProgramAddressSync(seeds, testProgramId)[0];
1114
const pdaPublicKeyWithProgramId = PublicKey.findProgramAddressSync(seeds, plainAccount)[0];
1215

@@ -27,7 +30,14 @@ describe('transferHookExtraAccounts', () => {
2730
Buffer.from([0]), // u8 index
2831
]);
2932

30-
const addressConfig = Buffer.concat([plainSeed, instructionDataSeed, accountKeySeed], 32);
33+
const accountDataSeed = Buffer.concat([
34+
Buffer.from([4]), // u8 discriminator
35+
Buffer.from([0]), // u8 account index
36+
Buffer.from([2]), // u8 account data offset
37+
Buffer.from([4]), // u8 account data length
38+
]);
39+
40+
const addressConfig = Buffer.concat([plainSeed, instructionDataSeed, accountKeySeed, accountDataSeed], 32);
3141

3242
const plainExtraAccountMeta = {
3343
discriminator: 0,
@@ -77,6 +87,19 @@ describe('transferHookExtraAccounts', () => {
7787
pdaExtraAccountWithProgramId,
7888
]);
7989

90+
before(async () => {
91+
connection = await getConnection();
92+
connection.getAccountInfo = async (
93+
_publicKey: PublicKey,
94+
_commitmentOrConfig?: Parameters<(typeof connection)['getAccountInfo']>[1]
95+
): ReturnType<(typeof connection)['getAccountInfo']> => ({
96+
data: Buffer.from([0, 0, 2, 2, 2, 2]),
97+
owner: PublicKey.default,
98+
executable: false,
99+
lamports: 0,
100+
});
101+
});
102+
80103
it('getExtraAccountMetas', () => {
81104
const accountInfo = {
82105
data: extraAccountList,
@@ -110,14 +133,21 @@ describe('transferHookExtraAccounts', () => {
110133
expect(parsedExtraAccounts[2].isSigner).to.be.false;
111134
expect(parsedExtraAccounts[2].isWritable).to.be.true;
112135
});
113-
it('resolveExtraAccountMeta', () => {
114-
const resolvedPlainAccount = resolveExtraAccountMeta(plainExtraAccountMeta, [], instructionData, testProgramId);
136+
it('resolveExtraAccountMeta', async () => {
137+
const resolvedPlainAccount = await resolveExtraAccountMeta(
138+
connection,
139+
plainExtraAccountMeta,
140+
[],
141+
instructionData,
142+
testProgramId
143+
);
115144

116145
expect(resolvedPlainAccount.pubkey).to.eql(plainAccount);
117146
expect(resolvedPlainAccount.isSigner).to.be.false;
118147
expect(resolvedPlainAccount.isWritable).to.be.false;
119148

120-
const resolvedPdaAccount = resolveExtraAccountMeta(
149+
const resolvedPdaAccount = await resolveExtraAccountMeta(
150+
connection,
121151
pdaExtraAccountMeta,
122152
[resolvedPlainAccount],
123153
instructionData,
@@ -128,7 +158,8 @@ describe('transferHookExtraAccounts', () => {
128158
expect(resolvedPdaAccount.isSigner).to.be.true;
129159
expect(resolvedPdaAccount.isWritable).to.be.false;
130160

131-
const resolvedPdaAccountWithProgramId = resolveExtraAccountMeta(
161+
const resolvedPdaAccountWithProgramId = await resolveExtraAccountMeta(
162+
connection,
132163
pdaExtraAccountMetaWithProgramId,
133164
[resolvedPlainAccount],
134165
instructionData,

0 commit comments

Comments
 (0)