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
76 changes: 76 additions & 0 deletions clients/js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"name": "@solana-program/spl-record",
"version": "0.1.0",
"description": "JavaScript client for the SPL Record program",
"sideEffects": false,
"module": "./dist/src/index.mjs",
"main": "./dist/src/index.js",
"types": "./dist/types/index.d.ts",
"type": "commonjs",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/src/index.mjs",
"require": "./dist/src/index.js"
}
},
"files": [
"./dist/src",
"./dist/types"
],
"scripts": {
"build": "rimraf dist && tsup && tsc -p ./tsconfig.declarations.json",
"build:docs": "typedoc",
"test": "ava",
"lint": "eslint --ext js,ts,tsx src",
"lint:fix": "eslint --fix --ext js,ts,tsx src",
"format": "prettier --check src test",
"format:fix": "prettier --write src test",
"prepublishOnly": "pnpm build"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/solana-program/token-2022.git"
},
"bugs": {
"url": "https://github.com/solana-program/token-2022/issues"
},
"homepage": "https://github.com/solana-program/token-2022#readme",
"peerDependencies": {
"@solana/kit": "^4.0",
"@solana/sysvars": "^4.0"
},
"devDependencies": {
"@ava/typescript": "^6.0.0",
"@solana-program/system": "^0.9.0",
"@solana/eslint-config-solana": "^3.0.3",
"@solana/kit": "^4.0",
"@types/node": "^24",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"ava": "^6.1.3",
"eslint": "^8.57.0",
"prettier": "^3.3.3",
"rimraf": "^6.0.1",
"tsup": "^8.1.2",
"typedoc": "^0.28.0",
"typescript": "^5.5.3"
},
"ava": {
"nodeArguments": [
"--no-warnings"
],
"typescript": {
"compile": false,
"rewritePaths": {
"test/": "dist/test/"
}
}
},
"packageManager": "[email protected]"
}
193 changes: 193 additions & 0 deletions clients/js/src/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import {
Address,
generateKeyPairSigner,
GetBalanceApi,
GetMinimumBalanceForRentExemptionApi,
Instruction,
KeyPairSigner,
Rpc,
TransactionSigner,
} from "@solana/kit";
import {
getCreateAccountInstruction,
getTransferSolInstruction,
} from "@solana-program/system";
import {
getCloseAccountInstruction,
getInitializeInstruction,
getReallocateInstruction,
getSetAuthorityInstruction,
getWriteInstruction,
SPL_RECORD_PROGRAM_ADDRESS,
} from "./generated";
import { RECORD_META_DATA_SIZE } from "./constants";

export interface CreateRecordArgs {
rpc: Rpc<GetMinimumBalanceForRentExemptionApi>;
payer: KeyPairSigner;
authority: Address;
dataLength: number | bigint;
programId?: Address;
/** Optional: Provide your own keypair for the record account. If not provided, one is generated. */
recordKeypair?: KeyPairSigner;
}

export interface CreateRecordResult {
recordKeypair: KeyPairSigner;
ixs: Instruction[];
}

/**
* High-level function to create and initialize a Record Account.
* Handles rent calculation and system account creation.
*/
export async function createRecord({
rpc,
payer,
authority,
dataLength,
programId = SPL_RECORD_PROGRAM_ADDRESS,
recordKeypair,
}: CreateRecordArgs): Promise<CreateRecordResult> {
const recordSigner = recordKeypair ?? (await generateKeyPairSigner());
const space = RECORD_META_DATA_SIZE + BigInt(dataLength);
const lamports = await rpc.getMinimumBalanceForRentExemption(space).send();

const createAccountIx = getCreateAccountInstruction({
payer: payer,
newAccount: recordSigner,
lamports,
space,
programAddress: programId,
});

const initializeIx = getInitializeInstruction(
{
recordAccount: recordSigner.address,
authority,
},
{ programAddress: programId },
);

return {
recordKeypair: recordSigner,
ixs: [createAccountIx, initializeIx],
};
}

export interface WriteRecordArgs {
recordAccount: Address;
authority: TransactionSigner;
offset: number | bigint;
data: Uint8Array;
programId?: Address;
}

/**
* Creates a Write instruction.
* Note: For large data, you should manually chunk this or use a loop helper.
*/
export function createWriteInstruction(args: WriteRecordArgs): Instruction {
return getWriteInstruction(
{
recordAccount: args.recordAccount,
authority: args.authority,
offset: BigInt(args.offset),
data: args.data,
},
{ programAddress: args.programId },
);
}

export interface ReallocateRecordArgs {
rpc: Rpc<GetBalanceApi & GetMinimumBalanceForRentExemptionApi>;
payer: KeyPairSigner;
recordAccount: Address;
authority: TransactionSigner;
newDataLength: number | bigint;
programId?: Address;
}

/**
* High-level function to reallocate a Record Account.
* Checks if additional lamports are needed for the new size and adds a transfer instruction if so.
*/
export async function reallocateRecord({
rpc,
payer,
recordAccount,
authority,
newDataLength,
programId = SPL_RECORD_PROGRAM_ADDRESS,
}: ReallocateRecordArgs): Promise<Instruction[]> {
const ixs: Instruction[] = [];
const newSpace = RECORD_META_DATA_SIZE + BigInt(newDataLength);
const requiredRent = await rpc
.getMinimumBalanceForRentExemption(newSpace)
.send();
const currentBalance = await rpc.getBalance(recordAccount).send();

if (requiredRent > currentBalance.value) {
const lamportsNeeded = requiredRent - currentBalance.value;
ixs.push(
getTransferSolInstruction({
source: payer,
destination: recordAccount,
amount: lamportsNeeded,
}),
);
}

ixs.push(
getReallocateInstruction(
{
recordAccount,
authority,
dataLength: BigInt(newDataLength),
},
{ programAddress: programId },
),
);

return ixs;
}

export interface SetAuthorityArgs {
recordAccount: Address;
authority: TransactionSigner;
newAuthority: Address;
programId?: Address;
}

export function createSetAuthorityInstruction(
args: SetAuthorityArgs,
): Instruction {
return getSetAuthorityInstruction(
{
recordAccount: args.recordAccount,
authority: args.authority,
newAuthority: args.newAuthority,
},
{ programAddress: args.programId },
);
}

export interface CloseRecordArgs {
recordAccount: Address;
authority: TransactionSigner;
receiver: Address;
programId?: Address;
}

export function createCloseRecordInstruction(
args: CloseRecordArgs,
): Instruction {
return getCloseAccountInstruction(
{
recordAccount: args.recordAccount,
authority: args.authority,
receiver: args.receiver,
},
{ programAddress: args.programId },
);
}
8 changes: 8 additions & 0 deletions clients/js/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** A record account size excluding the record payload */
export const RECORD_META_DATA_SIZE = 33n;

/** Maximum record chunk that can fit inside a transaction when initializing a record account */
export const RECORD_CHUNK_SIZE_PRE_INITIALIZE = 696;

/** Maximum record chunk that can fit inside a transaction when record account already initialized */
export const RECORD_CHUNK_SIZE_POST_INITIALIZE = 917;
9 changes: 9 additions & 0 deletions clients/js/src/generated/accounts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* This code was AUTOGENERATED using the Codama library.
* Please DO NOT EDIT THIS FILE, instead use visitors
* to add features, then rerun Codama to update it.
*
* @see https://github.com/codama-idl/codama
*/

export * from './recordData';
Loading