diff --git a/clients/js/src/generated/instructions/index.ts b/clients/js/src/generated/instructions/index.ts index a3eddc7..1c0b3c1 100644 --- a/clients/js/src/generated/instructions/index.ts +++ b/clients/js/src/generated/instructions/index.ts @@ -43,6 +43,7 @@ export * from './initializeMultisig2'; export * from './initializeTransferFeeConfig'; export * from './mintTo'; export * from './mintToChecked'; +export * from './reallocate'; export * from './recoverNestedAssociatedToken'; export * from './revoke'; export * from './setAuthority'; diff --git a/clients/js/src/generated/instructions/reallocate.ts b/clients/js/src/generated/instructions/reallocate.ts new file mode 100644 index 0000000..c724d97 --- /dev/null +++ b/clients/js/src/generated/instructions/reallocate.ts @@ -0,0 +1,267 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + AccountRole, + combineCodec, + getArrayDecoder, + getArrayEncoder, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type Address, + type Codec, + type Decoder, + type Encoder, + type IAccountMeta, + type IAccountSignerMeta, + type IInstruction, + type IInstructionWithAccounts, + type IInstructionWithData, + type ReadonlyAccount, + type ReadonlySignerAccount, + type TransactionSigner, + type WritableAccount, + type WritableSignerAccount, +} from '@solana/web3.js'; +import { TOKEN_2022_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; +import { + getExtensionTypeDecoder, + getExtensionTypeEncoder, + type ExtensionType, + type ExtensionTypeArgs, +} from '../types'; + +export const REALLOCATE_DISCRIMINATOR = 29; + +export function getReallocateDiscriminatorBytes() { + return getU8Encoder().encode(REALLOCATE_DISCRIMINATOR); +} + +export type ReallocateInstruction< + TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, + TAccountToken extends string | IAccountMeta = string, + TAccountPayer extends string | IAccountMeta = string, + TAccountSystemProgram extends + | string + | IAccountMeta = '11111111111111111111111111111111', + TAccountOwner extends string | IAccountMeta = string, + TRemainingAccounts extends readonly IAccountMeta[] = [], +> = IInstruction & + IInstructionWithData & + IInstructionWithAccounts< + [ + TAccountToken extends string + ? WritableAccount + : TAccountToken, + TAccountPayer extends string + ? WritableSignerAccount & + IAccountSignerMeta + : TAccountPayer, + TAccountSystemProgram extends string + ? ReadonlyAccount + : TAccountSystemProgram, + TAccountOwner extends string + ? ReadonlyAccount + : TAccountOwner, + ...TRemainingAccounts, + ] + >; + +export type ReallocateInstructionData = { + discriminator: number; + /** New extension types to include in the reallocated account. */ + newExtensionTypes: Array; +}; + +export type ReallocateInstructionDataArgs = { + /** New extension types to include in the reallocated account. */ + newExtensionTypes: Array; +}; + +export function getReallocateInstructionDataEncoder(): Encoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', getU8Encoder()], + [ + 'newExtensionTypes', + getArrayEncoder(getExtensionTypeEncoder(), { size: 'remainder' }), + ], + ]), + (value) => ({ ...value, discriminator: REALLOCATE_DISCRIMINATOR }) + ); +} + +export function getReallocateInstructionDataDecoder(): Decoder { + return getStructDecoder([ + ['discriminator', getU8Decoder()], + [ + 'newExtensionTypes', + getArrayDecoder(getExtensionTypeDecoder(), { size: 'remainder' }), + ], + ]); +} + +export function getReallocateInstructionDataCodec(): Codec< + ReallocateInstructionDataArgs, + ReallocateInstructionData +> { + return combineCodec( + getReallocateInstructionDataEncoder(), + getReallocateInstructionDataDecoder() + ); +} + +export type ReallocateInput< + TAccountToken extends string = string, + TAccountPayer extends string = string, + TAccountSystemProgram extends string = string, + TAccountOwner extends string = string, +> = { + /** The token account to reallocate. */ + token: Address; + /** The payer account to fund reallocation. */ + payer: TransactionSigner; + /** System program for reallocation funding. */ + systemProgram?: Address; + /** The account's owner or its multisignature account. */ + owner: Address | TransactionSigner; + newExtensionTypes: ReallocateInstructionDataArgs['newExtensionTypes']; + multiSigners?: Array; +}; + +export function getReallocateInstruction< + TAccountToken extends string, + TAccountPayer extends string, + TAccountSystemProgram extends string, + TAccountOwner extends string, +>( + input: ReallocateInput< + TAccountToken, + TAccountPayer, + TAccountSystemProgram, + TAccountOwner + > +): ReallocateInstruction< + typeof TOKEN_2022_PROGRAM_ADDRESS, + TAccountToken, + TAccountPayer, + TAccountSystemProgram, + (typeof input)['owner'] extends TransactionSigner + ? ReadonlySignerAccount & IAccountSignerMeta + : TAccountOwner +> { + // Program address. + const programAddress = TOKEN_2022_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + token: { value: input.token ?? null, isWritable: true }, + payer: { value: input.payer ?? null, isWritable: true }, + systemProgram: { value: input.systemProgram ?? null, isWritable: false }, + owner: { value: input.owner ?? null, isWritable: false }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + // Resolve default values. + if (!accounts.systemProgram.value) { + accounts.systemProgram.value = + '11111111111111111111111111111111' as Address<'11111111111111111111111111111111'>; + } + + // Remaining accounts. + const remainingAccounts: IAccountMeta[] = (args.multiSigners ?? []).map( + (signer) => ({ + address: signer.address, + role: AccountRole.READONLY_SIGNER, + signer, + }) + ); + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + const instruction = { + accounts: [ + getAccountMeta(accounts.token), + getAccountMeta(accounts.payer), + getAccountMeta(accounts.systemProgram), + getAccountMeta(accounts.owner), + ...remainingAccounts, + ], + programAddress, + data: getReallocateInstructionDataEncoder().encode( + args as ReallocateInstructionDataArgs + ), + } as ReallocateInstruction< + typeof TOKEN_2022_PROGRAM_ADDRESS, + TAccountToken, + TAccountPayer, + TAccountSystemProgram, + (typeof input)['owner'] extends TransactionSigner + ? ReadonlySignerAccount & IAccountSignerMeta + : TAccountOwner + >; + + return instruction; +} + +export type ParsedReallocateInstruction< + TProgram extends string = typeof TOKEN_2022_PROGRAM_ADDRESS, + TAccountMetas extends readonly IAccountMeta[] = readonly IAccountMeta[], +> = { + programAddress: Address; + accounts: { + /** The token account to reallocate. */ + token: TAccountMetas[0]; + /** The payer account to fund reallocation. */ + payer: TAccountMetas[1]; + /** System program for reallocation funding. */ + systemProgram: TAccountMetas[2]; + /** The account's owner or its multisignature account. */ + owner: TAccountMetas[3]; + }; + data: ReallocateInstructionData; +}; + +export function parseReallocateInstruction< + TProgram extends string, + TAccountMetas extends readonly IAccountMeta[], +>( + instruction: IInstruction & + IInstructionWithAccounts & + IInstructionWithData +): ParsedReallocateInstruction { + if (instruction.accounts.length < 4) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = instruction.accounts![accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { + token: getNextAccount(), + payer: getNextAccount(), + systemProgram: getNextAccount(), + owner: getNextAccount(), + }, + data: getReallocateInstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/js/src/generated/programs/token2022.ts b/clients/js/src/generated/programs/token2022.ts index c40632a..ff08e0a 100644 --- a/clients/js/src/generated/programs/token2022.ts +++ b/clients/js/src/generated/programs/token2022.ts @@ -48,6 +48,7 @@ import { type ParsedInitializeTransferFeeConfigInstruction, type ParsedMintToCheckedInstruction, type ParsedMintToInstruction, + type ParsedReallocateInstruction, type ParsedRevokeInstruction, type ParsedSetAuthorityInstruction, type ParsedSetTransferFeeInstruction, @@ -139,6 +140,7 @@ export enum Token2022Instruction { ConfidentialTransferWithFee, InitializeDefaultAccountState, UpdateDefaultAccountState, + Reallocate, } export function identifyToken2022Instruction( @@ -355,6 +357,9 @@ export function identifyToken2022Instruction( ) { return Token2022Instruction.UpdateDefaultAccountState; } + if (containsBytes(data, getU8Encoder().encode(29), 0)) { + return Token2022Instruction.Reallocate; + } throw new Error( 'The provided instruction could not be identified as a token-2022 instruction.' ); @@ -506,4 +511,7 @@ export type ParsedToken2022Instruction< } & ParsedInitializeDefaultAccountStateInstruction) | ({ instructionType: Token2022Instruction.UpdateDefaultAccountState; - } & ParsedUpdateDefaultAccountStateInstruction); + } & ParsedUpdateDefaultAccountStateInstruction) + | ({ + instructionType: Token2022Instruction.Reallocate; + } & ParsedReallocateInstruction); diff --git a/clients/js/src/generated/types/extensionType.ts b/clients/js/src/generated/types/extensionType.ts new file mode 100644 index 0000000..7b2d70f --- /dev/null +++ b/clients/js/src/generated/types/extensionType.ts @@ -0,0 +1,68 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/kinobi-so/kinobi + */ + +import { + combineCodec, + getEnumDecoder, + getEnumEncoder, + getU16Decoder, + getU16Encoder, + type Codec, + type Decoder, + type Encoder, +} from '@solana/web3.js'; + +/** + * Extensions that can be applied to mints or accounts. Mint extensions must + * only be applied to mint accounts, and account extensions must only be + * applied to token holding accounts. + */ + +export enum ExtensionType { + Uninitialized, + TransferFeeConfig, + TransferFeeAmount, + MintCloseAuthority, + ConfidentialTransferMint, + ConfidentialTransferAccount, + DefaultAccountState, + ImmutableOwner, + MemoTransfer, + NonTransferable, + InterestBearingConfig, + CpiGuard, + PermanentDelegate, + NonTransferableAccount, + TransferHook, + TransferHookAccount, + ConfidentialTransferFee, + ConfidentialTransferFeeAmount, + MetadataPointer, + TokenMetadata, + GroupPointer, + TokenGroup, + GroupMemberPointer, + TokenGroupMember, +} + +export type ExtensionTypeArgs = ExtensionType; + +export function getExtensionTypeEncoder(): Encoder { + return getEnumEncoder(ExtensionType, { size: getU16Encoder() }); +} + +export function getExtensionTypeDecoder(): Decoder { + return getEnumDecoder(ExtensionType, { size: getU16Decoder() }); +} + +export function getExtensionTypeCodec(): Codec< + ExtensionTypeArgs, + ExtensionType +> { + return combineCodec(getExtensionTypeEncoder(), getExtensionTypeDecoder()); +} diff --git a/clients/js/src/generated/types/index.ts b/clients/js/src/generated/types/index.ts index 3dfd13d..c94c807 100644 --- a/clients/js/src/generated/types/index.ts +++ b/clients/js/src/generated/types/index.ts @@ -11,4 +11,5 @@ export * from './authorityType'; export * from './decryptableBalance'; export * from './encryptedBalance'; export * from './extension'; +export * from './extensionType'; export * from './transferFee'; diff --git a/clients/js/test/extensions/reallocate.test.ts b/clients/js/test/extensions/reallocate.test.ts new file mode 100644 index 0000000..71d92b9 --- /dev/null +++ b/clients/js/test/extensions/reallocate.test.ts @@ -0,0 +1,69 @@ +import { + Address, + assertAccountExists, + fetchEncodedAccount, + generateKeyPairSigner, + GetAccountInfoApi, + Rpc, +} from '@solana/web3.js'; +import test from 'ava'; +import { ExtensionType, getReallocateInstruction } from '../../src'; +import { + createDefaultSolanaClient, + createMint, + createToken, + generateKeyPairSignerWithSol, + sendAndConfirmInstructions, +} from '../_setup'; + +test('it reallocates token accounts to fit the provided extensions', async (t) => { + // Given some signer accounts. + const client = createDefaultSolanaClient(); + const [authority, owner] = await Promise.all([ + generateKeyPairSignerWithSol(client), + generateKeyPairSigner(), + ]); + + // And a token account with no extensions. + const mint = await createMint({ + authority: authority.address, + client, + payer: authority, + }); + const token = await createToken({ + client, + mint, + owner: owner.address, + payer: authority, + }); + t.is(await getAccountLength(client, token), 165); + + // When + await sendAndConfirmInstructions(client, authority, [ + getReallocateInstruction({ + token, + owner, + newExtensionTypes: [ExtensionType.MemoTransfer], + payer: authority, + }), + ]); + + // Then + t.is( + await getAccountLength(client, token), + 165 /** base token length */ + + 1 /** account type discriminator */ + + 2 /** memo transfer discriminator */ + + 2 /** memo transfer length */ + + 1 /** memo transfer boolean */ + ); +}); + +async function getAccountLength( + client: { rpc: Rpc }, + address: Address +) { + const account = await fetchEncodedAccount(client.rpc, address); + assertAccountExists(account); + return account.data.length; +} diff --git a/program/idl.json b/program/idl.json index ae869fc..ead6597 100644 --- a/program/idl.json +++ b/program/idl.json @@ -5075,6 +5075,107 @@ "offset": 1 } ] + }, + { + "kind": "instructionNode", + "name": "reallocate", + "docs": [ + "Check to see if a token account is large enough for a list of", + "ExtensionTypes, and if not, use reallocation to increase the data", + "size." + ], + "optionalAccountStrategy": "programId", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "token", + "isWritable": true, + "isSigner": false, + "isOptional": false, + "docs": ["The token account to reallocate."] + }, + { + "kind": "instructionAccountNode", + "name": "payer", + "isWritable": true, + "isSigner": true, + "isOptional": false, + "docs": ["The payer account to fund reallocation."] + }, + { + "kind": "instructionAccountNode", + "name": "systemProgram", + "isWritable": false, + "isSigner": false, + "isOptional": false, + "docs": ["System program for reallocation funding."], + "defaultValue": { + "kind": "publicKeyValueNode", + "publicKey": "11111111111111111111111111111111" + } + }, + { + "kind": "instructionAccountNode", + "name": "owner", + "isWritable": false, + "isSigner": "either", + "isOptional": false, + "docs": ["The account's owner or its multisignature account."] + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "defaultValueStrategy": "omitted", + "docs": [], + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 29 + } + }, + { + "kind": "instructionArgumentNode", + "name": "newExtensionTypes", + "docs": [ + "New extension types to include in the reallocated account." + ], + "type": { + "kind": "arrayTypeNode", + "item": { + "kind": "definedTypeLinkNode", + "name": "extensionType" + }, + "count": { + "kind": "remainderCountNode" + } + } + } + ], + "remainingAccounts": [ + { + "kind": "instructionRemainingAccountsNode", + "isOptional": true, + "isSigner": true, + "docs": [], + "value": { + "kind": "argumentValueNode", + "name": "multiSigners" + } + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ] } ], "definedTypes": [ @@ -6345,6 +6446,177 @@ "endian": "le" } } + }, + { + "kind": "definedTypeNode", + "name": "extensionType", + "docs": [ + "Extensions that can be applied to mints or accounts. Mint extensions must", + "only be applied to mint accounts, and account extensions must only be", + "applied to token holding accounts." + ], + "type": { + "kind": "enumTypeNode", + "variants": [ + { + "kind": "enumEmptyVariantTypeNode", + "name": "uninitialized", + "docs": [ + "Used as padding if the account size would otherwise be 355, same as a multisig" + ] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "transferFeeConfig", + "docs": [ + "Includes transfer fee rate info and accompanying authorities to withdraw", + "and set the fee" + ] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "transferFeeAmount", + "docs": ["Includes withheld transfer fees"] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "mintCloseAuthority", + "docs": ["Includes an optional mint close authority"] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "confidentialTransferMint", + "docs": ["Auditor configuration for confidential transfers"] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "confidentialTransferAccount", + "docs": ["State for confidential transfers"] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "defaultAccountState", + "docs": ["Specifies the default Account::state for new Accounts"] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "immutableOwner", + "docs": [ + "Indicates that the Account owner authority cannot be changed" + ] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "memoTransfer", + "docs": ["Require inbound transfers to have memo"] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "nonTransferable", + "docs": [ + "Indicates that the tokens from this mint can't be transferred" + ] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "interestBearingConfig", + "docs": ["Tokens accrue interest over time,"] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "cpiGuard", + "docs": [ + "Locks privileged token operations from happening via CPI" + ] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "permanentDelegate", + "docs": ["Includes an optional permanent delegate"] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "nonTransferableAccount", + "docs": [ + "Indicates that the tokens in this account belong to a non-transferable", + "mint" + ] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "transferHook", + "docs": [ + "Mint requires a CPI to a program implementing the \"transfer hook\"", + "interface" + ] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "transferHookAccount", + "docs": [ + "Indicates that the tokens in this account belong to a mint with a", + "transfer hook" + ] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "confidentialTransferFee", + "docs": [ + "Includes encrypted withheld fees and the encryption public that they are", + "encrypted under" + ] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "confidentialTransferFeeAmount", + "docs": ["Includes confidential withheld transfer fees"] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "metadataPointer", + "docs": [ + "Mint contains a pointer to another account (or the same account) that", + "holds metadata" + ] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "tokenMetadata", + "docs": ["Mint contains token-metadata"] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "groupPointer", + "docs": [ + "Mint contains a pointer to another account (or the same account) that", + "holds group configurations" + ] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "tokenGroup", + "docs": ["Mint contains token group configurations"] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "groupMemberPointer", + "docs": [ + "Mint contains a pointer to another account (or the same account) that", + "holds group member configurations" + ] + }, + { + "kind": "enumEmptyVariantTypeNode", + "name": "tokenGroupMember", + "docs": ["Mint contains token group member configurations"] + } + ], + "size": { + "kind": "numberTypeNode", + "format": "u16", + "endian": "le" + } + } } ], "pdas": [],