From 8113c49df50bbd49f20a9a7d70905f9c476c1d8c Mon Sep 17 00:00:00 2001 From: Philip James Date: Fri, 18 Apr 2025 23:03:08 +0300 Subject: [PATCH 1/2] fix spl token metadata could not fetch metadata --- metadata/js/package.json | 5 +- metadata/js/src/client.ts | 75 ------------ metadata/js/src/constants.ts | 4 - metadata/js/src/deserialize.ts | 48 -------- metadata/js/src/index.ts | 197 +++++++++++++++++++++++++++++++- metadata/js/src/instructions.ts | 62 ---------- metadata/js/src/types.ts | 9 -- 7 files changed, 196 insertions(+), 204 deletions(-) delete mode 100644 metadata/js/src/client.ts delete mode 100644 metadata/js/src/constants.ts delete mode 100644 metadata/js/src/deserialize.ts delete mode 100644 metadata/js/src/instructions.ts delete mode 100644 metadata/js/src/types.ts diff --git a/metadata/js/package.json b/metadata/js/package.json index 3053095c..76067b7c 100644 --- a/metadata/js/package.json +++ b/metadata/js/package.json @@ -1,6 +1,6 @@ { "name": "@bbachain/spl-token-metadata", - "version": "0.0.2", + "version": "0.0.5", "author": "BBAChain Labs ", "repository": "https://github.com/bbachain/program-executor", "license": "Apache-2.0", @@ -27,11 +27,10 @@ "clean": "shx rm -rf lib", "build": "yarn clean && tsc -p tsconfig.json; tsc-esm -p tsconfig.json && tsc -p tsconfig.cjs.json", "postbuild": "echo '{\"type\":\"commonjs\"}' > lib/cjs/package.json && echo '{\"type\":\"module\"}' > lib/esm/package.json", - "deploy": "yarn docs && gh-pages --dist docs --dest token/js --dotfiles", + "deploy": "yarn docs && gh-pages --dist docs --dest metadata/js --dotfiles", "test": "yarn test:unit && yarn test:e2e-built && yarn test:e2e-native && yarn test:e2e-2022", "test:unit": "mocha test/unit", "test:e2e-built": "start-server-and-test 'solana-test-validator --bpf-program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA ../../target/deploy/spl_token.so --reset --quiet' http://localhost:8899/health 'mocha test/e2e'", - "test:e2e-2022": "TEST_PROGRAM_ID=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb start-server-and-test 'solana-test-validator --bpf-program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL ../../target/deploy/spl_associated_token_account.so --bpf-program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb ../../target/deploy/spl_token_2022.so --reset --quiet' http://localhost:8899/health 'mocha test/e2e*'", "test:e2e-native": "start-server-and-test 'solana-test-validator --reset --quiet' http://localhost:8899/health 'mocha test/e2e'", "test:build-programs": "cargo build-bpf --manifest-path ../program/Cargo.toml && cargo build-bpf --manifest-path ../program-2022/Cargo.toml && cargo build-bpf --manifest-path ../../associated-token-account/program/Cargo.toml", "docs": "shx rm -rf docs && NODE_OPTIONS=--max_old_space_size=4096 typedoc && shx cp .nojekyll docs/", diff --git a/metadata/js/src/client.ts b/metadata/js/src/client.ts deleted file mode 100644 index 68658a33..00000000 --- a/metadata/js/src/client.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - Connection, - PublicKey, - TransactionInstruction, - SystemProgram, - Transaction, - sendAndConfirmTransaction, - Signer, -} from '@bbachain/web3.js'; -import { METADATA_SEED, PROGRAM_ID } from './constants'; -import { deserializeMetadata } from './deserialize'; -import { encodeInitializeInstruction, encodeUpdateInstruction } from './instructions'; -import { TokenMetadata } from './types'; - -export async function getMetadataPDA(mint: PublicKey): Promise<[PublicKey, number]> { - return await PublicKey.findProgramAddress([Buffer.from(METADATA_SEED), mint.toBuffer()], PROGRAM_ID); -} - -export async function initializeMetadata( - connection: Connection, - payer: Signer, - mint: PublicKey, - name: string, - symbol: string, - uri: string -) { - const [metadataPDA, bump] = await getMetadataPDA(mint); - - const ix = new TransactionInstruction({ - programId: PROGRAM_ID, - keys: [ - { pubkey: metadataPDA, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - { pubkey: payer.publicKey, isSigner: true, isWritable: true }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - ], - data: encodeInitializeInstruction(name, symbol, uri), - }); - - const tx = new Transaction().add(ix); - return await sendAndConfirmTransaction(connection, tx, [payer]); -} - -export async function updateMetadata( - connection: Connection, - payer: Signer, - mint: PublicKey, - name: string, - symbol: string, - uri: string -) { - const [metadataPDA, _] = await getMetadataPDA(mint); - - const ix = new TransactionInstruction({ - programId: PROGRAM_ID, - keys: [ - { pubkey: metadataPDA, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - { pubkey: payer.publicKey, isSigner: true, isWritable: false }, - ], - data: encodeUpdateInstruction(name, symbol, uri), - }); - - const tx = new Transaction().add(ix); - return await sendAndConfirmTransaction(connection, tx, [payer]); -} - -export async function readMetadata(connection: Connection, mint: PublicKey): Promise { - const [metadataPDA] = await getMetadataPDA(mint); - const accountInfo = await connection.getAccountInfo(metadataPDA); - - if (!accountInfo) return null; - - return deserializeMetadata(accountInfo.data); -} diff --git a/metadata/js/src/constants.ts b/metadata/js/src/constants.ts deleted file mode 100644 index cb60513d..00000000 --- a/metadata/js/src/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PublicKey } from '@bbachain/web3.js'; - -export const PROGRAM_ID = new PublicKey('metaAig5QsCBSfstkwqPQxzdjXdUB8JxjfvtvEPNe3F'); -export const METADATA_SEED = 'metadata'; diff --git a/metadata/js/src/deserialize.ts b/metadata/js/src/deserialize.ts deleted file mode 100644 index dd4c34ba..00000000 --- a/metadata/js/src/deserialize.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { PublicKey } from '@bbachain/web3.js'; -import * as borsh from 'borsh'; -import { TokenMetadata } from './types'; - -// Classify for borsh.deserialize -class TokenMetadataBorsh { - mint: Uint8Array; - name: string; - symbol: string; - uri: string; - authority: Uint8Array; - - constructor(props: any) { - this.mint = props.mint; - this.name = props.name; - this.symbol = props.symbol; - this.uri = props.uri; - this.authority = props.authority; - } -} - -const METADATA_SCHEMA = new Map([ - [ - TokenMetadataBorsh, - { - kind: 'struct', - fields: [ - ['mint', [32]], - ['name', 'string'], - ['symbol', 'string'], - ['uri', 'string'], - ['authority', [32]], - ], - }, - ], -]); - -export function deserializeMetadata(data: Buffer): TokenMetadata { - const raw = borsh.deserialize(METADATA_SCHEMA, TokenMetadataBorsh, data); - - return { - mint: new PublicKey(raw.mint), - name: raw.name, - symbol: raw.symbol, - uri: raw.uri, - authority: new PublicKey(raw.authority), - }; -} diff --git a/metadata/js/src/index.ts b/metadata/js/src/index.ts index fa58e117..df334d00 100644 --- a/metadata/js/src/index.ts +++ b/metadata/js/src/index.ts @@ -1,3 +1,194 @@ -export * from './client'; -export * from './deserialize'; -export * from './types'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +// @bbachain/spl-token-metadata — TypeScript SDK (v0.1.0) +// -------------------------------------------------------------- +// * fetches / decodes on‑chain metadata safely +// * builds Initialise / Update instructions +// * no anchor / serializer runtime required – just `@solana/web3.js` + `borsh` +// * FIX: decode now tolerates the extra 1‑byte padding that the Rust program +// leaves at the tail of the account (root cause of the “Unexpected 1 bytes +// after deserialized data” error) +// -------------------------------------------------------------- + +import { Connection, PublicKey, SystemProgram, TransactionInstruction } from '@bbachain/web3.js'; +import { serialize, deserializeUnchecked, Schema } from 'borsh'; + +// ────────────────────────────────────────────────────────────────────────────── +// Constants +// ────────────────────────────────────────────────────────────────────────────── + +export const PROGRAM_ID = new PublicKey('metaAig5QsCBSfstkwqPQxzdjXdUB8JxjfvtvEPNe3F'); +const PDA_SEED = 'metadata'; + +export const MAX_NAME_LEN = 32; +export const MAX_SYMBOL_LEN = 10; +export const MAX_URI_LEN = 200; + +/** + * The on‑chain contract allocates `+1` extra byte when the account is + * created (see `processor.rs::data_size`) but never writes to it. When a + * strict Borsh deserializer parses the raw buffer it therefore complains about + * a single trailing byte, hence the runtime error the user encountered. We + * handle this by deserialising *unchecked* (which ignores the tail) and, as a + * belt‑and‑braces measure, falling back to slicing off that final padding byte + * if the unchecked path ever failed for another reason. + */ +const TAIL_PADDING = 1; + +// ────────────────────────────────────────────────────────────────────────────── +// Borsh schemas +// ────────────────────────────────────────────────────────────────────────────── + +export class InitializeArgs { + name!: string; + symbol!: string; + uri!: string; + constructor(args: InitializeArgs) { + Object.assign(this, args); + } +} + +export class UpdateArgs { + name!: string; + symbol!: string; + uri!: string; + constructor(args: UpdateArgs) { + Object.assign(this, args); + } +} + +export class TokenMetadata { + mint!: Uint8Array; // Pubkey – 32 bytes + name!: string; + symbol!: string; + uri!: string; + authority!: Uint8Array; // Pubkey – 32 bytes + constructor(args: TokenMetadata) { + Object.assign(this, args); + } +} + +const STATE_SCHEMA: Schema = new Map([ + [ + TokenMetadata, + { + kind: 'struct', + fields: [ + ['mint', [32]], + ['name', 'string'], + ['symbol', 'string'], + ['uri', 'string'], + ['authority', [32]], + ], + }, + ], +]); + +const INSTRUCTION_SCHEMA: Schema = new Map([ + [ + InitializeArgs, + { + kind: 'struct', + fields: [ + ['name', 'string'], + ['symbol', 'string'], + ['uri', 'string'], + ], + }, + ], + [ + UpdateArgs, + { + kind: 'struct', + fields: [ + ['name', 'string'], + ['symbol', 'string'], + ['uri', 'string'], + ], + }, + ], +]); + +// ────────────────────────────────────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────────────────────────────────────── + +export enum TokenInstructionKind { + Initialize = 0, + Update = 1, +} + +export const deriveMetadataPDA = (mint: PublicKey): [PublicKey, number] => + PublicKey.findProgramAddressSync([Buffer.from(PDA_SEED), mint.toBuffer()], PROGRAM_ID); + +function encodeInstructionData(kind: TokenInstructionKind, args: InitializeArgs | UpdateArgs): Buffer { + const payload = serialize(INSTRUCTION_SCHEMA, args); + return Buffer.concat([Buffer.from([kind]), Buffer.from(payload)]); +} + +function assertFieldLengths(args: { name: string; symbol: string; uri: string }) { + if (args.name.length > MAX_NAME_LEN) throw new Error('name too long'); + if (args.symbol.length > MAX_SYMBOL_LEN) throw new Error('symbol too long'); + if (args.uri.length > MAX_URI_LEN) throw new Error('uri too long'); +} + +// ────────────────────────────────────────────────────────────────────────────── +// Instruction builders +// ────────────────────────────────────────────────────────────────────────────── + +export const createInitializeMetadataIx = ( + payer: PublicKey, + mint: PublicKey, + args: InitializeArgs +): TransactionInstruction => { + assertFieldLengths(args); + + const [metadataPda] = deriveMetadataPDA(mint); + const keys = [ + { pubkey: metadataPda, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ]; + const data = encodeInstructionData(TokenInstructionKind.Initialize, args); + return new TransactionInstruction({ programId: PROGRAM_ID, keys, data }); +}; + +export const createUpdateMetadataIx = ( + authority: PublicKey, + mint: PublicKey, + args: UpdateArgs +): TransactionInstruction => { + assertFieldLengths(args); + + const [metadataPda] = deriveMetadataPDA(mint); + const keys = [ + { pubkey: metadataPda, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: authority, isSigner: true, isWritable: false }, + ]; + const data = encodeInstructionData(TokenInstructionKind.Update, args); + return new TransactionInstruction({ programId: PROGRAM_ID, keys, data }); +}; + +// ────────────────────────────────────────────────────────────────────────────── +// Read helpers +// ────────────────────────────────────────────────────────────────────────────── + +export const decodeMetadataAccount = (data: Buffer): TokenMetadata => { + // first, a forgiving attempt that ignores any trailing bytes + try { + return deserializeUnchecked(STATE_SCHEMA, TokenMetadata, data); + } catch (e: any) { + // second chance: slice off the occasional 1‑byte tail padding + if (data.length > 0) { + return deserializeUnchecked(STATE_SCHEMA, TokenMetadata, data.subarray(0, data.length - TAIL_PADDING)); + } + throw e; + } +}; + +export const fetchMetadata = async (connection: Connection, mint: PublicKey): Promise => { + const [metadataPda] = deriveMetadataPDA(mint); + const info = await connection.getAccountInfo(metadataPda); + return info ? decodeMetadataAccount(info.data) : null; +}; diff --git a/metadata/js/src/instructions.ts b/metadata/js/src/instructions.ts deleted file mode 100644 index f9de89e4..00000000 --- a/metadata/js/src/instructions.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as borsh from 'borsh'; - -class InitializeInstruction { - instruction = 0; - name: string; - symbol: string; - uri: string; - - constructor(props: { name: string; symbol: string; uri: string }) { - this.name = props.name; - this.symbol = props.symbol; - this.uri = props.uri; - } -} - -class UpdateInstruction { - instruction = 1; - name: string; - symbol: string; - uri: string; - - constructor(props: { name: string; symbol: string; uri: string }) { - this.name = props.name; - this.symbol = props.symbol; - this.uri = props.uri; - } -} - -const InstructionSchema = new Map([ - [ - InitializeInstruction, - { - kind: 'struct', - fields: [ - ['instruction', 'u8'], - ['name', 'string'], - ['symbol', 'string'], - ['uri', 'string'], - ], - }, - ], - [ - UpdateInstruction, - { - kind: 'struct', - fields: [ - ['instruction', 'u8'], - ['name', 'string'], - ['symbol', 'string'], - ['uri', 'string'], - ], - }, - ], -]); - -export function encodeInitializeInstruction(name: string, symbol: string, uri: string): Buffer { - return Buffer.from(borsh.serialize(InstructionSchema, new InitializeInstruction({ name, symbol, uri }))); -} - -export function encodeUpdateInstruction(name: string, symbol: string, uri: string): Buffer { - return Buffer.from(borsh.serialize(InstructionSchema, new UpdateInstruction({ name, symbol, uri }))); -} diff --git a/metadata/js/src/types.ts b/metadata/js/src/types.ts deleted file mode 100644 index ea148041..00000000 --- a/metadata/js/src/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { PublicKey } from '@bbachain/web3.js'; - -export type TokenMetadata = { - mint: PublicKey; - name: string; - symbol: string; - uri: string; - authority: PublicKey; -}; From 0cdbf2ae69b9b6cb221e8ff96c645f36126f833f Mon Sep 17 00:00:00 2001 From: Philip James Date: Sun, 20 Apr 2025 09:09:22 +0300 Subject: [PATCH 2/2] update metadata js lib --- metadata/js/package.json | 2 +- metadata/js/src/constants.ts | 18 +++ metadata/js/src/helpers.ts | 22 ++++ metadata/js/src/index.ts | 200 +------------------------------- metadata/js/src/instructions.ts | 41 +++++++ metadata/js/src/read.ts | 25 ++++ metadata/js/src/schema.ts | 45 +++++++ metadata/js/src/types.ts | 37 ++++++ 8 files changed, 195 insertions(+), 195 deletions(-) create mode 100644 metadata/js/src/constants.ts create mode 100644 metadata/js/src/helpers.ts create mode 100644 metadata/js/src/instructions.ts create mode 100644 metadata/js/src/read.ts create mode 100644 metadata/js/src/schema.ts create mode 100644 metadata/js/src/types.ts diff --git a/metadata/js/package.json b/metadata/js/package.json index 76067b7c..823733b7 100644 --- a/metadata/js/package.json +++ b/metadata/js/package.json @@ -1,6 +1,6 @@ { "name": "@bbachain/spl-token-metadata", - "version": "0.0.5", + "version": "0.0.6", "author": "BBAChain Labs ", "repository": "https://github.com/bbachain/program-executor", "license": "Apache-2.0", diff --git a/metadata/js/src/constants.ts b/metadata/js/src/constants.ts new file mode 100644 index 00000000..ffbfc0ff --- /dev/null +++ b/metadata/js/src/constants.ts @@ -0,0 +1,18 @@ +import { PublicKey } from '@bbachain/web3.js'; + +/** Program ID (replace with your actual address if it changes). */ +export const PROGRAM_ID = new PublicKey('metaAig5QsCBSfstkwqPQxzdjXdUB8JxjfvtvEPNe3F'); + +export const PDA_SEED = 'metadata'; + +/** Maximum on‑chain string lengths (bytes, not UTF‑16 code units). */ +export const MAX_NAME_LEN = 32; +export const MAX_SYMBOL_LEN = 10; +export const MAX_URI_LEN = 200; + +/** + * The Rust program over‑allocates one byte when it creates the account + * (`processor.rs::data_size`), but never writes to it. + * We drop that trailing byte when decoding. + */ +export const TAIL_PADDING = 1; diff --git a/metadata/js/src/helpers.ts b/metadata/js/src/helpers.ts new file mode 100644 index 00000000..1e8cb0b0 --- /dev/null +++ b/metadata/js/src/helpers.ts @@ -0,0 +1,22 @@ +import { PublicKey } from '@bbachain/web3.js'; +import { serialize } from 'borsh'; +import { PROGRAM_ID, PDA_SEED, MAX_NAME_LEN, MAX_SYMBOL_LEN, MAX_URI_LEN } from './constants'; +import { InitializeArgs, UpdateArgs, TokenInstructionKind } from './types'; +import { INSTRUCTION_SCHEMA } from './schema'; + +/** PDA derivation helper (sync, because web3.js v2). */ +export const deriveMetadataPDA = (mint: PublicKey): [PublicKey, number] => + PublicKey.findProgramAddressSync([Buffer.from(PDA_SEED), mint.toBuffer()], PROGRAM_ID); + +/** Length guards (throws early on oversized strings). */ +export function assertFieldLengths(obj: { name: string; symbol: string; uri: string }): void { + if (obj.name.length > MAX_NAME_LEN) throw new Error('name too long'); + if (obj.symbol.length > MAX_SYMBOL_LEN) throw new Error('symbol too long'); + if (obj.uri.length > MAX_URI_LEN) throw new Error('uri too long'); +} + +/** Prepends the enum discriminant to a Borsh‑encoded payload. */ +export function encodeInstructionData(kind: TokenInstructionKind, args: InitializeArgs | UpdateArgs): Buffer { + const data = serialize(INSTRUCTION_SCHEMA, args); + return Buffer.concat([Buffer.from([kind]), data]); +} diff --git a/metadata/js/src/index.ts b/metadata/js/src/index.ts index df334d00..7e1e567f 100644 --- a/metadata/js/src/index.ts +++ b/metadata/js/src/index.ts @@ -1,194 +1,6 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// @bbachain/spl-token-metadata — TypeScript SDK (v0.1.0) -// -------------------------------------------------------------- -// * fetches / decodes on‑chain metadata safely -// * builds Initialise / Update instructions -// * no anchor / serializer runtime required – just `@solana/web3.js` + `borsh` -// * FIX: decode now tolerates the extra 1‑byte padding that the Rust program -// leaves at the tail of the account (root cause of the “Unexpected 1 bytes -// after deserialized data” error) -// -------------------------------------------------------------- - -import { Connection, PublicKey, SystemProgram, TransactionInstruction } from '@bbachain/web3.js'; -import { serialize, deserializeUnchecked, Schema } from 'borsh'; - -// ────────────────────────────────────────────────────────────────────────────── -// Constants -// ────────────────────────────────────────────────────────────────────────────── - -export const PROGRAM_ID = new PublicKey('metaAig5QsCBSfstkwqPQxzdjXdUB8JxjfvtvEPNe3F'); -const PDA_SEED = 'metadata'; - -export const MAX_NAME_LEN = 32; -export const MAX_SYMBOL_LEN = 10; -export const MAX_URI_LEN = 200; - -/** - * The on‑chain contract allocates `+1` extra byte when the account is - * created (see `processor.rs::data_size`) but never writes to it. When a - * strict Borsh deserializer parses the raw buffer it therefore complains about - * a single trailing byte, hence the runtime error the user encountered. We - * handle this by deserialising *unchecked* (which ignores the tail) and, as a - * belt‑and‑braces measure, falling back to slicing off that final padding byte - * if the unchecked path ever failed for another reason. - */ -const TAIL_PADDING = 1; - -// ────────────────────────────────────────────────────────────────────────────── -// Borsh schemas -// ────────────────────────────────────────────────────────────────────────────── - -export class InitializeArgs { - name!: string; - symbol!: string; - uri!: string; - constructor(args: InitializeArgs) { - Object.assign(this, args); - } -} - -export class UpdateArgs { - name!: string; - symbol!: string; - uri!: string; - constructor(args: UpdateArgs) { - Object.assign(this, args); - } -} - -export class TokenMetadata { - mint!: Uint8Array; // Pubkey – 32 bytes - name!: string; - symbol!: string; - uri!: string; - authority!: Uint8Array; // Pubkey – 32 bytes - constructor(args: TokenMetadata) { - Object.assign(this, args); - } -} - -const STATE_SCHEMA: Schema = new Map([ - [ - TokenMetadata, - { - kind: 'struct', - fields: [ - ['mint', [32]], - ['name', 'string'], - ['symbol', 'string'], - ['uri', 'string'], - ['authority', [32]], - ], - }, - ], -]); - -const INSTRUCTION_SCHEMA: Schema = new Map([ - [ - InitializeArgs, - { - kind: 'struct', - fields: [ - ['name', 'string'], - ['symbol', 'string'], - ['uri', 'string'], - ], - }, - ], - [ - UpdateArgs, - { - kind: 'struct', - fields: [ - ['name', 'string'], - ['symbol', 'string'], - ['uri', 'string'], - ], - }, - ], -]); - -// ────────────────────────────────────────────────────────────────────────────── -// Helpers -// ────────────────────────────────────────────────────────────────────────────── - -export enum TokenInstructionKind { - Initialize = 0, - Update = 1, -} - -export const deriveMetadataPDA = (mint: PublicKey): [PublicKey, number] => - PublicKey.findProgramAddressSync([Buffer.from(PDA_SEED), mint.toBuffer()], PROGRAM_ID); - -function encodeInstructionData(kind: TokenInstructionKind, args: InitializeArgs | UpdateArgs): Buffer { - const payload = serialize(INSTRUCTION_SCHEMA, args); - return Buffer.concat([Buffer.from([kind]), Buffer.from(payload)]); -} - -function assertFieldLengths(args: { name: string; symbol: string; uri: string }) { - if (args.name.length > MAX_NAME_LEN) throw new Error('name too long'); - if (args.symbol.length > MAX_SYMBOL_LEN) throw new Error('symbol too long'); - if (args.uri.length > MAX_URI_LEN) throw new Error('uri too long'); -} - -// ────────────────────────────────────────────────────────────────────────────── -// Instruction builders -// ────────────────────────────────────────────────────────────────────────────── - -export const createInitializeMetadataIx = ( - payer: PublicKey, - mint: PublicKey, - args: InitializeArgs -): TransactionInstruction => { - assertFieldLengths(args); - - const [metadataPda] = deriveMetadataPDA(mint); - const keys = [ - { pubkey: metadataPda, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - { pubkey: payer, isSigner: true, isWritable: true }, - { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, - ]; - const data = encodeInstructionData(TokenInstructionKind.Initialize, args); - return new TransactionInstruction({ programId: PROGRAM_ID, keys, data }); -}; - -export const createUpdateMetadataIx = ( - authority: PublicKey, - mint: PublicKey, - args: UpdateArgs -): TransactionInstruction => { - assertFieldLengths(args); - - const [metadataPda] = deriveMetadataPDA(mint); - const keys = [ - { pubkey: metadataPda, isSigner: false, isWritable: true }, - { pubkey: mint, isSigner: false, isWritable: false }, - { pubkey: authority, isSigner: true, isWritable: false }, - ]; - const data = encodeInstructionData(TokenInstructionKind.Update, args); - return new TransactionInstruction({ programId: PROGRAM_ID, keys, data }); -}; - -// ────────────────────────────────────────────────────────────────────────────── -// Read helpers -// ────────────────────────────────────────────────────────────────────────────── - -export const decodeMetadataAccount = (data: Buffer): TokenMetadata => { - // first, a forgiving attempt that ignores any trailing bytes - try { - return deserializeUnchecked(STATE_SCHEMA, TokenMetadata, data); - } catch (e: any) { - // second chance: slice off the occasional 1‑byte tail padding - if (data.length > 0) { - return deserializeUnchecked(STATE_SCHEMA, TokenMetadata, data.subarray(0, data.length - TAIL_PADDING)); - } - throw e; - } -}; - -export const fetchMetadata = async (connection: Connection, mint: PublicKey): Promise => { - const [metadataPda] = deriveMetadataPDA(mint); - const info = await connection.getAccountInfo(metadataPda); - return info ? decodeMetadataAccount(info.data) : null; -}; +export * from './constants'; +export * from './types'; +export * from './schema'; +export * from './helpers'; +export * from './instructions'; +export * from './read'; diff --git a/metadata/js/src/instructions.ts b/metadata/js/src/instructions.ts new file mode 100644 index 00000000..c1956a9e --- /dev/null +++ b/metadata/js/src/instructions.ts @@ -0,0 +1,41 @@ +import { PublicKey, SystemProgram, TransactionInstruction } from '@bbachain/web3.js'; +import { InitializeArgs, UpdateArgs, TokenInstructionKind } from './types'; +import { PROGRAM_ID } from './constants'; +import { assertFieldLengths, deriveMetadataPDA, encodeInstructionData } from './helpers'; + +/** Builds an `Initialize` instruction. */ +export const createInitializeMetadataIx = ( + payer: PublicKey, + mint: PublicKey, + args: InitializeArgs +): TransactionInstruction => { + assertFieldLengths(args); + + const [metadataPda] = deriveMetadataPDA(mint); + const keys = [ + { pubkey: metadataPda, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ]; + const data = encodeInstructionData(TokenInstructionKind.Initialize, args); + return new TransactionInstruction({ programId: PROGRAM_ID, keys, data }); +}; + +/** Builds an `Update` instruction. */ +export const createUpdateMetadataIx = ( + authority: PublicKey, + mint: PublicKey, + args: UpdateArgs +): TransactionInstruction => { + assertFieldLengths(args); + + const [metadataPda] = deriveMetadataPDA(mint); + const keys = [ + { pubkey: metadataPda, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: authority, isSigner: true, isWritable: false }, + ]; + const data = encodeInstructionData(TokenInstructionKind.Update, args); + return new TransactionInstruction({ programId: PROGRAM_ID, keys, data }); +}; diff --git a/metadata/js/src/read.ts b/metadata/js/src/read.ts new file mode 100644 index 00000000..571ff5b0 --- /dev/null +++ b/metadata/js/src/read.ts @@ -0,0 +1,25 @@ +import { Connection, PublicKey } from '@bbachain/web3.js'; +import { deserializeUnchecked } from 'borsh'; +import { STATE_SCHEMA } from './schema'; +import { TokenMetadata } from './types'; +import { deriveMetadataPDA } from './helpers'; +import { TAIL_PADDING } from './constants'; + +/** Deserialises raw account data → `TokenMetadata`, forgiving the 1‑byte tail. */ +export const decodeMetadataAccount = (data: Buffer): TokenMetadata => { + try { + return deserializeUnchecked(STATE_SCHEMA, TokenMetadata, data); + } catch { + if (data.length > TAIL_PADDING) { + return deserializeUnchecked(STATE_SCHEMA, TokenMetadata, data.subarray(0, data.length - TAIL_PADDING)); + } + throw new Error('Failed to decode TokenMetadata'); + } +}; + +/** Fetches & decodes the account; returns `null` if it doesn’t exist. */ +export const fetchMetadata = async (connection: Connection, mint: PublicKey): Promise => { + const [pda] = deriveMetadataPDA(mint); + const info = await connection.getAccountInfo(pda); + return info ? decodeMetadataAccount(info.data) : null; +}; diff --git a/metadata/js/src/schema.ts b/metadata/js/src/schema.ts new file mode 100644 index 00000000..fc11d791 --- /dev/null +++ b/metadata/js/src/schema.ts @@ -0,0 +1,45 @@ +import { Schema } from 'borsh'; +import { InitializeArgs, UpdateArgs, TokenMetadata } from './types'; + +/** Borsh schema for account state. */ +export const STATE_SCHEMA: Schema = new Map([ + [ + TokenMetadata, + { + kind: 'struct', + fields: [ + ['mint', [32]], + ['name', 'string'], + ['symbol', 'string'], + ['uri', 'string'], + ['authority', [32]], + ], + }, + ], +]); + +/** Borsh schema for instruction payloads. */ +export const INSTRUCTION_SCHEMA: Schema = new Map([ + [ + InitializeArgs, + { + kind: 'struct', + fields: [ + ['name', 'string'], + ['symbol', 'string'], + ['uri', 'string'], + ], + }, + ], + [ + UpdateArgs, + { + kind: 'struct', + fields: [ + ['name', 'string'], + ['symbol', 'string'], + ['uri', 'string'], + ], + }, + ], +]); diff --git a/metadata/js/src/types.ts b/metadata/js/src/types.ts new file mode 100644 index 00000000..48316550 --- /dev/null +++ b/metadata/js/src/types.ts @@ -0,0 +1,37 @@ +/** Discriminant used on‑chain for the instruction enum. */ +export enum TokenInstructionKind { + Initialize = 0, + Update = 1, +} + +/** Args for the `Initialize` instruction. */ +export class InitializeArgs { + name!: string; + symbol!: string; + uri!: string; + constructor(props: InitializeArgs) { + Object.assign(this, props); + } +} + +/** Args for the `Update` instruction. */ +export class UpdateArgs { + name!: string; + symbol!: string; + uri!: string; + constructor(props: UpdateArgs) { + Object.assign(this, props); + } +} + +/** Layout of the on‑chain account. */ +export class TokenMetadata { + mint!: Uint8Array; // 32‑byte pubkey + name!: string; + symbol!: string; + uri!: string; + authority!: Uint8Array; // 32‑byte pubkey + constructor(props: TokenMetadata) { + Object.assign(this, props); + } +}