Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions basics/close-account/native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
36 changes: 0 additions & 36 deletions basics/close-account/native/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 16 additions & 2 deletions basics/close-account/native/tests/close-account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 }),
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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);
});
});
37 changes: 4 additions & 33 deletions basics/close-account/native/ts/instructions/close.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,18 @@
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: [
{ address: target, role: AccountRole.WRITABLE },
{ 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 }),
};
}
53 changes: 15 additions & 38 deletions basics/close-account/native/ts/instructions/create.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -41,18 +23,13 @@ export function createCreateUserInstruction(
programId: Address,
name: string,
) {
const instructionObject = new Create({
instruction: MyInstruction.CreateUser,
name,
});

return {
programAddress: programId,
accounts: [
{ address: target, role: AccountRole.WRITABLE },
{ 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 }),
};
}
34 changes: 3 additions & 31 deletions basics/close-account/native/ts/state/index.ts
Original file line number Diff line number Diff line change
@@ -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())]]);
Loading