diff --git a/clients/js/package.json b/clients/js/package.json new file mode 100644 index 0000000..1173407 --- /dev/null +++ b/clients/js/package.json @@ -0,0 +1,76 @@ +{ + "name": "@solana-program/spl-record", + "version": "0.1.0", + "description": "JavaScript client for the SPL Record program", + "sideEffects": false, + "module": "./dist/src/index.mjs", + "main": "./dist/src/index.js", + "types": "./dist/types/index.d.ts", + "type": "commonjs", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/src/index.mjs", + "require": "./dist/src/index.js" + } + }, + "files": [ + "./dist/src", + "./dist/types" + ], + "scripts": { + "build": "rimraf dist && tsup && tsc -p ./tsconfig.declarations.json", + "build:docs": "typedoc", + "test": "ava", + "lint": "eslint --ext js,ts,tsx src", + "lint:fix": "eslint --fix --ext js,ts,tsx src", + "format": "prettier --check src test", + "format:fix": "prettier --write src test", + "prepublishOnly": "pnpm build" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/solana-program/token-2022.git" + }, + "bugs": { + "url": "https://github.com/solana-program/token-2022/issues" + }, + "homepage": "https://github.com/solana-program/token-2022#readme", + "peerDependencies": { + "@solana/kit": "^4.0", + "@solana/sysvars": "^4.0" + }, + "devDependencies": { + "@ava/typescript": "^6.0.0", + "@solana-program/system": "^0.9.0", + "@solana/eslint-config-solana": "^3.0.3", + "@solana/kit": "^4.0", + "@types/node": "^24", + "@typescript-eslint/eslint-plugin": "^7.16.1", + "@typescript-eslint/parser": "^7.16.1", + "ava": "^6.1.3", + "eslint": "^8.57.0", + "prettier": "^3.3.3", + "rimraf": "^6.0.1", + "tsup": "^8.1.2", + "typedoc": "^0.28.0", + "typescript": "^5.5.3" + }, + "ava": { + "nodeArguments": [ + "--no-warnings" + ], + "typescript": { + "compile": false, + "rewritePaths": { + "test/": "dist/test/" + } + } + }, + "packageManager": "pnpm@9.1.0" +} diff --git a/clients/js/src/actions.ts b/clients/js/src/actions.ts new file mode 100644 index 0000000..718cc13 --- /dev/null +++ b/clients/js/src/actions.ts @@ -0,0 +1,193 @@ +import { + Address, + generateKeyPairSigner, + GetBalanceApi, + GetMinimumBalanceForRentExemptionApi, + Instruction, + KeyPairSigner, + Rpc, + TransactionSigner, +} from "@solana/kit"; +import { + getCreateAccountInstruction, + getTransferSolInstruction, +} from "@solana-program/system"; +import { + getCloseAccountInstruction, + getInitializeInstruction, + getReallocateInstruction, + getSetAuthorityInstruction, + getWriteInstruction, + SPL_RECORD_PROGRAM_ADDRESS, +} from "./generated"; +import { RECORD_META_DATA_SIZE } from "./constants"; + +export interface CreateRecordArgs { + rpc: Rpc; + payer: KeyPairSigner; + authority: Address; + dataLength: number | bigint; + programId?: Address; + /** Optional: Provide your own keypair for the record account. If not provided, one is generated. */ + recordKeypair?: KeyPairSigner; +} + +export interface CreateRecordResult { + recordKeypair: KeyPairSigner; + ixs: Instruction[]; +} + +/** + * High-level function to create and initialize a Record Account. + * Handles rent calculation and system account creation. + */ +export async function createRecord({ + rpc, + payer, + authority, + dataLength, + programId = SPL_RECORD_PROGRAM_ADDRESS, + recordKeypair, +}: CreateRecordArgs): Promise { + const recordSigner = recordKeypair ?? (await generateKeyPairSigner()); + const space = RECORD_META_DATA_SIZE + BigInt(dataLength); + const lamports = await rpc.getMinimumBalanceForRentExemption(space).send(); + + const createAccountIx = getCreateAccountInstruction({ + payer: payer, + newAccount: recordSigner, + lamports, + space, + programAddress: programId, + }); + + const initializeIx = getInitializeInstruction( + { + recordAccount: recordSigner.address, + authority, + }, + { programAddress: programId }, + ); + + return { + recordKeypair: recordSigner, + ixs: [createAccountIx, initializeIx], + }; +} + +export interface WriteRecordArgs { + recordAccount: Address; + authority: TransactionSigner; + offset: number | bigint; + data: Uint8Array; + programId?: Address; +} + +/** + * Creates a Write instruction. + * Note: For large data, you should manually chunk this or use a loop helper. + */ +export function createWriteInstruction(args: WriteRecordArgs): Instruction { + return getWriteInstruction( + { + recordAccount: args.recordAccount, + authority: args.authority, + offset: BigInt(args.offset), + data: args.data, + }, + { programAddress: args.programId }, + ); +} + +export interface ReallocateRecordArgs { + rpc: Rpc; + payer: KeyPairSigner; + recordAccount: Address; + authority: TransactionSigner; + newDataLength: number | bigint; + programId?: Address; +} + +/** + * High-level function to reallocate a Record Account. + * Checks if additional lamports are needed for the new size and adds a transfer instruction if so. + */ +export async function reallocateRecord({ + rpc, + payer, + recordAccount, + authority, + newDataLength, + programId = SPL_RECORD_PROGRAM_ADDRESS, +}: ReallocateRecordArgs): Promise { + const ixs: Instruction[] = []; + const newSpace = RECORD_META_DATA_SIZE + BigInt(newDataLength); + const requiredRent = await rpc + .getMinimumBalanceForRentExemption(newSpace) + .send(); + const currentBalance = await rpc.getBalance(recordAccount).send(); + + if (requiredRent > currentBalance.value) { + const lamportsNeeded = requiredRent - currentBalance.value; + ixs.push( + getTransferSolInstruction({ + source: payer, + destination: recordAccount, + amount: lamportsNeeded, + }), + ); + } + + ixs.push( + getReallocateInstruction( + { + recordAccount, + authority, + dataLength: BigInt(newDataLength), + }, + { programAddress: programId }, + ), + ); + + return ixs; +} + +export interface SetAuthorityArgs { + recordAccount: Address; + authority: TransactionSigner; + newAuthority: Address; + programId?: Address; +} + +export function createSetAuthorityInstruction( + args: SetAuthorityArgs, +): Instruction { + return getSetAuthorityInstruction( + { + recordAccount: args.recordAccount, + authority: args.authority, + newAuthority: args.newAuthority, + }, + { programAddress: args.programId }, + ); +} + +export interface CloseRecordArgs { + recordAccount: Address; + authority: TransactionSigner; + receiver: Address; + programId?: Address; +} + +export function createCloseRecordInstruction( + args: CloseRecordArgs, +): Instruction { + return getCloseAccountInstruction( + { + recordAccount: args.recordAccount, + authority: args.authority, + receiver: args.receiver, + }, + { programAddress: args.programId }, + ); +} diff --git a/clients/js/src/constants.ts b/clients/js/src/constants.ts new file mode 100644 index 0000000..0eb9564 --- /dev/null +++ b/clients/js/src/constants.ts @@ -0,0 +1,8 @@ +/** A record account size excluding the record payload */ +export const RECORD_META_DATA_SIZE = 33n; + +/** Maximum record chunk that can fit inside a transaction when initializing a record account */ +export const RECORD_CHUNK_SIZE_PRE_INITIALIZE = 696; + +/** Maximum record chunk that can fit inside a transaction when record account already initialized */ +export const RECORD_CHUNK_SIZE_POST_INITIALIZE = 917; diff --git a/clients/js/src/generated/accounts/index.ts b/clients/js/src/generated/accounts/index.ts new file mode 100644 index 0000000..e014e0b --- /dev/null +++ b/clients/js/src/generated/accounts/index.ts @@ -0,0 +1,9 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './recordData'; diff --git a/clients/js/src/generated/accounts/recordData.ts b/clients/js/src/generated/accounts/recordData.ts new file mode 100644 index 0000000..9b8bf71 --- /dev/null +++ b/clients/js/src/generated/accounts/recordData.ts @@ -0,0 +1,120 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + assertAccountExists, + assertAccountsExist, + combineCodec, + decodeAccount, + fetchEncodedAccount, + fetchEncodedAccounts, + getAddressDecoder, + getAddressEncoder, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type Account, + type Address, + type EncodedAccount, + type FetchAccountConfig, + type FetchAccountsConfig, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type MaybeAccount, + type MaybeEncodedAccount, +} from '@solana/kit'; + +export const RECORD_DATA_VERSION = 1; + +export function getRecordDataVersionBytes() { + return getU8Encoder().encode(RECORD_DATA_VERSION); +} + +export type RecordData = { version: number; authority: Address }; + +export type RecordDataArgs = { version?: number; authority: Address }; + +export function getRecordDataEncoder(): FixedSizeEncoder { + return transformEncoder( + getStructEncoder([ + ['version', getU8Encoder()], + ['authority', getAddressEncoder()], + ]), + (value) => ({ ...value, version: value.version ?? RECORD_DATA_VERSION }) + ); +} + +export function getRecordDataDecoder(): FixedSizeDecoder { + return getStructDecoder([ + ['version', getU8Decoder()], + ['authority', getAddressDecoder()], + ]); +} + +export function getRecordDataCodec(): FixedSizeCodec< + RecordDataArgs, + RecordData +> { + return combineCodec(getRecordDataEncoder(), getRecordDataDecoder()); +} + +export function decodeRecordData( + encodedAccount: EncodedAccount +): Account; +export function decodeRecordData( + encodedAccount: MaybeEncodedAccount +): MaybeAccount; +export function decodeRecordData( + encodedAccount: EncodedAccount | MaybeEncodedAccount +): Account | MaybeAccount { + return decodeAccount( + encodedAccount as MaybeEncodedAccount, + getRecordDataDecoder() + ); +} + +export async function fetchRecordData( + rpc: Parameters[0], + address: Address, + config?: FetchAccountConfig +): Promise> { + const maybeAccount = await fetchMaybeRecordData(rpc, address, config); + assertAccountExists(maybeAccount); + return maybeAccount; +} + +export async function fetchMaybeRecordData( + rpc: Parameters[0], + address: Address, + config?: FetchAccountConfig +): Promise> { + const maybeAccount = await fetchEncodedAccount(rpc, address, config); + return decodeRecordData(maybeAccount); +} + +export async function fetchAllRecordData( + rpc: Parameters[0], + addresses: Array
, + config?: FetchAccountsConfig +): Promise[]> { + const maybeAccounts = await fetchAllMaybeRecordData(rpc, addresses, config); + assertAccountsExist(maybeAccounts); + return maybeAccounts; +} + +export async function fetchAllMaybeRecordData( + rpc: Parameters[0], + addresses: Array
, + config?: FetchAccountsConfig +): Promise[]> { + const maybeAccounts = await fetchEncodedAccounts(rpc, addresses, config); + return maybeAccounts.map((maybeAccount) => decodeRecordData(maybeAccount)); +} diff --git a/clients/js/src/generated/errors/index.ts b/clients/js/src/generated/errors/index.ts new file mode 100644 index 0000000..813c8a5 --- /dev/null +++ b/clients/js/src/generated/errors/index.ts @@ -0,0 +1,9 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './splRecord'; diff --git a/clients/js/src/generated/errors/splRecord.ts b/clients/js/src/generated/errors/splRecord.ts new file mode 100644 index 0000000..2afdbd1 --- /dev/null +++ b/clients/js/src/generated/errors/splRecord.ts @@ -0,0 +1,54 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + isProgramError, + type Address, + type SOLANA_ERROR__INSTRUCTION_ERROR__CUSTOM, + type SolanaError, +} from '@solana/kit'; +import { SPL_RECORD_PROGRAM_ADDRESS } from '../programs'; + +export const SPL_RECORD_ERROR__INCORRECT_AUTHORITY = 0x0; // 0 +export const SPL_RECORD_ERROR__OVERFLOW = 0x1; // 1 + +export type SplRecordError = + | typeof SPL_RECORD_ERROR__INCORRECT_AUTHORITY + | typeof SPL_RECORD_ERROR__OVERFLOW; + +let splRecordErrorMessages: Record | undefined; +if (process.env.NODE_ENV !== 'production') { + splRecordErrorMessages = { + [SPL_RECORD_ERROR__INCORRECT_AUTHORITY]: `Incorrect authority provided on update or delete`, + [SPL_RECORD_ERROR__OVERFLOW]: `Calculation overflow`, + }; +} + +export function getSplRecordErrorMessage(code: SplRecordError): string { + if (process.env.NODE_ENV !== 'production') { + return (splRecordErrorMessages as Record)[code]; + } + + return 'Error message not available in production bundles.'; +} + +export function isSplRecordError( + error: unknown, + transactionMessage: { + instructions: Record; + }, + code?: TProgramErrorCode +): error is SolanaError & + Readonly<{ context: Readonly<{ code: TProgramErrorCode }> }> { + return isProgramError( + error, + transactionMessage, + SPL_RECORD_PROGRAM_ADDRESS, + code + ); +} diff --git a/clients/js/src/generated/index.ts b/clients/js/src/generated/index.ts new file mode 100644 index 0000000..0974e01 --- /dev/null +++ b/clients/js/src/generated/index.ts @@ -0,0 +1,12 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './accounts'; +export * from './errors'; +export * from './instructions'; +export * from './programs'; diff --git a/clients/js/src/generated/instructions/closeAccount.ts b/clients/js/src/generated/instructions/closeAccount.ts new file mode 100644 index 0000000..bc6746c --- /dev/null +++ b/clients/js/src/generated/instructions/closeAccount.ts @@ -0,0 +1,187 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + combineCodec, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlySignerAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, +} from '@solana/kit'; +import { SPL_RECORD_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const CLOSE_ACCOUNT_DISCRIMINATOR = 3; + +export function getCloseAccountDiscriminatorBytes() { + return getU8Encoder().encode(CLOSE_ACCOUNT_DISCRIMINATOR); +} + +export type CloseAccountInstruction< + TProgram extends string = typeof SPL_RECORD_PROGRAM_ADDRESS, + TAccountRecordAccount extends string | AccountMeta = string, + TAccountAuthority extends string | AccountMeta = string, + TAccountReceiver extends string | AccountMeta = string, + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountRecordAccount extends string + ? WritableAccount + : TAccountRecordAccount, + TAccountAuthority extends string + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountAuthority, + TAccountReceiver extends string + ? WritableAccount + : TAccountReceiver, + ...TRemainingAccounts, + ] + >; + +export type CloseAccountInstructionData = { discriminator: number }; + +export type CloseAccountInstructionDataArgs = {}; + +export function getCloseAccountInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder( + getStructEncoder([['discriminator', getU8Encoder()]]), + (value) => ({ ...value, discriminator: CLOSE_ACCOUNT_DISCRIMINATOR }) + ); +} + +export function getCloseAccountInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([['discriminator', getU8Decoder()]]); +} + +export function getCloseAccountInstructionDataCodec(): FixedSizeCodec< + CloseAccountInstructionDataArgs, + CloseAccountInstructionData +> { + return combineCodec( + getCloseAccountInstructionDataEncoder(), + getCloseAccountInstructionDataDecoder() + ); +} + +export type CloseAccountInput< + TAccountRecordAccount extends string = string, + TAccountAuthority extends string = string, + TAccountReceiver extends string = string, +> = { + recordAccount: Address; + authority: TransactionSigner; + receiver: Address; +}; + +export function getCloseAccountInstruction< + TAccountRecordAccount extends string, + TAccountAuthority extends string, + TAccountReceiver extends string, + TProgramAddress extends Address = typeof SPL_RECORD_PROGRAM_ADDRESS, +>( + input: CloseAccountInput< + TAccountRecordAccount, + TAccountAuthority, + TAccountReceiver + >, + config?: { programAddress?: TProgramAddress } +): CloseAccountInstruction< + TProgramAddress, + TAccountRecordAccount, + TAccountAuthority, + TAccountReceiver +> { + // Program address. + const programAddress = config?.programAddress ?? SPL_RECORD_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + recordAccount: { value: input.recordAccount ?? null, isWritable: true }, + authority: { value: input.authority ?? null, isWritable: false }, + receiver: { value: input.receiver ?? null, isWritable: true }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + return Object.freeze({ + accounts: [ + getAccountMeta(accounts.recordAccount), + getAccountMeta(accounts.authority), + getAccountMeta(accounts.receiver), + ], + data: getCloseAccountInstructionDataEncoder().encode({}), + programAddress, + } as CloseAccountInstruction< + TProgramAddress, + TAccountRecordAccount, + TAccountAuthority, + TAccountReceiver + >); +} + +export type ParsedCloseAccountInstruction< + TProgram extends string = typeof SPL_RECORD_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address; + accounts: { + recordAccount: TAccountMetas[0]; + authority: TAccountMetas[1]; + receiver: TAccountMetas[2]; + }; + data: CloseAccountInstructionData; +}; + +export function parseCloseAccountInstruction< + TProgram extends string, + TAccountMetas extends readonly AccountMeta[], +>( + instruction: Instruction & + InstructionWithAccounts & + InstructionWithData +): ParsedCloseAccountInstruction { + if (instruction.accounts.length < 3) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { + recordAccount: getNextAccount(), + authority: getNextAccount(), + receiver: getNextAccount(), + }, + data: getCloseAccountInstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/js/src/generated/instructions/index.ts b/clients/js/src/generated/instructions/index.ts new file mode 100644 index 0000000..461af33 --- /dev/null +++ b/clients/js/src/generated/instructions/index.ts @@ -0,0 +1,13 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './closeAccount'; +export * from './initialize'; +export * from './reallocate'; +export * from './setAuthority'; +export * from './write'; diff --git a/clients/js/src/generated/instructions/initialize.ts b/clients/js/src/generated/instructions/initialize.ts new file mode 100644 index 0000000..ee748eb --- /dev/null +++ b/clients/js/src/generated/instructions/initialize.ts @@ -0,0 +1,164 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + combineCodec, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type AccountMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlyAccount, + type ReadonlyUint8Array, + type WritableAccount, +} from '@solana/kit'; +import { SPL_RECORD_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const INITIALIZE_DISCRIMINATOR = 0; + +export function getInitializeDiscriminatorBytes() { + return getU8Encoder().encode(INITIALIZE_DISCRIMINATOR); +} + +export type InitializeInstruction< + TProgram extends string = typeof SPL_RECORD_PROGRAM_ADDRESS, + TAccountRecordAccount extends string | AccountMeta = string, + TAccountAuthority extends string | AccountMeta = string, + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountRecordAccount extends string + ? WritableAccount + : TAccountRecordAccount, + TAccountAuthority extends string + ? ReadonlyAccount + : TAccountAuthority, + ...TRemainingAccounts, + ] + >; + +export type InitializeInstructionData = { discriminator: number }; + +export type InitializeInstructionDataArgs = {}; + +export function getInitializeInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder( + getStructEncoder([['discriminator', getU8Encoder()]]), + (value) => ({ ...value, discriminator: INITIALIZE_DISCRIMINATOR }) + ); +} + +export function getInitializeInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([['discriminator', getU8Decoder()]]); +} + +export function getInitializeInstructionDataCodec(): FixedSizeCodec< + InitializeInstructionDataArgs, + InitializeInstructionData +> { + return combineCodec( + getInitializeInstructionDataEncoder(), + getInitializeInstructionDataDecoder() + ); +} + +export type InitializeInput< + TAccountRecordAccount extends string = string, + TAccountAuthority extends string = string, +> = { + recordAccount: Address; + authority: Address; +}; + +export function getInitializeInstruction< + TAccountRecordAccount extends string, + TAccountAuthority extends string, + TProgramAddress extends Address = typeof SPL_RECORD_PROGRAM_ADDRESS, +>( + input: InitializeInput, + config?: { programAddress?: TProgramAddress } +): InitializeInstruction< + TProgramAddress, + TAccountRecordAccount, + TAccountAuthority +> { + // Program address. + const programAddress = config?.programAddress ?? SPL_RECORD_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + recordAccount: { value: input.recordAccount ?? null, isWritable: true }, + authority: { value: input.authority ?? null, isWritable: false }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + return Object.freeze({ + accounts: [ + getAccountMeta(accounts.recordAccount), + getAccountMeta(accounts.authority), + ], + data: getInitializeInstructionDataEncoder().encode({}), + programAddress, + } as InitializeInstruction< + TProgramAddress, + TAccountRecordAccount, + TAccountAuthority + >); +} + +export type ParsedInitializeInstruction< + TProgram extends string = typeof SPL_RECORD_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address; + accounts: { + recordAccount: TAccountMetas[0]; + authority: TAccountMetas[1]; + }; + data: InitializeInstructionData; +}; + +export function parseInitializeInstruction< + TProgram extends string, + TAccountMetas extends readonly AccountMeta[], +>( + instruction: Instruction & + InstructionWithAccounts & + InstructionWithData +): ParsedInitializeInstruction { + if (instruction.accounts.length < 2) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { recordAccount: getNextAccount(), authority: getNextAccount() }, + data: getInitializeInstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/js/src/generated/instructions/reallocate.ts b/clients/js/src/generated/instructions/reallocate.ts new file mode 100644 index 0000000..c610b6e --- /dev/null +++ b/clients/js/src/generated/instructions/reallocate.ts @@ -0,0 +1,184 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + combineCodec, + getStructDecoder, + getStructEncoder, + getU64Decoder, + getU64Encoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlySignerAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, +} from '@solana/kit'; +import { SPL_RECORD_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const REALLOCATE_DISCRIMINATOR = 4; + +export function getReallocateDiscriminatorBytes() { + return getU8Encoder().encode(REALLOCATE_DISCRIMINATOR); +} + +export type ReallocateInstruction< + TProgram extends string = typeof SPL_RECORD_PROGRAM_ADDRESS, + TAccountRecordAccount extends string | AccountMeta = string, + TAccountAuthority extends string | AccountMeta = string, + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountRecordAccount extends string + ? WritableAccount + : TAccountRecordAccount, + TAccountAuthority extends string + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountAuthority, + ...TRemainingAccounts, + ] + >; + +export type ReallocateInstructionData = { + discriminator: number; + dataLength: bigint; +}; + +export type ReallocateInstructionDataArgs = { dataLength: number | bigint }; + +export function getReallocateInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', getU8Encoder()], + ['dataLength', getU64Encoder()], + ]), + (value) => ({ ...value, discriminator: REALLOCATE_DISCRIMINATOR }) + ); +} + +export function getReallocateInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([ + ['discriminator', getU8Decoder()], + ['dataLength', getU64Decoder()], + ]); +} + +export function getReallocateInstructionDataCodec(): FixedSizeCodec< + ReallocateInstructionDataArgs, + ReallocateInstructionData +> { + return combineCodec( + getReallocateInstructionDataEncoder(), + getReallocateInstructionDataDecoder() + ); +} + +export type ReallocateInput< + TAccountRecordAccount extends string = string, + TAccountAuthority extends string = string, +> = { + recordAccount: Address; + authority: TransactionSigner; + dataLength: ReallocateInstructionDataArgs['dataLength']; +}; + +export function getReallocateInstruction< + TAccountRecordAccount extends string, + TAccountAuthority extends string, + TProgramAddress extends Address = typeof SPL_RECORD_PROGRAM_ADDRESS, +>( + input: ReallocateInput, + config?: { programAddress?: TProgramAddress } +): ReallocateInstruction< + TProgramAddress, + TAccountRecordAccount, + TAccountAuthority +> { + // Program address. + const programAddress = config?.programAddress ?? SPL_RECORD_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + recordAccount: { value: input.recordAccount ?? null, isWritable: true }, + authority: { value: input.authority ?? null, isWritable: false }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + return Object.freeze({ + accounts: [ + getAccountMeta(accounts.recordAccount), + getAccountMeta(accounts.authority), + ], + data: getReallocateInstructionDataEncoder().encode( + args as ReallocateInstructionDataArgs + ), + programAddress, + } as ReallocateInstruction< + TProgramAddress, + TAccountRecordAccount, + TAccountAuthority + >); +} + +export type ParsedReallocateInstruction< + TProgram extends string = typeof SPL_RECORD_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address; + accounts: { + recordAccount: TAccountMetas[0]; + authority: TAccountMetas[1]; + }; + data: ReallocateInstructionData; +}; + +export function parseReallocateInstruction< + TProgram extends string, + TAccountMetas extends readonly AccountMeta[], +>( + instruction: Instruction & + InstructionWithAccounts & + InstructionWithData +): ParsedReallocateInstruction { + if (instruction.accounts.length < 2) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { recordAccount: getNextAccount(), authority: getNextAccount() }, + data: getReallocateInstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/js/src/generated/instructions/setAuthority.ts b/clients/js/src/generated/instructions/setAuthority.ts new file mode 100644 index 0000000..2e72bcd --- /dev/null +++ b/clients/js/src/generated/instructions/setAuthority.ts @@ -0,0 +1,188 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + combineCodec, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type FixedSizeCodec, + type FixedSizeDecoder, + type FixedSizeEncoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlyAccount, + type ReadonlySignerAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, +} from '@solana/kit'; +import { SPL_RECORD_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const SET_AUTHORITY_DISCRIMINATOR = 2; + +export function getSetAuthorityDiscriminatorBytes() { + return getU8Encoder().encode(SET_AUTHORITY_DISCRIMINATOR); +} + +export type SetAuthorityInstruction< + TProgram extends string = typeof SPL_RECORD_PROGRAM_ADDRESS, + TAccountRecordAccount extends string | AccountMeta = string, + TAccountAuthority extends string | AccountMeta = string, + TAccountNewAuthority extends string | AccountMeta = string, + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountRecordAccount extends string + ? WritableAccount + : TAccountRecordAccount, + TAccountAuthority extends string + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountAuthority, + TAccountNewAuthority extends string + ? ReadonlyAccount + : TAccountNewAuthority, + ...TRemainingAccounts, + ] + >; + +export type SetAuthorityInstructionData = { discriminator: number }; + +export type SetAuthorityInstructionDataArgs = {}; + +export function getSetAuthorityInstructionDataEncoder(): FixedSizeEncoder { + return transformEncoder( + getStructEncoder([['discriminator', getU8Encoder()]]), + (value) => ({ ...value, discriminator: SET_AUTHORITY_DISCRIMINATOR }) + ); +} + +export function getSetAuthorityInstructionDataDecoder(): FixedSizeDecoder { + return getStructDecoder([['discriminator', getU8Decoder()]]); +} + +export function getSetAuthorityInstructionDataCodec(): FixedSizeCodec< + SetAuthorityInstructionDataArgs, + SetAuthorityInstructionData +> { + return combineCodec( + getSetAuthorityInstructionDataEncoder(), + getSetAuthorityInstructionDataDecoder() + ); +} + +export type SetAuthorityInput< + TAccountRecordAccount extends string = string, + TAccountAuthority extends string = string, + TAccountNewAuthority extends string = string, +> = { + recordAccount: Address; + authority: TransactionSigner; + newAuthority: Address; +}; + +export function getSetAuthorityInstruction< + TAccountRecordAccount extends string, + TAccountAuthority extends string, + TAccountNewAuthority extends string, + TProgramAddress extends Address = typeof SPL_RECORD_PROGRAM_ADDRESS, +>( + input: SetAuthorityInput< + TAccountRecordAccount, + TAccountAuthority, + TAccountNewAuthority + >, + config?: { programAddress?: TProgramAddress } +): SetAuthorityInstruction< + TProgramAddress, + TAccountRecordAccount, + TAccountAuthority, + TAccountNewAuthority +> { + // Program address. + const programAddress = config?.programAddress ?? SPL_RECORD_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + recordAccount: { value: input.recordAccount ?? null, isWritable: true }, + authority: { value: input.authority ?? null, isWritable: false }, + newAuthority: { value: input.newAuthority ?? null, isWritable: false }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + return Object.freeze({ + accounts: [ + getAccountMeta(accounts.recordAccount), + getAccountMeta(accounts.authority), + getAccountMeta(accounts.newAuthority), + ], + data: getSetAuthorityInstructionDataEncoder().encode({}), + programAddress, + } as SetAuthorityInstruction< + TProgramAddress, + TAccountRecordAccount, + TAccountAuthority, + TAccountNewAuthority + >); +} + +export type ParsedSetAuthorityInstruction< + TProgram extends string = typeof SPL_RECORD_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address; + accounts: { + recordAccount: TAccountMetas[0]; + authority: TAccountMetas[1]; + newAuthority: TAccountMetas[2]; + }; + data: SetAuthorityInstructionData; +}; + +export function parseSetAuthorityInstruction< + TProgram extends string, + TAccountMetas extends readonly AccountMeta[], +>( + instruction: Instruction & + InstructionWithAccounts & + InstructionWithData +): ParsedSetAuthorityInstruction { + if (instruction.accounts.length < 3) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { + recordAccount: getNextAccount(), + authority: getNextAccount(), + newAuthority: getNextAccount(), + }, + data: getSetAuthorityInstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/js/src/generated/instructions/write.ts b/clients/js/src/generated/instructions/write.ts new file mode 100644 index 0000000..7d65e94 --- /dev/null +++ b/clients/js/src/generated/instructions/write.ts @@ -0,0 +1,193 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + addDecoderSizePrefix, + addEncoderSizePrefix, + combineCodec, + getBytesDecoder, + getBytesEncoder, + getStructDecoder, + getStructEncoder, + getU32Decoder, + getU32Encoder, + getU64Decoder, + getU64Encoder, + getU8Decoder, + getU8Encoder, + transformEncoder, + type AccountMeta, + type AccountSignerMeta, + type Address, + type Codec, + type Decoder, + type Encoder, + type Instruction, + type InstructionWithAccounts, + type InstructionWithData, + type ReadonlySignerAccount, + type ReadonlyUint8Array, + type TransactionSigner, + type WritableAccount, +} from '@solana/kit'; +import { SPL_RECORD_PROGRAM_ADDRESS } from '../programs'; +import { getAccountMetaFactory, type ResolvedAccount } from '../shared'; + +export const WRITE_DISCRIMINATOR = 1; + +export function getWriteDiscriminatorBytes() { + return getU8Encoder().encode(WRITE_DISCRIMINATOR); +} + +export type WriteInstruction< + TProgram extends string = typeof SPL_RECORD_PROGRAM_ADDRESS, + TAccountRecordAccount extends string | AccountMeta = string, + TAccountAuthority extends string | AccountMeta = string, + TRemainingAccounts extends readonly AccountMeta[] = [], +> = Instruction & + InstructionWithData & + InstructionWithAccounts< + [ + TAccountRecordAccount extends string + ? WritableAccount + : TAccountRecordAccount, + TAccountAuthority extends string + ? ReadonlySignerAccount & + AccountSignerMeta + : TAccountAuthority, + ...TRemainingAccounts, + ] + >; + +export type WriteInstructionData = { + discriminator: number; + offset: bigint; + data: ReadonlyUint8Array; +}; + +export type WriteInstructionDataArgs = { + offset: number | bigint; + data: ReadonlyUint8Array; +}; + +export function getWriteInstructionDataEncoder(): Encoder { + return transformEncoder( + getStructEncoder([ + ['discriminator', getU8Encoder()], + ['offset', getU64Encoder()], + ['data', addEncoderSizePrefix(getBytesEncoder(), getU32Encoder())], + ]), + (value) => ({ ...value, discriminator: WRITE_DISCRIMINATOR }) + ); +} + +export function getWriteInstructionDataDecoder(): Decoder { + return getStructDecoder([ + ['discriminator', getU8Decoder()], + ['offset', getU64Decoder()], + ['data', addDecoderSizePrefix(getBytesDecoder(), getU32Decoder())], + ]); +} + +export function getWriteInstructionDataCodec(): Codec< + WriteInstructionDataArgs, + WriteInstructionData +> { + return combineCodec( + getWriteInstructionDataEncoder(), + getWriteInstructionDataDecoder() + ); +} + +export type WriteInput< + TAccountRecordAccount extends string = string, + TAccountAuthority extends string = string, +> = { + recordAccount: Address; + authority: TransactionSigner; + offset: WriteInstructionDataArgs['offset']; + data: WriteInstructionDataArgs['data']; +}; + +export function getWriteInstruction< + TAccountRecordAccount extends string, + TAccountAuthority extends string, + TProgramAddress extends Address = typeof SPL_RECORD_PROGRAM_ADDRESS, +>( + input: WriteInput, + config?: { programAddress?: TProgramAddress } +): WriteInstruction { + // Program address. + const programAddress = config?.programAddress ?? SPL_RECORD_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + recordAccount: { value: input.recordAccount ?? null, isWritable: true }, + authority: { value: input.authority ?? null, isWritable: false }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + const getAccountMeta = getAccountMetaFactory(programAddress, 'programId'); + return Object.freeze({ + accounts: [ + getAccountMeta(accounts.recordAccount), + getAccountMeta(accounts.authority), + ], + data: getWriteInstructionDataEncoder().encode( + args as WriteInstructionDataArgs + ), + programAddress, + } as WriteInstruction< + TProgramAddress, + TAccountRecordAccount, + TAccountAuthority + >); +} + +export type ParsedWriteInstruction< + TProgram extends string = typeof SPL_RECORD_PROGRAM_ADDRESS, + TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[], +> = { + programAddress: Address; + accounts: { + recordAccount: TAccountMetas[0]; + authority: TAccountMetas[1]; + }; + data: WriteInstructionData; +}; + +export function parseWriteInstruction< + TProgram extends string, + TAccountMetas extends readonly AccountMeta[], +>( + instruction: Instruction & + InstructionWithAccounts & + InstructionWithData +): ParsedWriteInstruction { + if (instruction.accounts.length < 2) { + // TODO: Coded error. + throw new Error('Not enough accounts'); + } + let accountIndex = 0; + const getNextAccount = () => { + const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!; + accountIndex += 1; + return accountMeta; + }; + return { + programAddress: instruction.programAddress, + accounts: { recordAccount: getNextAccount(), authority: getNextAccount() }, + data: getWriteInstructionDataDecoder().decode(instruction.data), + }; +} diff --git a/clients/js/src/generated/programs/index.ts b/clients/js/src/generated/programs/index.ts new file mode 100644 index 0000000..813c8a5 --- /dev/null +++ b/clients/js/src/generated/programs/index.ts @@ -0,0 +1,9 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +export * from './splRecord'; diff --git a/clients/js/src/generated/programs/splRecord.ts b/clients/js/src/generated/programs/splRecord.ts new file mode 100644 index 0000000..f9191a0 --- /dev/null +++ b/clients/js/src/generated/programs/splRecord.ts @@ -0,0 +1,91 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + containsBytes, + getU8Encoder, + type Address, + type ReadonlyUint8Array, +} from '@solana/kit'; +import { + type ParsedCloseAccountInstruction, + type ParsedInitializeInstruction, + type ParsedReallocateInstruction, + type ParsedSetAuthorityInstruction, + type ParsedWriteInstruction, +} from '../instructions'; + +export const SPL_RECORD_PROGRAM_ADDRESS = + 'recr1L3PCGKLbckBqMNcJhuuyU1zgo8nBhfLVsJNwr5' as Address<'recr1L3PCGKLbckBqMNcJhuuyU1zgo8nBhfLVsJNwr5'>; + +export enum SplRecordAccount { + RecordData, +} + +export function identifySplRecordAccount( + account: { data: ReadonlyUint8Array } | ReadonlyUint8Array +): SplRecordAccount { + const data = 'data' in account ? account.data : account; + if (containsBytes(data, getU8Encoder().encode(1), 0)) { + return SplRecordAccount.RecordData; + } + throw new Error( + 'The provided account could not be identified as a splRecord account.' + ); +} + +export enum SplRecordInstruction { + Initialize, + Write, + SetAuthority, + CloseAccount, + Reallocate, +} + +export function identifySplRecordInstruction( + instruction: { data: ReadonlyUint8Array } | ReadonlyUint8Array +): SplRecordInstruction { + const data = 'data' in instruction ? instruction.data : instruction; + if (containsBytes(data, getU8Encoder().encode(0), 0)) { + return SplRecordInstruction.Initialize; + } + if (containsBytes(data, getU8Encoder().encode(1), 0)) { + return SplRecordInstruction.Write; + } + if (containsBytes(data, getU8Encoder().encode(2), 0)) { + return SplRecordInstruction.SetAuthority; + } + if (containsBytes(data, getU8Encoder().encode(3), 0)) { + return SplRecordInstruction.CloseAccount; + } + if (containsBytes(data, getU8Encoder().encode(4), 0)) { + return SplRecordInstruction.Reallocate; + } + throw new Error( + 'The provided instruction could not be identified as a splRecord instruction.' + ); +} + +export type ParsedSplRecordInstruction< + TProgram extends string = 'recr1L3PCGKLbckBqMNcJhuuyU1zgo8nBhfLVsJNwr5', +> = + | ({ + instructionType: SplRecordInstruction.Initialize; + } & ParsedInitializeInstruction) + | ({ + instructionType: SplRecordInstruction.Write; + } & ParsedWriteInstruction) + | ({ + instructionType: SplRecordInstruction.SetAuthority; + } & ParsedSetAuthorityInstruction) + | ({ + instructionType: SplRecordInstruction.CloseAccount; + } & ParsedCloseAccountInstruction) + | ({ + instructionType: SplRecordInstruction.Reallocate; + } & ParsedReallocateInstruction); diff --git a/clients/js/src/generated/shared/index.ts b/clients/js/src/generated/shared/index.ts new file mode 100644 index 0000000..83a3183 --- /dev/null +++ b/clients/js/src/generated/shared/index.ts @@ -0,0 +1,164 @@ +/** + * This code was AUTOGENERATED using the Codama library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun Codama to update it. + * + * @see https://github.com/codama-idl/codama + */ + +import { + AccountRole, + isProgramDerivedAddress, + isTransactionSigner as kitIsTransactionSigner, + type AccountMeta, + type AccountSignerMeta, + type Address, + type ProgramDerivedAddress, + type TransactionSigner, + upgradeRoleToSigner, +} from '@solana/kit'; + +/** + * Asserts that the given value is not null or undefined. + * @internal + */ +export function expectSome(value: T | null | undefined): T { + if (value === null || value === undefined) { + throw new Error('Expected a value but received null or undefined.'); + } + return value; +} + +/** + * Asserts that the given value is a PublicKey. + * @internal + */ +export function expectAddress( + value: + | Address + | ProgramDerivedAddress + | TransactionSigner + | null + | undefined +): Address { + if (!value) { + throw new Error('Expected a Address.'); + } + if (typeof value === 'object' && 'address' in value) { + return value.address; + } + if (Array.isArray(value)) { + return value[0] as Address; + } + return value as Address; +} + +/** + * Asserts that the given value is a PDA. + * @internal + */ +export function expectProgramDerivedAddress( + value: + | Address + | ProgramDerivedAddress + | TransactionSigner + | null + | undefined +): ProgramDerivedAddress { + if (!value || !Array.isArray(value) || !isProgramDerivedAddress(value)) { + throw new Error('Expected a ProgramDerivedAddress.'); + } + return value; +} + +/** + * Asserts that the given value is a TransactionSigner. + * @internal + */ +export function expectTransactionSigner( + value: + | Address + | ProgramDerivedAddress + | TransactionSigner + | null + | undefined +): TransactionSigner { + if (!value || !isTransactionSigner(value)) { + throw new Error('Expected a TransactionSigner.'); + } + return value; +} + +/** + * Defines an instruction account to resolve. + * @internal + */ +export type ResolvedAccount< + T extends string = string, + U extends + | Address + | ProgramDerivedAddress + | TransactionSigner + | null = + | Address + | ProgramDerivedAddress + | TransactionSigner + | null, +> = { + isWritable: boolean; + value: U; +}; + +/** + * Defines an instruction that stores additional bytes on-chain. + * @internal + */ +export type InstructionWithByteDelta = { + byteDelta: number; +}; + +/** + * Get account metas and signers from resolved accounts. + * @internal + */ +export function getAccountMetaFactory( + programAddress: Address, + optionalAccountStrategy: 'omitted' | 'programId' +) { + return ( + account: ResolvedAccount + ): AccountMeta | AccountSignerMeta | undefined => { + if (!account.value) { + if (optionalAccountStrategy === 'omitted') return; + return Object.freeze({ + address: programAddress, + role: AccountRole.READONLY, + }); + } + + const writableRole = account.isWritable + ? AccountRole.WRITABLE + : AccountRole.READONLY; + return Object.freeze({ + address: expectAddress(account.value), + role: isTransactionSigner(account.value) + ? upgradeRoleToSigner(writableRole) + : writableRole, + ...(isTransactionSigner(account.value) ? { signer: account.value } : {}), + }); + }; +} + +export function isTransactionSigner( + value: + | Address + | ProgramDerivedAddress + | TransactionSigner +): value is TransactionSigner { + return ( + !!value && + typeof value === 'object' && + 'address' in value && + kitIsTransactionSigner(value) + ); +} diff --git a/clients/js/src/index.ts b/clients/js/src/index.ts new file mode 100644 index 0000000..6bebd37 --- /dev/null +++ b/clients/js/src/index.ts @@ -0,0 +1,3 @@ +export * from "./constants"; +export * from "./generated"; +export * from "./actions"; diff --git a/clients/js/test/_setup.ts b/clients/js/test/_setup.ts new file mode 100644 index 0000000..b3495a8 --- /dev/null +++ b/clients/js/test/_setup.ts @@ -0,0 +1,94 @@ +import { + BaseTransactionMessage, + Commitment, + Instruction, + Rpc, + RpcSubscriptions, + SolanaRpcApi, + SolanaRpcSubscriptionsApi, + TransactionMessageWithBlockhashLifetime, + TransactionMessageWithFeePayer, + TransactionSigner, + airdropFactory, + appendTransactionMessageInstructions, + assertIsSendableTransaction, + createSolanaRpc, + createSolanaRpcSubscriptions, + createTransactionMessage, + generateKeyPairSigner, + getSignatureFromTransaction, + lamports, + pipe, + sendAndConfirmTransactionFactory, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + signTransactionMessageWithSigners, +} from "@solana/kit"; + +export type Client = { + rpc: Rpc; + rpcSubscriptions: RpcSubscriptions; +}; + +export const createDefaultSolanaClient = (): Client => { + const rpc = createSolanaRpc("http://127.0.0.1:8899"); + const rpcSubscriptions = createSolanaRpcSubscriptions("ws://127.0.0.1:8900"); + return { rpc, rpcSubscriptions }; +}; + +export const generateKeyPairSignerWithSol = async ( + client: Client, + putativeLamports: bigint = 1_000_000_000n, +) => { + const signer = await generateKeyPairSigner(); + await airdropFactory(client)({ + recipientAddress: signer.address, + lamports: lamports(putativeLamports), + commitment: "confirmed", + }); + return signer; +}; + +export const createDefaultTransaction = async ( + client: Client, + feePayer: TransactionSigner, +) => { + const { value: latestBlockhash } = await client.rpc + .getLatestBlockhash() + .send(); + return pipe( + createTransactionMessage({ version: 0 }), + (tx) => setTransactionMessageFeePayerSigner(feePayer, tx), + (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), + ); +}; + +export const signAndSendTransaction = async ( + client: Client, + transactionMessage: BaseTransactionMessage & + TransactionMessageWithFeePayer & + TransactionMessageWithBlockhashLifetime, + commitment: Commitment = "confirmed", +) => { + const signedTransaction = + await signTransactionMessageWithSigners(transactionMessage); + const signature = getSignatureFromTransaction(signedTransaction); + assertIsSendableTransaction(signedTransaction); + await sendAndConfirmTransactionFactory(client)(signedTransaction, { + commitment, + }); + return signature; +}; + +export const sendAndConfirmInstructions = async ( + client: Client, + payer: TransactionSigner, + instructions: Instruction[], +) => { + const signature = await pipe( + await createDefaultTransaction(client, payer), + (tx) => appendTransactionMessageInstructions(instructions, tx), + (tx) => signAndSendTransaction(client, tx), + ); + return signature; +}; diff --git a/clients/js/test/basic.test.ts b/clients/js/test/basic.test.ts new file mode 100644 index 0000000..45c6221 --- /dev/null +++ b/clients/js/test/basic.test.ts @@ -0,0 +1,120 @@ +import test from "ava"; +import { generateKeyPairSigner } from "@solana/kit"; +import { + fetchRecordData, + createRecord, + createWriteInstruction, + reallocateRecord, + createSetAuthorityInstruction, + createCloseRecordInstruction, + RECORD_META_DATA_SIZE, +} from "../src"; +import { + createDefaultSolanaClient, + generateKeyPairSignerWithSol, + sendAndConfirmInstructions, +} from "./_setup"; + +test("basic instructions flow", async (t) => { + const client = createDefaultSolanaClient(); + const payer = await generateKeyPairSignerWithSol(client); + + const recordAuthority = await generateKeyPairSigner(); + const newRecordAuthority = await generateKeyPairSigner(); + + const initialRecordSize = 0n; + const newRecordSize = 5n; + const recordData = new Uint8Array([0, 1, 2, 3, 4]); + + // --- 1. Initialize --- + const { recordKeypair: recordAccount, ixs: createIxs } = await createRecord({ + rpc: client.rpc, + payer, + authority: recordAuthority.address, + dataLength: initialRecordSize, + }); + + await sendAndConfirmInstructions(client, payer, createIxs); + + // Verify Initialize + let accountData = await fetchRecordData(client.rpc, recordAccount.address); + t.is(accountData.data.version, 1); + t.is(accountData.data.authority, recordAuthority.address); + + // --- 2. Reallocate --- + const reallocIxs = await reallocateRecord({ + rpc: client.rpc, + payer, + recordAccount: recordAccount.address, + authority: recordAuthority, + newDataLength: newRecordSize, + }); + + await sendAndConfirmInstructions(client, payer, reallocIxs); + + // Verify Reallocate + let rawAccount = await client.rpc + .getAccountInfo(recordAccount.address, { encoding: "base64" }) + .send(); + // Ensure RECORD_META_DATA_SIZE is defined (it is 33n), convert to Number for subarray + const offset = Number(RECORD_META_DATA_SIZE); + + let actualData = rawAccount.value?.data?.[0] + ? Buffer.from(rawAccount.value.data[0], "base64").subarray(offset) + : new Uint8Array([]); + + t.deepEqual(actualData, Buffer.from([0, 0, 0, 0, 0])); + + // --- 3. Write --- + const writeIx = createWriteInstruction({ + recordAccount: recordAccount.address, + authority: recordAuthority, + offset: 0n, + data: recordData, + }); + + await sendAndConfirmInstructions(client, payer, [writeIx]); + + // Verify Write + rawAccount = await client.rpc + .getAccountInfo(recordAccount.address, { encoding: "base64" }) + .send(); + actualData = rawAccount.value?.data?.[0] + ? Buffer.from(rawAccount.value.data[0], "base64").subarray(offset) + : new Uint8Array([]); + t.deepEqual(actualData, Buffer.from([0, 1, 2, 3, 4])); + + // // --- 4. Set Authority --- + // const setAuthIx = createSetAuthorityInstruction({ + // recordAccount: recordAccount.address, + // authority: recordAuthority, + // newAuthority: newRecordAuthority.address, + // }); + // + // await sendAndConfirmInstructions(client, payer, [setAuthIx]); + // + // // Verify Set Authority + // accountData = await fetchRecordData(client.rpc, recordAccount.address); + // t.is(accountData.data.authority, newRecordAuthority.address); + // + // // --- 5. Close Account --- + // const destination = await generateKeyPairSigner(); + // + // const closeIx = createCloseRecordInstruction({ + // recordAccount: recordAccount.address, + // authority: newRecordAuthority, + // receiver: destination.address, + // }); + // + // await sendAndConfirmInstructions(client, payer, [closeIx]); + // + // // Verify Close + // const closedAccount = await client.rpc + // .getAccountInfo(recordAccount.address) + // .send(); + // t.is(closedAccount.value, null); + // + // // Verify destination received funds + // const destBalance = await client.rpc.getBalance(destination.address).send(); + // t.true(destBalance.value > 0n); +}); diff --git a/clients/js/test/longRecord.test.ts b/clients/js/test/longRecord.test.ts new file mode 100644 index 0000000..1aa9544 --- /dev/null +++ b/clients/js/test/longRecord.test.ts @@ -0,0 +1,79 @@ +import test from "ava"; +import { generateKeyPairSigner } from "@solana/kit"; +import { + createRecord, + createWriteInstruction, + RECORD_META_DATA_SIZE, + RECORD_CHUNK_SIZE_PRE_INITIALIZE, + RECORD_CHUNK_SIZE_POST_INITIALIZE, +} from "../src"; +import { + createDefaultSolanaClient, + generateKeyPairSignerWithSol, + sendAndConfirmInstructions, +} from "./_setup"; + +test("long record data flow", async (t) => { + const client = createDefaultSolanaClient(); + const payer = await generateKeyPairSignerWithSol(client); + const recordAuthority = await generateKeyPairSigner(); + + // Create 5000 bytes of data + const recordData = new Uint8Array(Array(5000).fill(127)); + const recordSize = BigInt(recordData.length); + + // 1. Create Account + Initialize + First Write + + // Step A: Prepare Create/Init instructions + const { recordKeypair: recordAccount, ixs: initIxs } = await createRecord({ + rpc: client.rpc, + payer, + authority: recordAuthority.address, + dataLength: recordSize, + }); + + // Step B: Prepare first write (fits in same tx) + // Safe check for undefined constant + const preInitChunkSize = RECORD_CHUNK_SIZE_PRE_INITIALIZE || 696; + const firstChunk = recordData.slice(0, preInitChunkSize); + + const firstWriteIx = createWriteInstruction({ + recordAccount: recordAccount.address, + authority: recordAuthority, + offset: 0n, + data: firstChunk, + }); + + await sendAndConfirmInstructions(client, payer, [...initIxs, firstWriteIx]); + + // 2. Subsequent Writes (Loop) + const postInitChunkSize = RECORD_CHUNK_SIZE_POST_INITIALIZE || 919; + let offset = preInitChunkSize; + + while (offset < recordData.length) { + const chunk = recordData.slice(offset, offset + postInitChunkSize); + + const writeIx = createWriteInstruction({ + recordAccount: recordAccount.address, + authority: recordAuthority, + offset: BigInt(offset), + data: chunk, + }); + + await sendAndConfirmInstructions(client, payer, [writeIx]); + + offset += postInitChunkSize; + } + + // 3. Verify Data + const rawAccount = await client.rpc + .getAccountInfo(recordAccount.address, { encoding: "base64" }) + .send(); + + const headerSize = Number(RECORD_META_DATA_SIZE); + const actualData = rawAccount.value?.data?.[0] + ? Buffer.from(rawAccount.value.data[0], "base64").subarray(headerSize) + : new Uint8Array([]); + + t.deepEqual(actualData, Buffer.from(recordData)); +}); diff --git a/clients/js/tsconfig.declarations.json b/clients/js/tsconfig.declarations.json new file mode 100644 index 0000000..7c28787 --- /dev/null +++ b/clients/js/tsconfig.declarations.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "./dist/types", + }, + "extends": "./tsconfig.json", + "include": [ + "src" + ] +} diff --git a/clients/js/tsconfig.json b/clients/js/tsconfig.json new file mode 100644 index 0000000..6bb2958 --- /dev/null +++ b/clients/js/tsconfig.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "module": "ESNext", + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "./dist", + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext" + }, + "exclude": [ + "node_modules" + ], + "include": [ + "src", + "test" + ] +} diff --git a/clients/js/tsup.config.ts b/clients/js/tsup.config.ts new file mode 100644 index 0000000..cb10dec --- /dev/null +++ b/clients/js/tsup.config.ts @@ -0,0 +1,27 @@ +import { env } from 'node:process'; +import { defineConfig, Options } from 'tsup'; + +const SHARED_OPTIONS: Options = { + define: { __VERSION__: `"${env.npm_package_version}"` }, + entry: ['./src/index.ts'], + outDir: './dist/src', + outExtension: ({ format }) => ({ js: format === 'cjs' ? '.js' : '.mjs' }), + sourcemap: true, + treeshake: true, +}; + +export default defineConfig(() => [ + // Source. + { ...SHARED_OPTIONS, format: 'cjs' }, + { ...SHARED_OPTIONS, format: 'esm' }, + + // Tests. + { + ...SHARED_OPTIONS, + bundle: false, + entry: ['./test/**/*.ts'], + format: 'cjs', + outDir: './dist/test', + }, +]); + diff --git a/clients/js/typedoc.json b/clients/js/typedoc.json new file mode 100644 index 0000000..2473c00 --- /dev/null +++ b/clients/js/typedoc.json @@ -0,0 +1,8 @@ +{ + "entryPoints": [ + "src/index.ts" + ], + "includeVersion": true, + "readme": "none", + "out": "docs" +} diff --git a/program/idl.json b/program/idl.json new file mode 100644 index 0000000..912989f --- /dev/null +++ b/program/idl.json @@ -0,0 +1,318 @@ +{ + "kind": "rootNode", + "standard": "codama", + "version": "1.0.0", + "program": { + "kind": "programNode", + "name": "splRecord", + "publicKey": "recr1L3PCGKLbckBqMNcJhuuyU1zgo8nBhfLVsJNwr5", + "version": "0.4.0", + "accounts": [ + { + "kind": "accountNode", + "name": "recordData", + "data": { + "kind": "structTypeNode", + "fields": [ + { + "kind": "structFieldTypeNode", + "name": "version", + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 1 + } + }, + { + "kind": "structFieldTypeNode", + "name": "authority", + "type": { + "kind": "publicKeyTypeNode" + } + } + ] + }, + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "version", + "offset": 0 + } + ] + } + ], + "instructions": [ + { + "kind": "instructionNode", + "name": "initialize", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "recordAccount", + "isWritable": true, + "isSigner": false + }, + { + "kind": "instructionAccountNode", + "name": "authority", + "isWritable": false, + "isSigner": false + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "defaultValueStrategy": "omitted", + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 0 + } + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ] + }, + { + "kind": "instructionNode", + "name": "write", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "recordAccount", + "isWritable": true, + "isSigner": false + }, + { + "kind": "instructionAccountNode", + "name": "authority", + "isWritable": false, + "isSigner": true + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "defaultValueStrategy": "omitted", + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 1 + } + }, + { + "kind": "instructionArgumentNode", + "name": "offset", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + } + }, + { + "kind": "instructionArgumentNode", + "name": "data", + "type": { + "kind": "sizePrefixTypeNode", + "type": { + "kind": "bytesTypeNode" + }, + "prefix": { + "kind": "numberTypeNode", + "format": "u32", + "endian": "le" + } + } + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ] + }, + { + "kind": "instructionNode", + "name": "setAuthority", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "recordAccount", + "isWritable": true, + "isSigner": false + }, + { + "kind": "instructionAccountNode", + "name": "authority", + "isWritable": false, + "isSigner": true + }, + { + "kind": "instructionAccountNode", + "name": "newAuthority", + "isWritable": false, + "isSigner": false + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "defaultValueStrategy": "omitted", + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 2 + } + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ] + }, + { + "kind": "instructionNode", + "name": "closeAccount", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "recordAccount", + "isWritable": true, + "isSigner": false + }, + { + "kind": "instructionAccountNode", + "name": "authority", + "isWritable": false, + "isSigner": true + }, + { + "kind": "instructionAccountNode", + "name": "receiver", + "isWritable": true, + "isSigner": false + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "defaultValueStrategy": "omitted", + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 3 + } + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ] + }, + { + "kind": "instructionNode", + "name": "reallocate", + "accounts": [ + { + "kind": "instructionAccountNode", + "name": "recordAccount", + "isWritable": true, + "isSigner": false + }, + { + "kind": "instructionAccountNode", + "name": "authority", + "isWritable": false, + "isSigner": true + } + ], + "arguments": [ + { + "kind": "instructionArgumentNode", + "name": "discriminator", + "defaultValueStrategy": "omitted", + "type": { + "kind": "numberTypeNode", + "format": "u8", + "endian": "le" + }, + "defaultValue": { + "kind": "numberValueNode", + "number": 4 + } + }, + { + "kind": "instructionArgumentNode", + "name": "dataLength", + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + } + } + ], + "discriminators": [ + { + "kind": "fieldDiscriminatorNode", + "name": "discriminator", + "offset": 0 + } + ] + } + ], + "definedTypes": [], + "pdas": [], + "errors": [ + { + "kind": "errorNode", + "name": "incorrectAuthority", + "code": 0, + "message": "Incorrect authority provided on update or delete" + }, + { + "kind": "errorNode", + "name": "overflow", + "code": 1, + "message": "Calculation overflow" + } + ] + }, + "additionalPrograms": [] +}