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())]]);