diff --git a/clients/js/src/actions.ts b/clients/js/src/actions.ts index 747125b..0e77168 100644 --- a/clients/js/src/actions.ts +++ b/clients/js/src/actions.ts @@ -1,14 +1,20 @@ import { Address, + createAddressWithSeed, generateKeyPairSigner, GetBalanceApi, GetMinimumBalanceForRentExemptionApi, Instruction, KeyPairSigner, + ReadonlyUint8Array, Rpc, TransactionSigner, } from '@solana/kit'; -import { getCreateAccountInstruction, getTransferSolInstruction } from '@solana-program/system'; +import { + getCreateAccountInstruction, + getCreateAccountWithSeedInstruction, + getTransferSolInstruction, +} from '@solana-program/system'; import { getCloseAccountInstruction, getInitializeInstruction, @@ -72,6 +78,68 @@ export async function createRecord({ }; } +export interface CreateRecordWithSeedArgs { + rpc: Rpc; + payer: KeyPairSigner; + authority: Address; + dataLength: number | bigint; + programId?: Address; + seed: ReadonlyUint8Array | string; + /** Optional: Provide your own keypair for the base account. If not provided, the payer is used as base. */ + baseAccount?: KeyPairSigner; +} + +export interface CreateRecordWithSeedResult { + recordAccount: Address; + ixs: Instruction[]; +} + +/** + * High-level function to create and initialize a Record Account with seed. + * Handles rent calculation and system account creation with seed. + */ +export async function createRecordWithSeed({ + rpc, + payer, + authority, + dataLength, + programId = SPL_RECORD_PROGRAM_ADDRESS, + baseAccount = payer, + seed, +}: CreateRecordWithSeedArgs): Promise { + const space = RECORD_META_DATA_SIZE + BigInt(dataLength); + const amount = await rpc.getMinimumBalanceForRentExemption(space).send(); + const recordAccount = await createAddressWithSeed({ + baseAddress: baseAccount.address, + seed, + programAddress: programId, + }); + + const createAccountIx = getCreateAccountWithSeedInstruction({ + payer: payer, + newAccount: recordAccount, + baseAccount: baseAccount, + base: baseAccount.address, + seed: typeof seed === 'string' ? seed : new TextDecoder().decode(seed), + amount, + space, + programAddress: programId, + }); + + const initializeIx = getInitializeInstruction( + { + recordAccount, + authority, + }, + { programAddress: programId }, + ); + + return { + recordAccount, + ixs: [createAccountIx, initializeIx], + }; +} + export interface WriteRecordArgs { recordAccount: Address; authority: TransactionSigner; diff --git a/clients/js/test/createWithSeed.test.ts b/clients/js/test/createWithSeed.test.ts new file mode 100644 index 0000000..2acf0ec --- /dev/null +++ b/clients/js/test/createWithSeed.test.ts @@ -0,0 +1,106 @@ +import { createAddressWithSeed, generateKeyPairSigner } from '@solana/kit'; +import test from 'ava'; +import { createRecordWithSeed, fetchRecordData, SPL_RECORD_PROGRAM_ADDRESS } from '../src'; +import { + createDefaultSolanaClient, + generateKeyPairSignerWithSol, + sendAndConfirmInstructions, +} from './_setup'; + +test('create record with string seed', async t => { + const client = createDefaultSolanaClient(); + const payer = await generateKeyPairSignerWithSol(client); + + const initialRecordSize = 0n; + const seed = 'test-seed'; + + const expectedRecordAccount = await createAddressWithSeed({ + baseAddress: payer.address, + seed, + programAddress: SPL_RECORD_PROGRAM_ADDRESS, + }); + + // Initialize + const { recordAccount, ixs: createIxs } = await createRecordWithSeed({ + rpc: client.rpc, + payer, + authority: payer.address, + dataLength: initialRecordSize, + seed, + }); + + t.deepEqual(recordAccount, expectedRecordAccount); + + await sendAndConfirmInstructions(client, payer, createIxs); + + // Verify Initialize + let accountData = await fetchRecordData(client.rpc, recordAccount); + t.is(accountData.data.version, 1); + t.is(accountData.data.authority, payer.address); +}); + +test('create record with uint8array seed', async t => { + const client = createDefaultSolanaClient(); + const payer = await generateKeyPairSignerWithSol(client); + + const initialRecordSize = 0n; + const seed = new Uint8Array([0, 1, 2, 3, 4]); + + const expectedRecordAccount = await createAddressWithSeed({ + baseAddress: payer.address, + seed, + programAddress: SPL_RECORD_PROGRAM_ADDRESS, + }); + + // Initialize + const { recordAccount, ixs: createIxs } = await createRecordWithSeed({ + rpc: client.rpc, + payer, + authority: payer.address, + dataLength: initialRecordSize, + seed, + }); + + t.deepEqual(recordAccount, expectedRecordAccount); + + await sendAndConfirmInstructions(client, payer, createIxs); + + // Verify Initialize + let accountData = await fetchRecordData(client.rpc, recordAccount); + t.is(accountData.data.version, 1); + t.is(accountData.data.authority, payer.address); +}); + +test('create record with external base account', async t => { + const client = createDefaultSolanaClient(); + const payer = await generateKeyPairSignerWithSol(client); + const baseAccount = await generateKeyPairSigner(); + + const initialRecordSize = 0n; + const seed = 'test-seed'; + + const expectedRecordAccount = await createAddressWithSeed({ + baseAddress: baseAccount.address, + seed, + programAddress: SPL_RECORD_PROGRAM_ADDRESS, + }); + + // Initialize + const { recordAccount, ixs: createIxs } = await createRecordWithSeed({ + rpc: client.rpc, + payer, + authority: payer.address, + dataLength: initialRecordSize, + seed, + baseAccount: baseAccount, + }); + + t.deepEqual(recordAccount, expectedRecordAccount); + + await sendAndConfirmInstructions(client, payer, createIxs); + + // Verify Initialize + let accountData = await fetchRecordData(client.rpc, recordAccount); + t.is(accountData.data.version, 1); + t.is(accountData.data.authority, payer.address); +});