Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions modules/sdk-coin-sol/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ export const JITO_STAKE_POOL_RESERVE_ACCOUNT_TESTNET = 'rrWBQqRqBXYZw3CmPCCcjFxQ
export const JITO_MANAGER_FEE_ACCOUNT = 'feeeFLLsam6xZJFc6UQFrHqkvVt4jfmVvi2BRLkUZ4i';
export const JITO_MANAGER_FEE_ACCOUNT_TESTNET = 'DH7tmjoQ5zjqcgfYJU22JqmXhP5EY1tkbYpgVWUS2oNo';

/**
* On-chain program id of the sRFC-37 Token ACL program. This single program gates every
* allowlist/blocklist (DefaultAccountState) Token-2022 mint, so the value is generic and never
* tied to a specific issuer.
*/
export const TOKEN_ACL_PROGRAM_ID = 'TACLkU6CiCdkQN2MjoyDkVg2yAH9zkxiHDsiztQ52TP';

/**
* Instruction discriminator (a single u8 byte) for the Token ACL `ThawPermissionlessIdempotent`
* instruction.
*/
export const THAW_PERMISSIONLESS_IDEMPOTENT_DISCRIMINATOR = 9;

/** PDA seed prefix (under the Token ACL program) for a mint's MintConfig account. */
export const TOKEN_ACL_MINT_CONFIG_SEED = 'MINT_CONFIG';
/** PDA seed prefix (under the Token ACL program) for a token account's thaw flag account. */
export const TOKEN_ACL_FLAG_ACCOUNT_SEED = 'FLAG_ACCOUNT';
/** PDA seed prefix (under the gating program) for a mint's thaw ExtraAccountMetaList account. */
export const TOKEN_ACL_THAW_EXTRA_METAS_SEED = 'thaw_extra_account_metas';

