Skip to content

Commit

Permalink
token-js: added extra account resolution for transfer hook extension (#…
Browse files Browse the repository at this point in the history
…5112)

* Added extra account resolution for transfer hook extension

* A few of tweaks and improvements

* A few minor improvements and tweaks

* Ported over fix for tlv parsing of the ExtraMetaAccount account

* Split transfer fee with hook up into instruction creators and actions

* Minor improvements and tweaks
  • Loading branch information
wjthieme authored Aug 30, 2023
1 parent fd67d99 commit c79c727
Show file tree
Hide file tree
Showing 8 changed files with 643 additions and 8 deletions.
35 changes: 35 additions & 0 deletions token/js/examples/transferHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
getMintLen,
TOKEN_2022_PROGRAM_ID,
updateTransferHook,
transferCheckedWithHook,
getAssociatedTokenAddressSync,
ASSOCIATED_TOKEN_PROGRAM_ID,
} from '../src';

(async () => {
Expand All @@ -25,6 +28,9 @@ import {
const mintKeypair = Keypair.generate();
const mint = mintKeypair.publicKey;

const sender = Keypair.generate();
const recipient = Keypair.generate();

const extensions = [ExtensionType.TransferHook];
const mintLen = getMintLen(extensions);
const decimals = 9;
Expand Down Expand Up @@ -60,4 +66,33 @@ import {
undefined,
TOKEN_2022_PROGRAM_ID
);

const senderAta = getAssociatedTokenAddressSync(
mint,
sender.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
const recipientAta = getAssociatedTokenAddressSync(
mint,
recipient.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);

await transferCheckedWithHook(
connection,
payer,
senderAta,
mint,
recipientAta,
sender,
BigInt(1000000000),
9,
[],
undefined,
TOKEN_2022_PROGRAM_ID
);
})();
15 changes: 15 additions & 0 deletions token/js/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export class TokenInvalidAccountError extends TokenError {
name = 'TokenInvalidAccountError';
}

/** Thrown if a program state account does not contain valid data */
export class TokenInvalidAccountDataError extends TokenError {
name = 'TokenInvalidAccountDataError';
}

/** Thrown if a program state account is not owned by the expected token program */
export class TokenInvalidAccountOwnerError extends TokenError {
name = 'TokenInvalidAccountOwnerError';
Expand Down Expand Up @@ -64,3 +69,13 @@ export class TokenInvalidInstructionTypeError extends TokenError {
export class TokenUnsupportedInstructionError extends TokenError {
name = 'TokenUnsupportedInstructionError';
}

/** Thrown if the transfer hook extra accounts contains an invalid account index */
export class TokenTransferHookAccountNotFound extends TokenError {
name = 'TokenTransferHookAccountNotFound';
}

/** Thrown if the transfer hook extra accounts contains an invalid seed */
export class TokenTransferHookInvalidSeed extends TokenError {
name = 'TokenTransferHookInvalidSeed';
}
115 changes: 112 additions & 3 deletions token/js/src/extensions/transferHook/actions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type { ConfirmOptions, Connection, PublicKey, Signer, TransactionSignature } from '@solana/web3.js';
import type { ConfirmOptions, Connection, Signer, TransactionSignature } from '@solana/web3.js';
import type { PublicKey } from '@solana/web3.js';
import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js';
import { getSigners } from '../../actions/internal.js';
import { TOKEN_2022_PROGRAM_ID } from '../../constants.js';
import { createInitializeTransferHookInstruction, createUpdateTransferHookInstruction } from './instructions.js';
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../../constants.js';
import {
createInitializeTransferHookInstruction,
createTransferCheckedWithFeeAndTransferHookInstruction,
createTransferCheckedWithTransferHookInstruction,
createUpdateTransferHookInstruction,
} from './instructions.js';

/**
* Initialize a transfer hook on a mint
Expand Down Expand Up @@ -65,3 +71,106 @@ export async function updateTransferHook(

return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
}

/**
* Transfer tokens from one account to another, asserting the token mint, and decimals
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param source Source account
* @param mint Mint for the account
* @param destination Destination account
* @param authority Authority of the source account
* @param amount Number of tokens to transfer
* @param decimals Number of decimals in transfer amount
* @param multiSigners Signing accounts if `owner` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
export async function transferCheckedWithTransferHook(
connection: Connection,
payer: Signer,
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
authority: Signer | PublicKey,
amount: bigint,
decimals: number,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_PROGRAM_ID
): Promise<TransactionSignature> {
const [authorityPublicKey, signers] = getSigners(authority, multiSigners);

const transaction = new Transaction().add(
await createTransferCheckedWithTransferHookInstruction(
connection,
source,
mint,
destination,
authorityPublicKey,
amount,
decimals,
signers,
confirmOptions?.commitment,
programId
)
);

return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
}

/**
* Transfer tokens from one account to another, asserting the transfer fee, token mint, and decimals
*
* @param connection Connection to use
* @param payer Payer of the transaction fees
* @param source Source account
* @param mint Mint for the account
* @param destination Destination account
* @param authority Authority of the source account
* @param amount Number of tokens to transfer
* @param decimals Number of decimals in transfer amount
* @param fee The calculated fee for the transfer fee extension
* @param multiSigners Signing accounts if `owner` is a multisig
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
*
* @return Signature of the confirmed transaction
*/
export async function transferCheckedWithFeeAndTransferHook(
connection: Connection,
payer: Signer,
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
authority: Signer | PublicKey,
amount: bigint,
decimals: number,
fee: bigint,
multiSigners: Signer[] = [],
confirmOptions?: ConfirmOptions,
programId = TOKEN_PROGRAM_ID
): Promise<TransactionSignature> {
const [authorityPublicKey, signers] = getSigners(authority, multiSigners);

const transaction = new Transaction().add(
await createTransferCheckedWithFeeAndTransferHookInstruction(
connection,
source,
mint,
destination,
authorityPublicKey,
amount,
decimals,
fee,
signers,
confirmOptions?.commitment,
programId
)
);

return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions);
}
1 change: 1 addition & 0 deletions token/js/src/extensions/transferHook/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './actions.js';
export * from './instructions.js';
export * from './seeds.js';
export * from './state.js';
163 changes: 161 additions & 2 deletions token/js/src/extensions/transferHook/instructions.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { struct, u8 } from '@solana/buffer-layout';
import type { PublicKey, Signer } from '@solana/web3.js';
import type { Commitment, Connection, PublicKey, Signer } from '@solana/web3.js';
import { TransactionInstruction } from '@solana/web3.js';
import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID } from '../../constants.js';
import { programSupportsExtensions, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../../constants.js';
import { TokenUnsupportedInstructionError } from '../../errors.js';
import { addSigners } from '../../instructions/internal.js';
import { TokenInstruction } from '../../instructions/types.js';
import { publicKey } from '@solana/buffer-layout-utils';
import { createTransferCheckedInstruction } from '../../instructions/transferChecked.js';
import { createTransferCheckedWithFeeInstruction } from '../transferFee/instructions.js';
import { getMint } from '../../state/mint.js';
import { getExtraAccountMetaAccount, getExtraAccountMetas, getTransferHook, resolveExtraAccountMeta } from './state.js';

export enum TransferHookInstruction {
Initialize = 0,
Expand Down Expand Up @@ -112,3 +116,158 @@ export function createUpdateTransferHookInstruction(

return new TransactionInstruction({ keys, programId, data });
}

/**
* Add extra accounts needed for transfer hook to an instruction
*
* @param connection Connection to use
* @param instruction The transferChecked instruction to add accounts to
* @param commitment Commitment to use
* @param programId SPL Token program account
*
* @return Instruction to add to a transaction
*/
export async function addExtraAccountsToInstruction(
connection: Connection,
instruction: TransactionInstruction,
mint: PublicKey,
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID
): Promise<TransactionInstruction> {
if (!programSupportsExtensions(programId)) {
throw new TokenUnsupportedInstructionError();
}

const mintInfo = await getMint(connection, mint, commitment, programId);
const transferHook = getTransferHook(mintInfo);
if (transferHook == null) {
return instruction;
}

const extraAccountsAccount = getExtraAccountMetaAccount(transferHook.programId, mint);
const extraAccountsInfo = await connection.getAccountInfo(extraAccountsAccount, commitment);
if (extraAccountsInfo == null) {
return instruction;
}

const extraAccountMetas = getExtraAccountMetas(extraAccountsInfo);

const accountMetas = instruction.keys;
accountMetas.push({ pubkey: extraAccountsAccount, isSigner: false, isWritable: false });

for (const extraAccountMeta of extraAccountMetas) {
const accountMeta = resolveExtraAccountMeta(
extraAccountMeta,
accountMetas,
instruction.data,
transferHook.programId
);
accountMetas.push(accountMeta);
}
accountMetas.push({ pubkey: transferHook.programId, isSigner: false, isWritable: false });

return new TransactionInstruction({ keys: accountMetas, programId, data: instruction.data });
}

/**
* Construct an transferChecked instruction with extra accounts for transfer hook
*
* @param connection Connection to use
* @param source Source account
* @param mint Mint to update
* @param destination Destination account
* @param authority The mint's transfer hook authority
* @param amount The amount of tokens to transfer
* @param decimals Number of decimals in transfer amount
* @param multiSigners The signer account(s) for a multisig
* @param commitment Commitment to use
* @param programId SPL Token program account
*
* @return Instruction to add to a transaction
*/
export async function createTransferCheckedWithTransferHookInstruction(
connection: Connection,
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
authority: PublicKey,
amount: bigint,
decimals: number,
multiSigners: (Signer | PublicKey)[] = [],
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID
) {
const rawInstruction = createTransferCheckedInstruction(
source,
mint,
destination,
authority,
amount,
decimals,
multiSigners,
programId
);

const hydratedInstruction = await addExtraAccountsToInstruction(
connection,
rawInstruction,
mint,
commitment,
programId
);

return hydratedInstruction;
}

/**
* Construct an transferChecked instruction with extra accounts for transfer hook
*
* @param connection Connection to use
* @param source Source account
* @param mint Mint to update
* @param destination Destination account
* @param authority The mint's transfer hook authority
* @param amount The amount of tokens to transfer
* @param decimals Number of decimals in transfer amount
* @param fee The calculated fee for the transfer fee extension
* @param multiSigners The signer account(s) for a multisig
* @param commitment Commitment to use
* @param programId SPL Token program account
*
* @return Instruction to add to a transaction
*/
export async function createTransferCheckedWithFeeAndTransferHookInstruction(
connection: Connection,
source: PublicKey,
mint: PublicKey,
destination: PublicKey,
authority: PublicKey,
amount: bigint,
decimals: number,
fee: bigint,
multiSigners: (Signer | PublicKey)[] = [],
commitment?: Commitment,
programId = TOKEN_PROGRAM_ID
) {
const rawInstruction = createTransferCheckedWithFeeInstruction(
source,
mint,
destination,
authority,
amount,
decimals,
fee,
multiSigners,
programId
);

const hydratedInstruction = await addExtraAccountsToInstruction(
connection,
rawInstruction,
mint,
commitment,
programId
);

return hydratedInstruction;
}
Loading

0 comments on commit c79c727

Please sign in to comment.