From 7fd415bfd4ce6cdb88281825bc9012bab59a3838 Mon Sep 17 00:00:00 2001 From: amilz <85324096+amilz@users.noreply.github.com> Date: Thu, 6 Aug 2026 10:35:39 -0700 Subject: [PATCH] refactor(basics): replace hand-rolled byte packing with kit codecs Part of DEV-838 --- .../pinocchio/tests/index.test.ts | 110 +++++--------- .../pinocchio/tests/close-account.test.ts | 21 ++- basics/counter/native/package.json | 4 +- basics/counter/native/pnpm-lock.yaml | 18 --- basics/counter/native/tests/counter.test.ts | 12 +- basics/counter/native/ts/accounts/counter.ts | 6 +- basics/favorites/pinocchio/tests/test.ts | 55 ++++--- basics/pda-rent-payer/pinocchio/tests/test.ts | 20 ++- .../pinocchio/tests/test.ts | 19 +-- .../pinocchio/tests/test.ts | 27 ++-- .../realloc/pinocchio/tests/realloc.test.ts | 136 ++++++++++++------ basics/transfer-sol/asm/tests/instruction.ts | 7 +- basics/transfer-sol/pinocchio/tests/test.ts | 18 ++- 13 files changed, 246 insertions(+), 207 deletions(-) diff --git a/basics/account-data/pinocchio/tests/index.test.ts b/basics/account-data/pinocchio/tests/index.test.ts index 5c7208414..9ae0bf5a7 100644 --- a/basics/account-data/pinocchio/tests/index.test.ts +++ b/basics/account-data/pinocchio/tests/index.test.ts @@ -3,7 +3,15 @@ import { type Address, appendTransactionMessageInstruction, createTransactionMessage, + fixDecoderSize, + fixEncoderSize, generateKeyPairSigner, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + getUtf8Decoder, + getUtf8Encoder, type KeyPairSigner, lamports, pipe, @@ -13,67 +21,26 @@ import { import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system'; import { FailedTransactionMetadata, LiteSVM } from 'litesvm'; -interface AddressInfo { - name: string; - house_number: number; - street: string; - city: string; -} - -function toBytes(addressInfo: AddressInfo): Buffer { - const data: number[] = []; - - // Add instruction discriminator - data.push(0); - - // Pad name to 16 bytes (data[1..17]) - const nameBytes = Buffer.from(addressInfo.name, 'utf-8'); - const namePadded = Buffer.alloc(16); - nameBytes.copy(namePadded, 0, 0, Math.min(nameBytes.length, 16)); - data.push(...namePadded); - - // Add 1 byte padding at index 17 - data.push(0); - - // Add house_number at index 18 - data.push(addressInfo.house_number); - - // Pad street to 16 bytes (data[19..35]) - const streetBytes = Buffer.from(addressInfo.street, 'utf-8'); - const streetPadded = Buffer.alloc(16); - streetBytes.copy(streetPadded, 0, 0, Math.min(streetBytes.length, 16)); - data.push(...streetPadded); - - // Add 1 byte padding at index 35 - data.push(0); - - // Pad city to 16 bytes (data[36..52]) - const cityBytes = Buffer.from(addressInfo.city, 'utf-8'); - const cityPadded = Buffer.alloc(16); - cityBytes.copy(cityPadded, 0, 0, Math.min(cityBytes.length, 16)); - data.push(...cityPadded); - - return Buffer.from(data); -} - -function fromBytes(buffer: Buffer): AddressInfo { - // name: bytes 0..16 - const nameBytes = buffer.subarray(0, 16); - const name = nameBytes.toString('utf-8').replace(/\0/g, ''); - - // house_number: byte 17 - const house_number = buffer[17]; - - // street: bytes 18..34 - const streetBytes = buffer.subarray(18, 34); - const street = streetBytes.toString('utf-8').replace(/\0/g, ''); - - // city: bytes 35..51 - const cityBytes = buffer.subarray(35, 51); - const city = cityBytes.toString('utf-8').replace(/\0/g, ''); - - return { name, house_number, street, city }; -} +// The on-chain account stores each field padded to a fixed width, with a single +// alignment byte before `house_number` and before `city`. +const createAddressInfoEncoder = getStructEncoder([ + ['discriminator', getU8Encoder()], + ['name', fixEncoderSize(getUtf8Encoder(), 16)], + ['namePadding', getU8Encoder()], + ['houseNumber', getU8Encoder()], + ['street', fixEncoderSize(getUtf8Encoder(), 16)], + ['streetPadding', getU8Encoder()], + ['city', fixEncoderSize(getUtf8Encoder(), 16)], +]); + +const addressInfoDecoder = getStructDecoder([ + ['name', fixDecoderSize(getUtf8Decoder(), 16)], + ['namePadding', getU8Decoder()], + ['houseNumber', getU8Decoder()], + ['street', fixDecoderSize(getUtf8Decoder(), 16)], + ['streetPadding', getU8Decoder()], + ['city', fixDecoderSize(getUtf8Decoder(), 16)], +]); describe('Account Data!', () => { const litesvm = new LiteSVM(); @@ -97,13 +64,6 @@ describe('Account Data!', () => { console.log(`Payer Address : ${payer.address}`); console.log(`Address Info Acct : ${addressInfoAccount.address}`); - const addressInfo: AddressInfo = { - name: 'Joe C', - house_number: 136, - street: 'Mile High Dr.', - city: 'Solana Beach', - }; - const ix = { programAddress: programId, accounts: [ @@ -115,7 +75,15 @@ describe('Account Data!', () => { { address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer }, { address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY }, ], - data: new Uint8Array(toBytes(addressInfo)), + data: createAddressInfoEncoder.encode({ + discriminator: 0, + name: 'Joe C', + namePadding: 0, + houseNumber: 136, + street: 'Mile High Dr.', + streetPadding: 0, + city: 'Solana Beach', + }), }; const transactionMessage = pipe( @@ -139,10 +107,10 @@ describe('Account Data!', () => { throw new Error('Account not found'); } - const readAddressInfo = fromBytes(Buffer.from(accountInfo.data)); + const readAddressInfo = addressInfoDecoder.decode(accountInfo.data); console.log(`Name : ${readAddressInfo.name}`); - console.log(`House Num: ${readAddressInfo.house_number}`); + console.log(`House Num: ${readAddressInfo.houseNumber}`); console.log(`Street : ${readAddressInfo.street}`); console.log(`City : ${readAddressInfo.city}`); }); diff --git a/basics/close-account/pinocchio/tests/close-account.test.ts b/basics/close-account/pinocchio/tests/close-account.test.ts index bcb950c90..d5316a476 100644 --- a/basics/close-account/pinocchio/tests/close-account.test.ts +++ b/basics/close-account/pinocchio/tests/close-account.test.ts @@ -5,9 +5,15 @@ import { type Address, appendTransactionMessageInstruction, createTransactionMessage, + fixDecoderSize, + fixEncoderSize, generateKeyPairSigner, getAddressEncoder, getProgramDerivedAddress, + getStructEncoder, + getU8Encoder, + getUtf8Decoder, + getUtf8Encoder, type Instruction, type KeyPairSigner, lamports, @@ -23,6 +29,14 @@ const USER_ACCOUNT_SIZE = 16; const CREATE_DISCRIMINATOR = 0; const CLOSE_DISCRIMINATOR = 1; +const createUserEncoder = getStructEncoder([ + ['discriminator', getU8Encoder()], + ['bump', getU8Encoder()], + ['name', fixEncoderSize(getUtf8Encoder(), USER_ACCOUNT_SIZE)], +]); + +const userNameDecoder = fixDecoderSize(getUtf8Decoder(), USER_ACCOUNT_SIZE); + describe('Close Account!', () => { const svm = new LiteSVM(); let programId: Address; @@ -62,13 +76,10 @@ describe('Close Account!', () => { } it('Create the account', async () => { - const name = Buffer.alloc(USER_ACCOUNT_SIZE); - name.write('Jacob'); - const ix = { programAddress: programId, accounts: keys, - data: new Uint8Array(Buffer.concat([Buffer.from([CREATE_DISCRIMINATOR, bump]), name])), + data: createUserEncoder.encode({ discriminator: CREATE_DISCRIMINATOR, bump, name: 'Jacob' }), }; const result = await sendInstruction(ix); @@ -78,7 +89,7 @@ describe('Close Account!', () => { assert(account.exists, 'expected user account to exist'); assert.equal(account.data.length, USER_ACCOUNT_SIZE); assert.equal(account.programAddress, programId, 'expected user account to be owned by the program'); - assert.equal(Buffer.from(account.data.slice(0, 5)).toString(), 'Jacob'); + assert.equal(userNameDecoder.decode(account.data), 'Jacob'); }); it('Close the account', async () => { diff --git a/basics/counter/native/package.json b/basics/counter/native/package.json index 9e491a024..c3bf00869 100644 --- a/basics/counter/native/package.json +++ b/basics/counter/native/package.json @@ -14,7 +14,6 @@ "deploy": "solana program deploy ./program/target/so/program.so" }, "devDependencies": { - "@types/bn.js": "^5.1.0", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", "chai": "^6.2.2", @@ -26,7 +25,6 @@ }, "dependencies": { "@solana/kit": "^7.0.0", - "@solana-program/system": "^0.13.0", - "bn.js": "^5.2.2" + "@solana-program/system": "^0.13.0" } } diff --git a/basics/counter/native/pnpm-lock.yaml b/basics/counter/native/pnpm-lock.yaml index b66da0c1b..0221000cd 100644 --- a/basics/counter/native/pnpm-lock.yaml +++ b/basics/counter/native/pnpm-lock.yaml @@ -14,13 +14,7 @@ importers: '@solana/kit': specifier: ^7.0.0 version: 7.0.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) - bn.js: - specifier: ^5.2.2 - version: 5.2.2 devDependencies: - '@types/bn.js': - specifier: ^5.1.0 - version: 5.1.5 '@types/chai': specifier: ^5.2.3 version: 5.2.3 @@ -1001,9 +995,6 @@ packages: typescript: optional: true - '@types/bn.js@5.1.5': - resolution: {integrity: sha512-V46N0zwKRF5Q00AZ6hWtN0T8gGmDUaUzLWQvHFo5yThtVwK/VCenFY3wXVbOvNfajEpsTfQM4IN9k/d6gUVX3A==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1042,9 +1033,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - bn.js@5.2.2: - resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} - brace-expansion@2.1.3: resolution: {integrity: sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==} @@ -2489,10 +2477,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@types/bn.js@5.1.5': - dependencies: - '@types/node': 26.1.2 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -2522,8 +2506,6 @@ snapshots: balanced-match@1.0.2: {} - bn.js@5.2.2: {} - brace-expansion@2.1.3: dependencies: balanced-match: 1.0.2 diff --git a/basics/counter/native/tests/counter.test.ts b/basics/counter/native/tests/counter.test.ts index 8d53bc4be..d635ee4e1 100644 --- a/basics/counter/native/tests/counter.test.ts +++ b/basics/counter/native/tests/counter.test.ts @@ -67,8 +67,8 @@ describe('Counter Solana Native', () => { // Deserialize the counter & check count has been incremented const counterAccount = deserializeCounterAccount(Buffer.from(counterAccountInfo.data)); - assert(counterAccount.count.toNumber() === 1, 'Expected count to have been 1'); - console.log(`[alloc+increment] count is: ${counterAccount.count.toNumber()}`); + assert(counterAccount.count === 1n, 'Expected count to have been 1'); + console.log(`[alloc+increment] count is: ${counterAccount.count}`); }); it('Test allocate tx and increment tx', async () => { @@ -98,8 +98,8 @@ describe('Counter Solana Native', () => { assert(counterAccountInfo.exists, 'Expected counter account to have been created'); let counterAccount = deserializeCounterAccount(Buffer.from(counterAccountInfo.data)); - assert(counterAccount.count.toNumber() === 0, 'Expected count to have been 0'); - console.log(`[allocate] count is: ${counterAccount.count.toNumber()}`); + assert(counterAccount.count === 0n, 'Expected count to have been 0'); + console.log(`[allocate] count is: ${counterAccount.count}`); // Check increment tx const incrementIx = createIncrementInstruction({ counter }); @@ -118,7 +118,7 @@ describe('Counter Solana Native', () => { assert(counterAccountInfo.exists, 'Expected counter account to have been created'); counterAccount = deserializeCounterAccount(Buffer.from(counterAccountInfo.data)); - assert(counterAccount.count.toNumber() === 1, 'Expected count to have been 1'); - console.log(`[increment] count is: ${counterAccount.count.toNumber()}`); + assert(counterAccount.count === 1n, 'Expected count to have been 1'); + console.log(`[increment] count is: ${counterAccount.count}`); }); }); diff --git a/basics/counter/native/ts/accounts/counter.ts b/basics/counter/native/ts/accounts/counter.ts index e4a7fb42b..026daf316 100644 --- a/basics/counter/native/ts/accounts/counter.ts +++ b/basics/counter/native/ts/accounts/counter.ts @@ -1,7 +1,7 @@ -import BN from 'bn.js'; +import { getU64Decoder } from '@solana/kit'; export type Counter = { - count: BN; + count: bigint; }; export const COUNTER_ACCOUNT_SIZE = 8; @@ -12,6 +12,6 @@ export function deserializeCounterAccount(data: Buffer): Counter { } return { - count: new BN(data, 'le'), + count: getU64Decoder().decode(data), }; } diff --git a/basics/favorites/pinocchio/tests/test.ts b/basics/favorites/pinocchio/tests/test.ts index b98076fcf..177fc6dbd 100644 --- a/basics/favorites/pinocchio/tests/test.ts +++ b/basics/favorites/pinocchio/tests/test.ts @@ -3,9 +3,20 @@ import { type Address, appendTransactionMessageInstruction, createTransactionMessage, + fixDecoderSize, + fixEncoderSize, generateKeyPairSigner, getAddressEncoder, + getArrayDecoder, + getArrayEncoder, getProgramDerivedAddress, + getStructDecoder, + getStructEncoder, + getU8Encoder, + getU64Decoder, + getU64Encoder, + getUtf8Decoder, + getUtf8Encoder, type Instruction, type KeyPairSigner, lamports, @@ -20,11 +31,19 @@ import { FailedTransactionMetadata, LiteSVM } from 'litesvm'; const CREATE_PDA = 1; const GET_PDA = 2; -function fixedBytes(text: string, length: number): Buffer { - const buffer = Buffer.alloc(length); - buffer.write(text, 'utf8'); - return buffer; -} +const createFavoritesEncoder = getStructEncoder([ + ['discriminator', getU8Encoder()], + ['bump', getU8Encoder()], + ['number', getU64Encoder()], + ['color', fixEncoderSize(getUtf8Encoder(), 8)], + ['hobbies', getArrayEncoder(fixEncoderSize(getUtf8Encoder(), 16), { size: 4 })], +]); + +const favoritesDecoder = getStructDecoder([ + ['number', getU64Decoder()], + ['color', fixDecoderSize(getUtf8Decoder(), 8)], + ['hobbies', getArrayDecoder(fixDecoderSize(getUtf8Decoder(), 16), { size: 4 })], +]); describe('Favorites Solana Pinocchio', () => { const svm = new LiteSVM(); @@ -64,15 +83,6 @@ describe('Favorites Solana Pinocchio', () => { } it('Create the favorites PDA', async () => { - const number = Buffer.alloc(8); - number.writeBigUInt64LE(favorites.number); - const data = Buffer.concat([ - Buffer.from([CREATE_PDA, favoritesBump]), - number, - fixedBytes(favorites.color, 8), - ...favorites.hobbies.map(hobby => fixedBytes(hobby, 16)), - ]); - const ix = { programAddress: programId, accounts: [ @@ -80,7 +90,11 @@ describe('Favorites Solana Pinocchio', () => { { address: favoritesPda, role: AccountRole.WRITABLE }, { address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY }, ], - data: new Uint8Array(data), + data: createFavoritesEncoder.encode({ + discriminator: CREATE_PDA, + bump: favoritesBump, + ...favorites, + }), }; const result = await sendInstruction(ix); @@ -89,13 +103,10 @@ describe('Favorites Solana Pinocchio', () => { const account = svm.getAccount(favoritesPda); assert(account.exists); assert.equal(account.programAddress, programId); - const stored = Buffer.from(account.data); - assert.equal(stored.readBigUInt64LE(0), favorites.number); - assert.equal(stored.subarray(8, 8 + favorites.color.length).toString('utf8'), favorites.color); - favorites.hobbies.forEach((hobby, index) => { - const offset = 16 + index * 16; - assert.equal(stored.subarray(offset, offset + hobby.length).toString('utf8'), hobby); - }); + const stored = favoritesDecoder.decode(account.data); + assert.equal(stored.number, favorites.number); + assert.equal(stored.color, favorites.color); + assert.deepEqual(stored.hobbies, favorites.hobbies); }); it('Read the favorites PDA', async () => { diff --git a/basics/pda-rent-payer/pinocchio/tests/test.ts b/basics/pda-rent-payer/pinocchio/tests/test.ts index 8cabebd09..397df571c 100644 --- a/basics/pda-rent-payer/pinocchio/tests/test.ts +++ b/basics/pda-rent-payer/pinocchio/tests/test.ts @@ -5,6 +5,9 @@ import { createTransactionMessage, generateKeyPairSigner, getProgramDerivedAddress, + getStructEncoder, + getU8Encoder, + getU64Encoder, type Instruction, type KeyPairSigner, lamports, @@ -20,6 +23,12 @@ const INIT_RENT_VAULT_DISCRIMINATOR = 0; const CREATE_NEW_ACCOUNT_DISCRIMINATOR = 1; const FUND_LAMPORTS = 1_000_000_000n; +const initRentVaultEncoder = getStructEncoder([ + ['discriminator', getU8Encoder()], + ['bump', getU8Encoder()], + ['lamports', getU64Encoder()], +]); + describe('PDA Rent-Payer', () => { const svm = new LiteSVM(); let programId: Address; @@ -60,11 +69,6 @@ describe('PDA Rent-Payer', () => { } it('Initialize the Rent Vault', async () => { - const data = Buffer.alloc(10); - data.writeUInt8(INIT_RENT_VAULT_DISCRIMINATOR, 0); - data.writeUInt8(bump, 1); - data.writeBigUInt64LE(FUND_LAMPORTS, 2); - const ix = { programAddress: programId, accounts: [ @@ -72,7 +76,11 @@ describe('PDA Rent-Payer', () => { { address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer }, { address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY }, ], - data: new Uint8Array(data), + data: initRentVaultEncoder.encode({ + discriminator: INIT_RENT_VAULT_DISCRIMINATOR, + bump, + lamports: FUND_LAMPORTS, + }), }; const result = await sendInstruction(ix); diff --git a/basics/processing-instructions/pinocchio/tests/test.ts b/basics/processing-instructions/pinocchio/tests/test.ts index b73b9e64f..9f71fc330 100644 --- a/basics/processing-instructions/pinocchio/tests/test.ts +++ b/basics/processing-instructions/pinocchio/tests/test.ts @@ -1,10 +1,13 @@ -import { Buffer } from 'node:buffer'; import { AccountRole, type Address, appendTransactionMessageInstruction, createTransactionMessage, + fixEncoderSize, generateKeyPairSigner, + getStructEncoder, + getU32Encoder, + getUtf8Encoder, type KeyPairSigner, lamports, pipe, @@ -14,6 +17,11 @@ import { import { assert } from 'chai'; import { FailedTransactionMetadata, LiteSVM } from 'litesvm'; +const instructionDataEncoder = getStructEncoder([ + ['name', fixEncoderSize(getUtf8Encoder(), 8)], + ['height', getU32Encoder()], +]); + describe('custom-instruction-data', () => { const svm = new LiteSVM(); let programId: Address; @@ -27,18 +35,11 @@ describe('custom-instruction-data', () => { svm.airdrop(payer.address, lamports(1_000_000_000n)); }); - function encodeInstructionData(name: string, height: number): Buffer { - const data = Buffer.alloc(12); - data.write(name, 0, 8, 'utf8'); - data.writeUInt32LE(height, 8); - return data; - } - async function goToPark(name: string, height: number): Promise { const ix = { programAddress: programId, accounts: [{ address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer }], - data: new Uint8Array(encodeInstructionData(name, height)), + data: instructionDataEncoder.encode({ name, height }), }; const transactionMessage = pipe( diff --git a/basics/program-derived-addresses/pinocchio/tests/test.ts b/basics/program-derived-addresses/pinocchio/tests/test.ts index 000b80b53..a289308bf 100644 --- a/basics/program-derived-addresses/pinocchio/tests/test.ts +++ b/basics/program-derived-addresses/pinocchio/tests/test.ts @@ -1,5 +1,4 @@ import assert from 'node:assert'; -import { Buffer } from 'node:buffer'; import { AccountRole, type Address, @@ -8,6 +7,12 @@ import { generateKeyPairSigner, getAddressEncoder, getProgramDerivedAddress, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + getU32Decoder, + getU32Encoder, type Instruction, type KeyPairSigner, lamports, @@ -18,6 +23,17 @@ import { import { getCreateAccountInstruction, SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system'; import { FailedTransactionMetadata, LiteSVM } from 'litesvm'; +const createPageVisitsEncoder = getStructEncoder([ + ['discriminator', getU8Encoder()], + ['pageVisits', getU32Encoder()], + ['bump', getU8Encoder()], +]); + +const pageVisitsDecoder = getStructDecoder([ + ['pageVisits', getU32Decoder()], + ['bump', getU8Decoder()], +]); + describe('PDAs', () => { const svm = new LiteSVM(); let programId: Address; @@ -55,7 +71,7 @@ describe('PDAs', () => { function readPageVisits(): number { const account = svm.getAccount(pageVisitsPda); assert(account.exists, 'page visits account not found'); - return Buffer.from(account.data).readUInt32LE(0); + return pageVisitsDecoder.decode(account.data).pageVisits; } function incrementInstruction(): Instruction { @@ -79,11 +95,6 @@ describe('PDAs', () => { }); it('Create the page visits tracking PDA', async () => { - const data = Buffer.alloc(6); - data.writeUInt8(0, 0); - data.writeUInt32LE(0, 1); - data.writeUInt8(pageVisitsBump, 5); - const ix = { programAddress: programId, accounts: [ @@ -92,7 +103,7 @@ describe('PDAs', () => { { address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer }, { address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY }, ], - data: new Uint8Array(data), + data: createPageVisitsEncoder.encode({ discriminator: 0, pageVisits: 0, bump: pageVisitsBump }), }; await sendInstruction(ix); diff --git a/basics/realloc/pinocchio/tests/realloc.test.ts b/basics/realloc/pinocchio/tests/realloc.test.ts index 9ec5afd70..a205013fe 100644 --- a/basics/realloc/pinocchio/tests/realloc.test.ts +++ b/basics/realloc/pinocchio/tests/realloc.test.ts @@ -1,10 +1,19 @@ -import { Buffer } from 'node:buffer'; import { AccountRole, type Address, appendTransactionMessageInstruction, createTransactionMessage, + fixDecoderSize, + fixEncoderSize, generateKeyPairSigner, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + getU32Decoder, + getU32Encoder, + getUtf8Decoder, + getUtf8Encoder, type Instruction, type KeyPairSigner, lamports, @@ -16,6 +25,54 @@ import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system'; import { assert } from 'chai'; import { FailedTransactionMetadata, LiteSVM } from 'litesvm'; +const fixedStringEncoder = fixEncoderSize(getUtf8Encoder(), 8); +const fixedStringDecoder = fixDecoderSize(getUtf8Decoder(), 8); + +const createEncoder = getStructEncoder([ + ['discriminator', getU8Encoder()], + ['name', fixedStringEncoder], + ['houseNumber', getU8Encoder()], + ['street', fixedStringEncoder], + ['city', fixedStringEncoder], +]); + +const reallocWithoutZeroInitEncoder = getStructEncoder([ + ['discriminator', getU8Encoder()], + ['state', fixedStringEncoder], + ['zip', getU32Encoder()], +]); + +const reallocWithZeroInitEncoder = getStructEncoder([ + ['discriminator', getU8Encoder()], + ['name', fixedStringEncoder], + ['position', fixedStringEncoder], + ['company', fixedStringEncoder], + ['yearsEmployed', getU8Encoder()], +]); + +const addressInfoDecoder = getStructDecoder([ + ['name', fixedStringDecoder], + ['houseNumber', getU8Decoder()], + ['street', fixedStringDecoder], + ['city', fixedStringDecoder], +]); + +const enhancedAddressInfoDecoder = getStructDecoder([ + ['name', fixedStringDecoder], + ['houseNumber', getU8Decoder()], + ['street', fixedStringDecoder], + ['city', fixedStringDecoder], + ['state', fixedStringDecoder], + ['zip', getU32Decoder()], +]); + +const workInfoDecoder = getStructDecoder([ + ['name', fixedStringDecoder], + ['position', fixedStringDecoder], + ['company', fixedStringDecoder], + ['yearsEmployed', getU8Decoder()], +]); + describe('Realloc!', () => { const svm = new LiteSVM(); let programId: Address; @@ -32,18 +89,6 @@ describe('Realloc!', () => { testAccount = await generateKeyPairSigner(); }); - function fixedString(value: string): Buffer { - const bytes = Buffer.alloc(8); - bytes.write(value, 0, 8, 'utf8'); - return bytes; - } - - function readFixedString(data: Uint8Array, offset: number): string { - return Buffer.from(data.slice(offset, offset + 8)) - .toString('utf8') - .replace(/\0+$/, ''); - } - async function sendInstruction(ix: Instruction) { const transactionMessage = pipe( createTransactionMessage({ version: 0 }), @@ -70,31 +115,28 @@ describe('Realloc!', () => { { address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer }, { address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY }, ], - data: new Uint8Array( - Buffer.concat([ - Buffer.from([0]), - fixedString('Jacob'), - Buffer.from([123]), - fixedString('Main St.'), - fixedString('Chicago'), - ]), - ), + data: createEncoder.encode({ + discriminator: 0, + name: 'Jacob', + houseNumber: 123, + street: 'Main St.', + city: 'Chicago', + }), }; await sendInstruction(ix); const data = getTestAccountData(); assert.strictEqual(data.length, 25); - assert.strictEqual(readFixedString(data, 0), 'Jacob'); - assert.strictEqual(data[8], 123); - assert.strictEqual(readFixedString(data, 9), 'Main St.'); - assert.strictEqual(readFixedString(data, 17), 'Chicago'); + + const addressInfo = addressInfoDecoder.decode(data); + assert.strictEqual(addressInfo.name, 'Jacob'); + assert.strictEqual(addressInfo.houseNumber, 123); + assert.strictEqual(addressInfo.street, 'Main St.'); + assert.strictEqual(addressInfo.city, 'Chicago'); }); it('Reallocate WITHOUT zero init', async () => { - const zip = Buffer.alloc(4); - zip.writeUInt32LE(12345, 0); - const ix = { programAddress: programId, accounts: [ @@ -102,40 +144,42 @@ describe('Realloc!', () => { { address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer }, { address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY }, ], - data: new Uint8Array(Buffer.concat([Buffer.from([1]), fixedString('Illinois'), zip])), + data: reallocWithoutZeroInitEncoder.encode({ discriminator: 1, state: 'Illinois', zip: 12345 }), }; await sendInstruction(ix); const data = getTestAccountData(); assert.strictEqual(data.length, 37); - assert.strictEqual(readFixedString(data, 0), 'Jacob'); - assert.strictEqual(readFixedString(data, 25), 'Illinois'); - assert.strictEqual(Buffer.from(data).readUInt32LE(33), 12345); + + const addressInfo = enhancedAddressInfoDecoder.decode(data); + assert.strictEqual(addressInfo.name, 'Jacob'); + assert.strictEqual(addressInfo.state, 'Illinois'); + assert.strictEqual(addressInfo.zip, 12345); }); it('Reallocate WITH zero init', async () => { const ix = { programAddress: programId, accounts: [{ address: testAccount.address, role: AccountRole.WRITABLE }], - data: new Uint8Array( - Buffer.concat([ - Buffer.from([2]), - fixedString('Perelyn'), - fixedString('Eng'), - fixedString('Anza'), - Buffer.from([2]), - ]), - ), + data: reallocWithZeroInitEncoder.encode({ + discriminator: 2, + name: 'Perelyn', + position: 'Eng', + company: 'Anza', + yearsEmployed: 2, + }), }; await sendInstruction(ix); const data = getTestAccountData(); assert.strictEqual(data.length, 25); - assert.strictEqual(readFixedString(data, 0), 'Perelyn'); - assert.strictEqual(readFixedString(data, 8), 'Eng'); - assert.strictEqual(readFixedString(data, 16), 'Anza'); - assert.strictEqual(data[24], 2); + + const workInfo = workInfoDecoder.decode(data); + assert.strictEqual(workInfo.name, 'Perelyn'); + assert.strictEqual(workInfo.position, 'Eng'); + assert.strictEqual(workInfo.company, 'Anza'); + assert.strictEqual(workInfo.yearsEmployed, 2); }); }); diff --git a/basics/transfer-sol/asm/tests/instruction.ts b/basics/transfer-sol/asm/tests/instruction.ts index 149a24dec..ae02216e5 100644 --- a/basics/transfer-sol/asm/tests/instruction.ts +++ b/basics/transfer-sol/asm/tests/instruction.ts @@ -1,4 +1,4 @@ -import { AccountRole, type Address, type TransactionSigner } from '@solana/kit'; +import { AccountRole, type Address, getU64Encoder, type TransactionSigner } from '@solana/kit'; import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system'; export function createTransferInstruction( @@ -7,9 +7,6 @@ export function createTransferInstruction( programAddress: Address, lamports: bigint, ) { - const data = new Uint8Array(8); - new DataView(data.buffer).setBigUint64(0, lamports, true); - return { programAddress, accounts: [ @@ -17,6 +14,6 @@ export function createTransferInstruction( { address: recipientAddress, role: AccountRole.WRITABLE }, { address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY }, ], - data, + data: getU64Encoder().encode(lamports), }; } diff --git a/basics/transfer-sol/pinocchio/tests/test.ts b/basics/transfer-sol/pinocchio/tests/test.ts index ffeb77780..cba809b23 100644 --- a/basics/transfer-sol/pinocchio/tests/test.ts +++ b/basics/transfer-sol/pinocchio/tests/test.ts @@ -6,6 +6,9 @@ import { appendTransactionMessageInstruction, createTransactionMessage, generateKeyPairSigner, + getStructEncoder, + getU8Encoder, + getU64Encoder, type Instruction, type KeyPairSigner, lamports, @@ -20,6 +23,11 @@ import { FailedTransactionMetadata, LiteSVM } from 'litesvm'; const CPI_TRANSFER_DISCRIMINATOR = 0; const PROGRAM_TRANSFER_DISCRIMINATOR = 1; +const transferInstructionEncoder = getStructEncoder([ + ['discriminator', getU8Encoder()], + ['amount', getU64Encoder()], +]); + describe('transfer-sol', () => { const svm = new LiteSVM(); let programId: Address; @@ -36,10 +44,6 @@ describe('transfer-sol', () => { }); function createTransferInstruction(from: KeyPairSigner, to: Address, discriminator: number): Instruction { - const data = Buffer.alloc(9); - data.writeUInt8(discriminator, 0); - data.writeBigUInt64LE(transferAmount, 1); - const accounts: (AccountMeta | AccountSignerMeta)[] = [ { address: from.address, role: AccountRole.WRITABLE_SIGNER, signer: from }, { address: to, role: AccountRole.WRITABLE }, @@ -48,7 +52,11 @@ describe('transfer-sol', () => { accounts.push({ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY }); } - return { programAddress: programId, accounts, data: new Uint8Array(data) }; + return { + programAddress: programId, + accounts, + data: transferInstructionEncoder.encode({ discriminator, amount: transferAmount }), + }; } async function sendInstruction(ix: Instruction) {