// Sdk instructions, mainly to check decoded types.
export enum ValidInstructionTypesEnum {
AdvanceNonceAccount = 'AdvanceNonceAccount',
Expand Down Expand Up @@ -62,6 +82,7 @@ export enum ValidInstructionTypesEnum {
WithdrawStake = 'WithdrawStake',
Approve = 'Approve',
CustomInstruction = 'CustomInstruction',
PermissionlessThawIdempotent = 'PermissionlessThawIdempotent',
}

// Internal instructions types
Expand All @@ -87,6 +108,7 @@ export enum InstructionBuilderTypes {
VersionedCustomInstruction = 'VersionedCustomInstruction',
Approve = 'Approve',
WithdrawStake = 'WithdrawStake',
PermissionlessThawIdempotent = 'PermissionlessThawIdempotent',
}

export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [
Expand Down Expand Up @@ -115,6 +137,7 @@ export const VALID_SYSTEM_INSTRUCTION_TYPES: ValidInstructionTypes[] = [
ValidInstructionTypesEnum.DepositSol,
ValidInstructionTypesEnum.WithdrawStake,
ValidInstructionTypesEnum.CustomInstruction,
ValidInstructionTypesEnum.PermissionlessThawIdempotent,
];

/** Const to check the order of the Wallet Init instructions when decode */
Expand Down
44 changes: 42 additions & 2 deletions modules/sdk-coin-sol/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export type InstructionParams =
| Burn
| Approve
| CustomInstruction
| VersionedCustomInstruction;
| VersionedCustomInstruction
| PermissionlessThawIdempotent;

export interface Memo {
type: InstructionBuilderTypes.Memo;
Expand Down Expand Up @@ -115,6 +116,44 @@ export interface TokenTransfer {
};
}

/**
* sRFC-37 Token ACL permissionless thaw (idempotent) instruction.
*
* Emitted for allowlist/blocklist (DefaultAccountState) Token-2022 mints so a freshly created
* (frozen) token account can be thawed atomically in the same transaction as the transfer. The
* gating-program dependencies (`flagAccount`, `mintConfig`, `gatingProgram`, `extraAccounts`) are
* resolved live by the caller (see `Sol.resolvePermissionlessThaw`) because builders never fetch.
*/
export interface PermissionlessThawIdempotent {
type: InstructionBuilderTypes.PermissionlessThawIdempotent;
params: {
/** The signer invoking the thaw (fee payer / authority). */
authority: string;
/** The Token-2022 mint of the token account being thawed. */
mint: string;
/** The token account (ATA) to thaw. */
tokenAccount: string;
/** The owner of the token account. */
tokenAccountOwner: string;
/** The Token ACL gating program that authorizes the thaw. */
gatingProgram: string;
/** PDA (under the Token ACL program) tracking the token account's thaw flag. */
flagAccount: string;
/** PDA (under the Token ACL program) holding the mint's Token ACL config. */
mintConfig: string;
/** Token program id; defaults to the Token-2022 program when omitted. */
tokenProgram?: string;
/** System program id; defaults to the System program when omitted. */
systemProgram?: string;
/**
* Resolved extra account metas required by the gating program, in the exact order the
* gating program's thaw ExtraAccountMetaList requires. The first entry is the thaw
* ExtraAccountMetaList PDA itself, followed by any seed/account-derived dependencies.
*/
extraAccounts: ExtraAccountMeta[];
};
}

export interface MintTo {
type: InstructionBuilderTypes.MintTo;
params: {
Expand Down Expand Up @@ -271,7 +310,8 @@ export type ValidInstructionTypes =
| 'MintTo'
| 'Burn'
| 'Approve'
| 'CustomInstruction';
| 'CustomInstruction'
| 'PermissionlessThawIdempotent';

export type StakingAuthorizeParams = {
stakingAddress: string;
Expand Down
29 changes: 29 additions & 0 deletions modules/sdk-coin-sol/src/lib/instructionParamsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
Memo,
MintTo,
Nonce,
PermissionlessThawIdempotent,
StakingActivate,
StakingAuthorize,
StakingDeactivate,
Expand Down Expand Up @@ -156,6 +157,7 @@ function parseSendInstructions(
| MintTo
| Burn
| Approve
| PermissionlessThawIdempotent
> {
const instructionData: Array<
| Nonce
Expand All @@ -169,6 +171,7 @@ function parseSendInstructions(
| MintTo
| Burn
| Approve
| PermissionlessThawIdempotent
> = [];
for (const instruction of instructions) {
const type = getInstructionType(instruction);
Expand Down Expand Up @@ -387,6 +390,32 @@ function parseSendInstructions(
};
instructionData.push(burn);
break;
case ValidInstructionTypesEnum.PermissionlessThawIdempotent:
// Account order matches the fixed layout emitted by solInstructionFactory:
// [authority, mint, tokenAccount, flagAccount, tokenAccountOwner, mintConfig,
// tokenProgram, systemProgram, gatingProgram, ...extraAccounts].
const thawExtraAccounts: ExtraAccountMeta[] = instruction.keys.slice(9).map((key) => ({
pubkey: key.pubkey.toString(),
isSigner: key.isSigner,
isWritable: key.isWritable,
}));
const permissionlessThaw: PermissionlessThawIdempotent = {
type: InstructionBuilderTypes.PermissionlessThawIdempotent,
params: {
authority: instruction.keys[0].pubkey.toString(),
mint: instruction.keys[1].pubkey.toString(),
tokenAccount: instruction.keys[2].pubkey.toString(),
flagAccount: instruction.keys[3].pubkey.toString(),
tokenAccountOwner: instruction.keys[4].pubkey.toString(),
mintConfig: instruction.keys[5].pubkey.toString(),
tokenProgram: instruction.keys[6].pubkey.toString(),
systemProgram: instruction.keys[7].pubkey.toString(),
gatingProgram: instruction.keys[8].pubkey.toString(),
extraAccounts: thawExtraAccounts,
},
};
instructionData.push(permissionlessThaw);
break;
default:
throw new NotSupported(
'Invalid transaction, instruction type not supported: ' + getInstructionType(instruction)
Expand Down
61 changes: 60 additions & 1 deletion modules/sdk-coin-sol/src/lib/solInstructionFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import {
} from '@solana/web3.js';
import assert from 'assert';
import BigNumber from 'bignumber.js';
import { InstructionBuilderTypes, MEMO_PROGRAM_PK } from './constants';
import {
InstructionBuilderTypes,
MEMO_PROGRAM_PK,
THAW_PERMISSIONLESS_IDEMPOTENT_DISCRIMINATOR,
TOKEN_ACL_PROGRAM_ID,
} from './constants';
import {
AtaClose,
AtaInit,
Expand All @@ -36,6 +41,7 @@ import {
MintTo,
Burn,
Nonce,
PermissionlessThawIdempotent,
StakingActivate,
StakingAuthorize,
StakingDeactivate,
Expand Down Expand Up @@ -98,6 +104,8 @@ export function solInstructionFactory(instructionToBuild: InstructionParams): Tr
return burnInstruction(instructionToBuild);
case InstructionBuilderTypes.CustomInstruction:
return customInstruction(instructionToBuild);
case InstructionBuilderTypes.PermissionlessThawIdempotent:
return permissionlessThawIdempotentInstruction(instructionToBuild);
default:
throw new Error(`Invalid instruction type or not supported`);
}
Expand Down Expand Up @@ -777,6 +785,57 @@ function customInstruction(data: InstructionParams): TransactionInstruction[] {
return [convertedInstruction];
}

/**
* Construct the sRFC-37 Token ACL `ThawPermissionlessIdempotent` instruction.
*
* The first nine accounts are fixed (authority, mint, tokenAccount, flagAccount, tokenAccountOwner,
* mintConfig, tokenProgram, systemProgram, gatingProgram) in that exact order and with the flags the
* program expects. Any gating-program extra accounts (resolved live by the caller) are appended
* afterwards. The instruction data is the single discriminator byte.
*
* @param {PermissionlessThawIdempotent} data - the data to build the instruction
* @returns {TransactionInstruction[]} An array containing the permissionless thaw instruction
*/
function permissionlessThawIdempotentInstruction(data: PermissionlessThawIdempotent): TransactionInstruction[] {
const {
params: { authority, mint, tokenAccount, tokenAccountOwner, gatingProgram, flagAccount, mintConfig, extraAccounts },
} = data;
assert(authority, 'Missing authority param');
assert(mint, 'Missing mint param');
assert(tokenAccount, 'Missing tokenAccount param');
assert(tokenAccountOwner, 'Missing tokenAccountOwner param');
assert(gatingProgram, 'Missing gatingProgram param');
assert(flagAccount, 'Missing flagAccount param');
assert(mintConfig, 'Missing mintConfig param');

const tokenProgram = data.params.tokenProgram ?? TOKEN_2022_PROGRAM_ID.toString();
const systemProgram = data.params.systemProgram ?? SystemProgram.programId.toString();

const keys: AccountMeta[] = [
{ pubkey: new PublicKey(authority), isSigner: true, isWritable: false },
{ pubkey: new PublicKey(mint), isSigner: false, isWritable: false },
{ pubkey: new PublicKey(tokenAccount), isSigner: false, isWritable: true },
{ pubkey: new PublicKey(flagAccount), isSigner: false, isWritable: true },
{ pubkey: new PublicKey(tokenAccountOwner), isSigner: false, isWritable: false },
{ pubkey: new PublicKey(mintConfig), isSigner: false, isWritable: false },
{ pubkey: new PublicKey(tokenProgram), isSigner: false, isWritable: false },
{ pubkey: new PublicKey(systemProgram), isSigner: false, isWritable: false },
{ pubkey: new PublicKey(gatingProgram), isSigner: false, isWritable: false },
...(extraAccounts ?? []).map((meta) => ({
pubkey: new PublicKey(meta.pubkey),
isSigner: meta.isSigner,
isWritable: meta.isWritable,
})),
];

const thawInstruction = new TransactionInstruction({
keys,
programId: new PublicKey(TOKEN_ACL_PROGRAM_ID),
data: Buffer.from([THAW_PERMISSIONLESS_IDEMPOTENT_DISCRIMINATOR]),
});
return [thawInstruction];
}

function upsertAccountMeta(keys: AccountMeta[], meta: AccountMeta): void {
const existing = keys.find((account) => account.pubkey.equals(meta.pubkey));
if (existing) {
Expand Down
42 changes: 39 additions & 3 deletions modules/sdk-coin-sol/src/lib/tokenTransferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ import {
validateOwnerAddress,
} from './utils';
import { InstructionBuilderTypes } from './constants';
import { AtaInit, ExtraAccountMeta, TokenAssociateRecipient, TokenTransfer, SetPriorityFee } from './iface';
import {
AtaInit,
ExtraAccountMeta,
PermissionlessThawIdempotent,
TokenAssociateRecipient,
TokenTransfer,
SetPriorityFee,
} from './iface';
import assert from 'assert';
import { TransactionBuilder } from './transactionBuilder';
import _ from 'lodash';
Expand All @@ -30,12 +37,30 @@ export class TokenTransferBuilder extends TransactionBuilder {
private _sendParams: SendParams[] = [];
private _createAtaParams: TokenAssociateRecipient[];
private _transferHookAccounts?: ExtraAccountMeta[];
private _permissionlessThaw?: PermissionlessThawIdempotent['params'];

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._createAtaParams = [];
}

/**
* Set the resolved sRFC-37 Token ACL permissionless-thaw params for this transfer.
*
* These must be resolved live by the caller (e.g. via `Sol.resolvePermissionlessThaw`) since
* builders remain offline and never perform RPC. When set, the built transaction bundles a
* `PermissionlessThawIdempotent` instruction after any ATA creation and before the transfer, so
* a freshly created (frozen) allowlist/blocklist token account is thawed atomically with the
* transfer — all-or-nothing.
*
* @param {PermissionlessThawIdempotent['params']} params - resolved thaw params
* @returns {TokenTransferBuilder} This transaction builder
*/
permissionlessThaw(params: PermissionlessThawIdempotent['params']): this {
this._permissionlessThaw = params;
return this;
}

/**
* Set the resolved Token-2022 Transfer Hook extra account metas for this transfer.
*
Expand Down Expand Up @@ -224,11 +249,22 @@ export class TokenTransferBuilder extends TransactionBuilder {
},
};

// When resolved, emit the permissionless thaw between ATA creation and the transfer so the
// built order is [CreateATA?, PermissionlessThawIdempotent, TokenTransfer].
const thawInstructions: PermissionlessThawIdempotent[] = this._permissionlessThaw
? [{ type: InstructionBuilderTypes.PermissionlessThawIdempotent, params: this._permissionlessThaw }]
: [];

if (!this._priorityFee || this._priorityFee === Number(0)) {
this._instructionsData = [...createAtaInstructions, ...sendInstructions];
this._instructionsData = [...createAtaInstructions, ...thawInstructions, ...sendInstructions];
} else {
// order is important, createAtaInstructions must be before sendInstructions
this._instructionsData = [addPriorityFeeInstruction, ...createAtaInstructions, ...sendInstructions];
this._instructionsData = [
addPriorityFeeInstruction,
...createAtaInstructions,
...thawInstructions,
...sendInstructions,
];
}
return await super.buildImplementation();
}
Expand Down
Loading
Loading