From 36c2c0e00984d2c6d82b7abcdca6d895f028546b Mon Sep 17 00:00:00 2001 From: Adrian Henry Date: Tue, 9 Jun 2026 17:16:40 +0100 Subject: [PATCH 1/5] chore: Update ore stack --- stacks/ore/.arete/OreStream.stack.json | 46 +++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/stacks/ore/.arete/OreStream.stack.json b/stacks/ore/.arete/OreStream.stack.json index 61eaeb57..cb74b17c 100644 --- a/stacks/ore/.arete/OreStream.stack.json +++ b/stacks/ore/.arete/OreStream.stack.json @@ -4312,6 +4312,50 @@ } ], "instruction_hooks": [ + { + "instruction_type": "ore::DeployIxState", + "actions": [ + { + "RegisterPdaMapping": { + "pda_field": { + "segments": [ + "accounts", + "entropyVar" + ], + "offsets": null + }, + "seed_field": { + "segments": [ + "accounts", + "round" + ], + "offsets": null + }, + "lookup_name": "default_pda_lookup" + } + }, + { + "RegisterPdaMapping": { + "pda_field": { + "segments": [ + "accounts", + "entropyVar" + ], + "offsets": null + }, + "seed_field": { + "segments": [ + "accounts", + "round" + ], + "offsets": null + }, + "lookup_name": "default_pda_lookup" + } + } + ], + "lookup_by": null + }, { "instruction_type": "ore::ResetIxState", "actions": [ @@ -5226,7 +5270,7 @@ "result_type": "Option < f64 >" } ], - "content_hash": "19878988154c1337bf635e15e4b90ce0a04971d337855e5c598030d96a0f9a7f", + "content_hash": "035d43ead768f10318e1f0d37b1daed6110b07935c6ef53b7130cc06bd00d4d4", "views": [ { "id": "OreRound/latest", From ba5a16b8ab2dab2b6e493cf6950798dfd98b2dd6 Mon Sep 17 00:00:00 2001 From: Adrian Henry Date: Fri, 12 Jun 2026 02:06:45 +0100 Subject: [PATCH 2/5] feat: preserve richer IDL metadata in stack specs --- arete-idl/src/parse.rs | 15 ++++++++------- examples/ore-rust/src/generated/ore/types.rs | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/arete-idl/src/parse.rs b/arete-idl/src/parse.rs index 4b371267..d5006e94 100644 --- a/arete-idl/src/parse.rs +++ b/arete-idl/src/parse.rs @@ -692,13 +692,14 @@ fn codama_account_pda( for seed in pda_seeds { match seed { CodamaPdaSeedNode::Constant { seed_type, value } => { - let bytes = codama_constant_seed_bytes(value, seed_type.as_ref()).or_else(|| { - tracing::warn!( - account = %account.name, - "failed to encode constant PDA seed; degrading to user-provided" - ); - None - })?; + let bytes = + codama_constant_seed_bytes(value, seed_type.as_ref()).or_else(|| { + tracing::warn!( + account = %account.name, + "failed to encode constant PDA seed; degrading to user-provided" + ); + None + })?; idl_seeds.push(IdlPdaSeed::Const { value: bytes }); } CodamaPdaSeedNode::Variable { name, seed_type } => { diff --git a/examples/ore-rust/src/generated/ore/types.rs b/examples/ore-rust/src/generated/ore/types.rs index 0ee01fab..a05ca301 100644 --- a/examples/ore-rust/src/generated/ore/types.rs +++ b/examples/ore-rust/src/generated/ore/types.rs @@ -136,7 +136,7 @@ pub struct OreTreasury { #[serde(default)] pub state: OreTreasuryState, #[serde(default)] - pub treasury_snapshot: Option>, + pub treasury_snapshot: Option>, } @@ -232,9 +232,9 @@ pub struct OreMiner { #[serde(default)] pub automation: OreMinerAutomation, #[serde(default)] - pub miner_snapshot: Option>, + pub miner_snapshot: Option>, #[serde(default)] - pub automation_snapshot: Option>, + pub automation_snapshot: Option>, } From 5ac420230432c49e8e2ba927c0ae66a6fd0e5072 Mon Sep 17 00:00:00 2001 From: Adrian Henry Date: Fri, 12 Jun 2026 02:06:55 +0100 Subject: [PATCH 3/5] feat: add typed instruction runtime to the TypeScript SDK --- typescript/core/src/client.test.ts | 88 +++ typescript/core/src/client.ts | 215 ++++++- typescript/core/src/index.ts | 34 +- .../core/src/instructions/account-resolver.ts | 42 +- .../core/src/instructions/confirmation.ts | 114 ---- .../core/src/instructions/error-parser.ts | 18 + typescript/core/src/instructions/executor.ts | 365 +++++++---- typescript/core/src/instructions/index.ts | 32 +- .../src/instructions/instructions.test.ts | 586 ++++++++++++++++++ typescript/core/src/instructions/pda-dsl.ts | 47 +- typescript/core/src/instructions/pda.ts | 136 +++- .../src/instructions/seed-serializer.test.ts | 84 +++ .../core/src/instructions/seed-serializer.ts | 128 ++++ .../core/src/instructions/serializer.ts | 193 +++++- typescript/core/src/types.ts | 15 +- typescript/core/src/wallet/types.ts | 90 ++- typescript/react/src/hooks/use-mutation.ts | 41 +- typescript/react/src/index.ts | 11 +- typescript/react/src/provider.tsx | 3 +- typescript/react/src/stack.ts | 77 ++- 20 files changed, 1915 insertions(+), 404 deletions(-) delete mode 100644 typescript/core/src/instructions/confirmation.ts create mode 100644 typescript/core/src/instructions/instructions.test.ts create mode 100644 typescript/core/src/instructions/seed-serializer.test.ts create mode 100644 typescript/core/src/instructions/seed-serializer.ts diff --git a/typescript/core/src/client.test.ts b/typescript/core/src/client.test.ts index 350d080f..fafce044 100644 --- a/typescript/core/src/client.test.ts +++ b/typescript/core/src/client.test.ts @@ -87,3 +87,91 @@ describe('Frame parsing', () => { }); }); + +describe('Arete instructions (namespaced stacks)', () => { + const SIGNER = 'So11111111111111111111111111111111111111112'; + + async function makeClient(errors: { code: number; name: string; msg: string }[] = []) { + const { Arete, createInstructionHandler } = await import('./index'); + const handler = (programId: string) => + createInstructionHandler({ + programId, + discriminator: [9], + args: [], + accounts: [{ name: 'signer', isSigner: true, isWritable: true, category: 'signer' }], + errors, + }); + + const stack = { + name: 'demo', + url: 'wss://example.invalid', + views: {}, + instructions: { + ore: { close: handler('oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv') }, + entropy: { close: handler('3jSkUuYBoJzQPMEzTvkDFXCZUBksPamrVhrnHR9igu2X') }, + }, + } as const; + + // autoReconnect: false keeps the client fully offline. + return Arete.connect(stack, { autoReconnect: false }); + } + + it('mirrors per-program nesting and builds through the nested path', async () => { + const client = await makeClient(); + const wallet = { + publicKey: SIGNER, + async signAndSend() { + throw new Error('not used'); + }, + }; + + const ix = client.instructions.ore.close.build({}, { wallet }); + expect(ix.programId).toBe('oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv'); + expect(ix.keys[0]!.pubkey).toBe(SIGNER); + + const ix2 = client.instructions.entropy.close.build({}, { wallet }); + expect(ix2.programId).toBe('3jSkUuYBoJzQPMEzTvkDFXCZUBksPamrVhrnHR9igu2X'); + }); + + it('parses program errors in transaction() from aggregated handler metadata', async () => { + const { InstructionError } = await import('./index'); + const client = await makeClient([ + { code: 6000, name: 'SlippageExceeded', msg: 'Slippage tolerance exceeded' }, + ]); + const wallet = { + publicKey: SIGNER, + async signAndSend(): Promise<{ signature: string }> { + throw { InstructionError: [0, { Custom: 6000 }] }; + }, + }; + + const ix = client.instructions.ore.close.build({}, { wallet }); + await expect(client.transaction([ix], { wallet })).rejects.toMatchObject({ + name: 'InstructionError', + programError: { code: 6000, name: 'SlippageExceeded' }, + }); + await expect(client.transaction([ix], { wallet })).rejects.toBeInstanceOf(InstructionError); + }); + + it('prefers explicit errors over aggregated metadata in transaction()', async () => { + const client = await makeClient([ + { code: 6000, name: 'WrongName', msg: 'from aggregate' }, + ]); + const wallet = { + publicKey: SIGNER, + async signAndSend(): Promise<{ signature: string }> { + throw { InstructionError: [0, { Custom: 6000 }] }; + }, + }; + + const ix = client.instructions.ore.close.build({}, { wallet }); + await expect( + client.transaction([ix], { + wallet, + errors: [{ code: 6000, name: 'RightName', msg: 'from override' }], + }) + ).rejects.toMatchObject({ + programError: { code: 6000, name: 'RightName' }, + }); + }); +}); diff --git a/typescript/core/src/client.ts b/typescript/core/src/client.ts index 4e3b7d79..11ee20c6 100644 --- a/typescript/core/src/client.ts +++ b/typescript/core/src/client.ts @@ -16,9 +16,20 @@ import { SortedStorageDecorator } from './storage/sorted-decorator'; import { SubscriptionRegistry } from './subscription'; import { createTypedViews } from './views'; import type { Frame } from './frame'; -import type { WalletAdapter } from './wallet/types'; -import type { InstructionHandler, ExecuteOptions, ExecutionResult } from './instructions'; -import { executeInstruction } from './instructions'; +import type { WalletAdapter, BuiltInstruction, SendOptions } from './wallet/types'; +import type { + InstructionHandler, + ExecuteOptions, + ExecutionResult, + BuildOptions, +} from './instructions'; +import type { ErrorMetadata } from './instructions'; +import { + executeInstruction, + buildInstruction, + parseInstructionError, + InstructionError, +} from './instructions'; export interface ConnectOptions { url?: string; @@ -31,6 +42,8 @@ export interface ConnectOptions { validateFrames?: boolean; /** Authentication configuration */ auth?: import('./types').AuthConfig; + /** Default wallet adapter used for instruction execution (overridable per call). */ + wallet?: WalletAdapter; } /** @deprecated Use ConnectOptions instead */ @@ -39,21 +52,66 @@ export interface AreteOptionsWithStorage extends maxEntriesPerView?: number | null; flushIntervalMs?: number; auth?: import('./types').AuthConfig; + wallet?: WalletAdapter; } -export interface InstructionExecutorOptions extends Omit { - wallet: WalletAdapter; +/** + * Options accepted when calling a typed instruction. + * `wallet` is optional when a default wallet was provided to the client. + */ +export interface InstructionExecutorOptions extends ExecuteOptions { + wallet?: WalletAdapter; } -export type InstructionExecutor = ( - args: Record, - options: InstructionExecutorOptions -) => Promise; +/** + * A typed, callable instruction. + * + * Calling it builds + signs + sends the transaction. The attached `build` + * method is a pure prepare step that returns a {@link BuiltInstruction} for + * batching/composition. + */ +export type TypedInstruction = { + (params: TParams, options?: InstructionExecutorOptions): Promise; + build(params: TParams, options?: BuildOptions): BuiltInstruction; + /** Phantom error type for downstream inference. */ + readonly _error?: TError; +}; + +/** + * Maps one stack-definition instruction entry (handler or per-program map of + * handlers) to its typed call surface. + */ +type TypedInstructionFor = TEntry extends InstructionHandler + ? TypedInstruction + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + TEntry extends Record> + ? { + [K in keyof TEntry]: TEntry[K] extends InstructionHandler + ? TypedInstruction + : TypedInstruction, unknown>; + } + : TypedInstruction, unknown>; + +export type InstructionsInterface< + TInstructions extends Record | undefined, +> = + TInstructions extends Record + ? { [K in keyof TInstructions]: TypedInstructionFor } + : Record; -export type InstructionsInterface | undefined> = - TInstructions extends Record - ? { [K in keyof TInstructions]: InstructionExecutor } - : {}; +/** @deprecated Retained for backward compatibility; prefer {@link TypedInstruction}. */ +export type InstructionExecutor = TypedInstruction, unknown>; + +/** + * Distinguishes a handler from a per-program map of handlers in a stack + * definition's `instructions` block. Handlers are the only entries with a + * `build` function. + */ +export function isInstructionHandler( + entry: import('./types').StackInstructionEntry +): entry is InstructionHandler { + return typeof (entry as InstructionHandler).build === 'function'; +} export class Arete { private readonly connection: ConnectionManager; @@ -63,12 +121,15 @@ export class Arete { private readonly _views: TypedViews; private readonly stack: TStack; private readonly _instructions: InstructionsInterface; + private _wallet?: WalletAdapter; + private _aggregatedErrors?: ErrorMetadata[]; private constructor( url: string, options: AreteOptionsWithStorage ) { this.stack = options.stack; + this._wallet = options.wallet; this.storage = new SortedStorageDecorator(options.storage ?? new MemoryAdapter()); this.processor = new FrameProcessor(this.storage, { maxEntriesPerView: options.maxEntriesPerView, @@ -92,19 +153,56 @@ export class Arete { } private buildInstructions(): InstructionsInterface { - const instructions = {} as Record; - + const instructions = {} as Record< + string, + | TypedInstruction, unknown> + | Record, unknown>> + >; + if (this.stack.instructions) { - for (const [name, handler] of Object.entries(this.stack.instructions)) { - instructions[name] = (args: Record, options: InstructionExecutorOptions) => { - return executeInstruction(handler as InstructionHandler, args, options); - }; + for (const [name, entry] of Object.entries(this.stack.instructions)) { + if (isInstructionHandler(entry)) { + instructions[name] = this.createTypedInstruction(entry); + } else { + // Multi-program stacks namespace handlers one level deep. + const nested: Record, unknown>> = {}; + for (const [instructionName, handler] of Object.entries(entry)) { + nested[instructionName] = this.createTypedInstruction(handler as InstructionHandler); + } + instructions[name] = nested; + } } } - + return instructions as InstructionsInterface; } + private createTypedInstruction( + handler: InstructionHandler + ): TypedInstruction, unknown> { + const fn = (( + params: Record, + options?: InstructionExecutorOptions + ) => { + return executeInstruction(handler, params, this.withWallet(options)); + }) as TypedInstruction, unknown>; + + fn.build = (params: Record, options?: BuildOptions) => { + return buildInstruction(handler, params, this.withWallet(options)); + }; + + return fn; + } + + /** Merge the client's default wallet into call options (call options win). */ + private withWallet(options?: T): T { + const merged = { ...(options ?? {}) } as T; + if (!merged.wallet && this._wallet) { + merged.wallet = this._wallet; + } + return merged; + } + static async connect( stack: T, options?: ConnectOptions @@ -125,6 +223,7 @@ export class Arete { maxReconnectAttempts: options?.maxReconnectAttempts, validateFrames: options?.validateFrames, auth: options?.auth, + wallet: options?.wallet, }; const client = new Arete(url, internalOptions); @@ -144,6 +243,82 @@ export class Arete { return this._instructions; } + /** The default wallet adapter, if one was configured. */ + get wallet(): WalletAdapter | undefined { + return this._wallet; + } + + /** + * Set (or clear) the default wallet adapter used for instruction execution. + * Useful for connecting/disconnecting a wallet after the client is created. + */ + setWallet(wallet: WalletAdapter | undefined): void { + this._wallet = wallet; + } + + /** + * Sign and send a batch of pre-built instructions as a single transaction. + * + * Build instructions with `client.instructions..build(params)` and + * compose them here. RPC/compilation/confirmation are owned by the adapter. + * + * On failure, the error is parsed against `options.errors` when given, + * otherwise against error metadata aggregated from all the stack's handlers + * (deduped by code, first-wins — if the stack bundles programs with + * overlapping error codes, pass `options.errors` or use the per-instruction + * call path for precise attribution). + */ + async transaction( + instructions: BuiltInstruction[], + options?: { wallet?: WalletAdapter; send?: SendOptions; errors?: ErrorMetadata[] } + ): Promise { + const wallet = options?.wallet ?? this._wallet; + if (!wallet) { + throw new Error('Wallet required to sign and send transaction'); + } + try { + const result = await wallet.signAndSend(instructions, options?.send ?? {}); + return { signature: result.signature, slot: result.slot }; + } catch (err) { + const programError = parseInstructionError(err, options?.errors ?? this.aggregateErrors()); + if (programError) { + throw new InstructionError( + `${programError.name} (${programError.code}): ${programError.message}`, + programError, + err + ); + } + throw err; + } + } + + /** Error metadata from every handler in the stack, deduped by code. */ + private aggregateErrors(): ErrorMetadata[] { + if (!this._aggregatedErrors) { + const all: ErrorMetadata[] = []; + const seen = new Set(); + const collect = (handler: InstructionHandler) => { + for (const error of handler.errors ?? []) { + if (!seen.has(error.code)) { + seen.add(error.code); + all.push(error); + } + } + }; + for (const entry of Object.values(this.stack.instructions ?? {})) { + if (isInstructionHandler(entry)) { + collect(entry); + } else { + for (const handler of Object.values(entry)) { + collect(handler as InstructionHandler); + } + } + } + this._aggregatedErrors = all; + } + return this._aggregatedErrors; + } + get connectionState(): ConnectionState { return this.connection.getState(); } diff --git a/typescript/core/src/index.ts b/typescript/core/src/index.ts index 8219c1a1..6ac0eb6d 100644 --- a/typescript/core/src/index.ts +++ b/typescript/core/src/index.ts @@ -1,5 +1,12 @@ export { Arete } from './client'; -export type { AreteOptionsWithStorage, InstructionExecutorOptions, InstructionExecutor } from './client'; +export type { + ConnectOptions, + AreteOptionsWithStorage, + InstructionExecutorOptions, + InstructionExecutor, + TypedInstruction, + InstructionsInterface, +} from './client'; export { ConnectionManager } from './connection'; export { SubscriptionRegistry } from './subscription'; @@ -29,6 +36,7 @@ export type { RichUpdate, ViewDef, StackDefinition, + StackInstructionEntry, ViewGroup, Subscription, Schema, @@ -53,7 +61,16 @@ export type { export { DEFAULT_CONFIG, DEFAULT_MAX_ENTRIES_PER_VIEW, AreteError } from './types'; // Wallet types -export type { WalletAdapter, WalletState, WalletConnectOptions } from './wallet/types'; +export type { + WalletAdapter, + WalletState, + WalletConnectOptions, + BuiltInstruction, + BuiltAccountMeta, + ConfirmationLevel, + SendOptions, + SendResult, +} from './wallet/types'; // Instruction execution export type { @@ -67,14 +84,13 @@ export type { AccountResolutionOptions, ArgSchema, ArgType, - ConfirmationLevel, - ExecuteOptions, - ExecutionResult, ProgramError, ErrorMetadata, InstructionHandler, - InstructionDefinition, - BuiltInstruction, + InstructionHandlerConfig, + BuildOptions, + ExecuteOptions, + ExecutionResult, SeedDef, PdaDeriveContext, PdaFactory, @@ -92,10 +108,12 @@ export { decodeBase58, encodeBase58, serializeInstructionData, - waitForConfirmation, parseInstructionError, formatProgramError, + InstructionError, + buildInstruction, executeInstruction, + createInstructionHandler, createInstructionExecutor, literal, account, diff --git a/typescript/core/src/instructions/account-resolver.ts b/typescript/core/src/instructions/account-resolver.ts index 7da4e9b3..4175c545 100644 --- a/typescript/core/src/instructions/account-resolver.ts +++ b/typescript/core/src/instructions/account-resolver.ts @@ -1,5 +1,6 @@ import type { WalletAdapter } from '../wallet/types'; import { findProgramAddressSync, decodeBase58, createSeed } from './pda'; +import { serializeSeedValue } from './seed-serializer'; /** * Categories of accounts in an instruction. @@ -45,7 +46,8 @@ export interface PdaConfig { */ export type PdaSeed = | { type: 'literal'; value: string } - | { type: 'argRef'; argName: string } + | { type: 'bytes'; value: number[] } + | { type: 'argRef'; argName: string; argType?: string } | { type: 'accountRef'; accountName: string }; /** @@ -185,12 +187,36 @@ export function resolveAccounts( } } - // Return accounts in original order (as defined in accountMetas) + // Return accounts in original order (as defined in accountMetas). + // + // Omitted optional accounts that precede a resolved account cannot simply + // be dropped — that would shift every later account into the wrong slot. + // Anchor's convention is to pass the program ID as a placeholder; trailing + // omitted optionals are dropped as usual. + const lastResolvedIndex = accountMetas.reduce( + (last, meta, index) => (resolvedMap[meta.name] ? index : last), + -1 + ); + const orderedAccounts: ResolvedAccount[] = []; - for (const meta of accountMetas) { + for (let i = 0; i < accountMetas.length; i++) { + const meta = accountMetas[i]!; const resolved = resolvedMap[meta.name]; if (resolved) { orderedAccounts.push(resolved); + } else if (meta.isOptional && i < lastResolvedIndex) { + if (!options.programId) { + throw new Error( + 'Omitted optional account "' + meta.name + '" precedes other accounts and needs ' + + 'the program ID as a placeholder, but no programId was provided in options.' + ); + } + orderedAccounts.push({ + name: meta.name, + address: options.programId, + isSigner: false, + isWritable: false, + }); } } @@ -276,16 +302,20 @@ function resolvePdaAccount( case 'literal': seeds.push(createSeed(seed.value)); break; - + + case 'bytes': + seeds.push(Uint8Array.from(seed.value)); + break; + case 'argRef': { const argValue = args[seed.argName]; if (argValue === undefined) { throw new Error( - 'PDA seed references missing argument: ' + seed.argName + + 'PDA seed references missing argument: ' + seed.argName + ' (for account "' + meta.name + '")' ); } - seeds.push(createSeed(argValue as string | bigint | number)); + seeds.push(serializeSeedValue(argValue, seed.argType)); break; } diff --git a/typescript/core/src/instructions/confirmation.ts b/typescript/core/src/instructions/confirmation.ts deleted file mode 100644 index 1a5474a2..00000000 --- a/typescript/core/src/instructions/confirmation.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { WalletAdapter } from '../wallet/types'; - -/** - * Confirmation level for transaction processing. - * - `processed`: Transaction processed but not confirmed - * - `confirmed`: Transaction confirmed by cluster - * - `finalized`: Transaction finalized (recommended for production) - */ -export type ConfirmationLevel = 'processed' | 'confirmed' | 'finalized'; - -/** - * Options for executing an instruction. - */ -export interface ExecuteOptions { - /** Wallet adapter for signing */ - wallet?: WalletAdapter; - /** User-provided account addresses */ - accounts?: Record; - /** Confirmation level to wait for */ - confirmationLevel?: ConfirmationLevel; - /** Maximum time to wait for confirmation (ms) */ - timeout?: number; - /** Refresh view after transaction completes */ - refresh?: { - view: string; - key?: string; - }[]; -} - -/** - * Result of a successful instruction execution. - */ -export interface ExecutionResult { - /** Transaction signature */ - signature: string; - /** Confirmation level achieved */ - confirmationLevel: ConfirmationLevel; - /** Slot when transaction was processed */ - slot: number; - /** Error code if transaction failed */ - error?: string; -} - -/** - * Waits for transaction confirmation. - * - * @param signature - Transaction signature - * @param level - Desired confirmation level - * @param timeout - Maximum wait time in milliseconds - * @returns Confirmation result - */ -export async function waitForConfirmation( - signature: string, - level: ConfirmationLevel = 'confirmed', - timeout: number = 60000 -): Promise<{ level: ConfirmationLevel; slot: number }> { - const startTime = Date.now(); - - while (Date.now() - startTime < timeout) { - const status = await checkTransactionStatus(signature); - - if (status.err) { - throw new Error(`Transaction failed: ${JSON.stringify(status.err)}`); - } - - if (isConfirmationLevelSufficient(status.confirmations, level)) { - return { - level, - slot: status.slot, - }; - } - - await sleep(1000); - } - - throw new Error(`Transaction confirmation timeout after ${timeout}ms`); -} - -async function checkTransactionStatus(_signature: string): Promise<{ - err: unknown; - confirmations: number | null; - slot: number; -}> { - // In production, query the Solana RPC - return { - err: null, - confirmations: 32, - slot: 123456789, - }; -} - -function isConfirmationLevelSufficient( - confirmations: number | null, - level: ConfirmationLevel -): boolean { - if (confirmations === null) { - return false; - } - - switch (level) { - case 'processed': - return confirmations >= 0; - case 'confirmed': - return confirmations >= 1; - case 'finalized': - return confirmations >= 32; - default: - return false; - } -} - -function sleep(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/typescript/core/src/instructions/error-parser.ts b/typescript/core/src/instructions/error-parser.ts index 3ec97372..9e895ca5 100644 --- a/typescript/core/src/instructions/error-parser.ts +++ b/typescript/core/src/instructions/error-parser.ts @@ -93,3 +93,21 @@ function extractErrorCode(error: unknown): number | null { export function formatProgramError(error: ProgramError): string { return `${error.name} (${error.code}): ${error.message}`; } + +/** + * Error thrown when an instruction fails to send and the underlying failure + * could be parsed against the handler's IDL error definitions. + */ +export class InstructionError extends Error { + /** Parsed program error, if the failure matched a known error code. */ + readonly programError: ProgramError | null; + /** The original underlying error from the wallet adapter / RPC. */ + readonly cause: unknown; + + constructor(message: string, programError: ProgramError | null, cause: unknown) { + super(message); + this.name = 'InstructionError'; + this.programError = programError; + this.cause = cause; + } +} diff --git a/typescript/core/src/instructions/executor.ts b/typescript/core/src/instructions/executor.ts index 3cf3ba68..9200c12c 100644 --- a/typescript/core/src/instructions/executor.ts +++ b/typescript/core/src/instructions/executor.ts @@ -1,4 +1,10 @@ -import type { WalletAdapter } from '../wallet/types'; +import type { + WalletAdapter, + BuiltInstruction, + BuiltAccountMeta, + SendOptions, + ConfirmationLevel, +} from '../wallet/types'; import { resolveAccounts, validateAccountResolution, @@ -6,185 +12,282 @@ import { type AccountResolutionOptions, type ResolvedAccount, } from './account-resolver'; -import { waitForConfirmation, type ExecuteOptions, type ExecutionResult } from './confirmation'; -import type { ErrorMetadata } from './error-parser'; +import { serializeInstructionData, type ArgSchema } from './serializer'; +import { + parseInstructionError, + InstructionError, + type ErrorMetadata, +} from './error-parser'; /** - * Resolved accounts map passed to the instruction builder. + * Resolved accounts map passed to a handler's build function. * Keys are account names, values are base58 addresses. */ export type ResolvedAccounts = Record; +// Re-export the boundary instruction type for convenience. +export type { BuiltInstruction } from '../wallet/types'; + /** - * The instruction object returned by the handler's build function. - * This is a framework-agnostic representation that can be converted - * to @solana/web3.js TransactionInstruction. + * Instruction handler consumed by the core executor. + * + * Handlers are normally produced by {@link createInstructionHandler} (either + * hand-written or code-generated), so `build` is implemented generically and + * callers never deal with serialization directly. + * + * The phantom `_params` / `_error` fields carry compile-time type information + * for the typed client surface; they are never populated at runtime. */ -export interface BuiltInstruction { - /** Program ID (base58) */ +export interface InstructionHandler< + TParams = Record, + TError = unknown, +> { + /** Program ID for this instruction (base58). Used for PDA derivation. */ + programId?: string; + /** Ordered account metadata used by the core SDK for resolution. */ + accounts: AccountMeta[]; + /** Error definitions used for error parsing. */ + errors: ErrorMetadata[]; + /** + * Names of the instruction's serialized arguments. Everything in the merged + * params object that is NOT in this list is treated as a user-provided + * account address override. + */ + argNames: string[]; + /** + * Build the instruction from already-resolved, ordered accounts. + * Implemented by {@link createInstructionHandler}. + */ + build(args: Record, resolved: ResolvedAccount[]): BuiltInstruction; + /** Phantom: merged params type (args + user-provided accounts). */ + readonly _params?: TParams; + /** Phantom: typed error union. */ + readonly _error?: TError; +} + +/** + * Configuration accepted by {@link createInstructionHandler}. + */ +export interface InstructionHandlerConfig { + /** Program ID (base58). */ programId: string; - /** Account keys in order */ - keys: Array<{ - pubkey: string; - isSigner: boolean; - isWritable: boolean; - }>; - /** Serialized instruction data */ - data: Uint8Array; + /** Instruction discriminator bytes (8 for Anchor, 1 for Steel, etc.). */ + discriminator: Uint8Array | number[]; + /** Ordered account metadata. */ + accounts: AccountMeta[]; + /** Ordered argument schema for Borsh serialization. */ + args: ArgSchema[]; + /** Error definitions from the IDL. */ + errors?: ErrorMetadata[]; } /** - * Instruction handler from the generated stack SDK. - * The build() function is generated code that handles serialization. + * Creates a data-driven instruction handler. + * + * The returned handler implements `build()` generically: it serializes args + * via the schema-driven serializer and constructs the account key list from + * the resolved, ordered accounts. No imperative per-instruction code is + * required, which keeps generated SDKs tiny and puts all serialization logic + * in one tested place. */ -export interface InstructionHandler { - /** - * Build the instruction with resolved accounts. - * This is generated code - serialization logic lives here. +export function createInstructionHandler< + TParams = Record, + TError = unknown, +>(config: InstructionHandlerConfig): InstructionHandler { + const discriminator = + config.discriminator instanceof Uint8Array + ? config.discriminator + : Uint8Array.from(config.discriminator); + const argNames = config.args.map((a) => a.name); + const errors = config.errors ?? []; + + return { + programId: config.programId, + accounts: config.accounts, + errors, + argNames, + build(args: Record, resolved: ResolvedAccount[]): BuiltInstruction { + const data = serializeInstructionData(discriminator, args, config.args); + return { + programId: config.programId, + keys: resolved.map((r) => ({ + pubkey: r.address, + isSigner: r.isSigner, + isWritable: r.isWritable, + })), + data, + }; + }, + }; +} + +/** + * Options for building an instruction (no network access). + */ +export interface BuildOptions { + /** Wallet, used only for `publicKey` to resolve signer accounts. */ + wallet?: WalletAdapter; + /** Extra/override user-provided account addresses. */ + accounts?: Record; + /** + * Extra account metas appended after the instruction's declared accounts + * (Anchor's `remainingAccounts`) — for routers, transfer hooks, and other + * composition patterns the IDL cannot express. */ - build(args: Record, accounts: ResolvedAccounts): BuiltInstruction; - - /** Account metadata - used by core SDK for resolution */ - accounts: AccountMeta[]; - - /** Error definitions - used by core SDK for error parsing */ - errors: ErrorMetadata[]; - - /** Program ID for this instruction (used for PDA derivation) */ - programId?: string; + remainingAccounts?: BuiltAccountMeta[]; } /** - * @deprecated Use InstructionHandler instead. Will be removed in next major version. - * Legacy instruction definition for backwards compatibility. + * Options for executing (building + sending) an instruction. */ -export interface InstructionDefinition { - /** Instruction name */ - name: string; - /** Program ID (base58) */ - programId: string; - /** 8-byte discriminator */ - discriminator: Uint8Array; - /** Account metadata */ - accounts: AccountMeta[]; - /** Argument schema for serialization */ - argsSchema: import('./serializer').ArgSchema[]; - /** Error definitions */ - errors: ErrorMetadata[]; +export interface ExecuteOptions extends BuildOptions { + /** Wallet adapter that signs and broadcasts the transaction. */ + wallet?: WalletAdapter; + /** Confirmation level forwarded to the adapter. */ + confirmationLevel?: ConfirmationLevel; + /** Additional options forwarded verbatim to the wallet adapter. */ + send?: SendOptions; } /** - * Converts resolved account array to a map for the builder. + * Result of a successful instruction execution. */ -function toResolvedAccountsMap(accounts: ResolvedAccount[]): ResolvedAccounts { - const map: ResolvedAccounts = {}; - for (const account of accounts) { - map[account.name] = account.address; +export interface ExecutionResult { + /** Transaction signature. */ + signature: string; + /** Slot in which the transaction landed, if the adapter reports it. */ + slot?: number; +} + +/** + * Splits a merged params object into serialized args and account overrides. + * + * Keys matching a declared argument name are args; keys matching a declared + * account name (with a string value) are account address overrides. Anything + * else throws — a typo'd key silently dropped here would otherwise change the + * built instruction. `options.accounts` on {@link BuildOptions} remains an + * unvalidated escape hatch for advanced callers. + */ +function splitParams( + handler: InstructionHandler, + params: Record +): { args: Record; accountOverrides: Record } { + const argNameSet = new Set(handler.argNames); + const accountNameSet = new Set(handler.accounts.map((a) => a.name)); + const args: Record = {}; + const accountOverrides: Record = {}; + + for (const [key, value] of Object.entries(params)) { + if (argNameSet.has(key)) { + args[key] = value; + } else if (accountNameSet.has(key)) { + if (typeof value !== 'string') { + // Non-string values are not valid account addresses. + throw new Error( + `Parameter "${key}" is not a known argument and is not a base58 account address` + ); + } + accountOverrides[key] = value; + } else { + throw new Error( + `Unknown parameter "${key}". Expected one of args [${[...argNameSet].join(', ')}] ` + + `or accounts [${[...accountNameSet].join(', ')}]` + ); + } } - return map; + + return { args, accountOverrides }; } /** - * Executes an instruction handler with the given arguments and options. - * - * This is the main function for executing Solana instructions. It handles: - * 1. Account resolution (signer, PDA, user-provided) - * 2. Calling the generated build() function - * 3. Transaction signing and sending - * 4. Confirmation waiting - * - * @param handler - Instruction handler from generated SDK - * @param args - Instruction arguments - * @param options - Execution options - * @returns Execution result with signature + * Builds a {@link BuiltInstruction} from a handler and a merged params object. + * + * This is a pure function: it performs no network access. It is the unit of + * composition for batching (`wallet.signAndSend([a, b, c])`). */ -export async function executeInstruction( +export function buildInstruction( handler: InstructionHandler, - args: Record, - options: ExecuteOptions = {} -): Promise { - // Step 1: Resolve accounts using handler's account metadata + params: Record, + options: BuildOptions = {} +): BuiltInstruction { + const { args, accountOverrides } = splitParams(handler, params); + const resolutionOptions: AccountResolutionOptions = { - accounts: options.accounts, + accounts: { ...accountOverrides, ...options.accounts }, wallet: options.wallet, - programId: handler.programId, // Pass programId for PDA derivation + programId: handler.programId, }; - - const resolution = resolveAccounts( - handler.accounts, - args, - resolutionOptions - ); - + + const resolution = resolveAccounts(handler.accounts, args, resolutionOptions); validateAccountResolution(resolution); - - // Step 2: Call generated build() function - const resolvedAccountsMap = toResolvedAccountsMap(resolution.accounts); - const instruction = handler.build(args, resolvedAccountsMap); - - // Step 3: Build transaction from the built instruction - const transaction = buildTransaction(instruction); - - // Step 4: Sign and send - if (!options.wallet) { - throw new Error('Wallet required to sign transaction'); + + const instruction = handler.build(args, resolution.accounts); + if (options.remainingAccounts?.length) { + instruction.keys.push(...options.remainingAccounts); } - - const signature = await options.wallet.signAndSend(transaction); - - // Step 5: Wait for confirmation - const confirmationLevel = options.confirmationLevel ?? 'confirmed'; - const timeout = options.timeout ?? 60000; - - const confirmation = await waitForConfirmation( - signature, - confirmationLevel, - timeout - ); - - return { - signature, - confirmationLevel: confirmation.level, - slot: confirmation.slot, - }; + return instruction; } /** - * Creates a transaction object from a built instruction. - * - * @param instruction - Built instruction from handler - * @returns Transaction object ready for signing + * Builds, signs, and sends an instruction via the wallet adapter. + * + * The core SDK does not touch RPC: the adapter owns blockhash, compilation, + * signing, sending, and confirmation. On failure, program errors are parsed + * against the handler's IDL error definitions and surfaced as an + * {@link InstructionError}. */ -function buildTransaction(instruction: BuiltInstruction): unknown { - // This returns a framework-agnostic transaction representation. - // The wallet adapter is responsible for converting this to the - // appropriate format (@solana/web3.js Transaction, etc.) - return { - instructions: [{ - programId: instruction.programId, - keys: instruction.keys, - data: Array.from(instruction.data), - }], +export async function executeInstruction( + handler: InstructionHandler, + params: Record, + options: ExecuteOptions = {} +): Promise { + const instruction = buildInstruction(handler, params, options); + + if (!options.wallet) { + throw new Error('Wallet required to sign and send transaction'); + } + + const sendOptions: SendOptions = { + ...options.send, }; + if (options.confirmationLevel !== undefined) { + sendOptions.confirmationLevel = options.confirmationLevel; + } + + try { + const result = await options.wallet.signAndSend([instruction], sendOptions); + return { signature: result.signature, slot: result.slot }; + } catch (err) { + const programError = parseInstructionError(err, handler.errors); + if (programError) { + throw new InstructionError( + `${programError.name} (${programError.code}): ${programError.message}`, + programError, + err + ); + } + throw err; + } } /** * Creates an instruction executor bound to a specific wallet. - * - * @param wallet - Wallet adapter - * @returns Bound executor function */ export function createInstructionExecutor(wallet: WalletAdapter) { return { execute: async ( handler: InstructionHandler, - args: Record, + params: Record, options?: Omit ) => { - return executeInstruction(handler, args, { - ...options, - wallet, - }); + return executeInstruction(handler, params, { ...options, wallet }); + }, + build: ( + handler: InstructionHandler, + params: Record, + options?: Omit + ) => { + return buildInstruction(handler, params, { ...options, wallet }); }, }; } diff --git a/typescript/core/src/instructions/index.ts b/typescript/core/src/instructions/index.ts index 8aeca098..98321b58 100644 --- a/typescript/core/src/instructions/index.ts +++ b/typescript/core/src/instructions/index.ts @@ -1,4 +1,13 @@ -export type { WalletAdapter, WalletState, WalletConnectOptions } from '../wallet/types'; +export type { + WalletAdapter, + WalletState, + WalletConnectOptions, + BuiltInstruction, + BuiltAccountMeta, + ConfirmationLevel, + SendOptions, + SendResult, +} from '../wallet/types'; export type { AccountCategory, AccountMeta, @@ -18,18 +27,25 @@ export { decodeBase58, encodeBase58, } from './pda'; -export type { ArgSchema, ArgType } from './serializer'; +export type { ArgSchema, ArgType, ArgStructField, EnumVariant } from './serializer'; export { serializeInstructionData } from './serializer'; -export type { ConfirmationLevel, ExecuteOptions, ExecutionResult } from './confirmation'; -export { waitForConfirmation } from './confirmation'; +export type { CanonicalSeedType } from './seed-serializer'; +export { normalizeSeedType, serializeSeedValue } from './seed-serializer'; export type { ProgramError, ErrorMetadata } from './error-parser'; -export { parseInstructionError, formatProgramError } from './error-parser'; +export { parseInstructionError, formatProgramError, InstructionError } from './error-parser'; export type { InstructionHandler, - InstructionDefinition, - BuiltInstruction, + InstructionHandlerConfig, + BuildOptions, + ExecuteOptions, + ExecutionResult, ResolvedAccounts, } from './executor'; -export { executeInstruction, createInstructionExecutor } from './executor'; +export { + buildInstruction, + executeInstruction, + createInstructionHandler, + createInstructionExecutor, +} from './executor'; export type { SeedDef, PdaDeriveContext, PdaFactory, ProgramPdas } from './pda-dsl'; export { literal, account, arg, bytes, pda, createProgramPdas } from './pda-dsl'; diff --git a/typescript/core/src/instructions/instructions.test.ts b/typescript/core/src/instructions/instructions.test.ts new file mode 100644 index 00000000..45f5ce0f --- /dev/null +++ b/typescript/core/src/instructions/instructions.test.ts @@ -0,0 +1,586 @@ +import { describe, it, expect } from 'vitest'; + +import { + findProgramAddress, + findProgramAddressSync, + decodeBase58, + encodeBase58, + createSeed, +} from './pda'; +import { serializeInstructionData, type ArgSchema } from './serializer'; +import { + resolveAccounts, + validateAccountResolution, + type AccountMeta, +} from './account-resolver'; +import { parseInstructionError, formatProgramError } from './error-parser'; +import { + createInstructionHandler, + buildInstruction, + executeInstruction, +} from './executor'; +import type { WalletAdapter, BuiltInstruction, SendResult } from '../wallet/types'; + +// Well-known, valid 32-byte base58 program/account addresses. +const SYSTEM_PROGRAM = '11111111111111111111111111111111'; +const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'; +const WSOL_MINT = 'So11111111111111111111111111111111111111112'; + +describe('base58', () => { + it('decodes the system program to 32 zero bytes', () => { + const bytes = decodeBase58(SYSTEM_PROGRAM); + expect(bytes.length).toBe(32); + expect([...bytes].every((b) => b === 0)).toBe(true); + }); + + it('round-trips known program addresses', () => { + for (const addr of [TOKEN_PROGRAM, WSOL_MINT, SYSTEM_PROGRAM]) { + const decoded = decodeBase58(addr); + expect(decoded.length).toBe(32); + expect(encodeBase58(decoded)).toBe(addr); + } + }); + + it('rejects invalid base58 characters', () => { + expect(() => decodeBase58('0OIl')).toThrow(/Invalid base58/); + }); +}); + +describe('createSeed', () => { + it('encodes a u64 number/bigint as 8 little-endian bytes', () => { + expect([...createSeed(1n)]).toEqual([1, 0, 0, 0, 0, 0, 0, 0]); + expect([...createSeed(256)]).toEqual([0, 1, 0, 0, 0, 0, 0, 0]); + }); + + it('encodes strings as utf-8 bytes', () => { + expect([...createSeed('abc')]).toEqual([0x61, 0x62, 0x63]); + }); +}); + +describe('findProgramAddress', () => { + it('is deterministic and off-curve (sync)', () => { + const seeds = [createSeed('treasury')]; + const [addr1, bump1] = findProgramAddressSync(seeds, TOKEN_PROGRAM); + const [addr2, bump2] = findProgramAddressSync(seeds, TOKEN_PROGRAM); + expect(addr1).toBe(addr2); + expect(bump1).toBe(bump2); + // Canonical bump is the highest valid one, typically near 255. + expect(bump1).toBeGreaterThanOrEqual(0); + expect(bump1).toBeLessThanOrEqual(255); + expect(decodeBase58(addr1).length).toBe(32); + }); + + it('matches between sync and async implementations', async () => { + const seeds = [createSeed('miner'), decodeBase58(WSOL_MINT)]; + const [syncAddr, syncBump] = findProgramAddressSync(seeds, TOKEN_PROGRAM); + const [asyncAddr, asyncBump] = await findProgramAddress(seeds, TOKEN_PROGRAM); + expect(asyncAddr).toBe(syncAddr); + expect(asyncBump).toBe(syncBump); + }); + + it('produces different addresses for different seeds', () => { + const [a] = findProgramAddressSync([createSeed('a')], TOKEN_PROGRAM); + const [b] = findProgramAddressSync([createSeed('b')], TOKEN_PROGRAM); + expect(a).not.toBe(b); + }); + + it('rejects more than 16 seeds and oversized seeds', () => { + const tooMany = Array.from({ length: 17 }, () => createSeed('x')); + expect(() => findProgramAddressSync(tooMany, TOKEN_PROGRAM)).toThrow(/16 seeds/); + expect(() => findProgramAddressSync([new Uint8Array(33)], TOKEN_PROGRAM)).toThrow( + /maximum length/ + ); + }); +}); + +describe('serializeInstructionData', () => { + it('prefixes the discriminator and serializes primitives', () => { + const schema: ArgSchema[] = [ + { name: 'amount', type: 'u64' }, + { name: 'flag', type: 'bool' }, + { name: 'count', type: 'u8' }, + ]; + const data = serializeInstructionData( + Uint8Array.from([0xaa, 0xbb]), + { amount: 1n, flag: true, count: 7 }, + schema + ); + expect([...data]).toEqual([ + 0xaa, 0xbb, // discriminator + 1, 0, 0, 0, 0, 0, 0, 0, // u64 = 1 + 1, // bool = true + 7, // u8 = 7 + ]); + }); + + it('serializes a pubkey from a base58 string into 32 bytes', () => { + const schema: ArgSchema[] = [{ name: 'mint', type: 'pubkey' }]; + const data = serializeInstructionData(new Uint8Array(0), { mint: SYSTEM_PROGRAM }, schema); + expect(data.length).toBe(32); + expect([...data].every((b) => b === 0)).toBe(true); + }); + + it('rejects pubkeys that do not decode to 32 bytes', () => { + const schema: ArgSchema[] = [{ name: 'mint', type: 'pubkey' }]; + expect(() => + serializeInstructionData(new Uint8Array(0), { mint: 'abc' }, schema) + ).toThrow(/Invalid pubkey/); + }); + + it('serializes strings with a length prefix', () => { + const schema: ArgSchema[] = [{ name: 's', type: 'string' }]; + const data = serializeInstructionData(new Uint8Array(0), { s: 'hi' }, schema); + expect([...data]).toEqual([2, 0, 0, 0, 0x68, 0x69]); + }); + + it('serializes option None and Some', () => { + const schema: ArgSchema[] = [{ name: 'maybe', type: { option: 'u8' } }]; + expect([...serializeInstructionData(new Uint8Array(0), { maybe: null }, schema)]).toEqual([0]); + expect([...serializeInstructionData(new Uint8Array(0), { maybe: 9 }, schema)]).toEqual([1, 9]); + }); + + it('serializes negative i64/i128 values in two\'s complement', () => { + const schema: ArgSchema[] = [ + { name: 'small', type: 'i64' }, + { name: 'big', type: 'i128' }, + ]; + const data = serializeInstructionData( + new Uint8Array(0), + { small: -1n, big: -1n }, + schema + ); + // -1 is all 0xff in two's complement at any width. + expect(data.length).toBe(24); + expect([...data].every((b) => b === 0xff)).toBe(true); + + const min = serializeInstructionData( + new Uint8Array(0), + { small: -(2n ** 63n), big: -(2n ** 127n) }, + schema + ); + expect([...min.subarray(0, 8)]).toEqual([0, 0, 0, 0, 0, 0, 0, 0x80]); + expect([...min.subarray(8, 24)]).toEqual([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x80, + ]); + }); + + it('serializes f32/f64 little-endian and bytes with a length prefix', () => { + const schema: ArgSchema[] = [ + { name: 'ratio', type: 'f32' }, + { name: 'price', type: 'f64' }, + { name: 'blob', type: 'bytes' }, + ]; + const data = serializeInstructionData( + new Uint8Array(0), + { ratio: 1.5, price: 2.5, blob: Uint8Array.from([9, 8]) }, + schema + ); + const expectedF32 = Buffer.alloc(4); + expectedF32.writeFloatLE(1.5); + const expectedF64 = Buffer.alloc(8); + expectedF64.writeDoubleLE(2.5); + expect([...data.subarray(0, 4)]).toEqual([...expectedF32]); + expect([...data.subarray(4, 12)]).toEqual([...expectedF64]); + expect([...data.subarray(12)]).toEqual([2, 0, 0, 0, 9, 8]); // u32 len + raw + + // bytes accepts plain number arrays too. + const fromArray = serializeInstructionData( + new Uint8Array(0), + { ratio: 0, price: 0, blob: [1] }, + schema + ); + expect([...fromArray.subarray(12)]).toEqual([1, 0, 0, 0, 1]); + }); + + it('serializes structs in field order, including nesting', () => { + const schema: ArgSchema[] = [ + { + name: 'data', + type: { + struct: [ + { name: 'amount', type: 'u64' }, + { name: 'inner', type: { struct: [{ name: 'flag', type: 'bool' }] } }, + ], + }, + }, + ]; + const data = serializeInstructionData( + new Uint8Array(0), + { data: { inner: { flag: true }, amount: 3n } }, // intentionally reordered keys + schema + ); + expect([...data]).toEqual([3, 0, 0, 0, 0, 0, 0, 0, 1]); + }); + + it('rejects structs with missing required fields', () => { + const schema: ArgSchema[] = [ + { name: 'data', type: { struct: [{ name: 'amount', type: 'u64' }] } }, + ]; + expect(() => + serializeInstructionData(new Uint8Array(0), { data: {} }, schema) + ).toThrow(/Missing required struct field "amount"/); + }); + + it('serializes fieldless enums by name or index', () => { + const schema: ArgSchema[] = [{ name: 'status', type: { enum: ['active', 'sunset'] } }]; + expect([ + ...serializeInstructionData(new Uint8Array(0), { status: 'sunset' }, schema), + ]).toEqual([1]); + expect([ + ...serializeInstructionData(new Uint8Array(0), { status: 0 }, schema), + ]).toEqual([0]); + expect(() => + serializeInstructionData(new Uint8Array(0), { status: 'paused' }, schema) + ).toThrow(/Unknown enum variant "paused"/); + }); + + it('serializes data-carrying enum variants (struct and tuple)', () => { + const schema: ArgSchema[] = [ + { + name: 'op', + type: { + enum: [ + 'noop', + { name: 'transfer', fields: [{ name: 'amount', type: 'u64' }] }, + { name: 'pair', tuple: ['u8', 'u16'] }, + ], + }, + }, + ]; + expect([ + ...serializeInstructionData( + new Uint8Array(0), + { op: { transfer: { amount: 7n } } }, + schema + ), + ]).toEqual([1, 7, 0, 0, 0, 0, 0, 0, 0]); + expect([ + ...serializeInstructionData(new Uint8Array(0), { op: { pair: [5, 0x0102] } }, schema), + ]).toEqual([2, 5, 2, 1]); + // Data-carrying variants cannot be passed as bare names. + expect(() => + serializeInstructionData(new Uint8Array(0), { op: 'transfer' }, schema) + ).toThrow(/carries data/); + }); + + it('serializes vec with a length prefix and fixed arrays without one', () => { + const vecSchema: ArgSchema[] = [{ name: 'v', type: { vec: 'u8' } }]; + expect([...serializeInstructionData(new Uint8Array(0), { v: [1, 2] }, vecSchema)]).toEqual([ + 2, 0, 0, 0, 1, 2, + ]); + + const arrSchema: ArgSchema[] = [{ name: 'a', type: { array: ['u8', 3] } }]; + expect([...serializeInstructionData(new Uint8Array(0), { a: [4, 5, 6] }, arrSchema)]).toEqual([ + 4, 5, 6, + ]); + expect(() => + serializeInstructionData(new Uint8Array(0), { a: [1] }, arrSchema) + ).toThrow(/length mismatch/); + }); +}); + +describe('resolveAccounts', () => { + const wallet = { publicKey: WSOL_MINT } as WalletAdapter; + + it('resolves signer, known, and userProvided categories', () => { + const metas: AccountMeta[] = [ + { name: 'authority', isSigner: true, isWritable: true, category: 'signer' }, + { + name: 'systemProgram', + isSigner: false, + isWritable: false, + category: 'known', + knownAddress: SYSTEM_PROGRAM, + }, + { name: 'mint', isSigner: false, isWritable: false, category: 'userProvided' }, + ]; + const result = resolveAccounts(metas, {}, { + wallet, + accounts: { mint: TOKEN_PROGRAM }, + }); + validateAccountResolution(result); + expect(result.accounts.map((a) => a.name)).toEqual(['authority', 'systemProgram', 'mint']); + expect(result.accounts[0]!.address).toBe(WSOL_MINT); + expect(result.accounts[1]!.address).toBe(SYSTEM_PROGRAM); + expect(result.accounts[2]!.address).toBe(TOKEN_PROGRAM); + }); + + it('derives a PDA referencing a signer account and keeps original order', () => { + const metas: AccountMeta[] = [ + { name: 'authority', isSigner: true, isWritable: true, category: 'signer' }, + { + name: 'state', + isSigner: false, + isWritable: true, + category: 'pda', + pdaConfig: { + programId: TOKEN_PROGRAM, + seeds: [ + { type: 'literal', value: 'state' }, + { type: 'accountRef', accountName: 'authority' }, + ], + }, + }, + ]; + const result = resolveAccounts(metas, {}, { wallet }); + validateAccountResolution(result); + // Original (instruction) order is preserved even though PDAs resolve later. + expect(result.accounts.map((a) => a.name)).toEqual(['authority', 'state']); + + const expected = findProgramAddressSync( + [createSeed('state'), decodeBase58(WSOL_MINT)], + TOKEN_PROGRAM + )[0]; + expect(result.accounts[1]!.address).toBe(expected); + }); + + it('derives a PDA from raw byte seeds', () => { + const raw = [1, 2, 255]; + const metas: AccountMeta[] = [ + { + name: 'config', + isSigner: false, + isWritable: false, + category: 'pda', + pdaConfig: { + programId: TOKEN_PROGRAM, + seeds: [{ type: 'bytes', value: raw }], + }, + }, + ]; + const result = resolveAccounts(metas, {}, {}); + validateAccountResolution(result); + + const expected = findProgramAddressSync([Uint8Array.from(raw)], TOKEN_PROGRAM)[0]; + expect(result.accounts[0]!.address).toBe(expected); + }); + + it('reports missing required user-provided accounts', () => { + const metas: AccountMeta[] = [ + { name: 'mint', isSigner: false, isWritable: false, category: 'userProvided' }, + ]; + const result = resolveAccounts(metas, {}, {}); + expect(result.missingUserAccounts).toEqual(['mint']); + expect(() => validateAccountResolution(result)).toThrow(/Missing required accounts/); + }); + + it('substitutes the program id for omitted non-trailing optional accounts', () => { + const metas: AccountMeta[] = [ + { name: 'authority', isSigner: true, isWritable: true, category: 'signer' }, + { + name: 'referrer', + isSigner: false, + isWritable: false, + category: 'userProvided', + isOptional: true, + }, + { name: 'mint', isSigner: false, isWritable: false, category: 'userProvided' }, + ]; + const result = resolveAccounts(metas, {}, { + wallet, + accounts: { mint: TOKEN_PROGRAM }, + programId: SYSTEM_PROGRAM, + }); + validateAccountResolution(result); + expect(result.accounts.map((a) => a.name)).toEqual(['authority', 'referrer', 'mint']); + // Anchor convention: omitted optional in a non-trailing slot = program id. + expect(result.accounts[1]!.address).toBe(SYSTEM_PROGRAM); + expect(result.accounts[1]!.isSigner).toBe(false); + expect(result.accounts[1]!.isWritable).toBe(false); + }); + + it('drops omitted trailing optional accounts', () => { + const metas: AccountMeta[] = [ + { name: 'authority', isSigner: true, isWritable: true, category: 'signer' }, + { + name: 'referrer', + isSigner: false, + isWritable: false, + category: 'userProvided', + isOptional: true, + }, + ]; + const result = resolveAccounts(metas, {}, { wallet, programId: SYSTEM_PROGRAM }); + validateAccountResolution(result); + expect(result.accounts.map((a) => a.name)).toEqual(['authority']); + }); + + it('resolves provided optional accounts normally', () => { + const metas: AccountMeta[] = [ + { name: 'authority', isSigner: true, isWritable: true, category: 'signer' }, + { + name: 'referrer', + isSigner: false, + isWritable: false, + category: 'userProvided', + isOptional: true, + }, + { name: 'mint', isSigner: false, isWritable: false, category: 'userProvided' }, + ]; + const result = resolveAccounts(metas, {}, { + wallet, + accounts: { referrer: WSOL_MINT, mint: TOKEN_PROGRAM }, + programId: SYSTEM_PROGRAM, + }); + expect(result.accounts.map((a) => a.address)).toEqual([ + WSOL_MINT, + WSOL_MINT, + TOKEN_PROGRAM, + ]); + }); +}); + +describe('parseInstructionError', () => { + const errors = [{ code: 6000, name: 'SlippageExceeded', msg: 'Slippage tolerance exceeded' }]; + + it('maps an InstructionError Custom code to IDL metadata', () => { + const parsed = parseInstructionError( + { InstructionError: [0, { Custom: 6000 }] }, + errors + ); + expect(parsed).toEqual({ + code: 6000, + name: 'SlippageExceeded', + message: 'Slippage tolerance exceeded', + }); + expect(formatProgramError(parsed!)).toBe( + 'SlippageExceeded (6000): Slippage tolerance exceeded' + ); + }); + + it('falls back to a synthetic error for unknown codes', () => { + const parsed = parseInstructionError({ code: 12345 }, errors); + expect(parsed).toEqual({ + code: 12345, + name: 'CustomError12345', + message: 'Unknown error with code 12345', + }); + }); + + it('returns null for non-program errors', () => { + expect(parseInstructionError(null, errors)).toBeNull(); + expect(parseInstructionError(new Error('network down'), errors)).toBeNull(); + }); +}); + +describe('createInstructionHandler + buildInstruction', () => { + function makeHandler() { + return createInstructionHandler({ + programId: TOKEN_PROGRAM, + discriminator: [1], + accounts: [ + { name: 'authority', isSigner: true, isWritable: true, category: 'signer' }, + { name: 'mint', isSigner: false, isWritable: false, category: 'userProvided' }, + { + name: 'state', + isSigner: false, + isWritable: true, + category: 'pda', + pdaConfig: { + seeds: [ + { type: 'literal', value: 'state' }, + { type: 'accountRef', accountName: 'authority' }, + ], + }, + }, + ], + args: [{ name: 'amount', type: 'u64' }], + errors: [{ code: 6000, name: 'Boom', msg: 'boom' }], + }); + } + + const wallet = { publicKey: WSOL_MINT } as WalletAdapter; + + it('splits merged params into args and account overrides', () => { + const handler = makeHandler(); + const built = buildInstruction( + handler, + { amount: 100n, mint: SYSTEM_PROGRAM }, + { wallet } + ); + + expect(built.programId).toBe(TOKEN_PROGRAM); + expect(built.keys.map((k) => k.pubkey)).toEqual([ + WSOL_MINT, // authority (signer) + SYSTEM_PROGRAM, // mint (user-provided) + findProgramAddressSync( + [createSeed('state'), decodeBase58(WSOL_MINT)], + TOKEN_PROGRAM + )[0], // state (PDA derived from authority) + ]); + // discriminator [1] + u64 100 little-endian. + expect([...built.data]).toEqual([1, 100, 0, 0, 0, 0, 0, 0, 0]); + expect(built.keys[0]!.isSigner).toBe(true); + }); + + it('throws when a non-arg param is not a string address', () => { + const handler = makeHandler(); + expect(() => + buildInstruction(handler, { amount: 1n, mint: 42 as unknown as string }, { wallet }) + ).toThrow(/not a known argument/); + }); + + it('rejects unknown parameter names instead of silently dropping them', () => { + const handler = makeHandler(); + expect(() => + buildInstruction( + handler, + { amount: 1n, mint: SYSTEM_PROGRAM, mnit: TOKEN_PROGRAM }, + { wallet } + ) + ).toThrow(/Unknown parameter "mnit"/); + }); + + it('rejects missing required args instead of encoding zeros', () => { + const handler = makeHandler(); + expect(() => + buildInstruction(handler, { mint: SYSTEM_PROGRAM }, { wallet }) + ).toThrow(/Missing required argument "amount"/); + }); + + it('still treats an omitted option arg as None', () => { + const schema: ArgSchema[] = [{ name: 'maybe', type: { option: 'u8' } }]; + expect([...serializeInstructionData(new Uint8Array(0), {}, schema)]).toEqual([0]); + }); + + it('appends remainingAccounts after the declared accounts', () => { + const handler = makeHandler(); + const extra = { pubkey: TOKEN_PROGRAM, isSigner: false, isWritable: true }; + const built = buildInstruction( + handler, + { amount: 1n, mint: SYSTEM_PROGRAM }, + { wallet, remainingAccounts: [extra] } + ); + expect(built.keys[built.keys.length - 1]).toEqual(extra); + expect(built.keys).toHaveLength(4); // 3 declared + 1 remaining + }); + + it('executes via the wallet and parses program errors', async () => { + const handler = makeHandler(); + let sent: BuiltInstruction[] | null = null; + const okWallet: WalletAdapter = { + publicKey: WSOL_MINT, + async signAndSend(ixs): Promise { + sent = ixs; + return { signature: 'sig123', slot: 99 }; + }, + }; + const result = await executeInstruction( + handler, + { amount: 1n, mint: SYSTEM_PROGRAM }, + { wallet: okWallet } + ); + expect(result).toEqual({ signature: 'sig123', slot: 99 }); + expect(sent!).toHaveLength(1); + + const failWallet: WalletAdapter = { + publicKey: WSOL_MINT, + async signAndSend(): Promise { + throw { InstructionError: [0, { Custom: 6000 }] }; + }, + }; + await expect( + executeInstruction(handler, { amount: 1n, mint: SYSTEM_PROGRAM }, { wallet: failWallet }) + ).rejects.toMatchObject({ name: 'InstructionError', programError: { code: 6000, name: 'Boom' } }); + }); +}); diff --git a/typescript/core/src/instructions/pda-dsl.ts b/typescript/core/src/instructions/pda-dsl.ts index 2e461eb5..9cc9405a 100644 --- a/typescript/core/src/instructions/pda-dsl.ts +++ b/typescript/core/src/instructions/pda-dsl.ts @@ -1,4 +1,5 @@ import { findProgramAddress, findProgramAddressSync, decodeBase58 } from './pda'; +import { serializeSeedValue } from './seed-serializer'; export type SeedDef = | { type: 'literal'; value: string } @@ -48,7 +49,7 @@ function resolveSeeds(seeds: readonly SeedDef[], context: PdaDeriveContext): Uin if (value === undefined) { throw new Error(`Missing arg for PDA seed: ${seed.argName}`); } - return serializeArgForSeed(value, seed.argType); + return serializeSeedValue(value, seed.argType); } case 'accountRef': { const address = context.accounts?.[seed.accountName]; @@ -61,50 +62,6 @@ function resolveSeeds(seeds: readonly SeedDef[], context: PdaDeriveContext): Uin }); } -function serializeArgForSeed(value: unknown, argType?: string): Uint8Array { - if (value instanceof Uint8Array) { - return value; - } - - if (typeof value === 'string') { - if (value.length === 43 || value.length === 44) { - try { - return decodeBase58(value); - } catch { - return new TextEncoder().encode(value); - } - } - return new TextEncoder().encode(value); - } - - if (typeof value === 'bigint' || typeof value === 'number') { - const size = getArgSize(argType); - return serializeNumber(value, size); - } - - throw new Error(`Cannot serialize value for PDA seed: ${typeof value}`); -} - -function getArgSize(argType?: string): number { - if (!argType) return 8; - const match = argType.match(/^[ui](\d+)$/); - if (match && match[1]) { - return parseInt(match[1], 10) / 8; - } - if (argType === 'pubkey') return 32; - return 8; -} - -function serializeNumber(value: bigint | number, size: number): Uint8Array { - const buffer = new Uint8Array(size); - let n = typeof value === 'bigint' ? value : BigInt(value); - for (let i = 0; i < size; i++) { - buffer[i] = Number(n & BigInt(0xff)); - n >>= BigInt(8); - } - return buffer; -} - export function pda(programId: string, ...seeds: SeedDef[]): PdaFactory { return { seeds, diff --git a/typescript/core/src/instructions/pda.ts b/typescript/core/src/instructions/pda.ts index ff4f81d9..5fe7c872 100644 --- a/typescript/core/src/instructions/pda.ts +++ b/typescript/core/src/instructions/pda.ts @@ -4,6 +4,8 @@ * Implements Solana's PDA derivation algorithm without depending on @solana/web3.js. */ +import { Point } from '@noble/ed25519'; + // Base58 alphabet (Bitcoin/Solana style) const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; @@ -15,7 +17,10 @@ export function decodeBase58(str: string): Uint8Array { return new Uint8Array(0); } - const bytes: number[] = [0]; + // Big-endian byte accumulator (stored little-endian here, reversed at the + // end). Must start empty: a leading `[0]` produces a spurious extra byte for + // all-zero values such as the System Program ("111...1"). + const bytes: number[] = []; for (const char of str) { const value = BASE58_ALPHABET.indexOf(char); @@ -52,7 +57,9 @@ export function encodeBase58(bytes: Uint8Array): string { return ''; } - const digits: number[] = [0]; + // Must start empty for the same reason as `decodeBase58`: a leading `[0]` + // yields an extra '1' character when encoding all-zero inputs. + const digits: number[] = []; for (const byte of bytes) { let carry = byte; @@ -76,42 +83,129 @@ export function encodeBase58(bytes: Uint8Array): string { return digits.reverse().map(d => BASE58_ALPHABET[d]).join(''); } +// SHA-256 round constants (first 32 bits of the fractional parts of the cube +// roots of the first 64 primes). +const SHA256_K = new Uint32Array([ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +]); + /** - * SHA-256 hash function (synchronous, Node.js). + * Dependency-free, synchronous SHA-256. + * + * Used so PDA derivation works identically in browsers, Node (both CJS and + * ESM), and bundlers without relying on `require('crypto')` or async WebCrypto. + */ +function sha256Pure(data: Uint8Array): Uint8Array { + const h = new Uint32Array([ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, + ]); + + const bitLen = data.length * 8; + // Pad: append 0x80, then zeros, until length ≡ 56 (mod 64), then 8-byte length. + const paddedLen = ((data.length + 8) >> 6) * 64 + 64; + const msg = new Uint8Array(paddedLen); + msg.set(data); + msg[data.length] = 0x80; + // 64-bit big-endian bit length (high 32 bits assumed 0 for our seed sizes). + const dv = new DataView(msg.buffer); + dv.setUint32(paddedLen - 4, bitLen >>> 0, false); + dv.setUint32(paddedLen - 8, Math.floor(bitLen / 0x100000000), false); + + const w = new Uint32Array(64); + for (let offset = 0; offset < paddedLen; offset += 64) { + for (let i = 0; i < 16; i++) { + w[i] = dv.getUint32(offset + i * 4, false); + } + for (let i = 16; i < 64; i++) { + const w15 = w[i - 15]!; + const w2 = w[i - 2]!; + const s0 = ((w15 >>> 7) | (w15 << 25)) ^ ((w15 >>> 18) | (w15 << 14)) ^ (w15 >>> 3); + const s1 = ((w2 >>> 17) | (w2 << 15)) ^ ((w2 >>> 19) | (w2 << 13)) ^ (w2 >>> 10); + w[i] = (w[i - 16]! + s0 + w[i - 7]! + s1) >>> 0; + } + + let a = h[0]!, b = h[1]!, c = h[2]!, d = h[3]!; + let e = h[4]!, f = h[5]!, g = h[6]!, hh = h[7]!; + + for (let i = 0; i < 64; i++) { + const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); + const ch = (e & f) ^ (~e & g); + const t1 = (hh + S1 + ch + SHA256_K[i]! + w[i]!) >>> 0; + const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); + const maj = (a & b) ^ (a & c) ^ (b & c); + const t2 = (S0 + maj) >>> 0; + + hh = g; g = f; f = e; e = (d + t1) >>> 0; + d = c; c = b; b = a; a = (t1 + t2) >>> 0; + } + + h[0] = (h[0]! + a) >>> 0; + h[1] = (h[1]! + b) >>> 0; + h[2] = (h[2]! + c) >>> 0; + h[3] = (h[3]! + d) >>> 0; + h[4] = (h[4]! + e) >>> 0; + h[5] = (h[5]! + f) >>> 0; + h[6] = (h[6]! + g) >>> 0; + h[7] = (h[7]! + hh) >>> 0; + } + + const out = new Uint8Array(32); + const outView = new DataView(out.buffer); + for (let i = 0; i < 8; i++) { + outView.setUint32(i * 4, h[i]!, false); + } + return out; +} + +/** + * SHA-256 hash function (synchronous). */ function sha256Sync(data: Uint8Array): Uint8Array { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { createHash } = require('crypto'); - return new Uint8Array(createHash('sha256').update(Buffer.from(data)).digest()); + return sha256Pure(data); } /** - * SHA-256 hash function (async, works in browser and Node.js). + * SHA-256 hash function (async). Uses WebCrypto when available for speed, + * otherwise falls back to the pure implementation. */ async function sha256Async(data: Uint8Array): Promise { if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.subtle) { - // Create a copy of the data to ensure we have an ArrayBuffer const copy = new Uint8Array(data); const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', copy); return new Uint8Array(hashBuffer); } - return sha256Sync(data); + return sha256Pure(data); } /** - * Check if a point is on the ed25519 curve. - * A valid PDA must be OFF the curve. - * - * This is a simplified implementation. - * In practice, most PDAs are valid on first try with bump=255. + * Check if a 32-byte value is a valid point on the ed25519 curve. + * + * A valid PDA must be OFF the curve (it must NOT correspond to a real + * ed25519 public key, so that no private key can ever sign for it). + * + * We determine on-curve status by attempting to decompress the candidate + * as a compressed Edwards point. If decompression succeeds the value lies + * on the curve; if it throws, the value is off-curve and is a valid PDA. + * This matches the behaviour of `PublicKey.isOnCurve` in @solana/web3.js. */ -function isOnCurve(_publicKey: Uint8Array): boolean { - // Simplified heuristic: actual curve check requires ed25519 math - // For Solana PDAs, we try bumps from 255 down - // The first bump (255) almost always produces a valid off-curve point - // We return false here to accept the first result - // In production with @solana/web3.js, use PublicKey.isOnCurve() - return false; +function isOnCurve(publicKey: Uint8Array): boolean { + if (publicKey.length !== 32) { + return false; + } + try { + Point.fromHex(publicKey); + return true; + } catch { + return false; + } } /** diff --git a/typescript/core/src/instructions/seed-serializer.test.ts b/typescript/core/src/instructions/seed-serializer.test.ts new file mode 100644 index 00000000..7e21dd81 --- /dev/null +++ b/typescript/core/src/instructions/seed-serializer.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect } from 'vitest'; + +import { normalizeSeedType, serializeSeedValue } from './seed-serializer'; +import { decodeBase58 } from './pda'; + +const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'; + +describe('normalizeSeedType', () => { + it('canonicalizes pubkey and string spellings', () => { + for (const t of ['pubkey', 'Pubkey', 'publicKey', 'PublicKey', 'solana_pubkey::Pubkey']) { + expect(normalizeSeedType(t)).toBe('pubkey'); + } + for (const t of ['string', 'String', 'str']) { + expect(normalizeSeedType(t)).toBe('string'); + } + }); + + it('passes integer widths through and rejects everything else', () => { + expect(normalizeSeedType('u32')).toBe('u32'); + expect(normalizeSeedType('i64')).toBe('i64'); + expect(normalizeSeedType('u24')).toBeUndefined(); + expect(normalizeSeedType('Vec')).toBeUndefined(); + expect(normalizeSeedType(undefined)).toBeUndefined(); + }); +}); + +describe('serializeSeedValue (typed)', () => { + it('encodes integers little-endian at the declared width', () => { + expect([...serializeSeedValue(1, 'u8')]).toEqual([1]); + expect([...serializeSeedValue(0x0102, 'u16')]).toEqual([2, 1]); + expect([...serializeSeedValue(7, 'u32')]).toEqual([7, 0, 0, 0]); + expect([...serializeSeedValue(42n, 'u64')]).toEqual([42, 0, 0, 0, 0, 0, 0, 0]); + }); + + it('encodes negative signed integers in two\'s complement', () => { + expect([...serializeSeedValue(-1n, 'i64')]).toEqual([ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + ]); + }); + + it('rejects values that overflow the declared width', () => { + expect(() => serializeSeedValue(256, 'u8')).toThrow(/does not fit/); + expect(() => serializeSeedValue(-1, 'u32')).toThrow(/does not fit/); + }); + + it('decodes pubkey seeds from base58 to 32 bytes', () => { + const bytes = serializeSeedValue(TOKEN_PROGRAM, 'pubkey'); + expect(bytes.length).toBe(32); + expect([...bytes]).toEqual([...decodeBase58(TOKEN_PROGRAM)]); + // Path-qualified Rust spelling works too. + expect([...serializeSeedValue(TOKEN_PROGRAM, 'solana_pubkey::Pubkey')]).toEqual([...bytes]); + }); + + it('rejects non-pubkey strings for pubkey seeds', () => { + expect(() => serializeSeedValue('abc', 'pubkey')).toThrow(/expected 32/); + expect(() => serializeSeedValue(42, 'pubkey')).toThrow(/base58 string/); + }); + + it('utf8-encodes typed string seeds without base58 guessing', () => { + // 44 chars: heuristic path would base58-decode this; typed must not. + const fortyFour = 'a'.repeat(44); + expect([...serializeSeedValue(fortyFour, 'string')]).toEqual( + [...new TextEncoder().encode(fortyFour)] + ); + }); +}); + +describe('serializeSeedValue (untyped heuristics)', () => { + it('passes Uint8Array through', () => { + const raw = Uint8Array.from([1, 2, 3]); + expect(serializeSeedValue(raw)).toBe(raw); + }); + + it('tries base58 for 43/44-char strings and utf8 otherwise', () => { + expect(serializeSeedValue(TOKEN_PROGRAM).length).toBe(32); + expect([...serializeSeedValue('treasury')]).toEqual([ + ...new TextEncoder().encode('treasury'), + ]); + }); + + it('encodes numbers as 8-byte little-endian', () => { + expect([...serializeSeedValue(256)]).toEqual([0, 1, 0, 0, 0, 0, 0, 0]); + }); +}); diff --git a/typescript/core/src/instructions/seed-serializer.ts b/typescript/core/src/instructions/seed-serializer.ts new file mode 100644 index 00000000..ebb9fd2f --- /dev/null +++ b/typescript/core/src/instructions/seed-serializer.ts @@ -0,0 +1,128 @@ +/** + * Typed serialization of PDA seed values. + * + * Shared by the standalone PDA DSL (`pda-dsl.ts`) and instruction account + * resolution (`account-resolver.ts`). When a seed carries a declared type + * (from the IDL or the `pdas!` registry), encoding is exact: pubkeys are + * base58-decoded to 32 bytes, integers are little-endian at the declared + * width. Without a type, legacy heuristics apply for backward compatibility. + */ + +import { decodeBase58 } from './pda'; + +/** + * Canonical seed type names: 'pubkey', 'string', or 'u8'..'u128'/'i8'..'i128'. + */ +export type CanonicalSeedType = 'pubkey' | 'string' | `u${number}` | `i${number}`; + +/** + * Normalizes the type-name variants that IDLs and codegen produce + * ("Pubkey", "publicKey", "solana_pubkey::Pubkey", "String", ...) to a + * canonical seed type. Returns undefined for types that cannot be a seed. + */ +export function normalizeSeedType(argType?: string): CanonicalSeedType | undefined { + if (!argType) return undefined; + // Strip any path qualifier (e.g. solana_pubkey::Pubkey). + const parts = argType.split('::'); + const t = (parts[parts.length - 1] ?? argType).trim(); + + if (/^[ui](8|16|32|64|128)$/.test(t)) { + return t as CanonicalSeedType; + } + switch (t) { + case 'pubkey': + case 'Pubkey': + case 'publicKey': + case 'PublicKey': + return 'pubkey'; + case 'string': + case 'String': + case 'str': + return 'string'; + default: + return undefined; + } +} + +/** + * Serializes a PDA seed value. + * + * With a recognized `argType`, encoding is strict and width-exact; an + * incompatible value throws rather than deriving a wrong address. Without + * one, the legacy heuristics apply: raw bytes pass through, 43/44-character + * strings are tried as base58, other strings are utf-8, numbers are 8-byte + * little-endian u64. + */ +export function serializeSeedValue(value: unknown, argType?: string): Uint8Array { + if (value instanceof Uint8Array) { + return value; + } + + const t = normalizeSeedType(argType); + + if (t === 'pubkey') { + if (typeof value !== 'string') { + throw new Error(`Pubkey seed requires a base58 string, got ${typeof value}`); + } + const decoded = decodeBase58(value); + if (decoded.length !== 32) { + throw new Error( + `Pubkey seed '${value}' decoded to ${decoded.length} bytes, expected 32` + ); + } + return decoded; + } + + if (t === 'string') { + if (typeof value !== 'string') { + throw new Error(`String seed requires a string value, got ${typeof value}`); + } + return new TextEncoder().encode(value); + } + + if (t !== undefined) { + // Numeric type at a declared width. + if (typeof value !== 'bigint' && typeof value !== 'number') { + throw new Error(`Numeric seed of type ${t} requires a number/bigint, got ${typeof value}`); + } + const bits = parseInt(t.slice(1), 10); + return serializeNumber(value, bits / 8, t.startsWith('i')); + } + + // --- Untyped: legacy heuristics. --- + if (typeof value === 'string') { + if (value.length === 43 || value.length === 44) { + try { + return decodeBase58(value); + } catch { + return new TextEncoder().encode(value); + } + } + return new TextEncoder().encode(value); + } + + if (typeof value === 'bigint' || typeof value === 'number') { + return serializeNumber(value, 8, true); + } + + throw new Error(`Cannot serialize value for PDA seed: ${typeof value}`); +} + +/** + * Little-endian two's-complement encoding at a fixed byte width, with an + * overflow check so out-of-range values fail instead of silently truncating. + */ +function serializeNumber(value: bigint | number, size: number, signed: boolean): Uint8Array { + const buffer = new Uint8Array(size); + let n = typeof value === 'bigint' ? value : BigInt(value); + const original = n; + for (let i = 0; i < size; i++) { + buffer[i] = Number(n & BigInt(0xff)); + n >>= BigInt(8); + } + const fits = signed ? n === BigInt(0) || n === BigInt(-1) : n === BigInt(0); + if (!fits) { + throw new Error(`Seed value ${original} does not fit in ${size * 8} bits`); + } + return buffer; +} diff --git a/typescript/core/src/instructions/serializer.ts b/typescript/core/src/instructions/serializer.ts index 69539d44..f8373dc3 100644 --- a/typescript/core/src/instructions/serializer.ts +++ b/typescript/core/src/instructions/serializer.ts @@ -5,6 +5,8 @@ * expected by Solana programs using Borsh serialization. */ +import { decodeBase58 } from './pda'; + /** * Instruction argument schema for serialization. */ @@ -17,16 +19,42 @@ export interface ArgSchema { /** * Supported argument types for Borsh serialization. + * + * Struct and enum schemas are fully inlined (field names and types travel + * with the schema), so the serializer needs no runtime type registry. */ export type ArgType = | 'u8' | 'u16' | 'u32' | 'u64' | 'u128' | 'i8' | 'i16' | 'i32' | 'i64' | 'i128' + | 'f32' | 'f64' | 'bool' | 'string' | 'pubkey' + | 'bytes' | { vec: ArgType } | { option: ArgType } - | { array: [ArgType, number] }; + | { array: [ArgType, number] } + | { struct: ArgStructField[] } + | { enum: EnumVariant[] }; + +/** One field of a struct schema, in declaration (serialization) order. */ +export interface ArgStructField { + name: string; + type: ArgType; +} + +/** + * One enum variant: a bare string for fieldless variants, or a named variant + * carrying struct fields / tuple elements. + * + * Values: fieldless variants are passed as the variant name (or its index); + * data-carrying variants as a single-key object, e.g. `{ transfer: { amount } }` + * or `{ pair: [1, 2] }`. + */ +export type EnumVariant = + | string + | { name: string; fields: ArgStructField[] } + | { name: string; tuple: ArgType[] }; /** * Serializes instruction arguments into a Buffer using Borsh encoding. @@ -42,13 +70,20 @@ export function serializeInstructionData( schema: ArgSchema[] ): Buffer { const buffers: Buffer[] = [Buffer.from(discriminator)]; - + for (const field of schema) { const value = args[field.name]; + // Option fields treat undefined as None; everything else must be present + // (silently encoding zeros for a missing arg corrupts instruction data). + if (value === undefined && !(typeof field.type === 'object' && 'option' in field.type)) { + throw new Error( + `Missing required argument "${field.name}" (type ${JSON.stringify(field.type)})` + ); + } const serialized = serializeValue(value, field.type); buffers.push(serialized); } - + return Buffer.concat(buffers); } @@ -68,10 +103,110 @@ function serializeValue(value: unknown, type: ArgType): Buffer { if ('array' in type) { return serializeArray(value as unknown[], type.array[0], type.array[1]); } - + + if ('struct' in type) { + return serializeStruct(value, type.struct); + } + + if ('enum' in type) { + return serializeEnum(value, type.enum); + } + throw new Error(`Unknown type: ${JSON.stringify(type)}`); } +function serializeStruct(value: unknown, fields: ArgStructField[]): Buffer { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new Error(`Struct value must be a plain object, got ${typeof value}`); + } + const obj = value as Record; + const buffers: Buffer[] = []; + for (const field of fields) { + const fieldValue = obj[field.name]; + if ( + fieldValue === undefined && + !(typeof field.type === 'object' && 'option' in field.type) + ) { + throw new Error(`Missing required struct field "${field.name}"`); + } + buffers.push(serializeValue(fieldValue, field.type)); + } + return Buffer.concat(buffers); +} + +function variantName(variant: EnumVariant): string { + return typeof variant === 'string' ? variant : variant.name; +} + +function serializeEnum(value: unknown, variants: EnumVariant[]): Buffer { + // Numeric value: a bare variant index (fieldless variants only). + if (typeof value === 'number') { + if (!Number.isInteger(value) || value < 0 || value >= variants.length) { + throw new Error(`Enum variant index ${value} out of range (0..${variants.length - 1})`); + } + const variant = variants[value]!; + if (typeof variant !== 'string') { + throw new Error( + `Enum variant "${variantName(variant)}" carries data; pass { ${variantName(variant)}: ... } instead of an index` + ); + } + return Buffer.from([value]); + } + + // String value: a fieldless variant by name. + if (typeof value === 'string') { + const index = variants.findIndex((v) => variantName(v) === value); + if (index === -1) { + throw new Error( + `Unknown enum variant "${value}". Expected one of: ${variants.map(variantName).join(', ')}` + ); + } + const variant = variants[index]!; + if (typeof variant !== 'string') { + throw new Error( + `Enum variant "${value}" carries data; pass { ${value}: ... } instead of a bare name` + ); + } + return Buffer.from([index]); + } + + // Object value: { variantName: payload } for data-carrying variants. + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + const keys = Object.keys(value); + if (keys.length !== 1) { + throw new Error( + `Enum value must be a single-key object ({ variantName: payload }), got keys [${keys.join(', ')}]` + ); + } + const key = keys[0]!; + const payload = (value as Record)[key]; + const index = variants.findIndex((v) => variantName(v) === key); + if (index === -1) { + throw new Error( + `Unknown enum variant "${key}". Expected one of: ${variants.map(variantName).join(', ')}` + ); + } + const variant = variants[index]!; + if (typeof variant === 'string') { + throw new Error(`Enum variant "${key}" is fieldless; pass '${key}' instead of an object`); + } + const prefix = Buffer.from([index]); + if ('fields' in variant) { + return Buffer.concat([prefix, serializeStruct(payload, variant.fields)]); + } + const tuple = payload as unknown[]; + if (!Array.isArray(tuple) || tuple.length !== variant.tuple.length) { + throw new Error( + `Enum variant "${key}" expects a tuple of length ${variant.tuple.length}` + ); + } + const elements = variant.tuple.map((elementType, i) => serializeValue(tuple[i], elementType)); + return Buffer.concat([prefix, ...elements]); + } + + throw new Error(`Cannot serialize enum from value of type ${typeof value}`); +} + function serializePrimitive(value: unknown, type: string): Buffer { switch (type) { case 'u8': @@ -112,21 +247,61 @@ function serializePrimitive(value: unknown, type: string): Buffer { case 'i128': const i128 = Buffer.alloc(16); const bigI128 = BigInt(value as string | number | bigint); - i128.writeBigInt64LE(bigI128 & BigInt('0xFFFFFFFFFFFFFFFF'), 0); + // The masked low limb is always non-negative, so it must be written + // unsigned; the arithmetic-shifted high limb carries the sign. + i128.writeBigUInt64LE(bigI128 & BigInt('0xFFFFFFFFFFFFFFFF'), 0); i128.writeBigInt64LE(bigI128 >> BigInt(64), 8); return i128; + case 'f32': { + const f32 = Buffer.alloc(4); + f32.writeFloatLE(value as number, 0); + return f32; + } + case 'f64': { + const f64 = Buffer.alloc(8); + f64.writeDoubleLE(value as number, 0); + return f64; + } case 'bool': return Buffer.from([value as boolean ? 1 : 0]); + case 'bytes': { + // Borsh bytes: u32 LE length prefix + raw bytes. + const raw = + value instanceof Uint8Array + ? value + : Array.isArray(value) + ? Uint8Array.from(value as number[]) + : null; + if (raw === null) { + throw new Error(`Cannot serialize bytes from value of type ${typeof value}`); + } + const lenPrefix = Buffer.alloc(4); + lenPrefix.writeUInt32LE(raw.length, 0); + return Buffer.concat([lenPrefix, Buffer.from(raw)]); + } case 'string': const str = value as string; const strBytes = Buffer.from(str, 'utf-8'); const strLen = Buffer.alloc(4); strLen.writeUInt32LE(strBytes.length, 0); return Buffer.concat([strLen, strBytes]); - case 'pubkey': - // Public key is 32 bytes - // In production, decode base58 to 32 bytes - return Buffer.alloc(32, 0); + case 'pubkey': { + // Public key is 32 bytes. Accept base58 strings or raw 32-byte buffers. + if (value instanceof Uint8Array) { + if (value.length !== 32) { + throw new Error(`Invalid pubkey byte length: expected 32, got ${value.length}`); + } + return Buffer.from(value); + } + if (typeof value === 'string') { + const decoded = decodeBase58(value); + if (decoded.length !== 32) { + throw new Error(`Invalid pubkey: '${value}' decoded to ${decoded.length} bytes, expected 32`); + } + return Buffer.from(decoded); + } + throw new Error(`Cannot serialize pubkey from value of type ${typeof value}`); + } default: throw new Error(`Unknown primitive type: ${type}`); } diff --git a/typescript/core/src/types.ts b/typescript/core/src/types.ts index ef94ebb5..646e57cd 100644 --- a/typescript/core/src/types.ts +++ b/typescript/core/src/types.ts @@ -26,9 +26,22 @@ export interface StackDefinition { readonly url: string; readonly views: Record; readonly schemas?: Record>; - instructions?: Record; + // Handlers carry specific phantom param/error types; accept any of them + // here. Multi-program stacks nest handlers one level deep per program + // (e.g. instructions.ore.close); single-program stacks stay flat. + instructions?: Record; } +/** + * One entry in a stack definition's `instructions` block: either a handler + * (single-program stacks) or a per-program map of handlers (multi-program). + */ +export type StackInstructionEntry = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | import('./instructions').InstructionHandler + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | Record>; + export interface ViewGroup { state?: ViewDef; list?: ViewDef; diff --git a/typescript/core/src/wallet/types.ts b/typescript/core/src/wallet/types.ts index b4e22c77..3b08a92a 100644 --- a/typescript/core/src/wallet/types.ts +++ b/typescript/core/src/wallet/types.ts @@ -1,17 +1,97 @@ +/** + * Wallet adapter boundary for the Arete SDK. + * + * The core SDK is intentionally RPC-free: it only constructs `BuiltInstruction` + * objects. Everything network-related (recent blockhash, message compilation, + * signing, sending, and confirmation) lives behind the `WalletAdapter` + * boundary, implemented by adapters that wrap the Solana library of your choice + * (@solana/web3.js, @solana/kit, a raw Keypair signer for scripts, etc.). + */ + +/** + * A single account reference within a built instruction. + */ +export interface BuiltAccountMeta { + /** Account address as a base58-encoded string */ + pubkey: string; + /** Whether this account must sign the transaction */ + isSigner: boolean; + /** Whether this account is writable */ + isWritable: boolean; +} + +/** + * A framework-agnostic representation of a Solana instruction. + * + * This is the boundary type between the core SDK (which builds instructions) + * and wallet adapters (which broadcast them). It maps 1:1 onto a + * @solana/web3.js `TransactionInstruction` or a @solana/kit `Instruction`. + */ +export interface BuiltInstruction { + /** Program ID (base58) */ + programId: string; + /** Account keys, in the exact order required by the program */ + keys: BuiltAccountMeta[]; + /** Serialized instruction data (discriminator + Borsh-encoded args) */ + data: Uint8Array; +} + +/** + * Confirmation level for transaction processing. + * - `processed`: Transaction processed but not confirmed + * - `confirmed`: Transaction confirmed by cluster + * - `finalized`: Transaction finalized (recommended for production) + */ +export type ConfirmationLevel = 'processed' | 'confirmed' | 'finalized'; + +/** + * Options forwarded to the wallet adapter when sending a transaction. + * + * The core SDK does not interpret these; it passes them straight through to + * the adapter, which owns all RPC semantics. + */ +export interface SendOptions { + /** Confirmation level the adapter should wait for */ + confirmationLevel?: ConfirmationLevel; + /** Skip the RPC preflight simulation */ + skipPreflight?: boolean; + /** Adapter-specific passthrough options (priority fees, lookup tables, etc.) */ + [key: string]: unknown; +} + +/** + * Result returned by a wallet adapter after broadcasting a transaction. + */ +export interface SendResult { + /** Transaction signature (base58) */ + signature: string; + /** Slot in which the transaction landed, if the adapter reports it */ + slot?: number; +} + /** * Wallet adapter interface for signing and sending transactions. - * This is framework-agnostic and can be implemented by any wallet provider. + * + * Implementations own blockhash fetching, message compilation (legacy or v0), + * signing, sending, and confirmation. The core SDK only needs `publicKey` for + * signer-account resolution and `signAndSend` to broadcast built instructions. */ export interface WalletAdapter { /** The wallet's public key as a base58-encoded string */ publicKey: string; /** - * Sign and send a transaction. - * @param transaction - The transaction to sign and send (can be raw bytes, a Transaction object, or an array of instructions) - * @returns The transaction signature + * Compile, sign, and broadcast one or more built instructions as a single + * transaction. + * + * Accepting an array (rather than a single instruction) makes batching and + * composition fall out for free. + * + * @param instructions - Instructions to include in the transaction, in order + * @param options - Adapter-specific send/confirmation options + * @returns The transaction signature (and slot, if known) */ - signAndSend: (transaction: unknown) => Promise; + signAndSend(instructions: BuiltInstruction[], options?: SendOptions): Promise; } /** diff --git a/typescript/react/src/hooks/use-mutation.ts b/typescript/react/src/hooks/use-mutation.ts index 39f5a7e3..dab862ae 100644 --- a/typescript/react/src/hooks/use-mutation.ts +++ b/typescript/react/src/hooks/use-mutation.ts @@ -1,10 +1,10 @@ import { useState, useCallback } from 'react'; -import type { - InstructionExecutor, - InstructionExecutorOptions, +import type { + TypedInstruction, + InstructionExecutorOptions, ExecutionResult, } from '@usearete/sdk'; -import { parseInstructionError } from '@usearete/sdk'; +import { InstructionError } from '@usearete/sdk'; export type MutationStatus = 'idle' | 'pending' | 'success' | 'error'; @@ -13,8 +13,15 @@ export interface UseMutationOptions extends InstructionExecutorOptions { onError?: (error: Error) => void; } -export interface UseMutationResult { - submit: (args: Record, options?: Partial) => Promise; +/** + * Result of {@link useInstructionMutation}. + * + * `TParams` is the merged params object accepted by the instruction (IDL args + * plus any user-provided account addresses), inferred from the generated + * handler. `TError` is the handler's typed program-error union. + */ +export interface UseMutationResult, _TError = unknown> { + submit: (args: TParams, options?: Partial) => Promise; status: MutationStatus; error: string | null; signature: string | null; @@ -22,15 +29,15 @@ export interface UseMutationResult { reset: () => void; } -export function useInstructionMutation( - execute: InstructionExecutor -): UseMutationResult { +export function useInstructionMutation, TError = unknown>( + execute: TypedInstruction +): UseMutationResult { const [status, setStatus] = useState('idle'); const [error, setError] = useState(null); const [signature, setSignature] = useState(null); const submit = useCallback(async ( - args: Record, + args: TParams, options?: Partial ): Promise => { setStatus('pending'); @@ -49,12 +56,14 @@ export function useInstructionMutation( return result; } catch (err) { - const errorMessage = err instanceof Error ? err.message : String(err); - - const programError = parseInstructionError(err, []); - const displayError = programError - ? `${programError.name}: ${programError.message}` - : errorMessage; + // The core executor already parses program errors against the handler's + // IDL error definitions and throws an InstructionError. + const displayError = + err instanceof InstructionError && err.programError + ? `${err.programError.name}: ${err.programError.message}` + : err instanceof Error + ? err.message + : String(err); setStatus('error'); setError(displayError); diff --git a/typescript/react/src/index.ts b/typescript/react/src/index.ts index e931da37..8488e186 100644 --- a/typescript/react/src/index.ts +++ b/typescript/react/src/index.ts @@ -25,10 +25,12 @@ export { createSeed, createPublicKeySeed, serializeInstructionData, - waitForConfirmation, parseInstructionError, formatProgramError, + InstructionError, + buildInstruction, executeInstruction, + createInstructionHandler, createInstructionExecutor, } from '@usearete/sdk'; @@ -66,13 +68,18 @@ export type { ArgSchema, ArgType, ConfirmationLevel, + SendOptions, + SendResult, + BuildOptions, ExecuteOptions, ExecutionResult, ProgramError, ErrorMetadata, InstructionHandler, - InstructionDefinition, + InstructionHandlerConfig, + TypedInstruction, BuiltInstruction, + BuiltAccountMeta, } from '@usearete/sdk'; export type { diff --git a/typescript/react/src/provider.tsx b/typescript/react/src/provider.tsx index def53394..cf412791 100644 --- a/typescript/react/src/provider.tsx +++ b/typescript/react/src/provider.tsx @@ -64,6 +64,7 @@ export function AreteProvider({ maxEntriesPerView: config.maxEntriesPerView, flushIntervalMs: config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS, auth: config.auth, + wallet: config.wallet, }).then((client) => { client.onConnectionStateChange((state, error) => { adapter.setConnectionState(state, error); @@ -81,7 +82,7 @@ export function AreteProvider({ connectingRef.current.set(cacheKey, connectionPromise); return connectionPromise as Promise>; - }, [config.autoConnect, config.reconnectIntervals, config.maxReconnectAttempts, config.maxEntriesPerView, config.flushIntervalMs, config.auth, notifyClientChange]); + }, [config.autoConnect, config.reconnectIntervals, config.maxReconnectAttempts, config.maxEntriesPerView, config.flushIntervalMs, config.auth, config.wallet, notifyClientChange]); const getClient = useCallback((stack: TStack | undefined): Arete | null => { if (!stack) { diff --git a/typescript/react/src/stack.ts b/typescript/react/src/stack.ts index 35177873..04aa0ca7 100644 --- a/typescript/react/src/stack.ts +++ b/typescript/react/src/stack.ts @@ -3,7 +3,7 @@ import type { ConnectionState } from '@usearete/sdk'; import type { StoreApi, UseBoundStore } from 'zustand'; import { useAreteContext } from './provider'; import { createStateViewHook, createListViewHook } from './view-hooks'; -import { useInstructionMutation, UseMutationResult } from './hooks'; +import { useInstructionMutation, type UseMutationResult } from './hooks'; import type { StackDefinition, ViewDef, @@ -17,7 +17,7 @@ import type { UseAreteOptions } from './types'; import { ZustandAdapter, type AreteStore } from './zustand-adapter'; -import type { InstructionHandler, InstructionExecutor } from '@usearete/sdk'; +import type { InstructionHandler, TypedInstruction, StackInstructionEntry } from '@usearete/sdk'; import type { Arete } from '@usearete/sdk'; type ViewHookForDef = TDef extends ViewDef @@ -57,15 +57,38 @@ type BuildViewInterface> = { }; }; -type InstructionHook = { - useMutation: () => UseMutationResult; - execute: InstructionExecutor; -}; - -type BuildInstructionInterface | undefined> = - TInstructions extends Record - ? { [K in keyof TInstructions]: InstructionHook } - : {}; +/** + * Per-instruction hook surface, with params/error types inferred from the + * generated handler's phantom types. + */ +type InstructionHookFor = THandler extends InstructionHandler + ? { + useMutation: () => UseMutationResult; + execute: TypedInstruction; + } + : { + useMutation: () => UseMutationResult; + execute: TypedInstruction, unknown>; + }; + +/** + * Maps one stack-definition instruction entry to its hook surface. Handlers + * map directly; per-program maps (multi-program stacks) nest one level. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type InstructionEntryHooks = TEntry extends InstructionHandler + ? InstructionHookFor + : // eslint-disable-next-line @typescript-eslint/no-explicit-any + TEntry extends Record> + ? { [K in keyof TEntry]: InstructionHookFor } + : InstructionHookFor; + +type BuildInstructionInterface< + TInstructions extends Record | undefined, +> = + TInstructions extends Record + ? { [K in keyof TInstructions]: InstructionEntryHooks } + : Record; type StackClient = { views: BuildViewInterface; @@ -150,14 +173,34 @@ export function useArete( }, [stack, client]); const instructions = useMemo(() => { - const result: Record = {}; + type Hook = { + execute: TypedInstruction, unknown>; + useMutation: () => UseMutationResult; + }; + const toHook = (executeFn: unknown): Hook => { + const execute = executeFn as TypedInstruction, unknown>; + return { + execute, + useMutation: () => useInstructionMutation(execute), + }; + }; + + const result: Record> = {}; if (client?.instructions) { - for (const [instructionName, executeFn] of Object.entries(client.instructions)) { - result[instructionName] = { - execute: executeFn as InstructionExecutor, - useMutation: () => useInstructionMutation(executeFn as InstructionExecutor) - }; + for (const [name, entry] of Object.entries(client.instructions)) { + if (typeof entry === 'function') { + result[name] = toHook(entry); + } else { + // Multi-program stacks: one nested hook map per program. + const nested: Record = {}; + for (const [instructionName, executeFn] of Object.entries( + entry as Record + )) { + nested[instructionName] = toHook(executeFn); + } + result[name] = nested; + } } } From ae481cb0361f26ac59e71e75c51780f70571fd28 Mon Sep 17 00:00:00 2001 From: Adrian Henry Date: Fri, 12 Jun 2026 02:07:01 +0100 Subject: [PATCH 4/5] feat: add reference wallet adapters for instruction execution --- typescript/adapters/kit/README.md | 37 + typescript/adapters/kit/package.json | 60 + typescript/adapters/kit/rollup.config.js | 22 + typescript/adapters/kit/src/index.ts | 122 + typescript/adapters/kit/tsconfig.json | 23 + typescript/adapters/web3js/README.md | 55 + typescript/adapters/web3js/package-lock.json | 3638 ++++++++++++++++++ typescript/adapters/web3js/package.json | 60 + typescript/adapters/web3js/rollup.config.js | 22 + typescript/adapters/web3js/src/index.ts | 142 + typescript/adapters/web3js/tsconfig.json | 23 + 11 files changed, 4204 insertions(+) create mode 100644 typescript/adapters/kit/README.md create mode 100644 typescript/adapters/kit/package.json create mode 100644 typescript/adapters/kit/rollup.config.js create mode 100644 typescript/adapters/kit/src/index.ts create mode 100644 typescript/adapters/kit/tsconfig.json create mode 100644 typescript/adapters/web3js/README.md create mode 100644 typescript/adapters/web3js/package-lock.json create mode 100644 typescript/adapters/web3js/package.json create mode 100644 typescript/adapters/web3js/rollup.config.js create mode 100644 typescript/adapters/web3js/src/index.ts create mode 100644 typescript/adapters/web3js/tsconfig.json diff --git a/typescript/adapters/kit/README.md b/typescript/adapters/kit/README.md new file mode 100644 index 00000000..e85c17ec --- /dev/null +++ b/typescript/adapters/kit/README.md @@ -0,0 +1,37 @@ +# @usearete/adapter-kit + +A reference [`WalletAdapter`](../../core/src/wallet/types.ts) for the Arete SDK, backed by [`@solana/kit`](https://github.com/anza-xyz/kit) (the functional successor to `@solana/web3.js`). + +The Arete core SDK is intentionally RPC-free: it only constructs `BuiltInstruction` objects. This adapter owns blockhash fetching, transaction message construction, signing, sending, and confirmation. + +## Install + +```bash +npm install @usearete/adapter-kit @solana/kit @usearete/sdk +``` + +## Usage + +```ts +import { Arete } from '@usearete/sdk'; +import { createWalletAdapter } from '@usearete/adapter-kit'; +import { + createSolanaRpc, + createSolanaRpcSubscriptions, + createKeyPairSignerFromBytes, +} from '@solana/kit'; +import { MY_STACK } from './generated/my-stack'; + +const rpc = createSolanaRpc('https://api.devnet.solana.com'); +const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.devnet.solana.com'); +const signer = await createKeyPairSignerFromBytes(secretKeyBytes); + +const wallet = createWalletAdapter({ rpc, rpcSubscriptions, signer }); +const client = await Arete.connect(MY_STACK, { wallet }); + +const { signature } = await client.instructions.buy({ + amount: 1_000_000n, + maxSolCost: 100_000_000n, + mint: 'So11111111111111111111111111111111111111112', +}); +``` diff --git a/typescript/adapters/kit/package.json b/typescript/adapters/kit/package.json new file mode 100644 index 00000000..9e0d77dd --- /dev/null +++ b/typescript/adapters/kit/package.json @@ -0,0 +1,60 @@ +{ + "name": "@usearete/adapter-kit", + "version": "0.1.0", + "type": "module", + "description": "@solana/kit wallet adapter for the Arete SDK instruction boundary", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.esm.js", + "require": "./dist/index.js" + } + }, + "repository": { + "type": "git", + "url": "git+https://github.com/AreteA4/arete.git", + "directory": "typescript/adapters/kit" + }, + "scripts": { + "build": "rollup -c", + "dev": "rollup -c -w", + "lint": "eslint src/**/*.ts", + "typecheck": "tsc --noEmit", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "solana", + "kit", + "wallet", + "arete" + ], + "author": "Arete Team", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@solana/kit": "^2.1.0", + "@usearete/sdk": "^0.1.3" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^11.0.0", + "@solana/kit": "^2.1.0", + "@types/node": "^20.0.0", + "@usearete/sdk": "^0.1.3", + "rollup": "^3.0.0", + "rollup-plugin-dts": "^6.0.0", + "tslib": "^2.8.1", + "typescript": "^5.0.0" + }, + "files": [ + "dist", + "README.md" + ] +} diff --git a/typescript/adapters/kit/rollup.config.js b/typescript/adapters/kit/rollup.config.js new file mode 100644 index 00000000..3b4fe8a5 --- /dev/null +++ b/typescript/adapters/kit/rollup.config.js @@ -0,0 +1,22 @@ +import typescript from '@rollup/plugin-typescript'; +import dts from 'rollup-plugin-dts'; + +export default [ + { + input: 'src/index.ts', + output: [ + { file: 'dist/index.js', format: 'cjs', sourcemap: true }, + { file: 'dist/index.esm.js', format: 'esm', sourcemap: true }, + ], + external: ['@solana/kit', '@usearete/sdk'], + plugins: [ + typescript({ tsconfig: './tsconfig.json', declaration: false }), + ], + }, + { + input: 'src/index.ts', + output: { file: 'dist/index.d.ts', format: 'es' }, + external: ['@solana/kit', '@usearete/sdk'], + plugins: [dts()], + }, +]; diff --git a/typescript/adapters/kit/src/index.ts b/typescript/adapters/kit/src/index.ts new file mode 100644 index 00000000..32be6f9c --- /dev/null +++ b/typescript/adapters/kit/src/index.ts @@ -0,0 +1,122 @@ +/** + * @usearete/adapter-kit + * + * A reference {@link WalletAdapter} implementation backed by @solana/kit + * (the functional successor to @solana/web3.js). + * + * The Arete core SDK is RPC-free: it only builds `BuiltInstruction` objects. + * This adapter owns blockhash fetching, message construction, signing, + * sending, and confirmation. + */ + +import { + address, + pipe, + createTransactionMessage, + setTransactionMessageFeePayerSigner, + setTransactionMessageLifetimeUsingBlockhash, + appendTransactionMessageInstructions, + signTransactionMessageWithSigners, + getSignatureFromTransaction, + sendAndConfirmTransactionFactory, + AccountRole, + type Rpc, + type RpcSubscriptions, + type SolanaRpcApi, + type SolanaRpcSubscriptionsApi, + type TransactionSigner, + type IInstruction, + type IAccountMeta, + type Commitment, +} from '@solana/kit'; +import type { + WalletAdapter, + BuiltInstruction, + BuiltAccountMeta, + SendOptions, + SendResult, + ConfirmationLevel, +} from '@usearete/sdk'; + +export interface KitAdapterConfig { + /** A Solana RPC client (from `createSolanaRpc`). */ + rpc: Rpc; + /** A Solana RPC subscriptions client (from `createSolanaRpcSubscriptions`). */ + rpcSubscriptions: RpcSubscriptions; + /** The fee-payer / signer for transactions. */ + signer: TransactionSigner; + /** Default commitment used when the caller does not specify one. */ + defaultCommitment?: Commitment; +} + +function toCommitment( + level: ConfirmationLevel | undefined, + fallback: Commitment +): Commitment { + return (level as Commitment | undefined) ?? fallback; +} + +/** Map an Arete account meta to a kit AccountRole. */ +function toAccountRole(meta: BuiltAccountMeta): AccountRole { + if (meta.isSigner && meta.isWritable) return AccountRole.WRITABLE_SIGNER; + if (meta.isSigner && !meta.isWritable) return AccountRole.READONLY_SIGNER; + if (!meta.isSigner && meta.isWritable) return AccountRole.WRITABLE; + return AccountRole.READONLY; +} + +/** Convert an Arete BuiltInstruction to a kit IInstruction. */ +function toKitInstruction(ix: BuiltInstruction): IInstruction { + const accounts: IAccountMeta[] = ix.keys.map((k) => ({ + address: address(k.pubkey), + role: toAccountRole(k), + })); + return { + programAddress: address(ix.programId), + accounts, + data: ix.data, + }; +} + +/** + * Create a {@link WalletAdapter} from a kit RPC pair and a signer. + */ +export function createWalletAdapter(config: KitAdapterConfig): WalletAdapter { + const { rpc, rpcSubscriptions, signer } = config; + const fallbackCommitment = config.defaultCommitment ?? 'confirmed'; + const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); + + return { + publicKey: signer.address, + + async signAndSend( + instructions: BuiltInstruction[], + options?: SendOptions + ): Promise { + if (instructions.length === 0) { + throw new Error('signAndSend requires at least one instruction'); + } + + const commitment = toCommitment(options?.confirmationLevel, fallbackCommitment); + const { value: latestBlockhash } = await rpc + .getLatestBlockhash({ commitment }) + .send(); + + const message = pipe( + createTransactionMessage({ version: 0 }), + (m) => setTransactionMessageFeePayerSigner(signer, m), + (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), + (m) => appendTransactionMessageInstructions(instructions.map(toKitInstruction), m) + ); + + const signedTransaction = await signTransactionMessageWithSigners(message); + + await sendAndConfirm(signedTransaction, { + commitment, + skipPreflight: options?.skipPreflight ?? false, + }); + + const signature = getSignatureFromTransaction(signedTransaction); + return { signature }; + }, + }; +} diff --git a/typescript/adapters/kit/tsconfig.json b/typescript/adapters/kit/tsconfig.json new file mode 100644 index 00000000..e252c6d2 --- /dev/null +++ b/typescript/adapters/kit/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM"], + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/typescript/adapters/web3js/README.md b/typescript/adapters/web3js/README.md new file mode 100644 index 00000000..b71755e6 --- /dev/null +++ b/typescript/adapters/web3js/README.md @@ -0,0 +1,55 @@ +# @usearete/adapter-web3js + +A reference [`WalletAdapter`](../../core/src/wallet/types.ts) for the Arete SDK, backed by [`@solana/web3.js`](https://github.com/solana-labs/solana-web3.js). + +The Arete core SDK is intentionally RPC-free: it only constructs `BuiltInstruction` objects. This adapter owns everything network-related: fetching a recent blockhash, compiling a v0 message, signing, sending, and confirming. + +## Install + +```bash +npm install @usearete/adapter-web3js @solana/web3.js @usearete/sdk +``` + +## Usage (Node / scripts / bots) + +```ts +import { Arete } from '@usearete/sdk'; +import { createKeypairWalletAdapter } from '@usearete/adapter-web3js'; +import { Connection, Keypair } from '@solana/web3.js'; +import { MY_STACK } from './generated/my-stack'; + +const connection = new Connection('https://api.devnet.solana.com', 'confirmed'); +const keypair = Keypair.fromSecretKey(/* ... */); +const wallet = createKeypairWalletAdapter({ connection, keypair }); + +const client = await Arete.connect(MY_STACK, { wallet }); + +// Single flat params object: instruction args + any user-provided accounts. +const { signature } = await client.instructions.buy({ + amount: 1_000_000n, + maxSolCost: 100_000_000n, + mint: 'So11111111111111111111111111111111111111112', +}); +``` + +## Usage (browser / wallet-standard) + +```ts +import { createWalletAdapter } from '@usearete/adapter-web3js'; + +const wallet = createWalletAdapter({ + connection, + signer: { + publicKey: walletStandardAccount.publicKey, + signTransaction: (tx) => walletStandardSigner.signTransaction(tx), + }, +}); +``` + +## Batching + +```ts +const buy = client.instructions.buy.build({ amount: 1000n, mint }); +const stake = client.instructions.stake.build({ amount: 1000n }); +const { signature } = await client.transaction([buy, stake]); +``` diff --git a/typescript/adapters/web3js/package-lock.json b/typescript/adapters/web3js/package-lock.json new file mode 100644 index 00000000..81645514 --- /dev/null +++ b/typescript/adapters/web3js/package-lock.json @@ -0,0 +1,3638 @@ +{ + "name": "@usearete/adapter-web3js", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@usearete/adapter-web3js", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@rollup/plugin-typescript": "^11.0.0", + "@solana/web3.js": "^1.95.0", + "@types/node": "^20.0.0", + "@usearete/sdk": "^0.1.3", + "rollup": "^3.0.0", + "rollup-plugin-dts": "^6.0.0", + "tslib": "^2.8.1", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.0", + "@usearete/sdk": "^0.1.3" + } + }, + "../../core": { + "name": "@usearete/sdk", + "version": "0.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/ed25519": "^2.3.0", + "jsonwebtoken": "^9.0.2", + "pako": "^2.1.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^11.0.0", + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^20.0.0", + "@types/pako": "^2.0.3", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.0.0", + "rollup": "^3.0.0", + "rollup-plugin-dts": "^6.0.0", + "tslib": "^2.8.1", + "typescript": "^5.0.0", + "vitest": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "../../core/node_modules/@babel/code-frame": { + "version": "7.28.6", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "../../core/node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "../../core/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "../../core/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "../../core/node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "../../core/node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../core/node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "../../core/node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "../../core/node_modules/@eslint/js": { + "version": "8.57.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "../../core/node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "../../core/node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "../../core/node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "../../core/node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "../../core/node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "../../core/node_modules/@jest/schemas": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "../../core/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/@noble/ed25519": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "../../core/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "../../core/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "../../core/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "../../core/node_modules/@rollup/plugin-typescript": { + "version": "11.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "../../core/node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "../../core/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "../../core/node_modules/@sinclair/typebox": { + "version": "0.27.8", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "../../core/node_modules/@types/ms": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/@types/node": { + "version": "20.19.30", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "../../core/node_modules/@types/pako": { + "version": "2.0.4", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/@types/semver": { + "version": "7.7.1", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../core/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../core/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../../core/node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../core/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../../core/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../../core/node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "../../core/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../../core/node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "dev": true, + "license": "ISC" + }, + "../../core/node_modules/@vitest/expect": { + "version": "1.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../core/node_modules/@vitest/runner": { + "version": "1.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../core/node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/@vitest/snapshot": { + "version": "1.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../core/node_modules/@vitest/spy": { + "version": "1.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../core/node_modules/@vitest/utils": { + "version": "1.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../core/node_modules/@vitest/utils/node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "../../core/node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "../../core/node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "../../core/node_modules/acorn-walk": { + "version": "8.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "../../core/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "../../core/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "../../core/node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "../../core/node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/assertion-error": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "../../core/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "../../core/node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "../../core/node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../../core/node_modules/chai": { + "version": "4.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "../../core/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "../../core/node_modules/check-error": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "../../core/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "../../core/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/confbox": { + "version": "0.1.8", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "../../core/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "../../core/node_modules/deep-eql": { + "version": "4.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "../../core/node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/diff-sequences": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "../../core/node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "../../core/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "../../core/node_modules/esbuild": { + "version": "0.21.5", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "../../core/node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/eslint": { + "version": "8.57.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../core/node_modules/eslint-scope": { + "version": "7.2.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../core/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../core/node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "../../core/node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "../../core/node_modules/espree": { + "version": "9.6.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../../core/node_modules/esquery": { + "version": "1.7.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "../../core/node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "../../core/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "../../core/node_modules/estree-walker": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../../core/node_modules/execa": { + "version": "8.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "../../core/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/fast-glob": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "../../core/node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "../../core/node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/fastq": { + "version": "1.20.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "../../core/node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "../../core/node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/flat-cache": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "../../core/node_modules/flatted": { + "version": "3.3.3", + "dev": true, + "license": "ISC" + }, + "../../core/node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "../../core/node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "../../core/node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../core/node_modules/get-func-name": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "../../core/node_modules/get-stream": { + "version": "8.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../core/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "../../core/node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "../../core/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "../../core/node_modules/globals": { + "version": "13.24.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "../../core/node_modules/human-signals": { + "version": "5.0.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "../../core/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "../../core/node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "../../core/node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "../../core/node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "../../core/node_modules/is-core-module": { + "version": "2.16.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../core/node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../core/node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "../../core/node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "../../core/node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/is-stream": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../../core/node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "optional": true + }, + "../../core/node_modules/js-yaml": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "../../core/node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/jsonwebtoken": { + "version": "9.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "../../core/node_modules/jwa": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "../../core/node_modules/jws": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "../../core/node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "../../core/node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../../core/node_modules/local-pkg": { + "version": "0.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "../../core/node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/lodash.includes": { + "version": "4.3.0", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/lodash.isboolean": { + "version": "3.0.3", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/lodash.isinteger": { + "version": "4.0.4", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/lodash.isnumber": { + "version": "3.0.3", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/lodash.isplainobject": { + "version": "4.0.6", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/lodash.isstring": { + "version": "4.0.1", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/lodash.once": { + "version": "4.1.1", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/loupe": { + "version": "2.3.7", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "../../core/node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "../../core/node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "../../core/node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "../../core/node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "../../core/node_modules/mimic-fn": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/minimatch": { + "version": "9.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../core/node_modules/mlly": { + "version": "1.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "../../core/node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "../../core/node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/npm-run-path": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "../../core/node_modules/onetime": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../../core/node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/pako": { + "version": "2.1.0", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "../../core/node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "../../core/node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../core/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/pathe": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/pathval": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "../../core/node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "../../core/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "../../core/node_modules/pkg-types": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "../../core/node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/postcss": { + "version": "8.5.6", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "../../core/node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "../../core/node_modules/pretty-format": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "../../core/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "../../core/node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../../core/node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "../../core/node_modules/react-is": { + "version": "18.3.1", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/resolve": { + "version": "1.22.11", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../core/node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "../../core/node_modules/reusify": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "../../core/node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../core/node_modules/rollup": { + "version": "3.29.5", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "../../core/node_modules/rollup-plugin-dts": { + "version": "6.3.0", + "dev": true, + "license": "LGPL-3.0-only", + "dependencies": { + "magic-string": "^0.30.21" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/Swatinem" + }, + "optionalDependencies": { + "@babel/code-frame": "^7.27.1" + }, + "peerDependencies": { + "rollup": "^3.29.4 || ^4", + "typescript": "^4.5 || ^5.0" + } + }, + "../../core/node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "../../core/node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "../../core/node_modules/semver": { + "version": "7.7.3", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "../../core/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../../core/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../../core/node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../../core/node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/std-env": { + "version": "3.10.0", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/strip-final-newline": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/strip-literal": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "../../core/node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../core/node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/tinypool": { + "version": "0.8.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../../core/node_modules/tinyspy": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../../core/node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "../../core/node_modules/ts-api-utils": { + "version": "1.4.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "../../core/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "license": "0BSD" + }, + "../../core/node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../../core/node_modules/type-detect": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "../../core/node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "../../core/node_modules/ufo": { + "version": "1.6.3", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + }, + "../../core/node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "../../core/node_modules/vite": { + "version": "5.4.21", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "../../core/node_modules/vite-node": { + "version": "1.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../core/node_modules/vite/node_modules/rollup": { + "version": "4.55.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "../../core/node_modules/vitest": { + "version": "1.6.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "../../core/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "../../core/node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "../../core/node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../core/node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "../../core/node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../core/node_modules/zod": { + "version": "3.25.76", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "11.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/codecs-core": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/errors": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js": { + "version": "1.98.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^2.1.0", + "agentkeepalive": "^4.5.0", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@usearete/sdk": { + "resolved": "../../core", + "link": true + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/base-x": { + "version": "3.0.11", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/borsh": { + "version": "0.7.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, + "node_modules/bs58": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bufferutil": { + "version": "4.1.0", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/delay": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "dev": true, + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/eyes": { + "version": "0.1.8", + "dev": true, + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jayson": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/@types/node": { + "version": "12.20.55", + "dev": true, + "license": "MIT" + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/picomatch": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "3.30.0", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-dts": { + "version": "6.4.1", + "dev": true, + "license": "LGPL-3.0-only", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.5.5", + "convert-source-map": "^2.0.0", + "magic-string": "^0.30.21" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/Swatinem" + }, + "optionalDependencies": { + "@babel/code-frame": "^7.29.0" + }, + "peerDependencies": { + "rollup": "^3.29.4 || ^4", + "typescript": "^4.5 || ^5.0 || ^6.0" + } + }, + "node_modules/rpc-websockets": { + "version": "9.3.9", + "dev": true, + "license": "LGPL-3.0-only", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^14.0.0", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^6.0.0" + } + }, + "node_modules/rpc-websockets/node_modules/@types/ws": { + "version": "8.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/rpc-websockets/node_modules/utf-8-validate": { + "version": "6.0.6", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/rpc-websockets/node_modules/uuid": { + "version": "14.0.0", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/rpc-websockets/node_modules/ws": { + "version": "8.21.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/superstruct": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "dev": true + }, + "node_modules/tr46": { + "version": "0.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.11", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/typescript/adapters/web3js/package.json b/typescript/adapters/web3js/package.json new file mode 100644 index 00000000..804a5854 --- /dev/null +++ b/typescript/adapters/web3js/package.json @@ -0,0 +1,60 @@ +{ + "name": "@usearete/adapter-web3js", + "version": "0.1.0", + "type": "module", + "description": "@solana/web3.js wallet adapter for the Arete SDK instruction boundary", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.esm.js", + "require": "./dist/index.js" + } + }, + "repository": { + "type": "git", + "url": "git+https://github.com/AreteA4/arete.git", + "directory": "typescript/adapters/web3js" + }, + "scripts": { + "build": "rollup -c", + "dev": "rollup -c -w", + "lint": "eslint src/**/*.ts", + "typecheck": "tsc --noEmit", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "solana", + "web3.js", + "wallet", + "arete" + ], + "author": "Arete Team", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.0", + "@usearete/sdk": "^0.1.3" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^11.0.0", + "@solana/web3.js": "^1.95.0", + "@types/node": "^20.0.0", + "@usearete/sdk": "^0.1.3", + "rollup": "^3.0.0", + "rollup-plugin-dts": "^6.0.0", + "tslib": "^2.8.1", + "typescript": "^5.0.0" + }, + "files": [ + "dist", + "README.md" + ] +} diff --git a/typescript/adapters/web3js/rollup.config.js b/typescript/adapters/web3js/rollup.config.js new file mode 100644 index 00000000..99d648d6 --- /dev/null +++ b/typescript/adapters/web3js/rollup.config.js @@ -0,0 +1,22 @@ +import typescript from '@rollup/plugin-typescript'; +import dts from 'rollup-plugin-dts'; + +export default [ + { + input: 'src/index.ts', + output: [ + { file: 'dist/index.js', format: 'cjs', sourcemap: true }, + { file: 'dist/index.esm.js', format: 'esm', sourcemap: true }, + ], + external: ['@solana/web3.js', '@usearete/sdk'], + plugins: [ + typescript({ tsconfig: './tsconfig.json', declaration: false }), + ], + }, + { + input: 'src/index.ts', + output: { file: 'dist/index.d.ts', format: 'es' }, + external: ['@solana/web3.js', '@usearete/sdk'], + plugins: [dts()], + }, +]; diff --git a/typescript/adapters/web3js/src/index.ts b/typescript/adapters/web3js/src/index.ts new file mode 100644 index 00000000..160ed64e --- /dev/null +++ b/typescript/adapters/web3js/src/index.ts @@ -0,0 +1,142 @@ +/** + * @usearete/adapter-web3js + * + * A reference {@link WalletAdapter} implementation backed by @solana/web3.js. + * + * The Arete core SDK is RPC-free: it only builds `BuiltInstruction` objects. + * This adapter owns everything network-related: fetching a recent blockhash, + * compiling a v0 message, signing, sending, and confirming. + * + * Two construction helpers are provided: + * - {@link createKeypairWalletAdapter} for Node scripts / bots (signs with a Keypair). + * - {@link createWalletAdapter} for browser / wallet-standard signers. + */ + +import { + Connection, + PublicKey, + TransactionInstruction, + TransactionMessage, + VersionedTransaction, + type Keypair, + type Commitment, +} from '@solana/web3.js'; +import type { + WalletAdapter, + BuiltInstruction, + SendOptions, + SendResult, + ConfirmationLevel, +} from '@usearete/sdk'; + +/** + * Minimal signer interface. A browser wallet, wallet-standard signer, or a + * Keypair wrapper can all satisfy this. + */ +export interface VersionedTransactionSigner { + publicKey: PublicKey; + signTransaction(tx: VersionedTransaction): Promise; +} + +export interface Web3JsAdapterConfig { + /** A connected @solana/web3.js Connection. */ + connection: Connection; + /** The signer that will sign compiled transactions. */ + signer: VersionedTransactionSigner; + /** Default commitment used when the caller does not specify one. */ + defaultCommitment?: Commitment; +} + +/** Convert a confirmation level to a web3.js Commitment (they share names). */ +function toCommitment( + level: ConfirmationLevel | undefined, + fallback: Commitment +): Commitment { + return (level as Commitment | undefined) ?? fallback; +} + +/** Convert an Arete BuiltInstruction to a web3.js TransactionInstruction. */ +function toTransactionInstruction(ix: BuiltInstruction): TransactionInstruction { + return new TransactionInstruction({ + programId: new PublicKey(ix.programId), + keys: ix.keys.map((k) => ({ + pubkey: new PublicKey(k.pubkey), + isSigner: k.isSigner, + isWritable: k.isWritable, + })), + data: Buffer.from(ix.data), + }); +} + +/** + * Create a {@link WalletAdapter} from a connection and a signer. + */ +export function createWalletAdapter(config: Web3JsAdapterConfig): WalletAdapter { + const { connection, signer } = config; + const fallbackCommitment = config.defaultCommitment ?? 'confirmed'; + + return { + publicKey: signer.publicKey.toBase58(), + + async signAndSend( + instructions: BuiltInstruction[], + options?: SendOptions + ): Promise { + if (instructions.length === 0) { + throw new Error('signAndSend requires at least one instruction'); + } + + const commitment = toCommitment(options?.confirmationLevel, fallbackCommitment); + const { blockhash, lastValidBlockHeight } = + await connection.getLatestBlockhash(commitment); + + const message = new TransactionMessage({ + payerKey: signer.publicKey, + recentBlockhash: blockhash, + instructions: instructions.map(toTransactionInstruction), + }).compileToV0Message(); + + const transaction = new VersionedTransaction(message); + const signed = await signer.signTransaction(transaction); + + const signature = await connection.sendRawTransaction(signed.serialize(), { + skipPreflight: options?.skipPreflight ?? false, + preflightCommitment: commitment, + }); + + const confirmation = await connection.confirmTransaction( + { signature, blockhash, lastValidBlockHeight }, + commitment + ); + + if (confirmation.value.err) { + throw confirmation.value.err; + } + + return { signature, slot: confirmation.context.slot }; + }, + }; +} + +/** + * Create a {@link WalletAdapter} that signs with a local Keypair. + * Convenient for Node scripts, bots, and tests. + */ +export function createKeypairWalletAdapter(config: { + connection: Connection; + keypair: Keypair; + defaultCommitment?: Commitment; +}): WalletAdapter { + const { connection, keypair, defaultCommitment } = config; + return createWalletAdapter({ + connection, + defaultCommitment, + signer: { + publicKey: keypair.publicKey, + async signTransaction(tx: VersionedTransaction): Promise { + tx.sign([keypair]); + return tx; + }, + }, + }); +} diff --git a/typescript/adapters/web3js/tsconfig.json b/typescript/adapters/web3js/tsconfig.json new file mode 100644 index 00000000..e252c6d2 --- /dev/null +++ b/typescript/adapters/web3js/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM"], + "declaration": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} From 2181c061f9d5a420786f7945e4ac58e540c52299 Mon Sep 17 00:00:00 2001 From: Adrian Henry Date: Fri, 12 Jun 2026 02:07:14 +0100 Subject: [PATCH 5/5] feat: generate typed instruction handlers from stack specs --- cli/src/commands/sdk.rs | 4 + interpreter/src/lib.rs | 1 + interpreter/src/rust.rs | 1 + interpreter/src/typescript.rs | 75 +- interpreter/src/typescript_instructions.rs | 1973 +++++++++++++++++ .../golden/ore-close-instruction.expected.ts | 26 + 6 files changed, 2072 insertions(+), 8 deletions(-) create mode 100644 interpreter/src/typescript_instructions.rs create mode 100644 interpreter/tests/golden/ore-close-instruction.expected.ts diff --git a/cli/src/commands/sdk.rs b/cli/src/commands/sdk.rs index 54423818..2f023840 100644 --- a/cli/src/commands/sdk.rs +++ b/cli/src/commands/sdk.rs @@ -526,6 +526,10 @@ fn generate_typescript_sdk_from_source( let output = arete_interpreter::typescript::compile_stack_spec(stack_spec, Some(config)) .map_err(|e| anyhow::anyhow!("Failed to compile TypeScript: {}", e))?; + for warning in &output.warnings { + println!("{} {}", "⚠".yellow().bold(), warning); + } + arete_interpreter::typescript::write_stack_typescript_to_file(&output, output_path) .with_context(|| format!("Failed to write TypeScript to {}", output_path.display()))?; diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs index be663e8e..b6576e55 100644 --- a/interpreter/src/lib.rs +++ b/interpreter/src/lib.rs @@ -39,6 +39,7 @@ pub mod scheduler; pub mod slot_hash_cache; pub mod spec_trait; pub mod typescript; +pub mod typescript_instructions; pub mod versioned; pub mod vm; pub mod vm_metrics; diff --git a/interpreter/src/rust.rs b/interpreter/src/rust.rs index c88241a0..6e9d7af9 100644 --- a/interpreter/src/rust.rs +++ b/interpreter/src/rust.rs @@ -695,6 +695,7 @@ fn normalized_integer_kind(rust_type_name: &str) -> &'static str { { "u64" } else { + // Signed small ints (i16/i8/isize) and anything unknown widen to i64. "i64" } } diff --git a/interpreter/src/typescript.rs b/interpreter/src/typescript.rs index de865664..82e7d18e 100644 --- a/interpreter/src/typescript.rs +++ b/interpreter/src/typescript.rs @@ -1889,7 +1889,7 @@ fn unique_resolved_type_name_ts( } /// Convert snake_case to PascalCase -fn to_pascal_case(s: &str) -> String { +pub(crate) fn to_pascal_case(s: &str) -> String { s.split(['_', '-', '.']) .map(|word| { let mut chars = word.chars(); @@ -2017,6 +2017,9 @@ pub struct TypeScriptStackOutput { pub interfaces: String, pub stack_definition: String, pub imports: String, + /// Non-fatal codegen warnings (skipped instructions, PDAs degraded to + /// user-provided accounts). Callers should surface these to the user. + pub warnings: Vec, } impl TypeScriptStackOutput { @@ -2099,9 +2102,47 @@ pub fn compile_stack_spec( schema_names.extend(output.schema_names); } - let interfaces = all_interfaces.join("\n\n"); + let mut interfaces = all_interfaces.join("\n\n"); - // 2. Generate unified stack definition with all entity views + // 2. Generate instruction-construction handlers from the stack spec. + // Program errors live once at the stack level (in the IDL snapshots) and + // are scoped per program by the instruction codegen. Entity interface + // names are reserved so defined-type interfaces cannot collide with them. + let mut reserved_type_names: std::collections::HashSet = + std::collections::HashSet::new(); + for line in interfaces.lines() { + for prefix in ["export interface ", "export type "] { + if let Some(rest) = line.strip_prefix(prefix) { + let name: String = rest + .chars() + .take_while(|c| c.is_alphanumeric() || *c == '_') + .collect(); + if !name.is_empty() { + reserved_type_names.insert(name); + } + } + } + } + let instructions_codegen = crate::typescript_instructions::generate_instructions_code( + stack_name, + &stack_spec.instructions, + &stack_spec.idls, + &stack_spec.pdas, + &stack_spec.program_ids, + &reserved_type_names, + ); + if !instructions_codegen.code.is_empty() { + if interfaces.is_empty() { + interfaces = instructions_codegen.code.clone(); + } else { + interfaces = format!("{}\n\n{}", interfaces, instructions_codegen.code); + } + } + + // 3. Generate unified stack definition with all entity views and handlers. + let instructions_block = crate::typescript_instructions::render_instructions_stack_block( + &instructions_codegen.stack_entries, + ); let stack_definition = generate_stack_definition_multi( stack_name, &stack_kebab, @@ -2110,19 +2151,35 @@ pub fn compile_stack_spec( &stack_spec.pdas, &stack_spec.program_ids, &schema_names, + &instructions_block, &config, ); - let imports = if stack_spec.pdas.values().any(|p| !p.is_empty()) { - "import { z } from 'zod';\nimport { pda, literal, account, arg, bytes } from '@usearete/sdk';".to_string() - } else { + // 4. Assemble `@usearete/sdk` imports based on what was actually emitted. + let mut sdk_named: Vec = Vec::new(); + if stack_spec.pdas.values().any(|p| !p.is_empty()) { + for helper in ["pda", "literal", "account", "arg", "bytes"] { + sdk_named.push(helper.to_string()); + } + } + if instructions_codegen.needs_runtime_import { + sdk_named.push("createInstructionHandler".to_string()); + sdk_named.push("type ErrorMetadata".to_string()); + } + let imports = if sdk_named.is_empty() { "import { z } from 'zod';".to_string() + } else { + format!( + "import {{ z }} from 'zod';\nimport {{ {} }} from '@usearete/sdk';", + sdk_named.join(", ") + ) }; Ok(TypeScriptStackOutput { imports, interfaces, stack_definition, + warnings: instructions_codegen.warnings, }) } @@ -2166,6 +2223,7 @@ fn generate_stack_definition_multi( pdas: &BTreeMap>, program_ids: &[String], schema_names: &[String], + instructions_block: &str, config: &TypeScriptStackConfig, ) -> String { let export_name = format!( @@ -2262,7 +2320,7 @@ export const {export_name} = {{ {url_line} views: {{ {views_body} - }},{schemas_section}{pdas_section} + }},{schemas_section}{pdas_section}{instructions_section} }} as const; /** Type alias for the stack */ @@ -2282,6 +2340,7 @@ export default {export_name};"#, views_body = views_body, schemas_section = schemas_block, pdas_section = pdas_block, + instructions_section = instructions_block, entity_union = entity_types.join(" | "), ) } @@ -2375,7 +2434,7 @@ function listView(view: string): ViewDef { } /// Convert PascalCase to SCREAMING_SNAKE_CASE (e.g., "OreStream" -> "ORE_STREAM") -fn to_screaming_snake_case(s: &str) -> String { +pub(crate) fn to_screaming_snake_case(s: &str) -> String { let mut result = String::new(); for (i, ch) in s.chars().enumerate() { if ch.is_uppercase() && i > 0 { diff --git a/interpreter/src/typescript_instructions.rs b/interpreter/src/typescript_instructions.rs new file mode 100644 index 00000000..9cd9cc94 --- /dev/null +++ b/interpreter/src/typescript_instructions.rs @@ -0,0 +1,1973 @@ +//! TypeScript codegen for instruction-construction handlers. +//! +//! This module consumes the `InstructionDef[]` serialized into a stack spec and +//! emits data-driven instruction handlers that target the core SDK's +//! [`createInstructionHandler`] factory. No imperative serialization code is +//! generated: the output is metadata (discriminator, ordered accounts, arg +//! schema, errors) plus typed `Params`/`Error` shapes the core runtime +//! interprets. +//! +//! The generated handlers are the codegen counterpart to the hand-written +//! golden fixture in `examples/subscriptions-instructions/src/handlers.ts`. + +use crate::ast::{ + AccountResolution, IdlArrayElementSnapshot, IdlDefinedInnerSnapshot, IdlErrorSnapshot, + IdlSnapshot, IdlTypeDefKindSnapshot, IdlTypeDefSnapshot, IdlTypeSnapshot, + InstructionAccountDef, InstructionDef, PdaDefinition, PdaSeedDef, +}; +use crate::typescript::{to_pascal_case, to_screaming_snake_case}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; + +/// One entry in the stack definition's `instructions` block. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StackInstructionEntry { + /// Program namespace key (camelCase IDL name) for multi-program stacks. + /// `None` for single-program stacks, where the block stays flat. + pub program_key: Option, + /// Key inside the (possibly nested) instructions block. + pub instruction_name: String, + /// Name of the generated handler const. + pub handler_const: String, +} + +/// Result of generating instruction handler code for a stack. +#[derive(Debug, Clone, Default)] +pub struct InstructionsCodegen { + /// Generated TypeScript: program-error consts, per-instruction param/error + /// types and handler consts. Empty when there are no emittable handlers. + pub code: String, + /// Entries to wire into the stack definition's `instructions` block. + pub stack_entries: Vec, + /// Whether the generated code references the `@usearete/sdk` runtime + /// (`createInstructionHandler` / `ErrorMetadata`). + pub needs_runtime_import: bool, + /// Human-readable warnings (skipped instructions, degraded PDAs). + pub warnings: Vec, +} + +/// Per-program error const/type names plus the rendered declarations. +struct ProgramErrorScope { + const_name: String, + type_name: String, + errors: Vec, + used: bool, +} + +/// Generate instruction handler code for a stack. +/// +/// `idls` are the stack's IDL snapshots; each handler's errors are scoped to +/// its own program (matched via `InstructionDef.program_id`). Single-program +/// stacks keep flat naming; multi-program stacks prefix handler/type names +/// with the program name and namespace the `instructions` block per program. +pub fn generate_instructions_code( + stack_name: &str, + instructions: &[InstructionDef], + idls: &[IdlSnapshot], + pdas: &BTreeMap>, + program_ids: &[String], + reserved_type_names: &HashSet, +) -> InstructionsCodegen { + if instructions.is_empty() { + return InstructionsCodegen::default(); + } + + let multi_program = idls.len() > 1; + let mut defined_types = DefinedTypes::new(idls, reserved_type_names); + + // Flatten the PDA registry into a name -> definition lookup. PDA names are + // expected to be unique across programs within a stack; conflicting + // definitions keep the first and warn. + let mut warnings: Vec = Vec::new(); + let mut pda_lookup: BTreeMap<&str, &PdaDefinition> = BTreeMap::new(); + for program_pdas in pdas.values() { + for (name, def) in program_pdas { + if let Some(existing) = pda_lookup.get(name.as_str()) { + if format!("{:?}", existing) != format!("{:?}", def) { + warnings.push(format!( + "PDA '{}' is defined differently in multiple programs; using the first definition", + name + )); + } + } else { + pda_lookup.insert(name.as_str(), def); + } + } + } + + let default_program_id = program_ids.first().cloned().unwrap_or_default(); + + let mut blocks: Vec = Vec::new(); + let mut stack_entries: Vec = Vec::new(); + + let stack_screaming = to_screaming_snake_case(stack_name); + let stack_pascal = to_pascal_case(stack_name); + + // Per-program error scopes. The fallback scope (stack-level naming, all + // errors flattened) serves single-program stacks, stacks without IDL + // snapshots, and instructions that cannot be matched to a program. + let mut program_scopes: Vec = idls + .iter() + .map(|idl| { + let (const_name, type_name) = if multi_program { + ( + format!( + "{}_{}_PROGRAM_ERRORS", + stack_screaming, + to_screaming_snake_case(&to_pascal_case(&idl.name)) + ), + format!("{}{}ProgramError", stack_pascal, to_pascal_case(&idl.name)), + ) + } else { + ( + format!("{}_PROGRAM_ERRORS", stack_screaming), + format!("{}ProgramError", stack_pascal), + ) + }; + ProgramErrorScope { + const_name, + type_name, + errors: dedupe_errors_by_code(&idl.errors), + used: false, + } + }) + .collect(); + let mut fallback_scope = ProgramErrorScope { + const_name: format!("{}_PROGRAM_ERRORS", stack_screaming), + type_name: format!("{}ProgramError", stack_pascal), + errors: dedupe_errors_by_code( + &idls + .iter() + .flat_map(|idl| idl.errors.iter().cloned()) + .collect::>(), + ), + used: false, + }; + + for instr in instructions { + // Match the instruction to its program for error scoping and naming. + let program_index: Option = if multi_program { + instr.program_id.as_deref().and_then(|pid| { + idls.iter() + .position(|idl| idl.program_id.as_deref() == Some(pid)) + }) + } else if idls.len() == 1 { + Some(0) + } else { + None + }; + if multi_program && program_index.is_none() { + warnings.push(format!( + "instruction '{}' could not be matched to a program IDL; using stack-wide error metadata and unprefixed naming", + instr.name + )); + } + + // Naming: multi-program handlers are prefixed with their program name + // so duplicate instruction names across programs cannot collide. + let program_name = program_index.map(|i| idls[i].name.as_str()); + let (pascal, handler_const, program_key) = match program_name { + Some(name) if multi_program => { + let program_pascal = to_pascal_case(name); + let instr_pascal = to_pascal_case(&instr.name); + ( + format!("{}{}", program_pascal, instr_pascal), + format!("{}{}Instruction", to_camel_case(name), instr_pascal), + Some(to_camel_case(name)), + ) + } + _ => ( + to_pascal_case(&instr.name), + format!("{}Instruction", instr.name), + None, + ), + }; + let (program_errors_const, program_error_type) = match program_index { + Some(i) => { + program_scopes[i].used = true; + ( + program_scopes[i].const_name.clone(), + program_scopes[i].type_name.clone(), + ) + } + None => { + fallback_scope.used = true; + ( + fallback_scope.const_name.clone(), + fallback_scope.type_name.clone(), + ) + } + }; + + // --- Parse args; skip the whole instruction on unsupported types. --- + let mut parsed_args: Vec<(String, ParsedArgType)> = Vec::new(); + let mut unsupported_arg: Option<(String, String)> = None; + for arg in &instr.args { + let parsed = defined_types.parse_arg_type(&arg.arg_type); + if !parsed.supported { + unsupported_arg = Some((arg.name.clone(), arg.arg_type.clone())); + break; + } + parsed_args.push((arg.name.clone(), parsed)); + } + + if let Some((arg_name, arg_type)) = unsupported_arg { + let warning = format!( + "skipped instruction '{}': arg '{}' has unsupported type '{}'", + instr.name, arg_name, arg_type + ); + warnings.push(warning.clone()); + blocks.push(format!("// [arete codegen] {}", warning)); + continue; + } + + // --- Map accounts. --- + let instr_account_names: HashSet<&str> = + instr.accounts.iter().map(|a| a.name.as_str()).collect(); + // name -> raw type string, used both for arg-existence checks and to + // type PDA seeds that reference args. + let instr_arg_types: BTreeMap<&str, &str> = instr + .args + .iter() + .map(|a| (a.name.as_str(), a.arg_type.as_str())) + .collect(); + + let mut account_literals: Vec = Vec::new(); + let mut user_params: Vec = Vec::new(); + for acc in &instr.accounts { + let mapped = map_account( + acc, + &pda_lookup, + &instr_account_names, + &instr_arg_types, + &instr.name, + &mut warnings, + ); + account_literals.push(mapped.literal); + if let Some(param) = mapped.param { + user_params.push(param); + } + } + + // --- Params interface. --- + let mut param_lines: Vec = Vec::new(); + for (name, parsed) in &parsed_args { + param_lines.push(format!(" {}: {};", name, parsed.ts_type)); + } + for param in &user_params { + let optional = if param.optional { "?" } else { "" }; + param_lines.push(format!(" {}{}: string;", param.name, optional)); + } + let params_body = if param_lines.is_empty() { + " // This instruction takes no arguments or user-provided accounts.".to_string() + } else { + param_lines.join("\n") + }; + let params_type = format!("{}Params", pascal); + let params_interface = format!( + "export interface {} {{\n{}\n}}", + params_type, params_body + ); + + // --- Error type. Program errors are stack-wide (IDLs do not scope + // errors to instructions), so each handler's typed error is an alias of + // the program-wide union. --- + let error_type = format!("{}Error", pascal); + let error_decl = format!("export type {} = {};", error_type, program_error_type); + + // --- Args schema literal. --- + let args_literal = if parsed_args.is_empty() { + "[]".to_string() + } else { + let entries: Vec = parsed_args + .iter() + .map(|(name, parsed)| { + format!(" {{ name: '{}', type: {} }},", name, parsed.schema) + }) + .collect(); + format!("[\n{}\n ]", entries.join("\n")) + }; + + // --- Accounts literal. --- + let accounts_literal = if account_literals.is_empty() { + "[]".to_string() + } else { + format!("[\n{}\n ]", account_literals.join("\n")) + }; + + let program_id = instr.program_id.clone().unwrap_or_else(|| default_program_id.clone()); + let discriminator = format!( + "[{}]", + instr + .discriminator + .iter() + .map(|b| b.to_string()) + .collect::>() + .join(", ") + ); + + let docs = render_docs(&instr.docs); + let handler = format!( + "{docs}export const {handler_const} = createInstructionHandler<{params_type}, {error_type}>({{\n programId: '{program_id}',\n discriminator: {discriminator},\n args: {args_literal},\n accounts: {accounts_literal},\n errors: {program_errors_const},\n}});", + docs = docs, + handler_const = handler_const, + params_type = params_type, + error_type = error_type, + program_id = program_id, + discriminator = discriminator, + args_literal = args_literal, + accounts_literal = accounts_literal, + program_errors_const = program_errors_const, + ); + + blocks.push(format!( + "{}\n\n{}\n\n{}", + params_interface, error_decl, handler + )); + // Mark the error scope as referenced only when a handler is emitted, + // so fully-skipped programs do not produce dangling consts. + match program_index { + Some(i) => program_scopes[i].used = true, + None => fallback_scope.used = true, + } + stack_entries.push(StackInstructionEntry { + program_key, + instruction_name: instr.name.clone(), + handler_const, + }); + } + + warnings.append(&mut defined_types.warnings); + + if stack_entries.is_empty() { + // Nothing emittable (all instructions skipped). Still surface warnings. + return InstructionsCodegen { + code: String::new(), + stack_entries, + needs_runtime_import: false, + warnings, + }; + } + + // Program-level error metadata blocks, one per referenced scope. Errors + // live on the stack's IDL snapshots (not duplicated onto instructions). + let mut error_blocks: Vec = Vec::new(); + for scope in program_scopes.iter().chain(std::iter::once(&fallback_scope)) { + if scope.used { + error_blocks.push(render_program_errors( + &scope.const_name, + &scope.type_name, + &scope.errors, + )); + } + } + if error_blocks.is_empty() { + // Stacks without IDL snapshots still need the fallback scope that + // every handler references. + error_blocks.push(render_program_errors( + &fallback_scope.const_name, + &fallback_scope.type_name, + &fallback_scope.errors, + )); + } + + let header = "// ============================================================================\n// Instruction Handlers\n// ============================================================================"; + + // Defined-type declarations referenced by arg schemas, in dependency order. + let type_decls = if defined_types.decls.is_empty() { + String::new() + } else { + format!("{}\n\n", defined_types.decls.join("\n\n")) + }; + + let code = format!( + "{header}\n\n{program_errors_block}\n\n{type_decls}{blocks}", + header = header, + program_errors_block = error_blocks.join("\n\n"), + type_decls = type_decls, + blocks = blocks.join("\n\n") + ); + + InstructionsCodegen { + code, + stack_entries, + needs_runtime_import: true, + warnings, + } +} + +/// Render the `instructions: { ... }` block for the stack definition const. +/// +/// Entries without a `program_key` render flat; entries with one are grouped +/// under their program's key (multi-program stacks). +pub fn render_instructions_stack_block(entries: &[StackInstructionEntry]) -> String { + if entries.is_empty() { + return String::new(); + } + + let mut lines: Vec = Vec::new(); + // Flat entries first (single-program stacks, or unmatched instructions). + for entry in entries.iter().filter(|e| e.program_key.is_none()) { + lines.push(format!( + " {}: {},", + entry.instruction_name, entry.handler_const + )); + } + // Then one nested block per program, preserving first-seen program order. + let mut program_order: Vec<&str> = Vec::new(); + for entry in entries { + if let Some(key) = entry.program_key.as_deref() { + if !program_order.contains(&key) { + program_order.push(key); + } + } + } + for program in program_order { + let nested: Vec = entries + .iter() + .filter(|e| e.program_key.as_deref() == Some(program)) + .map(|e| format!(" {}: {},", e.instruction_name, e.handler_const)) + .collect(); + lines.push(format!( + " {}: {{\n{}\n }},", + program, + nested.join("\n") + )); + } + + format!("\n instructions: {{\n{}\n }},", lines.join("\n")) +} + +/// Convert a program name to camelCase for use as a namespace key / const +/// prefix (e.g. "ore_boost" -> "oreBoost"). +fn to_camel_case(s: &str) -> String { + let pascal = to_pascal_case(s); + let mut chars = pascal.chars(); + match chars.next() { + Some(first) => first.to_lowercase().collect::() + chars.as_str(), + None => pascal, + } +} + +// ============================================================================ +// Argument type parsing +// ============================================================================ + +/// A parsed instruction argument type. +#[derive(Debug, Clone)] +struct ParsedArgType { + /// TypeScript literal for the core `ArgType` (e.g. `'u64'`, `{ option: 'u64' }`). + schema: String, + /// TypeScript parameter type (e.g. `bigint`, `string`, `number[]`). + ts_type: String, + /// Whether the type is representable by the core serializer. + supported: bool, +} + +fn unsupported() -> ParsedArgType { + ParsedArgType { + schema: "'u8'".to_string(), + ts_type: "unknown".to_string(), + supported: false, + } +} + +/// Parse an arg type without any defined-type lookup (primitives and wrappers +/// only). Defined types come back unsupported. +#[cfg(test)] +fn parse_arg_type(raw: &str) -> ParsedArgType { + DefinedTypes::empty().parse_arg_type(raw) +} + +/// Resolver for IDL-defined types (structs/enums) referenced by instruction +/// args. Resolved types are emitted as `export interface` / `export type` +/// declarations (collected in `decls`) and inlined into arg schemas as +/// `{ struct: [...] }` / `{ enum: [...] }` literals, so the runtime needs no +/// type registry. +struct DefinedTypes<'a> { + /// IDL type definitions by name, first-wins across programs. + defs: BTreeMap, + /// lowercase name -> canonical key, for case-insensitive fallback lookup. + lower: BTreeMap, + /// Emitted TS declarations, in dependency order. + decls: Vec, + /// Memoized resolutions by original IDL name (None = unsupported). + resolved: BTreeMap>, + /// TS identifiers already in use (entity interfaces + emitted types). + taken_names: HashSet, + /// Names currently being resolved (cycle guard). + visiting: HashSet, + warnings: Vec, +} + +impl<'a> DefinedTypes<'a> { + fn new(idls: &'a [IdlSnapshot], reserved_type_names: &HashSet) -> Self { + let mut defs: BTreeMap = BTreeMap::new(); + let mut lower: BTreeMap = BTreeMap::new(); + let mut warnings: Vec = Vec::new(); + for idl in idls { + for def in &idl.types { + if let Some(existing) = defs.get(def.name.as_str()) { + if format!("{:?}", existing.type_def) != format!("{:?}", def.type_def) { + warnings.push(format!( + "type '{}' is defined differently in multiple programs; using the first definition", + def.name + )); + } + } else { + defs.insert(def.name.clone(), def); + lower.insert(def.name.to_lowercase(), def.name.clone()); + } + } + } + DefinedTypes { + defs, + lower, + decls: Vec::new(), + resolved: BTreeMap::new(), + taken_names: reserved_type_names.clone(), + visiting: HashSet::new(), + warnings, + } + } + + #[cfg(test)] + fn empty() -> DefinedTypes<'static> { + DefinedTypes::new(&[], &HashSet::new()) + } + + /// Parse a stringified Rust-ish arg type (what `to_rust_type_string` + /// produces), resolving bare names against the IDL type definitions. + fn parse_arg_type(&mut self, raw: &str) -> ParsedArgType { + let t = raw.trim().trim_start_matches('&').trim(); + + // Generic wrappers: Option, Vec. + if let Some((name, inner)) = split_generic(t) { + match name { + "Option" => { + let inner = self.parse_arg_type(inner); + return ParsedArgType { + schema: format!("{{ option: {} }}", inner.schema), + ts_type: format!("{} | null", inner.ts_type), + supported: inner.supported, + }; + } + "Vec" => { + let inner = self.parse_arg_type(inner); + return ParsedArgType { + schema: format!("{{ vec: {} }}", inner.schema), + ts_type: format!("{}[]", maybe_paren(&inner.ts_type)), + supported: inner.supported, + }; + } + _ => return unsupported(), + } + } + + // Fixed-size array: [T; N]. + if let Some(stripped) = t.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { + if let Some((ty, n)) = stripped.rsplit_once(';') { + let inner = self.parse_arg_type(ty.trim()); + let n = n.trim(); + if n.parse::().is_ok() { + return ParsedArgType { + schema: format!("{{ array: [{}, {}] }}", inner.schema, n), + ts_type: format!("{}[]", maybe_paren(&inner.ts_type)), + supported: inner.supported, + }; + } + } + } + + // Primitive (possibly path-qualified, e.g. solana_pubkey::Pubkey). + let last = t.rsplit("::").next().unwrap_or(t); + match last { + "u8" => prim("u8", "number"), + "u16" => prim("u16", "number"), + "u32" => prim("u32", "number"), + "u64" => prim("u64", "bigint"), + "u128" => prim("u128", "bigint"), + "i8" => prim("i8", "number"), + "i16" => prim("i16", "number"), + "i32" => prim("i32", "number"), + "i64" => prim("i64", "bigint"), + "i128" => prim("i128", "bigint"), + "f32" => prim("f32", "number"), + "f64" => prim("f64", "number"), + "bool" => prim("bool", "boolean"), + "String" | "string" | "str" => prim("string", "string"), + "Pubkey" | "pubkey" | "PublicKey" | "publicKey" => prim("pubkey", "string"), + // IDL `bytes` (instruction args reach here as Vec instead and + // keep the wire-identical `{ vec: 'u8' }` schema). + "bytes" => ParsedArgType { + schema: "'bytes'".to_string(), + ts_type: "Uint8Array | number[]".to_string(), + supported: true, + }, + _ => self.resolve_defined(last).unwrap_or_else(unsupported), + } + } + + /// Parse an IDL snapshot type (used inside struct fields / enum variants). + fn parse_snapshot_type(&mut self, t: &IdlTypeSnapshot) -> ParsedArgType { + match t { + IdlTypeSnapshot::Simple(s) => self.parse_arg_type(s), + IdlTypeSnapshot::Option(o) => { + let inner = self.parse_snapshot_type(&o.option); + ParsedArgType { + schema: format!("{{ option: {} }}", inner.schema), + ts_type: format!("{} | null", inner.ts_type), + supported: inner.supported, + } + } + IdlTypeSnapshot::Vec(v) => { + let inner = self.parse_snapshot_type(&v.vec); + ParsedArgType { + schema: format!("{{ vec: {} }}", inner.schema), + ts_type: format!("{}[]", maybe_paren(&inner.ts_type)), + supported: inner.supported, + } + } + IdlTypeSnapshot::Array(arr) => { + let mut element: Option = None; + let mut size: Option = None; + for part in &arr.array { + match part { + IdlArrayElementSnapshot::Type(inner) => { + element = Some(self.parse_snapshot_type(inner)) + } + IdlArrayElementSnapshot::TypeName(name) => { + element = Some(self.parse_arg_type(name)) + } + IdlArrayElementSnapshot::Size(n) => size = Some(*n), + } + } + match (element, size) { + (Some(inner), Some(n)) => ParsedArgType { + schema: format!("{{ array: [{}, {}] }}", inner.schema, n), + ts_type: format!("{}[]", maybe_paren(&inner.ts_type)), + supported: inner.supported, + }, + _ => unsupported(), + } + } + IdlTypeSnapshot::HashMap(_) => unsupported(), + IdlTypeSnapshot::Defined(d) => { + let name = match &d.defined { + IdlDefinedInnerSnapshot::Named { name } => name.as_str(), + IdlDefinedInnerSnapshot::Simple(s) => s.as_str(), + }; + self.resolve_defined(name).unwrap_or_else(unsupported) + } + } + } + + /// Resolve a bare type name against the IDL type definitions, emitting a + /// TS declaration on first use. Returns `None` when unsupported. + fn resolve_defined(&mut self, name: &str) -> Option { + if let Some(cached) = self.resolved.get(name) { + return cached.clone(); + } + if self.visiting.contains(name) { + self.warnings.push(format!( + "type '{}' is recursive; recursive types are not supported by instruction codegen", + name + )); + return None; + } + + let key = if self.defs.contains_key(name) { + name.to_string() + } else { + // `to_rust_type_string` passes IDL names through verbatim, but the + // referencing spelling occasionally differs in case. + match self.lower.get(&name.to_lowercase()) { + Some(canonical) => canonical.clone(), + None => { + self.resolved.insert(name.to_string(), None); + return None; + } + } + }; + + self.visiting.insert(key.clone()); + let def = self.defs[&key]; + let result = match &def.type_def { + IdlTypeDefKindSnapshot::Struct { fields, .. } => { + let fields = fields.clone(); + self.resolve_struct(&key, &fields) + } + IdlTypeDefKindSnapshot::TupleStruct { .. } => { + self.warnings.push(format!( + "type '{}' is a tuple struct, which instruction codegen does not support yet", + key + )); + None + } + IdlTypeDefKindSnapshot::Enum { variants, .. } => { + let variants = variants.clone(); + self.resolve_enum(&key, &variants) + } + }; + self.visiting.remove(&key); + self.resolved.insert(name.to_string(), result.clone()); + if name != key { + self.resolved.insert(key, result.clone()); + } + result + } + + fn resolve_struct( + &mut self, + name: &str, + fields: &[crate::ast::IdlFieldSnapshot], + ) -> Option { + let mut schema_fields: Vec = Vec::new(); + let mut ts_fields: Vec = Vec::new(); + for field in fields { + let parsed = self.parse_snapshot_type(&field.type_); + if !parsed.supported { + self.warnings.push(format!( + "type '{}': field '{}' has an unsupported type", + name, field.name + )); + return None; + } + schema_fields.push(format!("{{ name: '{}', type: {} }}", field.name, parsed.schema)); + ts_fields.push(format!(" {}: {};", field.name, parsed.ts_type)); + } + + let ts_name = self.claim_ts_name(name); + self.decls.push(format!( + "export interface {} {{\n{}\n}}", + ts_name, + ts_fields.join("\n") + )); + Some(ParsedArgType { + schema: format!("{{ struct: [{}] }}", schema_fields.join(", ")), + ts_type: ts_name, + supported: true, + }) + } + + fn resolve_enum( + &mut self, + name: &str, + variants: &[crate::ast::IdlEnumVariantSnapshot], + ) -> Option { + use crate::ast::IdlEnumVariantFieldSnapshot; + + let mut schema_variants: Vec = Vec::new(); + let mut ts_variants: Vec = Vec::new(); + for variant in variants { + if variant.fields.is_empty() { + schema_variants.push(format!("'{}'", variant.name)); + ts_variants.push(format!("'{}'", variant.name)); + continue; + } + + let named: Vec<_> = variant + .fields + .iter() + .filter_map(|f| match f { + IdlEnumVariantFieldSnapshot::Named(field) => Some(field), + IdlEnumVariantFieldSnapshot::Tuple(_) => None, + }) + .collect(); + + if named.len() == variant.fields.len() { + // Struct variant: { name: 'x', fields: [...] }. + let mut field_schemas: Vec = Vec::new(); + let mut field_ts: Vec = Vec::new(); + for field in named { + let parsed = self.parse_snapshot_type(&field.type_); + if !parsed.supported { + self.warnings.push(format!( + "enum '{}': variant '{}' field '{}' has an unsupported type", + name, variant.name, field.name + )); + return None; + } + field_schemas + .push(format!("{{ name: '{}', type: {} }}", field.name, parsed.schema)); + field_ts.push(format!("{}: {}", field.name, parsed.ts_type)); + } + schema_variants.push(format!( + "{{ name: '{}', fields: [{}] }}", + variant.name, + field_schemas.join(", ") + )); + ts_variants.push(format!( + "{{ {}: {{ {} }} }}", + variant.name, + field_ts.join("; ") + )); + } else if named.is_empty() { + // Tuple variant: { name: 'x', tuple: [...] }. + let mut element_schemas: Vec = Vec::new(); + let mut element_ts: Vec = Vec::new(); + for field in &variant.fields { + let IdlEnumVariantFieldSnapshot::Tuple(ty) = field else { + unreachable!("named.is_empty() guarantees tuple fields"); + }; + let parsed = self.parse_snapshot_type(ty); + if !parsed.supported { + self.warnings.push(format!( + "enum '{}': variant '{}' has an unsupported tuple element type", + name, variant.name + )); + return None; + } + element_schemas.push(parsed.schema); + element_ts.push(parsed.ts_type); + } + schema_variants.push(format!( + "{{ name: '{}', tuple: [{}] }}", + variant.name, + element_schemas.join(", ") + )); + ts_variants.push(format!( + "{{ {}: [{}] }}", + variant.name, + element_ts.join(", ") + )); + } else { + self.warnings.push(format!( + "enum '{}': variant '{}' mixes named and tuple fields, which is not supported", + name, variant.name + )); + return None; + } + } + + let ts_name = self.claim_ts_name(name); + self.decls.push(format!( + "export type {} =\n | {};", + ts_name, + ts_variants.join("\n | ") + )); + Some(ParsedArgType { + schema: format!("{{ enum: [{}] }}", schema_variants.join(", ")), + ts_type: ts_name, + supported: true, + }) + } + + /// Pick a unique TS identifier for a defined type, suffixing `Input` (then + /// a counter) when the pascal-cased name collides with an entity interface + /// or another emitted type. + fn claim_ts_name(&mut self, name: &str) -> String { + let base = to_pascal_case(name); + let mut candidate = base.clone(); + if self.taken_names.contains(&candidate) { + candidate = format!("{}Input", base); + let mut counter = 2; + while self.taken_names.contains(&candidate) { + candidate = format!("{}Input{}", base, counter); + counter += 1; + } + self.warnings.push(format!( + "type '{}' collides with an existing interface; emitted as '{}'", + name, candidate + )); + } + self.taken_names.insert(candidate.clone()); + candidate + } +} + +fn prim(schema: &str, ts: &str) -> ParsedArgType { + ParsedArgType { + schema: format!("'{}'", schema), + ts_type: ts.to_string(), + supported: true, + } +} + +/// Split `Name` into `(Name, inner)`, ignoring path qualifiers on `Name`. +fn split_generic(t: &str) -> Option<(&str, &str)> { + let open = t.find('<')?; + if !t.ends_with('>') { + return None; + } + let name = t[..open].rsplit("::").next().unwrap_or(&t[..open]).trim(); + let inner = t[open + 1..t.len() - 1].trim(); + Some((name, inner)) +} + +/// Wrap union types in parentheses so `T | null` arrays read as `(T | null)[]`. +fn maybe_paren(ts: &str) -> String { + if ts.contains('|') { + format!("({})", ts) + } else { + ts.to_string() + } +} + +// ============================================================================ +// Account mapping +// ============================================================================ + +/// A user-provided account that must surface as a `Params` field. +#[derive(Debug, Clone)] +struct UserParam { + name: String, + optional: bool, +} + +/// Result of mapping a single instruction account. +struct MappedAccount { + /// TypeScript `AccountMeta` object literal. + literal: String, + /// Set when the account is caller-supplied (`userProvided`). + param: Option, +} + +fn map_account( + acc: &InstructionAccountDef, + pda_lookup: &BTreeMap<&str, &PdaDefinition>, + instr_account_names: &HashSet<&str>, + instr_arg_types: &BTreeMap<&str, &str>, + instr_name: &str, + warnings: &mut Vec, +) -> MappedAccount { + let base = format!( + "name: '{}', isSigner: {}, isWritable: {}", + acc.name, acc.is_signer, acc.is_writable + ); + let optional_suffix = if acc.is_optional { + ", isOptional: true".to_string() + } else { + String::new() + }; + + let user_provided = |warn: Option, warnings: &mut Vec| -> MappedAccount { + // Degradations are surfaced both to the compiler caller (warnings) and + // in the generated code, so SDK readers can see why an account that + // looks derivable must be passed in manually. + let comment = match &warn { + Some(w) => format!(" // [arete codegen] {}\n", w), + None => String::new(), + }; + if let Some(w) = warn { + warnings.push(w); + } + MappedAccount { + literal: format!( + "{} {{ {}, category: 'userProvided'{} }},", + comment, base, optional_suffix + ), + param: Some(UserParam { + name: acc.name.clone(), + optional: acc.is_optional, + }), + } + }; + + match &acc.resolution { + AccountResolution::Signer => MappedAccount { + literal: format!(" {{ {}, category: 'signer'{} }},", base, optional_suffix), + param: None, + }, + AccountResolution::Known { address } => MappedAccount { + literal: format!( + " {{ {}, category: 'known', knownAddress: '{}'{} }},", + base, address, optional_suffix + ), + param: None, + }, + AccountResolution::UserProvided => user_provided(None, warnings), + AccountResolution::PdaInline { seeds, program_id } => { + match build_pda_config(seeds, program_id.as_deref(), instr_account_names, instr_arg_types) + { + Ok((pda_config, seed_warnings)) => { + for w in seed_warnings { + warnings.push(format!( + "instruction '{}': account '{}': {}", + instr_name, acc.name, w + )); + } + MappedAccount { + literal: format!( + " {{ {}, category: 'pda', pdaConfig: {}{} }},", + base, pda_config, optional_suffix + ), + param: None, + } + } + Err(reason) => user_provided( + Some(format!( + "instruction '{}': account '{}' inline PDA degraded to userProvided ({})", + instr_name, acc.name, reason + )), + warnings, + ), + } + } + AccountResolution::PdaRef { pda_name } => match pda_lookup.get(pda_name.as_str()) { + Some(def) => match build_pda_config( + &def.seeds, + def.program_id.as_deref(), + instr_account_names, + instr_arg_types, + ) { + Ok((pda_config, seed_warnings)) => { + for w in seed_warnings { + warnings.push(format!( + "instruction '{}': account '{}': {}", + instr_name, acc.name, w + )); + } + MappedAccount { + literal: format!( + " {{ {}, category: 'pda', pdaConfig: {}{} }},", + base, pda_config, optional_suffix + ), + param: None, + } + } + Err(reason) => user_provided( + Some(format!( + "instruction '{}': account '{}' PDA '{}' degraded to userProvided ({})", + instr_name, acc.name, pda_name, reason + )), + warnings, + ), + }, + None => user_provided( + Some(format!( + "instruction '{}': account '{}' references unknown PDA '{}'; degraded to userProvided", + instr_name, acc.name, pda_name + )), + warnings, + ), + }, + } +} + +/// Build a TypeScript `PdaConfig` literal from seed definitions. +/// +/// Returns `Err(reason)` when the PDA cannot be represented by the core +/// resolver (e.g. seeds referencing accounts/args that do not exist in this +/// instruction), so the caller can degrade to `userProvided`. On success the +/// second tuple element carries soft warnings (e.g. an arg seed whose type +/// could not be determined, leaving the runtime to encode heuristically). +fn build_pda_config( + seeds: &[PdaSeedDef], + program_id: Option<&str>, + instr_account_names: &HashSet<&str>, + instr_arg_types: &BTreeMap<&str, &str>, +) -> Result<(String, Vec), String> { + let mut seed_literals: Vec = Vec::new(); + let mut soft_warnings: Vec = Vec::new(); + for seed in seeds { + match seed { + PdaSeedDef::Literal { value } => { + seed_literals.push(format!( + "{{ type: 'literal', value: '{}' }}", + escape_single_quotes(value) + )); + } + PdaSeedDef::AccountRef { account_name } => { + if !instr_account_names.contains(account_name.as_str()) { + return Err(format!( + "seed references account '{}' not present in this instruction", + account_name + )); + } + seed_literals.push(format!( + "{{ type: 'accountRef', accountName: '{}' }}", + account_name + )); + } + PdaSeedDef::ArgRef { arg_name, arg_type } => { + if !instr_arg_types.contains_key(arg_name.as_str()) { + return Err(format!( + "seed references arg '{}' not present in this instruction", + arg_name + )); + } + // Prefer the seed's declared type; fall back to the + // instruction arg's type (Anchor seeds carry no type info). + let raw_type = arg_type + .as_deref() + .or_else(|| instr_arg_types.get(arg_name.as_str()).copied()); + match raw_type.and_then(normalize_seed_arg_type) { + Some(canonical) => seed_literals.push(format!( + "{{ type: 'argRef', argName: '{}', argType: '{}' }}", + arg_name, canonical + )), + None => { + soft_warnings.push(format!( + "seed arg '{}' has non-primitive type '{}'; runtime will use heuristic encoding", + arg_name, + raw_type.unwrap_or("") + )); + seed_literals + .push(format!("{{ type: 'argRef', argName: '{}' }}", arg_name)); + } + } + } + PdaSeedDef::Bytes { value } => { + let bytes: Vec = value.iter().map(|b| b.to_string()).collect(); + seed_literals.push(format!( + "{{ type: 'bytes', value: [{}] }}", + bytes.join(", ") + )); + } + } + } + + let seeds_str = seed_literals.join(", "); + let config = match program_id { + Some(pid) => format!("{{ programId: '{}', seeds: [{}] }}", pid, seeds_str), + None => format!("{{ seeds: [{}] }}", seeds_str), + }; + Ok((config, soft_warnings)) +} + +/// Normalize a raw arg-type string (IDL or `pdas!` DSL spelling) to the +/// canonical seed type the runtime's `serializeSeedValue` understands. +/// Returns `None` for types that cannot be encoded as a seed. +fn normalize_seed_arg_type(raw: &str) -> Option { + let t = raw.rsplit("::").next().unwrap_or(raw).trim(); + if let Some(width) = t.strip_prefix('u').or_else(|| t.strip_prefix('i')) { + if matches!(width, "8" | "16" | "32" | "64" | "128") { + return Some(t.to_string()); + } + return None; + } + match t { + "Pubkey" | "pubkey" | "publicKey" | "PublicKey" => Some("pubkey".to_string()), + "String" | "string" | "str" => Some("string".to_string()), + _ => None, + } +} + +// ============================================================================ +// Errors +// ============================================================================ + +/// Dedupe errors by code, preserving first-seen definitions, sorted ascending. +fn dedupe_errors_by_code(errors: &[IdlErrorSnapshot]) -> Vec { + let mut seen: BTreeSet = BTreeSet::new(); + let mut by_code: BTreeMap = BTreeMap::new(); + for err in errors { + if seen.insert(err.code) { + by_code.insert(err.code, err.clone()); + } + } + by_code.into_values().collect() +} + +fn render_program_errors( + const_name: &str, + type_name: &str, + errors: &[IdlErrorSnapshot], +) -> String { + if errors.is_empty() { + return format!( + "/** Program errors for this stack (none declared in the IDL). */\nexport type {} = never;\n\nconst {}: ErrorMetadata[] = [];", + type_name, const_name + ); + } + + let type_decl = format!( + "/** Union of all program errors declared across this stack's instructions. */\nexport type {} =\n{};", + type_name, + error_union_variants(errors) + ); + + let entries: Vec = errors + .iter() + .map(|err| { + format!( + " {{ code: {}, name: '{}', msg: '{}' }},", + err.code, + err.name, + escape_single_quotes(err.msg.as_deref().unwrap_or("")) + ) + }) + .collect(); + let const_decl = format!( + "const {}: ErrorMetadata[] = [\n{}\n];", + const_name, + entries.join("\n") + ); + + format!("{}\n\n{}", type_decl, const_decl) +} + +/// Render the `| { code; name; msg } | ...` body of an error union type. +fn error_union_variants(errors: &[IdlErrorSnapshot]) -> String { + errors + .iter() + .map(|err| { + format!( + " | {{ code: {}; name: '{}'; msg: string }}", + err.code, err.name + ) + }) + .collect::>() + .join("\n") +} + +// ============================================================================ +// Helpers +// ============================================================================ + +fn escape_single_quotes(s: &str) -> String { + s.replace('\\', "\\\\") + .replace('\'', "\\'") + .replace(['\n', '\r'], " ") +} + +fn render_docs(docs: &[String]) -> String { + if docs.is_empty() { + return String::new(); + } + let lines: Vec = docs + .iter() + .map(|line| format!(" * {}", line.trim())) + .collect(); + format!("/**\n{}\n */\n", lines.join("\n")) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{InstructionAccountDef, InstructionArgDef}; + + fn arg(name: &str, ty: &str) -> InstructionArgDef { + InstructionArgDef { + name: name.to_string(), + arg_type: ty.to_string(), + docs: vec![], + } + } + + fn idl(name: &str, program_id: &str, errors: Vec) -> IdlSnapshot { + IdlSnapshot { + name: name.to_string(), + program_id: Some(program_id.to_string()), + version: "0.1.0".to_string(), + accounts: vec![], + instructions: vec![], + types: vec![], + events: vec![], + errors, + discriminant_size: 1, + } + } + + #[test] + fn parses_primitive_and_wrapper_arg_types() { + let u64 = parse_arg_type("u64"); + assert_eq!(u64.schema, "'u64'"); + assert_eq!(u64.ts_type, "bigint"); + assert!(u64.supported); + + let pk = parse_arg_type("solana_pubkey::Pubkey"); + assert_eq!(pk.schema, "'pubkey'"); + assert_eq!(pk.ts_type, "string"); + + let opt = parse_arg_type("Option"); + assert_eq!(opt.schema, "{ option: 'u64' }"); + assert_eq!(opt.ts_type, "bigint | null"); + + let vec = parse_arg_type("Vec"); + assert_eq!(vec.schema, "{ vec: 'u8' }"); + assert_eq!(vec.ts_type, "number[]"); + + let arr = parse_arg_type("[u8; 32]"); + assert_eq!(arr.schema, "{ array: ['u8', 32] }"); + assert_eq!(arr.ts_type, "number[]"); + + let opt_vec = parse_arg_type("Vec>"); + assert_eq!(opt_vec.ts_type, "(bigint | null)[]"); + } + + #[test] + fn defined_types_are_unsupported_without_a_lookup() { + let defined = parse_arg_type("createFixedDelegationData"); + assert!(!defined.supported); + } + + fn struct_def(name: &str, fields: Vec<(&str, IdlTypeSnapshot)>) -> IdlTypeDefSnapshot { + IdlTypeDefSnapshot { + name: name.to_string(), + docs: vec![], + serialization: None, + type_def: IdlTypeDefKindSnapshot::Struct { + kind: "struct".to_string(), + fields: fields + .into_iter() + .map(|(n, t)| crate::ast::IdlFieldSnapshot { + name: n.to_string(), + type_: t, + }) + .collect(), + }, + } + } + + fn simple(t: &str) -> IdlTypeSnapshot { + IdlTypeSnapshot::Simple(t.to_string()) + } + + fn defined(name: &str) -> IdlTypeSnapshot { + IdlTypeSnapshot::Defined(crate::ast::IdlDefinedTypeSnapshot { + defined: IdlDefinedInnerSnapshot::Named { + name: name.to_string(), + }, + }) + } + + #[test] + fn resolves_struct_args_with_nesting_and_enums() { + let mut idl = idl("demo", "Prog111", vec![]); + idl.types = vec![ + struct_def( + "transferData", + vec![ + ("amount", simple("u64")), + ("terms", defined("planTerms")), + ("status", defined("planStatus")), + ], + ), + struct_def("planTerms", vec![("periodHours", simple("u64"))]), + IdlTypeDefSnapshot { + name: "planStatus".to_string(), + docs: vec![], + serialization: None, + type_def: IdlTypeDefKindSnapshot::Enum { + kind: "enum".to_string(), + variants: vec![ + crate::ast::IdlEnumVariantSnapshot { + name: "Active".to_string(), + fields: vec![], + }, + crate::ast::IdlEnumVariantSnapshot { + name: "Sunset".to_string(), + fields: vec![crate::ast::IdlEnumVariantFieldSnapshot::Named( + crate::ast::IdlFieldSnapshot { + name: "endTs".to_string(), + type_: simple("i64"), + }, + )], + }, + ], + }, + }, + ]; + let idls = vec![idl]; + + let instr = InstructionDef { + name: "transfer".to_string(), + discriminator: vec![4], + discriminator_size: 1, + accounts: vec![], + args: vec![arg("transferData", "transferData")], + errors: vec![], + program_id: Some("Prog111".to_string()), + docs: vec![], + }; + + let out = generate_instructions_code( + "Demo", + std::slice::from_ref(&instr), + &idls, + &BTreeMap::new(), + &["Prog111".to_string()], + &HashSet::new(), + ); + + assert_eq!(out.stack_entries.len(), 1, "warnings: {:?}", out.warnings); + let code = &out.code; + // Emitted TS declarations for every referenced defined type. + assert!(code.contains("export interface TransferData")); + assert!(code.contains("export interface PlanTerms")); + assert!(code.contains("export type PlanStatus")); + assert!(code.contains("'Active'")); + assert!(code.contains("{ Sunset: { endTs: bigint } }")); + // Inlined schemas, including the nested struct and fielded enum. + assert!(code.contains("{ name: 'periodHours', type: 'u64' }")); + assert!(code.contains( + "{ name: 'status', type: { enum: ['Active', { name: 'Sunset', fields: [{ name: 'endTs', type: 'i64' }] }] } }" + )); + // Params reference the generated interface type. + assert!(code.contains("transferData: TransferData;")); + } + + #[test] + fn recursive_defined_types_skip_with_warning() { + let mut idl_snap = idl("demo", "Prog111", vec![]); + idl_snap.types = vec![struct_def("node", vec![("next", defined("node"))])]; + let idls = vec![idl_snap]; + + let instr = InstructionDef { + name: "insert".to_string(), + discriminator: vec![1], + discriminator_size: 1, + accounts: vec![], + args: vec![arg("node", "node")], + errors: vec![], + program_id: Some("Prog111".to_string()), + docs: vec![], + }; + let out = generate_instructions_code( + "Demo", + std::slice::from_ref(&instr), + &idls, + &BTreeMap::new(), + &["Prog111".to_string()], + &HashSet::new(), + ); + assert!(out.stack_entries.is_empty()); + assert!(out.warnings.iter().any(|w| w.contains("recursive"))); + } + + #[test] + fn defined_type_name_collisions_get_input_suffix() { + let mut idl_snap = idl("demo", "Prog111", vec![]); + idl_snap.types = vec![struct_def("planTerms", vec![("amount", simple("u64"))])]; + let idls = vec![idl_snap]; + + let instr = InstructionDef { + name: "setTerms".to_string(), + discriminator: vec![2], + discriminator_size: 1, + accounts: vec![], + args: vec![arg("terms", "planTerms")], + errors: vec![], + program_id: Some("Prog111".to_string()), + docs: vec![], + }; + + // Simulate an entity interface already named PlanTerms. + let reserved: HashSet = ["PlanTerms".to_string()].into_iter().collect(); + let out = generate_instructions_code( + "Demo", + std::slice::from_ref(&instr), + &idls, + &BTreeMap::new(), + &["Prog111".to_string()], + &reserved, + ); + assert!(out.code.contains("export interface PlanTermsInput")); + assert!(out.code.contains("terms: PlanTermsInput;")); + assert!(out.warnings.iter().any(|w| w.contains("collides"))); + } + + #[test] + fn skips_instructions_with_unsupported_args() { + let instr = InstructionDef { + name: "subscribe".to_string(), + discriminator: vec![3], + discriminator_size: 1, + accounts: vec![], + args: vec![arg("data", "subscribeData")], + errors: vec![], + program_id: None, + docs: vec![], + }; + let out = generate_instructions_code( + "Subscriptions", + std::slice::from_ref(&instr), + &[], + &BTreeMap::new(), + &["Prog111".to_string()], + &HashSet::new(), + ); + assert!(out.stack_entries.is_empty()); + assert!(out.warnings.iter().any(|w| w.contains("subscribe"))); + } + + #[test] + fn emits_handler_with_signer_known_and_user_provided_accounts() { + let instr = InstructionDef { + name: "closeSubscriptionAuthority".to_string(), + discriminator: vec![6], + discriminator_size: 1, + accounts: vec![ + InstructionAccountDef { + name: "user".to_string(), + is_signer: true, + is_writable: true, + resolution: AccountResolution::Signer, + is_optional: false, + docs: vec![], + }, + InstructionAccountDef { + name: "subscriptionAuthority".to_string(), + is_signer: false, + is_writable: true, + resolution: AccountResolution::UserProvided, + is_optional: false, + docs: vec![], + }, + ], + args: vec![], + errors: vec![], + program_id: Some("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()), + docs: vec![], + }; + + let idls = vec![idl( + "subscriptions", + "De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44", + vec![IdlErrorSnapshot { + code: 130, + name: "unauthorized".to_string(), + msg: Some("Caller not authorized".to_string()), + }], + )]; + let out = generate_instructions_code( + "Subscriptions", + std::slice::from_ref(&instr), + &idls, + &BTreeMap::new(), + &["De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()], + &HashSet::new(), + ); + + assert_eq!( + out.stack_entries, + vec![StackInstructionEntry { + program_key: None, + instruction_name: "closeSubscriptionAuthority".to_string(), + handler_const: "closeSubscriptionAuthorityInstruction".to_string(), + }] + ); + assert!(out.needs_runtime_import); + let code = &out.code; + assert!(code.contains("export interface CloseSubscriptionAuthorityParams")); + assert!(code.contains("subscriptionAuthority: string;")); + assert!(code.contains("category: 'signer'")); + assert!(code.contains("category: 'userProvided'")); + assert!(code.contains( + "export const closeSubscriptionAuthorityInstruction = createInstructionHandler" + )); + assert!(code.contains("SUBSCRIPTIONS_PROGRAM_ERRORS: ErrorMetadata[]")); + assert!(code.contains("code: 130, name: 'unauthorized'")); + } + + #[test] + fn inlines_pda_ref_seeds_including_raw_bytes() { + let mut program_pdas: BTreeMap = BTreeMap::new(); + program_pdas.insert( + "subscriptionAuthority".to_string(), + PdaDefinition { + name: "subscriptionAuthority".to_string(), + seeds: vec![ + PdaSeedDef::Literal { + value: "SubscriptionAuthority".to_string(), + }, + PdaSeedDef::Bytes { + value: vec![1, 2, 255], + }, + PdaSeedDef::AccountRef { + account_name: "owner".to_string(), + }, + PdaSeedDef::AccountRef { + account_name: "tokenMint".to_string(), + }, + ], + program_id: None, + }, + ); + let mut pdas = BTreeMap::new(); + pdas.insert("subscriptions".to_string(), program_pdas); + + let instr = InstructionDef { + name: "initSubscriptionAuthority".to_string(), + discriminator: vec![0], + discriminator_size: 1, + accounts: vec![ + InstructionAccountDef { + name: "owner".to_string(), + is_signer: true, + is_writable: true, + resolution: AccountResolution::Signer, + is_optional: false, + docs: vec![], + }, + InstructionAccountDef { + name: "subscriptionAuthority".to_string(), + is_signer: false, + is_writable: true, + resolution: AccountResolution::PdaRef { + pda_name: "subscriptionAuthority".to_string(), + }, + is_optional: false, + docs: vec![], + }, + InstructionAccountDef { + name: "tokenMint".to_string(), + is_signer: false, + is_writable: false, + resolution: AccountResolution::UserProvided, + is_optional: false, + docs: vec![], + }, + ], + args: vec![], + errors: vec![], + program_id: Some("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()), + docs: vec![], + }; + + let out = generate_instructions_code( + "Subscriptions", + std::slice::from_ref(&instr), + &[], + &pdas, + &["De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()], + &HashSet::new(), + ); + let code = &out.code; + assert!(code.contains("category: 'pda'")); + assert!(code.contains("{ type: 'literal', value: 'SubscriptionAuthority' }")); + assert!(code.contains("{ type: 'bytes', value: [1, 2, 255] }")); + assert!(code.contains("{ type: 'accountRef', accountName: 'owner' }")); + // PDA account is resolved internally, so it is NOT a param; tokenMint is. + assert!(code.contains("tokenMint: string;")); + assert!(!code.contains("subscriptionAuthority: string;")); + assert!(out.warnings.is_empty(), "no degradation expected: {:?}", out.warnings); + } + + #[test] + fn emits_typed_arg_seeds_with_instr_args_fallback() { + let mut program_pdas: BTreeMap = BTreeMap::new(); + program_pdas.insert( + "round".to_string(), + PdaDefinition { + name: "round".to_string(), + seeds: vec![ + PdaSeedDef::Literal { + value: "round".to_string(), + }, + // Declared type on the seed itself (pdas! DSL style). + PdaSeedDef::ArgRef { + arg_name: "roundId".to_string(), + arg_type: Some("u32".to_string()), + }, + // No declared type: must fall back to the instruction arg. + PdaSeedDef::ArgRef { + arg_name: "owner".to_string(), + arg_type: None, + }, + ], + program_id: None, + }, + ); + let mut pdas = BTreeMap::new(); + pdas.insert("demo".to_string(), program_pdas); + + let instr = InstructionDef { + name: "commit".to_string(), + discriminator: vec![1], + discriminator_size: 1, + accounts: vec![InstructionAccountDef { + name: "round".to_string(), + is_signer: false, + is_writable: true, + resolution: AccountResolution::PdaRef { + pda_name: "round".to_string(), + }, + is_optional: false, + docs: vec![], + }], + args: vec![ + arg("roundId", "u32"), + arg("owner", "solana_pubkey::Pubkey"), + ], + errors: vec![], + program_id: Some("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()), + docs: vec![], + }; + + let out = generate_instructions_code( + "Demo", + std::slice::from_ref(&instr), + &[], + &pdas, + &["De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()], + &HashSet::new(), + ); + let code = &out.code; + assert!(code.contains("{ type: 'argRef', argName: 'roundId', argType: 'u32' }")); + assert!( + code.contains("{ type: 'argRef', argName: 'owner', argType: 'pubkey' }"), + "path-qualified Pubkey arg type should normalize via instr.args fallback: {}", + code + ); + } + + #[test] + fn untypeable_arg_seed_emits_without_arg_type_and_warns() { + let mut program_pdas: BTreeMap = BTreeMap::new(); + program_pdas.insert( + "vault".to_string(), + PdaDefinition { + name: "vault".to_string(), + seeds: vec![PdaSeedDef::ArgRef { + arg_name: "data".to_string(), + arg_type: None, + }], + program_id: None, + }, + ); + let mut pdas = BTreeMap::new(); + pdas.insert("demo".to_string(), program_pdas); + + let instr = InstructionDef { + name: "store".to_string(), + discriminator: vec![2], + discriminator_size: 1, + accounts: vec![InstructionAccountDef { + name: "vault".to_string(), + is_signer: false, + is_writable: true, + resolution: AccountResolution::PdaRef { + pda_name: "vault".to_string(), + }, + is_optional: false, + docs: vec![], + }], + args: vec![arg("data", "Vec")], + errors: vec![], + program_id: Some("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()), + docs: vec![], + }; + + let out = generate_instructions_code( + "Demo", + std::slice::from_ref(&instr), + &[], + &pdas, + &["De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44".to_string()], + &HashSet::new(), + ); + assert!(out.code.contains("{ type: 'argRef', argName: 'data' }")); + assert!( + out.warnings + .iter() + .any(|w| w.contains("heuristic encoding")), + "expected soft warning, got {:?}", + out.warnings + ); + } + + /// Golden test: drive the codegen from the real, compiler-produced ore + /// stack JSON and assert the expected handlers and PDA configs appear. This + /// exercises the full `stack.json -> TypeScript` path against actual data + /// (the Steel `pdas!` registry resolving instruction accounts to `PdaRef`). + #[test] + fn golden_ore_stack_json_emits_pda_handlers() { + let path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../stacks/ore/.arete/OreStream.stack.json" + ); + let json = match std::fs::read_to_string(path) { + Ok(c) => c, + // Stack JSON is generated by the macro build; skip if not present. + Err(_) => return, + }; + let spec: crate::ast::SerializableStackSpec = + serde_json::from_str(&json).expect("ore stack json should deserialize"); + + let out = generate_instructions_code( + &to_pascal_case(&spec.stack_name), + &spec.instructions, + &spec.idls, + &spec.pdas, + &spec.program_ids, + &HashSet::new(), + ); + + assert!( + !out.stack_entries.is_empty(), + "expected at least one emitted ore handler" + ); + let code = &out.code; + assert!(code.contains("createInstructionHandler")); + // Pure-literal PDA (treasury) and authority-keyed PDA (miner) both appear. + assert!( + code.contains("{ type: 'literal', value: 'treasury' }"), + "treasury PDA seed should be inlined" + ); + assert!( + code.contains("{ type: 'literal', value: 'miner' }") + && code.contains("{ type: 'accountRef', accountName: 'authority' }"), + "miner PDA seeds should be inlined with an authority accountRef" + ); + assert!(code.contains("category: 'pda'")); + + // The ore stack bundles two programs (ore + entropy) that BOTH define + // a `close` instruction: handlers must be program-prefixed, the stack + // block namespaced, and errors scoped per program. + assert!(spec.idls.len() > 1, "ore stack should bundle two programs"); + assert!(code.contains("export const oreCloseInstruction")); + assert!(code.contains("export const entropyCloseInstruction")); + assert!(!code.contains("export const closeInstruction")); + assert!(code.contains("ORE_STREAM_ORE_PROGRAM_ERRORS")); + assert!(code.contains("ORE_STREAM_ENTROPY_PROGRAM_ERRORS")); + let block = render_instructions_stack_block(&out.stack_entries); + assert!(block.contains(" ore: {")); + assert!(block.contains(" entropy: {")); + assert!(block.contains(" close: oreCloseInstruction,")); + assert!(block.contains(" close: entropyCloseInstruction,")); + + // Exact-string golden: the full oreClose block (params interface, + // error alias, docs, handler) must match the checked-in fixture. This + // catches naming/formatting churn before the CI regenerate-diff does. + // To update intentionally: regenerate the examples, then copy the + // block: awk '/^export interface OreCloseParams/,/^}\);$/' \ + // examples/ore-typescript/src/generated/ore-stack.ts \ + // > interpreter/tests/golden/ore-close-instruction.expected.ts + let expected_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/golden/ore-close-instruction.expected.ts" + ); + let expected = std::fs::read_to_string(expected_path) + .expect("golden fixture should exist") + .trim_end() + .to_string(); + let start = code + .find("export interface OreCloseParams") + .expect("OreCloseParams block present"); + let end_marker = "});"; + let end = code[start..] + .find(&format!( + "export const oreCloseInstruction" + )) + .and_then(|handler_offset| { + code[start + handler_offset..] + .find(end_marker) + .map(|e| start + handler_offset + e + end_marker.len()) + }) + .expect("oreCloseInstruction block terminates"); + let actual = code[start..end].trim_end(); + assert_eq!( + actual, expected, + "generated oreClose block diverged from the golden fixture" + ); + } + + #[test] + fn multi_program_scopes_errors_and_prefixes_names() { + let idls = vec![ + idl( + "ore", + "Prog111111111111111111111111111111111111111", + vec![IdlErrorSnapshot { + code: 0, + name: "OreBroke".to_string(), + msg: Some("ore broke".to_string()), + }], + ), + idl( + "entropy", + "Prog222222222222222222222222222222222222222", + vec![IdlErrorSnapshot { + code: 0, + name: "EntropyBroke".to_string(), + msg: Some("entropy broke".to_string()), + }], + ), + ]; + + let close = |program_id: &str| InstructionDef { + name: "close".to_string(), + discriminator: vec![9], + discriminator_size: 1, + accounts: vec![InstructionAccountDef { + name: "signer".to_string(), + is_signer: true, + is_writable: true, + resolution: AccountResolution::Signer, + is_optional: false, + docs: vec![], + }], + args: vec![], + errors: vec![], + program_id: Some(program_id.to_string()), + docs: vec![], + }; + let instructions = vec![ + close("Prog111111111111111111111111111111111111111"), + close("Prog222222222222222222222222222222222222222"), + ]; + + let out = generate_instructions_code( + "Demo", + &instructions, + &idls, + &BTreeMap::new(), + &[ + "Prog111111111111111111111111111111111111111".to_string(), + "Prog222222222222222222222222222222222222222".to_string(), + ], + &HashSet::new(), + ); + + let code = &out.code; + // Names prefixed per program; no collisions. + assert!(code.contains("export const oreCloseInstruction")); + assert!(code.contains("export const entropyCloseInstruction")); + assert!(code.contains("export interface OreCloseParams")); + assert!(code.contains("export interface EntropyCloseParams")); + // Overlapping error code 0 is attributed per program, not deduped away. + assert!(code.contains("DEMO_ORE_PROGRAM_ERRORS")); + assert!(code.contains("DEMO_ENTROPY_PROGRAM_ERRORS")); + assert!(code.contains("name: 'OreBroke'")); + assert!(code.contains("name: 'EntropyBroke'")); + // Each handler references its own program's errors. + assert!(code.contains("errors: DEMO_ORE_PROGRAM_ERRORS")); + assert!(code.contains("errors: DEMO_ENTROPY_PROGRAM_ERRORS")); + + assert_eq!(out.stack_entries.len(), 2); + assert_eq!(out.stack_entries[0].program_key.as_deref(), Some("ore")); + assert_eq!(out.stack_entries[1].program_key.as_deref(), Some("entropy")); + + let block = render_instructions_stack_block(&out.stack_entries); + assert!(block.contains(" ore: {\n close: oreCloseInstruction,\n },")); + assert!(block.contains(" entropy: {\n close: entropyCloseInstruction,\n },")); + } + + #[test] + fn multi_program_unmatched_instruction_falls_back_with_warning() { + let idls = vec![ + idl("ore", "Prog111111111111111111111111111111111111111", vec![]), + idl("entropy", "Prog222222222222222222222222222222222222222", vec![]), + ]; + let instr = InstructionDef { + name: "mystery".to_string(), + discriminator: vec![1], + discriminator_size: 1, + accounts: vec![], + args: vec![], + errors: vec![], + program_id: None, + docs: vec![], + }; + + let out = generate_instructions_code( + "Demo", + std::slice::from_ref(&instr), + &idls, + &BTreeMap::new(), + &["Prog111111111111111111111111111111111111111".to_string()], + &HashSet::new(), + ); + + assert!(out + .warnings + .iter() + .any(|w| w.contains("could not be matched to a program IDL"))); + // Unmatched: unprefixed name, flat stack entry, stack-wide errors. + assert!(out.code.contains("export const mysteryInstruction")); + assert!(out.code.contains("errors: DEMO_PROGRAM_ERRORS")); + assert_eq!(out.stack_entries[0].program_key, None); + } +} diff --git a/interpreter/tests/golden/ore-close-instruction.expected.ts b/interpreter/tests/golden/ore-close-instruction.expected.ts new file mode 100644 index 00000000..1f1b24a1 --- /dev/null +++ b/interpreter/tests/golden/ore-close-instruction.expected.ts @@ -0,0 +1,26 @@ +export interface OreCloseParams { + rentPayer: string; + round: string; +} + +export type OreCloseError = OreStreamOreProgramError; + +/** + * Closes an expired round account and returns rent to the payer. + * Round PDA seeds: ["round", round_id]. + * Treasury PDA seeds: ["treasury"]. + */ +export const oreCloseInstruction = createInstructionHandler({ + programId: 'oreV3EG1i9BEgiAJ8b177Z2S2rMarzak4NMv1kULvWv', + discriminator: [5], + args: [], + accounts: [ + { name: 'signer', isSigner: true, isWritable: false, category: 'signer' }, + { name: 'board', isSigner: false, isWritable: true, category: 'pda', pdaConfig: { seeds: [{ type: 'literal', value: 'board' }] } }, + { name: 'rentPayer', isSigner: false, isWritable: true, category: 'userProvided' }, + { name: 'round', isSigner: false, isWritable: true, category: 'userProvided' }, + { name: 'treasury', isSigner: false, isWritable: true, category: 'pda', pdaConfig: { seeds: [{ type: 'literal', value: 'treasury' }] } }, + { name: 'systemProgram', isSigner: false, isWritable: false, category: 'known', knownAddress: '11111111111111111111111111111111' }, + ], + errors: ORE_STREAM_ORE_PROGRAM_ERRORS, +});