diff --git a/metadata/js/package.json b/metadata/js/package.json index 3053095c..823733b7 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.6", "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 index cb60513d..ffbfc0ff 100644 --- a/metadata/js/src/constants.ts +++ b/metadata/js/src/constants.ts @@ -1,4 +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 METADATA_SEED = 'metadata'; + +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/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/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 fa58e117..7e1e567f 100644 --- a/metadata/js/src/index.ts +++ b/metadata/js/src/index.ts @@ -1,3 +1,6 @@ -export * from './client'; -export * from './deserialize'; +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 index f9de89e4..c1956a9e 100644 --- a/metadata/js/src/instructions.ts +++ b/metadata/js/src/instructions.ts @@ -1,62 +1,41 @@ -import * as borsh from 'borsh'; +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'; -class InitializeInstruction { - instruction = 0; - name: string; - symbol: string; - uri: string; +/** Builds an `Initialize` instruction. */ +export const createInitializeMetadataIx = ( + payer: PublicKey, + mint: PublicKey, + args: InitializeArgs +): TransactionInstruction => { + assertFieldLengths(args); - constructor(props: { name: string; symbol: string; uri: string }) { - this.name = props.name; - this.symbol = props.symbol; - this.uri = props.uri; - } -} + 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 }); +}; -class UpdateInstruction { - instruction = 1; - name: string; - symbol: string; - uri: string; +/** Builds an `Update` instruction. */ +export const createUpdateMetadataIx = ( + authority: PublicKey, + mint: PublicKey, + args: UpdateArgs +): TransactionInstruction => { + assertFieldLengths(args); - 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 }))); -} + 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 index ea148041..48316550 100644 --- a/metadata/js/src/types.ts +++ b/metadata/js/src/types.ts @@ -1,9 +1,37 @@ -import { PublicKey } from '@bbachain/web3.js'; +/** Discriminant used on‑chain for the instruction enum. */ +export enum TokenInstructionKind { + Initialize = 0, + Update = 1, +} -export type TokenMetadata = { - mint: PublicKey; - name: string; - symbol: string; - uri: string; - authority: PublicKey; -}; +/** 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); + } +}