From dbd99db8be536e98201110797aa6e7c5d6106dd2 Mon Sep 17 00:00:00 2001 From: amilz <85324096+amilz@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:28:41 -0700 Subject: [PATCH] refactor(basics/close-account): replace borsh with kit codecs in native example The native example was the last package pinned to borsh 0.7, whose deserialize(schema, Class, buffer) signature no longer exists in borsh 2.x. Instruction data and the User account layout are now expressed as @solana/kit struct codecs, and the test asserts the decoded account name plus the post-close owner and data length. Refs: DEV-838 --- basics/close-account/native/package.json | 3 +- basics/close-account/native/pnpm-lock.yaml | 36 ------------- .../native/tests/close-account.test.ts | 18 ++++++- .../native/ts/instructions/close.ts | 37 ++----------- .../native/ts/instructions/create.ts | 53 ++++++------------- basics/close-account/native/ts/state/index.ts | 34 ++---------- 6 files changed, 39 insertions(+), 142 deletions(-) diff --git a/basics/close-account/native/package.json b/basics/close-account/native/package.json index c28256f92..4b164f961 100644 --- a/basics/close-account/native/package.json +++ b/basics/close-account/native/package.json @@ -8,8 +8,7 @@ }, "dependencies": { "@solana/kit": "^7.0.0", - "@solana-program/system": "^0.13.0", - "borsh": "^0.7.0" + "@solana-program/system": "^0.13.0" }, "devDependencies": { "@types/chai": "^5.2.3", diff --git a/basics/close-account/native/pnpm-lock.yaml b/basics/close-account/native/pnpm-lock.yaml index 1bf5ab7cd..0fc905212 100644 --- a/basics/close-account/native/pnpm-lock.yaml +++ b/basics/close-account/native/pnpm-lock.yaml @@ -14,9 +14,6 @@ importers: '@solana/kit': specifier: ^7.0.0 version: 7.0.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - borsh: - specifier: ^0.7.0 - version: 0.7.0 devDependencies: '@types/chai': specifier: ^5.2.3 @@ -1036,24 +1033,12 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base-x@3.0.11: - resolution: {integrity: sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==} - - bn.js@5.2.2: - resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} - - borsh@0.7.0: - resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} - brace-expansion@2.1.3: resolution: {integrity: sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==} browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - bs58@4.0.1: - resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} - bufferutil@4.0.9: resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} engines: {node: '>=6.14.2'} @@ -1357,9 +1342,6 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} - text-encoding-utf-8@1.0.2: - resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} - tsx@4.23.1: resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} engines: {node: '>=18.0.0'} @@ -2524,28 +2506,12 @@ snapshots: balanced-match@1.0.2: {} - base-x@3.0.11: - dependencies: - safe-buffer: 5.2.1 - - bn.js@5.2.2: {} - - borsh@0.7.0: - dependencies: - bn.js: 5.2.2 - bs58: 4.0.1 - text-encoding-utf-8: 1.0.2 - brace-expansion@2.1.3: dependencies: balanced-match: 1.0.2 browser-stdout@1.3.1: {} - bs58@4.0.1: - dependencies: - base-x: 3.0.11 - bufferutil@4.0.9: dependencies: node-gyp-build: 4.8.4 @@ -2843,8 +2809,6 @@ snapshots: dependencies: has-flag: 4.0.0 - text-encoding-utf-8@1.0.2: {} - tsx@4.23.1: dependencies: esbuild: 0.28.1 diff --git a/basics/close-account/native/tests/close-account.test.ts b/basics/close-account/native/tests/close-account.test.ts index 841e3eb8a..b324ab542 100644 --- a/basics/close-account/native/tests/close-account.test.ts +++ b/basics/close-account/native/tests/close-account.test.ts @@ -11,12 +11,14 @@ import { setTransactionMessageFeePayerSigner, signTransactionMessageWithSigners, } from '@solana/kit'; +import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system'; import { assert } from 'chai'; import { FailedTransactionMetadata, LiteSVM } from 'litesvm'; -import { createCloseUserInstruction, createCreateUserInstruction } from '../ts'; +import { createCloseUserInstruction, createCreateUserInstruction, userDecoder } from '../ts'; describe('Close Account!', () => { const svm = new LiteSVM(); + const userName = 'Jacob'; let programId: Address; let payer: KeyPairSigner; let testAccountAddress: Address; @@ -34,7 +36,7 @@ describe('Close Account!', () => { }); it('Create the account', async () => { - const ix = createCreateUserInstruction(testAccountAddress, payer, programId, 'Jacob'); + const ix = createCreateUserInstruction(testAccountAddress, payer, programId, userName); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), @@ -46,6 +48,11 @@ describe('Close Account!', () => { const result = svm.sendTransaction(signedTx); assert(!(result instanceof FailedTransactionMetadata), `transaction failed: ${result.toString()}`); + + const account = svm.getAccount(testAccountAddress); + assert(account.exists); + assert.equal(account.programAddress, programId); + assert.equal(userDecoder.decode(account.data).name, userName); }); it("An attacker cannot close another user's account", async () => { @@ -88,5 +95,12 @@ describe('Close Account!', () => { const result = svm.sendTransaction(signedTx); assert(!(result instanceof FailedTransactionMetadata), `transaction failed: ${result.toString()}`); + + // Closing resizes the account to zero and hands ownership back to the + // System Program, leaving only the rent-exempt minimum for a 0-byte account. + const account = svm.getAccount(testAccountAddress); + assert(account.exists); + assert.equal(account.programAddress, SYSTEM_PROGRAM_ADDRESS); + assert.equal(account.data.length, 0); }); }); diff --git a/basics/close-account/native/ts/instructions/close.ts b/basics/close-account/native/ts/instructions/close.ts index 80131d2e1..03c33137e 100644 --- a/basics/close-account/native/ts/instructions/close.ts +++ b/basics/close-account/native/ts/instructions/close.ts @@ -1,40 +1,11 @@ -import { Buffer } from 'node:buffer'; -import { AccountRole, type Address, type TransactionSigner } from '@solana/kit'; +import { AccountRole, type Address, getStructEncoder, getU8Encoder, type TransactionSigner } from '@solana/kit'; import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system'; -import * as borsh from 'borsh'; import { MyInstruction } from '.'; -export class Close { - instruction: MyInstruction; - - constructor(props: { instruction: MyInstruction }) { - this.instruction = props.instruction; - } - - toBuffer() { - return Buffer.from(borsh.serialize(CloseSchema, this)); - } - - static fromBuffer(buffer: Buffer) { - return borsh.deserialize(CloseSchema, Close, buffer); - } -} - -export const CloseSchema = new Map([ - [ - Close, - { - kind: 'struct', - fields: [['instruction', 'u8']], - }, - ], -]); +// Instruction data layout, matching the program's `MyInstruction::CloseUser` variant. +export const closeUserEncoder = getStructEncoder([['instruction', getU8Encoder()]]); export function createCloseUserInstruction(target: Address, payer: TransactionSigner, programId: Address) { - const instructionObject = new Close({ - instruction: MyInstruction.CloseUser, - }); - return { programAddress: programId, accounts: [ @@ -42,6 +13,6 @@ export function createCloseUserInstruction(target: Address, payer: TransactionSi { address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer }, { address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY }, ], - data: new Uint8Array(instructionObject.toBuffer()), + data: closeUserEncoder.encode({ instruction: MyInstruction.CloseUser }), }; } diff --git a/basics/close-account/native/ts/instructions/create.ts b/basics/close-account/native/ts/instructions/create.ts index babea1a16..10b6e4a6a 100644 --- a/basics/close-account/native/ts/instructions/create.ts +++ b/basics/close-account/native/ts/instructions/create.ts @@ -1,38 +1,20 @@ -import { Buffer } from 'node:buffer'; -import { AccountRole, type Address, type TransactionSigner } from '@solana/kit'; +import { + AccountRole, + addEncoderSizePrefix, + type Address, + getStructEncoder, + getU8Encoder, + getU32Encoder, + getUtf8Encoder, + type TransactionSigner, +} from '@solana/kit'; import { SYSTEM_PROGRAM_ADDRESS } from '@solana-program/system'; -import * as borsh from 'borsh'; import { MyInstruction } from '.'; -export class Create { - instruction: MyInstruction; - name: string; - - constructor(props: { instruction: MyInstruction; name: string }) { - this.instruction = props.instruction; - this.name = props.name; - } - - toBuffer() { - return Buffer.from(borsh.serialize(CreateSchema, this)); - } - - static fromBuffer(buffer: Buffer) { - return borsh.deserialize(CreateSchema, Create, buffer); - } -} - -export const CreateSchema = new Map([ - [ - Create, - { - kind: 'struct', - fields: [ - ['instruction', 'u8'], - ['name', 'string'], - ], - }, - ], +// Instruction data layout, matching the program's `MyInstruction::CreateUser(User)` variant. +export const createUserEncoder = getStructEncoder([ + ['instruction', getU8Encoder()], + ['name', addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], ]); export function createCreateUserInstruction( @@ -41,11 +23,6 @@ export function createCreateUserInstruction( programId: Address, name: string, ) { - const instructionObject = new Create({ - instruction: MyInstruction.CreateUser, - name, - }); - return { programAddress: programId, accounts: [ @@ -53,6 +30,6 @@ export function createCreateUserInstruction( { address: payer.address, role: AccountRole.WRITABLE_SIGNER, signer: payer }, { address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY }, ], - data: new Uint8Array(instructionObject.toBuffer()), + data: createUserEncoder.encode({ instruction: MyInstruction.CreateUser, name }), }; } diff --git a/basics/close-account/native/ts/state/index.ts b/basics/close-account/native/ts/state/index.ts index 6df225699..95a2b7927 100644 --- a/basics/close-account/native/ts/state/index.ts +++ b/basics/close-account/native/ts/state/index.ts @@ -1,32 +1,4 @@ -import { Buffer } from 'node:buffer'; -import * as borsh from 'borsh'; +import { addDecoderSizePrefix, getStructDecoder, getU32Decoder, getUtf8Decoder } from '@solana/kit'; -export class User { - name: string; - - constructor(props: { name: string }) { - this.name = props.name; - } - - toBase58() { - return borsh.serialize(UserSchema, this).toString(); - } - - toBuffer() { - return Buffer.from(borsh.serialize(UserSchema, this)); - } - - static fromBuffer(buffer: Buffer) { - return borsh.deserialize(UserSchema, User, buffer); - } -} - -export const UserSchema = new Map([ - [ - User, - { - kind: 'struct', - fields: [['name', 'string']], - }, - ], -]); +// Account data layout, matching the program's `User` struct. +export const userDecoder = getStructDecoder([['name', addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())]]);