Skip to content

Commit

Permalink
[token js]: transfer-hook: add support for account data seeds
Browse files Browse the repository at this point in the history
This PR adds support for the `AccountData` seed from the
`spl-tlv-account-resolution` library and Token2022.

This addresses point number 2 in #5685.
  • Loading branch information
buffalojoec committed Nov 14, 2023
1 parent 7a4af0a commit 7b3fef1
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 17 deletions.
5 changes: 5 additions & 0 deletions token/js/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,8 @@ export class TokenTransferHookAccountNotFound extends TokenError {
export class TokenTransferHookInvalidSeed extends TokenError {
name = 'TokenTransferHookInvalidSeed';
}

/** Thrown if account data required by an extra account meta seed config could not be fetched */
export class TokenTransferHookAccountDataNotFound extends TokenError {
name = 'TokenTransferHookAccountDataNotFound';
}
3 changes: 2 additions & 1 deletion token/js/src/extensions/transferHook/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ export async function addExtraAccountsToInstruction(
accountMetas.push({ pubkey: extraAccountsAccount, isSigner: false, isWritable: false });

for (const extraAccountMeta of extraAccountMetas) {
const accountMeta = resolveExtraAccountMeta(
const accountMeta = await resolveExtraAccountMeta(
connection,
extraAccountMeta,
accountMetas,
instruction.data,
Expand Down
51 changes: 46 additions & 5 deletions token/js/src/extensions/transferHook/seeds.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { AccountMeta } from '@solana/web3.js';
import { TokenTransferHookInvalidSeed } from '../../errors.js';
import type { AccountMeta, Connection } from '@solana/web3.js';
import { TokenTransferHookAccountDataNotFound, TokenTransferHookInvalidSeed } from '../../errors.js';

interface Seed {
data: Buffer;
Expand All @@ -11,6 +11,9 @@ const LITERAL_LENGTH_SPAN = 1;
const INSTRUCTION_ARG_OFFSET_SPAN = 1;
const INSTRUCTION_ARG_LENGTH_SPAN = 1;
const ACCOUNT_KEY_INDEX_SPAN = 1;
const ACCOUNT_DATA_ACCOUNT_INDEX_SPAN = 1;
const ACCOUNT_DATA_OFFSET_SPAN = 1;
const ACCOUNT_DATA_LENGTH_SPAN = 1;

function unpackSeedLiteral(seeds: Uint8Array): Seed {
if (seeds.length < 1) {
Expand Down Expand Up @@ -54,7 +57,38 @@ function unpackSeedAccountKey(seeds: Uint8Array, previousMetas: AccountMeta[]):
};
}

function unpackFirstSeed(seeds: Uint8Array, previousMetas: AccountMeta[], instructionData: Buffer): Seed | null {
async function unpackSeedAccountData(
seeds: Uint8Array,
previousMetas: AccountMeta[],
connection: Connection
): Promise<Seed> {
if (seeds.length < 3) {
throw new TokenTransferHookInvalidSeed();
}
const [accountIndex, dataIndex, length] = seeds;
if (previousMetas.length <= accountIndex) {
throw new TokenTransferHookInvalidSeed();
}
const accountInfo = await connection.getAccountInfo(previousMetas[accountIndex].pubkey);
if (accountInfo == null) {
throw new TokenTransferHookAccountDataNotFound();
}
if (accountInfo.data.length < dataIndex + length) {
throw new TokenTransferHookInvalidSeed();
}
return {
data: accountInfo.data.subarray(dataIndex, dataIndex + length),
packedLength:
DISCRIMINATOR_SPAN + ACCOUNT_DATA_ACCOUNT_INDEX_SPAN + ACCOUNT_DATA_OFFSET_SPAN + ACCOUNT_DATA_LENGTH_SPAN,
};
}

async function unpackFirstSeed(
seeds: Uint8Array,
previousMetas: AccountMeta[],
instructionData: Buffer,
connection: Connection
): Promise<Seed | null> {
const [discriminator, ...rest] = seeds;
const remaining = new Uint8Array(rest);
switch (discriminator) {
Expand All @@ -66,16 +100,23 @@ function unpackFirstSeed(seeds: Uint8Array, previousMetas: AccountMeta[], instru
return unpackSeedInstructionArg(remaining, instructionData);
case 3:
return unpackSeedAccountKey(remaining, previousMetas);
case 4:
return unpackSeedAccountData(remaining, previousMetas, connection);
default:
throw new TokenTransferHookInvalidSeed();
}
}

export function unpackSeeds(seeds: Uint8Array, previousMetas: AccountMeta[], instructionData: Buffer): Buffer[] {
export async function unpackSeeds(
seeds: Uint8Array,
previousMetas: AccountMeta[],
instructionData: Buffer,
connection: Connection
): Promise<Buffer[]> {
const unpackedSeeds: Buffer[] = [];
let i = 0;
while (i < 32) {
const seed = unpackFirstSeed(seeds.slice(i), previousMetas, instructionData);
const seed = await unpackFirstSeed(seeds.slice(i), previousMetas, instructionData, connection);
if (seed == null) {
break;
}
Expand Down
9 changes: 5 additions & 4 deletions token/js/src/extensions/transferHook/state.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { blob, greedy, seq, struct, u32, u8 } from '@solana/buffer-layout';
import type { Mint } from '../../state/mint.js';
import { ExtensionType, getExtensionData } from '../extensionType.js';
import type { AccountInfo, AccountMeta } from '@solana/web3.js';
import type { AccountInfo, AccountMeta, Connection } from '@solana/web3.js';
import { PublicKey } from '@solana/web3.js';
import { bool, publicKey, u64 } from '@solana/buffer-layout-utils';
import type { Account } from '../../state/account.js';
Expand Down Expand Up @@ -106,12 +106,13 @@ export function getExtraAccountMetas(account: AccountInfo<Buffer>): ExtraAccount
}

/** Take an ExtraAccountMeta and construct that into an acutal AccountMeta */
export function resolveExtraAccountMeta(
export async function resolveExtraAccountMeta(
connection: Connection,
extraMeta: ExtraAccountMeta,
previousMetas: AccountMeta[],
instructionData: Buffer,
transferHookProgramId: PublicKey
): AccountMeta {
): Promise<AccountMeta> {
if (extraMeta.discriminator === 0) {
return {
pubkey: new PublicKey(extraMeta.addressConfig),
Expand All @@ -132,7 +133,7 @@ export function resolveExtraAccountMeta(
programId = previousMetas[accountIndex].pubkey;
}

const seeds = unpackSeeds(extraMeta.addressConfig, previousMetas, instructionData);
const seeds = await unpackSeeds(extraMeta.addressConfig, previousMetas, instructionData, connection);
const pubkey = PublicKey.findProgramAddressSync(seeds, programId)[0];

return { pubkey, isSigner: extraMeta.isSigner, isWritable: extraMeta.isWritable };
Expand Down
1 change: 0 additions & 1 deletion token/js/test/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export async function newAccountWithLamports(connection: Connection, lamports =
export async function getConnection(): Promise<Connection> {
const url = 'http://127.0.0.1:8899';
const connection = new Connection(url, 'confirmed');
await connection.getVersion();
return connection;
}

Expand Down
43 changes: 37 additions & 6 deletions token/js/test/unit/transferHook.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { getExtraAccountMetas, resolveExtraAccountMeta } from '../../src';
import { expect } from 'chai';
import type { Connection } from '@solana/web3.js';
import { PublicKey } from '@solana/web3.js';
import { getConnection } from '../common';

describe('transferHookExtraAccounts', () => {
let connection: Connection;
const testProgramId = new PublicKey('7N4HggYEJAtCLJdnHGCtFqfxcB5rhQCsQTze3ftYstVj');
const instructionData = Buffer.from(Array.from(Array(32).keys()));
const plainAccount = new PublicKey('6c5q79ccBTWvZTEx3JkdHThtMa2eALba5bfvHGf8kA2c');
const seeds = [Buffer.from('seed'), Buffer.from([4, 5, 6, 7]), plainAccount.toBuffer()];
const seeds = [Buffer.from('seed'), Buffer.from([4, 5, 6, 7]), plainAccount.toBuffer(), Buffer.from([2, 2, 2, 2])];
const pdaPublicKey = PublicKey.findProgramAddressSync(seeds, testProgramId)[0];
const pdaPublicKeyWithProgramId = PublicKey.findProgramAddressSync(seeds, plainAccount)[0];

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

const addressConfig = Buffer.concat([plainSeed, instructionDataSeed, accountKeySeed], 32);
const accountDataSeed = Buffer.concat([
Buffer.from([4]), // u8 discriminator
Buffer.from([0]), // u8 account index
Buffer.from([2]), // u8 account data offset
Buffer.from([4]), // u8 account data length
]);

const addressConfig = Buffer.concat([plainSeed, instructionDataSeed, accountKeySeed, accountDataSeed], 32);

const plainExtraAccountMeta = {
discriminator: 0,
Expand Down Expand Up @@ -77,6 +87,19 @@ describe('transferHookExtraAccounts', () => {
pdaExtraAccountWithProgramId,
]);

before(async () => {
connection = await getConnection();
connection.getAccountInfo = async (
_publicKey: PublicKey,
_commitmentOrConfig?: Parameters<(typeof connection)['getAccountInfo']>[1]
): ReturnType<(typeof connection)['getAccountInfo']> => ({
data: Buffer.from([0, 0, 2, 2, 2, 2]),
owner: PublicKey.default,
executable: false,
lamports: 0,
});
});

it('getExtraAccountMetas', () => {
const accountInfo = {
data: extraAccountList,
Expand Down Expand Up @@ -110,14 +133,21 @@ describe('transferHookExtraAccounts', () => {
expect(parsedExtraAccounts[2].isSigner).to.be.false;
expect(parsedExtraAccounts[2].isWritable).to.be.true;
});
it('resolveExtraAccountMeta', () => {
const resolvedPlainAccount = resolveExtraAccountMeta(plainExtraAccountMeta, [], instructionData, testProgramId);
it('resolveExtraAccountMeta', async () => {
const resolvedPlainAccount = await resolveExtraAccountMeta(
connection,
plainExtraAccountMeta,
[],
instructionData,
testProgramId
);

expect(resolvedPlainAccount.pubkey).to.eql(plainAccount);
expect(resolvedPlainAccount.isSigner).to.be.false;
expect(resolvedPlainAccount.isWritable).to.be.false;

const resolvedPdaAccount = resolveExtraAccountMeta(
const resolvedPdaAccount = await resolveExtraAccountMeta(
connection,
pdaExtraAccountMeta,
[resolvedPlainAccount],
instructionData,
Expand All @@ -128,7 +158,8 @@ describe('transferHookExtraAccounts', () => {
expect(resolvedPdaAccount.isSigner).to.be.true;
expect(resolvedPdaAccount.isWritable).to.be.false;

const resolvedPdaAccountWithProgramId = resolveExtraAccountMeta(
const resolvedPdaAccountWithProgramId = await resolveExtraAccountMeta(
connection,
pdaExtraAccountMetaWithProgramId,
[resolvedPlainAccount],
instructionData,
Expand Down

0 comments on commit 7b3fef1

Please sign in to comment.