From a83c4023d0aa8bbe2fe3e6ec35058367b0b48ab2 Mon Sep 17 00:00:00 2001 From: baha Date: Wed, 25 Feb 2026 16:13:02 +0300 Subject: [PATCH 1/6] feat: add plugin-owned primitive registry and firmware-gated requirements Migrate EVM/Solana/Cosmos/XRP lattice signers to resolve primitives via registry, add transactional chain registration, and extend unit test coverage. --- packages/chains/chain-core/src/index.ts | 26 ++ .../chain-core/src/primitiveRegistry.ts | 220 +++++++++++++++++ packages/chains/cosmos/src/devices/lattice.ts | 52 ++-- packages/chains/evm/src/devices/lattice.ts | 107 +++++--- packages/chains/solana/src/devices/lattice.ts | 52 ++-- packages/chains/xrp/src/devices/lattice.ts | 52 ++-- .../unit/chainRuntimePrimitives.test.ts | 166 +++++++++++++ .../sdk/src/__test__/unit/context.test.ts | 22 ++ .../unit/latticeSignerPrimitives.test.ts | 230 ++++++++++++++++++ .../unit/primitiveRegistry.core.test.ts | 94 +++++++ .../sdk/src/__test__/unit/primitives.test.ts | 155 ++++++++++++ .../__test__/unit/setupChainPlugins.test.ts | 29 ++- packages/sdk/src/api/setup.ts | 2 + packages/sdk/src/chains/context.ts | 8 +- packages/sdk/src/chains/index.ts | 11 + packages/sdk/src/chains/primitives.ts | 154 ++++++++++++ packages/sdk/src/chains/registry.ts | 60 ++++- packages/types/src/firmware.ts | 3 + 18 files changed, 1335 insertions(+), 108 deletions(-) create mode 100644 packages/chains/chain-core/src/primitiveRegistry.ts create mode 100644 packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts create mode 100644 packages/sdk/src/__test__/unit/context.test.ts create mode 100644 packages/sdk/src/__test__/unit/latticeSignerPrimitives.test.ts create mode 100644 packages/sdk/src/__test__/unit/primitiveRegistry.core.test.ts create mode 100644 packages/sdk/src/__test__/unit/primitives.test.ts create mode 100644 packages/sdk/src/chains/primitives.ts diff --git a/packages/chains/chain-core/src/index.ts b/packages/chains/chain-core/src/index.ts index c31caf00..b7066af9 100644 --- a/packages/chains/chain-core/src/index.ts +++ b/packages/chains/chain-core/src/index.ts @@ -33,6 +33,25 @@ export type ChainCapabilities = { getXpub?: boolean; }; +export type PrimitiveKind = 'hash' | 'curve' | 'encoding'; + +export type PrimitiveDefinition = { + kind: PrimitiveKind; + name: string; + code: number; +}; + +export type PrimitiveRequirement = { + kind: PrimitiveKind; + name: string; + minFirmware: [number, number, number]; +}; + +export type PluginPrimitives = { + definitions?: PrimitiveDefinition[]; + requirements?: PrimitiveRequirement[]; +}; + export type GetAddressParams = { path?: DerivationPath; accountIndex?: number; @@ -127,6 +146,7 @@ export type ChainPlugin< signer: TSigner, options?: TOptions, ) => Promise | TAdapter; + primitives?: PluginPrimitives; }; export type ChainRegistryResolveOptions = { @@ -248,6 +268,12 @@ export function createChainRegistry( }; } +export { + createPrimitiveRegistry, + PrimitiveConflictError, + type PrimitiveRegistry, +} from './primitiveRegistry'; + // --------------------------------------------------------------------------- // Shared chain utilities // --------------------------------------------------------------------------- diff --git a/packages/chains/chain-core/src/primitiveRegistry.ts b/packages/chains/chain-core/src/primitiveRegistry.ts new file mode 100644 index 00000000..ca22640a --- /dev/null +++ b/packages/chains/chain-core/src/primitiveRegistry.ts @@ -0,0 +1,220 @@ +import type { PrimitiveDefinition, PrimitiveKind } from './index'; + +const PRIMITIVE_KINDS: PrimitiveKind[] = ['hash', 'curve', 'encoding']; + +type PrimitiveDefinitionMap = { + [K in PrimitiveKind]: Map; +}; + +type PrimitiveReverseDefinitionMap = { + [K in PrimitiveKind]: Map; +}; + +const createNameMaps = (): PrimitiveDefinitionMap => ({ + hash: new Map(), + curve: new Map(), + encoding: new Map(), +}); + +const createCodeMaps = (): PrimitiveReverseDefinitionMap => ({ + hash: new Map(), + curve: new Map(), + encoding: new Map(), +}); + +const normalizePrimitiveKind = (kind: unknown): PrimitiveKind => { + if (kind === 'hash' || kind === 'curve' || kind === 'encoding') { + return kind; + } + throw new Error(`Invalid primitive kind: ${String(kind)}`); +}; + +const normalizePrimitiveName = (name: unknown): string => { + if (typeof name !== 'string') { + throw new Error(`Invalid primitive name: ${String(name)}`); + } + const normalized = name.trim().toUpperCase(); + if (!normalized) { + throw new Error('Primitive name cannot be empty'); + } + return normalized; +}; + +const normalizePrimitiveCode = (code: unknown): number => { + if ( + typeof code !== 'number' || + !Number.isFinite(code) || + !Number.isInteger(code) || + code < 0 + ) { + throw new Error(`Invalid primitive code: ${String(code)}`); + } + return code; +}; + +const normalizeDefinition = (definition: PrimitiveDefinition) => { + const kind = normalizePrimitiveKind(definition.kind); + const name = normalizePrimitiveName(definition.name); + const code = normalizePrimitiveCode(definition.code); + return { kind, name, code }; +}; + +const sortDefinitions = ( + definitions: PrimitiveDefinition[], +): PrimitiveDefinition[] => { + return [...definitions].sort((a, b) => { + if (a.kind !== b.kind) return a.kind.localeCompare(b.kind); + if (a.name !== b.name) return a.name.localeCompare(b.name); + return a.code - b.code; + }); +}; + +const normalizeDefinitions = ( + definitions: PrimitiveDefinition[], +): PrimitiveDefinition[] => { + if (!Array.isArray(definitions)) { + throw new Error('Primitive definitions must be an array'); + } + return definitions.map(normalizeDefinition); +}; + +export class PrimitiveConflictError extends Error { + public readonly kind: PrimitiveKind; + public readonly primitiveName: string; + public readonly code: number; + + constructor(message: string, def: PrimitiveDefinition) { + super(message); + this.name = 'PrimitiveConflictError'; + this.kind = def.kind; + this.primitiveName = def.name; + this.code = def.code; + } +} + +export type PrimitiveRegistry = { + register: (definitions: PrimitiveDefinition[]) => void; + preflight: (definitions: PrimitiveDefinition[]) => void; + resolve: (kind: PrimitiveKind, name: string) => number | undefined; + resolveOrThrow: (kind: PrimitiveKind, name: string) => number; + reverseResolve: (kind: PrimitiveKind, code: number) => string | undefined; + has: (kind: PrimitiveKind, name: string) => boolean; + list: (kind?: PrimitiveKind) => PrimitiveDefinition[]; + reset: () => void; +}; + +export function createPrimitiveRegistry(): PrimitiveRegistry { + const nameToCode = createNameMaps(); + const codeToName = createCodeMaps(); + + const checkConflict = ( + stagedNameToCode: PrimitiveDefinitionMap, + stagedCodeToName: PrimitiveReverseDefinitionMap, + def: PrimitiveDefinition, + ) => { + const existingCode = stagedNameToCode[def.kind].get(def.name); + if (existingCode !== undefined && existingCode !== def.code) { + throw new PrimitiveConflictError( + `Primitive conflict for ${def.kind}:${def.name}. Existing code=${existingCode}, new code=${def.code}.`, + def, + ); + } + + const existingName = stagedCodeToName[def.kind].get(def.code); + if (existingName !== undefined && existingName !== def.name) { + throw new PrimitiveConflictError( + `Primitive conflict for ${def.kind} code=${def.code}. Existing name=${existingName}, new name=${def.name}.`, + def, + ); + } + }; + + const preflight = (definitions: PrimitiveDefinition[]) => { + const normalized = normalizeDefinitions(definitions); + const stagedNameToCode = createNameMaps(); + const stagedCodeToName = createCodeMaps(); + + for (const kind of PRIMITIVE_KINDS) { + nameToCode[kind].forEach((code, name) => { + stagedNameToCode[kind].set(name, code); + }); + codeToName[kind].forEach((name, code) => { + stagedCodeToName[kind].set(code, name); + }); + } + + for (const def of normalized) { + checkConflict(stagedNameToCode, stagedCodeToName, def); + stagedNameToCode[def.kind].set(def.name, def.code); + stagedCodeToName[def.kind].set(def.code, def.name); + } + }; + + const register = (definitions: PrimitiveDefinition[]) => { + const normalized = normalizeDefinitions(definitions); + preflight(normalized); + + for (const def of normalized) { + nameToCode[def.kind].set(def.name, def.code); + codeToName[def.kind].set(def.code, def.name); + } + }; + + const resolve = (kind: PrimitiveKind, name: string): number | undefined => { + return nameToCode[kind].get(normalizePrimitiveName(name)); + }; + + const resolveOrThrow = (kind: PrimitiveKind, name: string): number => { + const code = resolve(kind, name); + if (code === undefined) { + throw new Error(`Primitive not found: ${kind}:${name}`); + } + return code; + }; + + const reverseResolve = ( + kind: PrimitiveKind, + code: number, + ): string | undefined => { + return codeToName[kind].get(normalizePrimitiveCode(code)); + }; + + const has = (kind: PrimitiveKind, name: string): boolean => { + return resolve(kind, name) !== undefined; + }; + + const list = (kind?: PrimitiveKind): PrimitiveDefinition[] => { + const definitions: PrimitiveDefinition[] = []; + const kinds = kind ? [kind] : PRIMITIVE_KINDS; + + for (const currentKind of kinds) { + nameToCode[currentKind].forEach((code, name) => { + definitions.push({ + kind: currentKind, + name, + code, + }); + }); + } + + return sortDefinitions(definitions); + }; + + const reset = () => { + for (const kind of PRIMITIVE_KINDS) { + nameToCode[kind].clear(); + codeToName[kind].clear(); + } + }; + + return { + register, + preflight, + resolve, + resolveOrThrow, + reverseResolve, + has, + list, + reset, + }; +} diff --git a/packages/chains/cosmos/src/devices/lattice.ts b/packages/chains/cosmos/src/devices/lattice.ts index 999d5845..2407bbd8 100644 --- a/packages/chains/cosmos/src/devices/lattice.ts +++ b/packages/chains/cosmos/src/devices/lattice.ts @@ -3,6 +3,7 @@ import type { ChainPlugin, DerivationPath, DeviceContext, + PrimitiveKind, PublicKey, SignResult, } from '@gridplus/chain-core'; @@ -16,48 +17,44 @@ import { import { buildSigResultFromRsv, compressSecp256k1Pubkey } from './shared'; type LatticeCosmosContext = DeviceContext & { + resolvePrimitive: (kind: PrimitiveKind, name: string) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { SECP256K1_PUB: number; }; - SIGNING: { - CURVES: { - SECP256K1: number; - }; - HASHES: { - SHA256: number; - }; - ENCODINGS: { - COSMOS: number; - }; - }; }; }; }; -function getLatticeCosmosConstants( +function getLatticeCosmosContext( context: DeviceContext, -): LatticeCosmosContext['constants'] { - const constants = (context as LatticeCosmosContext).constants; +): LatticeCosmosContext { + const typed = context as LatticeCosmosContext; + const constants = typed.constants; const hasNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value); + if (typeof typed.resolvePrimitive !== 'function') { + throw new Error('Lattice Cosmos signer requires primitive resolver'); + } if ( - !hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.SECP256K1_PUB) || - !hasNumber(constants?.EXTERNAL?.SIGNING?.CURVES?.SECP256K1) || - !hasNumber(constants?.EXTERNAL?.SIGNING?.HASHES?.SHA256) || - !hasNumber(constants?.EXTERNAL?.SIGNING?.ENCODINGS?.COSMOS) + !hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.SECP256K1_PUB) ) { throw new Error('Lattice Cosmos signer requires EXTERNAL constants'); } - return constants; + return typed; } export function createLatticeCosmosSigner( context: DeviceContext, ): CosmosSigner { - const { queue } = context; - const { EXTERNAL } = getLatticeCosmosConstants(context); + const { queue, resolvePrimitive, constants } = getLatticeCosmosContext( + context, + ); + const { EXTERNAL } = constants; + const curveSecp256k1 = resolvePrimitive('curve', 'SECP256K1'); + const hashSha256 = resolvePrimitive('hash', 'SHA256'); + const encodingCosmos = resolvePrimitive('encoding', 'COSMOS'); return { getAddress: async (path: DerivationPath): Promise
=> { @@ -101,9 +98,9 @@ export function createLatticeCosmosSigner( const signPayload = { signerPath: path, - curveType: EXTERNAL.SIGNING.CURVES.SECP256K1, - hashType: EXTERNAL.SIGNING.HASHES.SHA256, - encodingType: EXTERNAL.SIGNING.ENCODINGS.COSMOS, + curveType: curveSecp256k1, + hashType: hashSha256, + encodingType: encodingCosmos, payload: Buffer.from(request.payload as any), }; @@ -141,4 +138,11 @@ export const latticePlugin: ChainPlugin< device: 'lattice', module: cosmos, createSigner: createLatticeCosmosSigner, + primitives: { + requirements: [ + { kind: 'curve', name: 'SECP256K1', minFirmware: [0, 14, 0] }, + { kind: 'hash', name: 'SHA256', minFirmware: [0, 14, 0] }, + { kind: 'encoding', name: 'COSMOS', minFirmware: [0, 18, 10] }, + ], + }, }; diff --git a/packages/chains/evm/src/devices/lattice.ts b/packages/chains/evm/src/devices/lattice.ts index 552c14a1..ad88eb57 100644 --- a/packages/chains/evm/src/devices/lattice.ts +++ b/packages/chains/evm/src/devices/lattice.ts @@ -3,6 +3,7 @@ import type { ChainPlugin, DerivationPath, DeviceContext, + PrimitiveKind, PublicKey, SignResult, } from '@gridplus/chain-core'; @@ -33,24 +34,12 @@ export type LatticeEvmSignerOptions = { }; type LatticeEvmContext = DeviceContext & { + resolvePrimitive: (kind: PrimitiveKind, name: string) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { SECP256K1_PUB: number; }; - SIGNING: { - CURVES: { - SECP256K1: number; - }; - HASHES: { - KECCAK256: number; - }; - ENCODINGS: { - EVM: number; - EIP7702_AUTH: number; - EIP7702_AUTH_LIST: number; - }; - }; }; CURRENCIES: { ETH_MSG: string; @@ -70,11 +59,11 @@ function getLatticeEvmContext(context: DeviceContext): LatticeEvmContext { const constants = typed.constants; const hasNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value); + if (typeof typed.resolvePrimitive !== 'function') { + throw new Error('Lattice EVM signer requires primitive resolver'); + } if ( !hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.SECP256K1_PUB) || - !hasNumber(constants?.EXTERNAL?.SIGNING?.CURVES?.SECP256K1) || - !hasNumber(constants?.EXTERNAL?.SIGNING?.HASHES?.KECCAK256) || - !hasNumber(constants?.EXTERNAL?.SIGNING?.ENCODINGS?.EVM) || !constants?.CURRENCIES?.ETH_MSG ) { throw new Error( @@ -103,25 +92,71 @@ function normalizeRawEvmTx(tx: EvmRawTransaction): Hex | Buffer { function getEvmEncodingType( tx: TransactionSerializable, - EXTERNAL: LatticeEvmContext['constants']['EXTERNAL'], + resolvePrimitive: LatticeEvmContext['resolvePrimitive'], ): number { if ((tx as any).type === 'eip7702') { const eip7702 = tx as TransactionSerializableEIP7702; const hasAuthList = eip7702.authorizationList && eip7702.authorizationList.length > 0; return hasAuthList - ? EXTERNAL.SIGNING.ENCODINGS.EIP7702_AUTH_LIST - : EXTERNAL.SIGNING.ENCODINGS.EIP7702_AUTH; + ? resolvePrimitive('encoding', 'EIP7702_AUTH_LIST') + : resolvePrimitive('encoding', 'EIP7702_AUTH'); } - return EXTERNAL.SIGNING.ENCODINGS.EVM; + return resolvePrimitive('encoding', 'EVM'); } +const EIP7702_MIN_FIRMWARE: [number, number, number] = [0, 18, 0]; + +const getFirmwareVersion = (client: unknown): [number, number, number] => { + const maybeClient = client as { + getFwVersion?: () => { major?: unknown; minor?: unknown; fix?: unknown }; + }; + if (typeof maybeClient?.getFwVersion !== 'function') return [0, 0, 0]; + const fw = maybeClient.getFwVersion(); + const normalize = (value: unknown): number => + typeof value === 'number' && Number.isFinite(value) + ? Math.max(0, Math.trunc(value)) + : 0; + return [normalize(fw?.major), normalize(fw?.minor), normalize(fw?.fix)]; +}; + +const isAtLeastFirmware = ( + current: [number, number, number], + minimum: [number, number, number], +): boolean => { + if (current[0] !== minimum[0]) return current[0] > minimum[0]; + if (current[1] !== minimum[1]) return current[1] > minimum[1]; + return current[2] >= minimum[2]; +}; + +const assertEip7702FirmwareSupport = async ( + context: DeviceContext, + resolvePrimitive: LatticeEvmContext['resolvePrimitive'], + encodingType: number, +): Promise => { + const eip7702Encodings = new Set([ + resolvePrimitive('encoding', 'EIP7702_AUTH'), + resolvePrimitive('encoding', 'EIP7702_AUTH_LIST'), + ]); + if (!eip7702Encodings.has(encodingType)) return; + const client = await context.getClient(); + const fwVersion = getFirmwareVersion(client); + if (!isAtLeastFirmware(fwVersion, EIP7702_MIN_FIRMWARE)) { + throw new Error( + `EIP-7702 signing requires firmware ${EIP7702_MIN_FIRMWARE.join('.')} or newer. Device firmware: ${fwVersion.join('.')}.`, + ); + } +}; + export function createLatticeEvmSigner( context: DeviceContext, options: LatticeEvmSignerOptions = {}, ): EvmSigner { - const { queue, services } = getLatticeEvmContext(context); - const { EXTERNAL, CURRENCIES } = getLatticeEvmContext(context).constants; + const latticeContext = getLatticeEvmContext(context); + const { queue, services, resolvePrimitive } = latticeContext; + const { EXTERNAL, CURRENCIES } = latticeContext.constants; + const curveSecp256k1 = resolvePrimitive('curve', 'SECP256K1'); + const hashKeccak256 = resolvePrimitive('hash', 'KECCAK256'); return { getAddress: async (path: DerivationPath): Promise
=> { @@ -169,11 +204,16 @@ export function createLatticeEvmSigner( : serializeTransaction(request.payload as TransactionSerializable); const encodingType = isRaw - ? EXTERNAL.SIGNING.ENCODINGS.EVM + ? resolvePrimitive('encoding', 'EVM') : getEvmEncodingType( request.payload as TransactionSerializable, - EXTERNAL, + resolvePrimitive, ); + await assertEip7702FirmwareSupport( + latticeContext, + resolvePrimitive, + encodingType, + ); let decoder: Buffer | undefined; const fetchDecoder = services?.fetchDecoder; @@ -190,8 +230,8 @@ export function createLatticeEvmSigner( const signPayload = { signerPath: path, - curveType: EXTERNAL.SIGNING.CURVES.SECP256K1, - hashType: EXTERNAL.SIGNING.HASHES.KECCAK256, + curveType: curveSecp256k1, + hashType: hashKeccak256, encodingType, payload, decoder, @@ -232,8 +272,8 @@ export function createLatticeEvmSigner( client.sign({ data: { signerPath: path, - curveType: EXTERNAL.SIGNING.CURVES.SECP256K1, - hashType: EXTERNAL.SIGNING.HASHES.KECCAK256, + curveType: curveSecp256k1, + hashType: hashKeccak256, payload: request.payload as any, protocol, }, @@ -255,8 +295,8 @@ export function createLatticeEvmSigner( client.sign({ data: { signerPath: path, - curveType: EXTERNAL.SIGNING.CURVES.SECP256K1, - hashType: EXTERNAL.SIGNING.HASHES.KECCAK256, + curveType: curveSecp256k1, + hashType: hashKeccak256, payload: request.payload as any, protocol: 'eip712', }, @@ -291,4 +331,11 @@ export const latticePlugin: ChainPlugin< device: 'lattice', module: evm, createSigner: (context) => createLatticeEvmSigner(context), + primitives: { + requirements: [ + { kind: 'curve', name: 'SECP256K1', minFirmware: [0, 14, 0] }, + { kind: 'hash', name: 'KECCAK256', minFirmware: [0, 14, 0] }, + { kind: 'encoding', name: 'EVM', minFirmware: [0, 15, 0] }, + ], + }, }; diff --git a/packages/chains/solana/src/devices/lattice.ts b/packages/chains/solana/src/devices/lattice.ts index a32cfeab..c2682ddd 100644 --- a/packages/chains/solana/src/devices/lattice.ts +++ b/packages/chains/solana/src/devices/lattice.ts @@ -3,6 +3,7 @@ import type { ChainPlugin, DerivationPath, DeviceContext, + PrimitiveKind, PublicKey, SignResult, } from '@gridplus/chain-core'; @@ -17,48 +18,44 @@ import { import { buildSigResultFromRsv, toBuffer } from './shared'; type LatticeSolanaContext = DeviceContext & { + resolvePrimitive: (kind: PrimitiveKind, name: string) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { ED25519_PUB: number; }; - SIGNING: { - CURVES: { - ED25519: number; - }; - HASHES: { - NONE: number; - }; - ENCODINGS: { - SOLANA: number; - }; - }; }; }; }; -function getLatticeSolanaConstants( +function getLatticeSolanaContext( context: DeviceContext, -): LatticeSolanaContext['constants'] { - const constants = (context as LatticeSolanaContext).constants; +): LatticeSolanaContext { + const typed = context as LatticeSolanaContext; + const constants = typed.constants; const hasNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value); + if (typeof typed.resolvePrimitive !== 'function') { + throw new Error('Lattice Solana signer requires primitive resolver'); + } if ( - !hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.ED25519_PUB) || - !hasNumber(constants?.EXTERNAL?.SIGNING?.CURVES?.ED25519) || - !hasNumber(constants?.EXTERNAL?.SIGNING?.HASHES?.NONE) || - !hasNumber(constants?.EXTERNAL?.SIGNING?.ENCODINGS?.SOLANA) + !hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.ED25519_PUB) ) { throw new Error('Lattice Solana signer requires EXTERNAL constants'); } - return constants; + return typed; } export function createLatticeSolanaSigner( context: DeviceContext, ): SolanaSigner { - const { queue } = context; - const { EXTERNAL } = getLatticeSolanaConstants(context); + const { queue, resolvePrimitive, constants } = getLatticeSolanaContext( + context, + ); + const { EXTERNAL } = constants; + const curveEd25519 = resolvePrimitive('curve', 'ED25519'); + const hashNone = resolvePrimitive('hash', 'NONE'); + const encodingSolana = resolvePrimitive('encoding', 'SOLANA'); const getPublicKey = async (path: DerivationPath): Promise => { const res = (await queue((client: any) => @@ -90,9 +87,9 @@ export function createLatticeSolanaSigner( const signPayload = { signerPath: path, - curveType: EXTERNAL.SIGNING.CURVES.ED25519, - hashType: EXTERNAL.SIGNING.HASHES.NONE, - encodingType: EXTERNAL.SIGNING.ENCODINGS.SOLANA, + curveType: curveEd25519, + hashType: hashNone, + encodingType: encodingSolana, payload: toBuffer(request.payload as any), }; @@ -130,4 +127,11 @@ export const latticePlugin: ChainPlugin< device: 'lattice', module: solana, createSigner: createLatticeSolanaSigner, + primitives: { + requirements: [ + { kind: 'curve', name: 'ED25519', minFirmware: [0, 14, 0] }, + { kind: 'hash', name: 'NONE', minFirmware: [0, 14, 0] }, + { kind: 'encoding', name: 'SOLANA', minFirmware: [0, 14, 0] }, + ], + }, }; diff --git a/packages/chains/xrp/src/devices/lattice.ts b/packages/chains/xrp/src/devices/lattice.ts index 7ac1d9d8..2f5d8a13 100644 --- a/packages/chains/xrp/src/devices/lattice.ts +++ b/packages/chains/xrp/src/devices/lattice.ts @@ -3,6 +3,7 @@ import type { ChainPlugin, DerivationPath, DeviceContext, + PrimitiveKind, PublicKey, SignResult, } from '@gridplus/chain-core'; @@ -21,48 +22,40 @@ import { } from './shared'; type LatticeXrpContext = DeviceContext & { + resolvePrimitive: (kind: PrimitiveKind, name: string) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { SECP256K1_PUB: number; }; - SIGNING: { - CURVES: { - SECP256K1: number; - }; - HASHES: { - SHA512HALF: number; - }; - ENCODINGS: { - XRP: number; - }; - }; }; }; }; -function getLatticeXrpConstants( +function getLatticeXrpContext( context: DeviceContext, -): LatticeXrpContext['constants'] { - const constants = (context as LatticeXrpContext).constants; +): LatticeXrpContext { + const typed = context as LatticeXrpContext; + const constants = typed.constants; const hasNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value); + if (typeof typed.resolvePrimitive !== 'function') { + throw new Error('Lattice XRP signer requires primitive resolver'); + } - if ( - !hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.SECP256K1_PUB) || - !hasNumber(constants?.EXTERNAL?.SIGNING?.CURVES?.SECP256K1) || - !hasNumber(constants?.EXTERNAL?.SIGNING?.HASHES?.SHA512HALF) || - !hasNumber(constants?.EXTERNAL?.SIGNING?.ENCODINGS?.XRP) - ) { + if (!hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.SECP256K1_PUB)) { throw new Error('Lattice XRP signer requires EXTERNAL constants'); } - return constants; + return typed; } export function createLatticeXrpSigner(context: DeviceContext): XrpSigner { - const { queue } = context; - const { EXTERNAL } = getLatticeXrpConstants(context); + const { queue, resolvePrimitive, constants } = getLatticeXrpContext(context); + const { EXTERNAL } = constants; + const curveSecp256k1 = resolvePrimitive('curve', 'SECP256K1'); + const hashSha512Half = resolvePrimitive('hash', 'SHA512HALF'); + const encodingXrp = resolvePrimitive('encoding', 'XRP'); const getPublicKey = async ( path: DerivationPath, @@ -107,9 +100,9 @@ export function createLatticeXrpSigner(context: DeviceContext): XrpSigner { const signPayload = { signerPath: path, - curveType: EXTERNAL.SIGNING.CURVES.SECP256K1, - hashType: EXTERNAL.SIGNING.HASHES.SHA512HALF, - encodingType: EXTERNAL.SIGNING.ENCODINGS.XRP, + curveType: curveSecp256k1, + hashType: hashSha512Half, + encodingType: encodingXrp, payload: toBuffer(request.payload as any), }; @@ -149,4 +142,11 @@ export const latticePlugin: ChainPlugin< device: 'lattice', module: xrp, createSigner: createLatticeXrpSigner, + primitives: { + requirements: [ + { kind: 'curve', name: 'SECP256K1', minFirmware: [0, 14, 0] }, + { kind: 'hash', name: 'SHA512HALF', minFirmware: [0, 18, 10] }, + { kind: 'encoding', name: 'XRP', minFirmware: [0, 18, 10] }, + ], + }, }; diff --git a/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts b/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts new file mode 100644 index 00000000..63206306 --- /dev/null +++ b/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts @@ -0,0 +1,166 @@ +import type { + ChainAdapter, + ChainModule, + ChainPlugin, + PluginPrimitives, + Signer, +} from '@gridplus/chain-core'; +import { setLoadClient } from '../../api/state'; +import { + configureChainRuntime, + getChain, + registerChainPlugin, + unregisterChain, + useChain, +} from '../../chains'; +import { + getPrimitiveRegistry, + resetPrimitiveRegistry, +} from '../../chains/primitives'; + +const mockSigner: Signer = { + getAddress: async () => 'mock', + getPublicKey: async () => new Uint8Array([1]), + sign: async () => ({ signature: { bytes: new Uint8Array([2]) } }), +}; + +const mockAdapter: ChainAdapter = { + getAddress: async () => 'mock', + getAddresses: async () => ['mock'], + getPublicKey: async () => new Uint8Array([1]), + sign: async () => ({ signature: { bytes: new Uint8Array([2]) } }), +}; + +const buildPlugin = ( + chainId: string, + primitives?: PluginPrimitives, +): ChainPlugin => { + const module: ChainModule = { + id: chainId, + name: chainId, + coinType: 1, + curve: 'secp256k1', + defaultPath: [44, 60, 0, 0, 0], + supports: { + signTransaction: true, + signMessage: false, + signTypedData: false, + signArbitrary: false, + getPublicKey: true, + }, + create: () => mockAdapter, + utils: {}, + }; + + return { + chainId, + device: 'lattice', + module, + createSigner: async () => mockSigner, + primitives, + }; +}; + +describe('chain runtime primitive integration', () => { + afterEach(() => { + unregisterChain('chain-a', 'lattice'); + unregisterChain('chain-b', 'lattice'); + unregisterChain('dup-chain', 'lattice'); + unregisterChain('req-chain', 'lattice'); + resetPrimitiveRegistry(); + configureChainRuntime({ + autoRegisterChains: true, + defaultDevice: 'lattice', + resetCache: true, + }); + setLoadClient(async () => undefined); + }); + + test('primitive conflict does not register conflicting chain', () => { + registerChainPlugin( + buildPlugin('chain-a', { + definitions: [{ kind: 'encoding', name: 'TESTCHAIN', code: 99 }], + }), + ); + + expect(() => + registerChainPlugin( + buildPlugin('chain-b', { + definitions: [{ kind: 'encoding', name: 'TESTCHAIN', code: 100 }], + }), + ), + ).toThrow(); + + expect(getChain('chain-a', 'lattice')).toBeDefined(); + expect(getChain('chain-b', 'lattice')).toBeUndefined(); + }); + + test('chain registration failure does not leave new primitive definitions', () => { + registerChainPlugin( + buildPlugin('dup-chain', { + definitions: [{ kind: 'encoding', name: 'DUP_A', code: 201 }], + }), + ); + + expect(() => + registerChainPlugin( + buildPlugin('dup-chain', { + definitions: [{ kind: 'encoding', name: 'DUP_B', code: 202 }], + }), + ), + ).toThrow('already registered'); + + const primitiveRegistry = getPrimitiveRegistry(); + expect(primitiveRegistry.resolve('encoding', 'DUP_A')).toBe(201); + expect(primitiveRegistry.resolve('encoding', 'DUP_B')).toBeUndefined(); + }); + + test('useChain enforces primitive minFirmware requirements', async () => { + configureChainRuntime({ + autoRegisterChains: false, + defaultDevice: 'lattice', + resetCache: true, + }); + registerChainPlugin( + buildPlugin('req-chain', { + definitions: [{ kind: 'encoding', name: 'REQ_CHAIN', code: 88 }], + requirements: [ + { kind: 'encoding', name: 'REQ_CHAIN', minFirmware: [0, 20, 0] }, + ], + }), + ); + + setLoadClient( + async () => + ({ + getFwVersion: () => ({ major: 0, minor: 19, fix: 0 }), + }) as any, + ); + + await expect(useChain('req-chain')).rejects.toThrow('Please update firmware'); + }); + + test('useChain succeeds when firmware satisfies requirements', async () => { + configureChainRuntime({ + autoRegisterChains: false, + defaultDevice: 'lattice', + resetCache: true, + }); + registerChainPlugin( + buildPlugin('req-chain', { + definitions: [{ kind: 'encoding', name: 'REQ_CHAIN', code: 88 }], + requirements: [ + { kind: 'encoding', name: 'REQ_CHAIN', minFirmware: [0, 20, 0] }, + ], + }), + ); + setLoadClient( + async () => + ({ + getFwVersion: () => ({ major: 0, minor: 20, fix: 0 }), + }) as any, + ); + + await expect(useChain('req-chain')).resolves.toBeDefined(); + }); +}); diff --git a/packages/sdk/src/__test__/unit/context.test.ts b/packages/sdk/src/__test__/unit/context.test.ts new file mode 100644 index 00000000..79dd2848 --- /dev/null +++ b/packages/sdk/src/__test__/unit/context.test.ts @@ -0,0 +1,22 @@ +import { createDeviceContext } from '../../chains/context'; +import { resetPrimitiveRegistry } from '../../chains/primitives'; + +describe('chain context primitive resolution', () => { + afterEach(() => { + resetPrimitiveRegistry(); + }); + + test('resolvePrimitive resolves seeded builtins', () => { + const context = createDeviceContext(); + expect(context.resolvePrimitive('hash', 'KECCAK256')).toBeDefined(); + expect(context.resolvePrimitive('curve', 'SECP256K1')).toBeDefined(); + expect(context.resolvePrimitive('encoding', 'EVM')).toBeDefined(); + }); + + test('resolvePrimitive throws for unknown primitive', () => { + const context = createDeviceContext(); + expect(() => + context.resolvePrimitive('encoding', 'UNKNOWN_CHAIN'), + ).toThrow('Primitive not found'); + }); +}); diff --git a/packages/sdk/src/__test__/unit/latticeSignerPrimitives.test.ts b/packages/sdk/src/__test__/unit/latticeSignerPrimitives.test.ts new file mode 100644 index 00000000..4bf5f8a6 --- /dev/null +++ b/packages/sdk/src/__test__/unit/latticeSignerPrimitives.test.ts @@ -0,0 +1,230 @@ +import { createLatticeCosmosSigner } from '@gridplus/cosmos'; +import { createLatticeEvmSigner } from '@gridplus/evm'; +import { createLatticeSolanaSigner } from '@gridplus/solana'; +import { createLatticeXrpSigner } from '@gridplus/xrp'; +import type { PrimitiveKind } from '@gridplus/chain-core'; + +type PrimitiveMap = Record; + +type MockContextOptions = { + primitives: PrimitiveMap; + firmware?: [number, number, number]; +}; + +const primitiveKey = (kind: PrimitiveKind, name: string): string => + `${kind}:${name}`; + +const buildMockContext = (options: MockContextOptions) => { + const firmware = options.firmware ?? [1, 0, 0]; + const signCalls: Array = []; + + const client = { + sign: vi.fn(async (request: any) => { + signCalls.push(request); + return { sig: {} }; + }), + getFwVersion: () => ({ + major: firmware[0], + minor: firmware[1], + fix: firmware[2], + }), + getAddresses: vi.fn(async () => []), + }; + + const resolvePrimitive = vi.fn((kind: PrimitiveKind, name: string) => { + const key = primitiveKey(kind, name); + const code = options.primitives[key]; + if (code === undefined) { + throw new Error(`Missing primitive mapping for ${key}`); + } + return code; + }); + + const context = { + queue: async (fn: (client: unknown) => Promise) => fn(client), + getClient: async () => client, + resolvePrimitive, + constants: { + EXTERNAL: { + GET_ADDR_FLAGS: { + SECP256K1_PUB: 1, + ED25519_PUB: 2, + }, + }, + CURRENCIES: { + ETH_MSG: 'ETH_MSG', + }, + }, + services: {}, + } as any; + + return { + context, + client, + signCalls, + resolvePrimitive, + }; +}; + +const buildEip7702AuthListTx = () => ({ + type: 'eip7702', + chainId: 1, + nonce: 0, + maxPriorityFeePerGas: 1n, + maxFeePerGas: 2n, + gas: 21000n, + to: '0x1111111111111111111111111111111111111111', + value: 0n, + data: '0x', + accessList: [], + authorizationList: [ + { + chainId: 1, + address: '0x2222222222222222222222222222222222222222', + nonce: 0, + signature: { + yParity: 0, + r: `0x${'1'.repeat(64)}`, + s: `0x${'2'.repeat(64)}`, + }, + }, + ], +}); + +describe('lattice signer primitive resolution', () => { + test('evm signer uses resolved primitive codes in sign payload', async () => { + const { context, signCalls } = buildMockContext({ + primitives: { + 'curve:SECP256K1': 91, + 'hash:KECCAK256': 92, + 'encoding:EVM': 93, + 'encoding:EIP7702_AUTH': 94, + 'encoding:EIP7702_AUTH_LIST': 95, + }, + }); + const signer = createLatticeEvmSigner(context); + + await signer.sign({ + kind: 'transaction', + payload: '0x01', + options: { path: [44, 60, 0] }, + }); + + expect(signCalls).toHaveLength(1); + expect(signCalls[0].data.curveType).toBe(91); + expect(signCalls[0].data.hashType).toBe(92); + expect(signCalls[0].data.encodingType).toBe(93); + }); + + test('solana signer uses resolved primitive codes in sign payload', async () => { + const { context, signCalls } = buildMockContext({ + primitives: { + 'curve:ED25519': 11, + 'hash:NONE': 12, + 'encoding:SOLANA': 13, + }, + }); + const signer = createLatticeSolanaSigner(context); + + await signer.sign({ + kind: 'transaction', + payload: new Uint8Array([1, 2, 3]), + options: { path: [44, 501, 0] }, + }); + + expect(signCalls).toHaveLength(1); + expect(signCalls[0].data.curveType).toBe(11); + expect(signCalls[0].data.hashType).toBe(12); + expect(signCalls[0].data.encodingType).toBe(13); + }); + + test('cosmos signer uses resolved primitive codes in sign payload', async () => { + const { context, signCalls } = buildMockContext({ + primitives: { + 'curve:SECP256K1': 21, + 'hash:SHA256': 22, + 'encoding:COSMOS': 23, + }, + }); + const signer = createLatticeCosmosSigner(context); + + await signer.sign({ + kind: 'transaction', + payload: new Uint8Array([4, 5, 6]), + options: { path: [44, 118, 0, 0, 0] }, + }); + + expect(signCalls).toHaveLength(1); + expect(signCalls[0].data.curveType).toBe(21); + expect(signCalls[0].data.hashType).toBe(22); + expect(signCalls[0].data.encodingType).toBe(23); + }); + + test('xrp signer uses resolved primitive codes in sign payload', async () => { + const { context, signCalls } = buildMockContext({ + primitives: { + 'curve:SECP256K1': 31, + 'hash:SHA512HALF': 32, + 'encoding:XRP': 33, + }, + }); + const signer = createLatticeXrpSigner(context); + + await signer.sign({ + kind: 'transaction', + payload: new Uint8Array([7, 8, 9]), + options: { path: [44, 144, 0, 0, 0] }, + }); + + expect(signCalls).toHaveLength(1); + expect(signCalls[0].data.curveType).toBe(31); + expect(signCalls[0].data.hashType).toBe(32); + expect(signCalls[0].data.encodingType).toBe(33); + }); + + test('evm EIP-7702 signing fails below minimum firmware', async () => { + const { context, client } = buildMockContext({ + primitives: { + 'curve:SECP256K1': 41, + 'hash:KECCAK256': 42, + 'encoding:EVM': 43, + 'encoding:EIP7702_AUTH': 44, + 'encoding:EIP7702_AUTH_LIST': 45, + }, + firmware: [0, 17, 9], + }); + const signer = createLatticeEvmSigner(context); + + await expect( + signer.sign({ + kind: 'transaction', + payload: buildEip7702AuthListTx() as any, + options: { path: [44, 60, 0] }, + }), + ).rejects.toThrow('requires firmware 0.18.0'); + expect(client.sign).not.toHaveBeenCalled(); + }); + + test('evm EIP-7702 signing succeeds at minimum firmware and uses EIP-7702 encoding', async () => { + const { context, signCalls } = buildMockContext({ + primitives: { + 'curve:SECP256K1': 51, + 'hash:KECCAK256': 52, + 'encoding:EVM': 53, + 'encoding:EIP7702_AUTH': 54, + 'encoding:EIP7702_AUTH_LIST': 55, + }, + firmware: [0, 18, 0], + }); + const signer = createLatticeEvmSigner(context); + + await signer.sign({ + kind: 'transaction', + payload: buildEip7702AuthListTx() as any, + options: { path: [44, 60, 0] }, + }); + + expect(signCalls).toHaveLength(1); + expect(signCalls[0].data.encodingType).toBe(55); + }); +}); diff --git a/packages/sdk/src/__test__/unit/primitiveRegistry.core.test.ts b/packages/sdk/src/__test__/unit/primitiveRegistry.core.test.ts new file mode 100644 index 00000000..a2fa8b23 --- /dev/null +++ b/packages/sdk/src/__test__/unit/primitiveRegistry.core.test.ts @@ -0,0 +1,94 @@ +import { + PrimitiveConflictError, + createPrimitiveRegistry, + type PrimitiveDefinition, +} from '@gridplus/chain-core'; + +describe('primitive registry core', () => { + test('registers and resolves by name and code', () => { + const registry = createPrimitiveRegistry(); + registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); + + expect(registry.resolve('hash', 'SHA256')).toBe(2); + expect(registry.reverseResolve('hash', 2)).toBe('SHA256'); + expect(registry.has('hash', 'SHA256')).toBe(true); + }); + + test('ignores exact duplicates', () => { + const registry = createPrimitiveRegistry(); + registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); + registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); + + expect(registry.resolve('hash', 'SHA256')).toBe(2); + expect(registry.list('hash')).toHaveLength(1); + }); + + test('throws on name collision', () => { + const registry = createPrimitiveRegistry(); + registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); + + expect(() => + registry.register([{ kind: 'hash', name: 'SHA256', code: 99 }]), + ).toThrow(PrimitiveConflictError); + }); + + test('throws on code collision', () => { + const registry = createPrimitiveRegistry(); + registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); + + expect(() => + registry.register([{ kind: 'hash', name: 'BLAKE2B', code: 2 }]), + ).toThrow(PrimitiveConflictError); + }); + + test('namespaces collisions by kind', () => { + const registry = createPrimitiveRegistry(); + registry.register([ + { kind: 'hash', name: 'SHA256', code: 2 }, + { kind: 'encoding', name: 'SHA256', code: 2 }, + ]); + + expect(registry.resolve('hash', 'SHA256')).toBe(2); + expect(registry.resolve('encoding', 'SHA256')).toBe(2); + }); + + test('preflight catches conflicts without mutation', () => { + const registry = createPrimitiveRegistry(); + registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); + + expect(() => + registry.preflight([{ kind: 'hash', name: 'SHA256', code: 77 }]), + ).toThrow(PrimitiveConflictError); + expect(registry.resolve('hash', 'SHA256')).toBe(2); + expect(registry.resolve('hash', 'BLAKE2B')).toBeUndefined(); + }); + + test('register is atomic for a batch', () => { + const registry = createPrimitiveRegistry(); + registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); + + const batch: PrimitiveDefinition[] = [ + { kind: 'hash', name: 'BLAKE2B', code: 4 }, + { kind: 'hash', name: 'SHA256', code: 99 }, + ]; + + expect(() => registry.register(batch)).toThrow(PrimitiveConflictError); + expect(registry.resolve('hash', 'BLAKE2B')).toBeUndefined(); + }); + + test('resolveOrThrow throws for missing primitive', () => { + const registry = createPrimitiveRegistry(); + expect(() => registry.resolveOrThrow('hash', 'MISSING')).toThrow( + 'Primitive not found', + ); + }); + + test('reset clears registered mappings', () => { + const registry = createPrimitiveRegistry(); + registry.register([{ kind: 'curve', name: 'SECP256K1', code: 0 }]); + registry.reset(); + + expect(registry.resolve('curve', 'SECP256K1')).toBeUndefined(); + expect(registry.list()).toHaveLength(0); + }); +}); diff --git a/packages/sdk/src/__test__/unit/primitives.test.ts b/packages/sdk/src/__test__/unit/primitives.test.ts new file mode 100644 index 00000000..f606bce6 --- /dev/null +++ b/packages/sdk/src/__test__/unit/primitives.test.ts @@ -0,0 +1,155 @@ +import type { + ChainAdapter, + ChainModule, + ChainPlugin, + PluginPrimitives, + Signer, +} from '@gridplus/chain-core'; +import { + ensurePrimitivesSeeded, + getPrimitiveRegistry, + registerPluginPrimitives, + resetPrimitiveRegistry, + validatePluginPrimitiveRequirements, +} from '../../chains/primitives'; + +const mockSigner: Signer = { + getAddress: async () => 'mock', + getPublicKey: async () => new Uint8Array([1]), + sign: async () => ({ signature: { bytes: new Uint8Array([2]) } }), +}; + +const mockAdapter: ChainAdapter = { + getAddress: async () => 'mock', + getAddresses: async () => ['mock'], + getPublicKey: async () => new Uint8Array([1]), + sign: async () => ({ signature: { bytes: new Uint8Array([2]) } }), +}; + +const buildPlugin = ( + chainId: string, + primitives?: PluginPrimitives, +): ChainPlugin => { + const module: ChainModule = { + id: chainId, + name: chainId, + coinType: 1, + curve: 'secp256k1', + defaultPath: [44, 60, 0, 0, 0], + supports: { + signTransaction: true, + signMessage: true, + signTypedData: false, + signArbitrary: false, + getPublicKey: true, + }, + create: () => mockAdapter, + utils: {}, + }; + + return { + chainId, + device: 'lattice', + module, + createSigner: async () => mockSigner, + primitives, + }; +}; + +describe('sdk primitives module', () => { + afterEach(() => { + resetPrimitiveRegistry(); + }); + + test('ensurePrimitivesSeeded is idempotent and registers builtins', () => { + ensurePrimitivesSeeded(); + ensurePrimitivesSeeded(); + + const registry = getPrimitiveRegistry(); + expect(registry.resolve('hash', 'KECCAK256')).toBeDefined(); + expect(registry.resolve('curve', 'SECP256K1')).toBeDefined(); + expect(registry.resolve('encoding', 'EVM')).toBeDefined(); + }); + + test('registerPluginPrimitives registers custom definitions', () => { + const plugin = buildPlugin('testchain', { + definitions: [{ kind: 'encoding', name: 'TESTCHAIN', code: 99 }], + requirements: [ + { kind: 'encoding', name: 'TESTCHAIN', minFirmware: [0, 20, 0] }, + ], + }); + + registerPluginPrimitives(plugin); + expect(getPrimitiveRegistry().resolve('encoding', 'TESTCHAIN')).toBe(99); + }); + + test('registerPluginPrimitives fails on conflicts without partial commit', () => { + registerPluginPrimitives( + buildPlugin('chain-a', { + definitions: [{ kind: 'encoding', name: 'TESTCHAIN', code: 99 }], + }), + ); + + const conflicting = buildPlugin('chain-b', { + definitions: [ + { kind: 'encoding', name: 'TESTCHAIN', code: 100 }, + { kind: 'hash', name: 'BLAKE2B', code: 4 }, + ], + }); + + expect(() => registerPluginPrimitives(conflicting)).toThrow(); + expect(getPrimitiveRegistry().resolve('hash', 'BLAKE2B')).toBeUndefined(); + }); + + test('validatePluginPrimitiveRequirements passes when firmware requirement is met', () => { + ensurePrimitivesSeeded(); + const plugin = buildPlugin('cosmos-like', { + requirements: [ + { kind: 'encoding', name: 'COSMOS', minFirmware: [0, 18, 10] }, + ], + }); + + expect(() => + validatePluginPrimitiveRequirements(plugin, [0, 19, 0]), + ).not.toThrow(); + }); + + test('validatePluginPrimitiveRequirements throws when firmware requirement is unmet', () => { + ensurePrimitivesSeeded(); + const plugin = buildPlugin('cosmos-like', { + requirements: [ + { kind: 'encoding', name: 'COSMOS', minFirmware: [0, 18, 10] }, + ], + }); + + expect(() => + validatePluginPrimitiveRequirements(plugin, [0, 18, 9]), + ).toThrow('Please update firmware'); + }); + + test('validatePluginPrimitiveRequirements throws for missing primitive mapping', () => { + const plugin = buildPlugin('custom-chain', { + requirements: [ + { kind: 'encoding', name: 'UNREGISTERED_CHAIN', minFirmware: [0, 20, 0] }, + ], + }); + + expect(() => + validatePluginPrimitiveRequirements(plugin, [0, 20, 0]), + ).toThrow('not registered'); + }); + + test('rejects invalid requirement shape (fail-closed)', () => { + const plugin = buildPlugin('invalid') as ChainPlugin; + (plugin as any).primitives = { + requirements: [{ kind: 'hash', name: 'SHA256' }], + }; + + expect(() => registerPluginPrimitives(plugin)).toThrow( + 'invalid primitive requirement', + ); + expect(() => + validatePluginPrimitiveRequirements(plugin, [9, 9, 9]), + ).toThrow('invalid primitive requirement'); + }); +}); diff --git a/packages/sdk/src/__test__/unit/setupChainPlugins.test.ts b/packages/sdk/src/__test__/unit/setupChainPlugins.test.ts index 2c280305..22218dbc 100644 --- a/packages/sdk/src/__test__/unit/setupChainPlugins.test.ts +++ b/packages/sdk/src/__test__/unit/setupChainPlugins.test.ts @@ -2,6 +2,7 @@ import type { ChainAdapter, ChainModule, ChainPlugin, + PluginPrimitives, Signer, } from '@gridplus/chain-core'; import { setup } from '../../api/setup'; @@ -10,6 +11,10 @@ import { DEFAULT_CHAIN_PLUGIN_KEYS, DEFAULT_CHAIN_PLUGINS, } from '../../chains/defaultManifest'; +import { + getPrimitiveRegistry, + resetPrimitiveRegistry, +} from '../../chains/primitives'; const mockSigner: Signer = { getAddress: async () => 'custom', @@ -24,7 +29,11 @@ const mockAdapter: ChainAdapter = { sign: async () => ({ signature: { bytes: new Uint8Array([2]) } }), }; -const buildPlugin = (chainId: string, device: string): ChainPlugin => { +const buildPlugin = ( + chainId: string, + device: string, + primitives?: PluginPrimitives, +): ChainPlugin => { const module: ChainModule = { id: chainId, name: `test-${chainId}`, @@ -47,6 +56,7 @@ const buildPlugin = (chainId: string, device: string): ChainPlugin => { device, module, createSigner: async () => mockSigner, + primitives, }; }; @@ -58,6 +68,7 @@ const setupParamsBase = { describe('setup chainPlugins', () => { afterEach(() => { + resetPrimitiveRegistry(); unregisterChain('unit-test-chain', 'unit-test-device'); DEFAULT_CHAIN_PLUGIN_KEYS.forEach((key) => { const [chainId, device] = key.split(':'); @@ -120,4 +131,20 @@ describe('setup chainPlugins', () => { }), ).rejects.toThrow('Invalid chain plugin in setup().chainPlugins'); }); + + test('registerChainPlugin works without setup and lazily seeds builtins', () => { + const customPlugin = buildPlugin('unit-test-chain', 'unit-test-device', { + definitions: [{ kind: 'encoding', name: 'TESTCHAIN', code: 99 }], + requirements: [ + { kind: 'encoding', name: 'TESTCHAIN', minFirmware: [0, 14, 0] }, + ], + }); + + registerChainPlugin(customPlugin); + + const registry = getPrimitiveRegistry(); + expect(registry.resolve('encoding', 'TESTCHAIN')).toBe(99); + expect(registry.resolve('encoding', 'EVM')).toBeDefined(); + expect(getChain('unit-test-chain', 'unit-test-device')).toBeDefined(); + }); }); diff --git a/packages/sdk/src/api/setup.ts b/packages/sdk/src/api/setup.ts index c0e3b80e..8d9ac4ae 100644 --- a/packages/sdk/src/api/setup.ts +++ b/packages/sdk/src/api/setup.ts @@ -7,6 +7,7 @@ import { import { configureChainRuntime, discoverAndRegisterChains, + ensurePrimitivesSeeded, getChain, registerChainPlugin, unregisterChain, @@ -99,6 +100,7 @@ export const setup = async (params: SetupParameters): Promise => { if (!params.setStoredClient) throw new Error('Client data setter required'); setSaveClient(buildSaveClientFn(params.setStoredClient)); + ensurePrimitivesSeeded(); configureChainRuntime({ autoRegisterChains: params.autoRegisterChains ?? true, defaultDevice: params.defaultDevice ?? 'lattice', diff --git a/packages/sdk/src/chains/context.ts b/packages/sdk/src/chains/context.ts index 4e5e4907..1b938d3d 100644 --- a/packages/sdk/src/chains/context.ts +++ b/packages/sdk/src/chains/context.ts @@ -1,8 +1,9 @@ -import type { DeviceContext } from '@gridplus/chain-core'; +import type { DeviceContext, PrimitiveKind } from '@gridplus/chain-core'; import { CURRENCIES } from '@gridplus/types'; import { getClient, queue } from '../api/utilities'; import { EXTERNAL } from '../constants'; import { fetchDecoder } from '../functions/fetchDecoder'; +import { ensurePrimitivesSeeded, getPrimitiveRegistry } from './primitives'; export type SdkDeviceContext = DeviceContext & { constants: { @@ -12,6 +13,7 @@ export type SdkDeviceContext = DeviceContext & { services: { fetchDecoder: typeof fetchDecoder; }; + resolvePrimitive: (kind: PrimitiveKind, name: string) => number; }; // Bridges SDK runtime primitives into the generic chain-core DeviceContext shape. @@ -25,4 +27,8 @@ export const createDeviceContext = (): SdkDeviceContext => ({ services: { fetchDecoder, }, + resolvePrimitive: (kind, name) => { + ensurePrimitivesSeeded(); + return getPrimitiveRegistry().resolveOrThrow(kind, name); + }, }); diff --git a/packages/sdk/src/chains/index.ts b/packages/sdk/src/chains/index.ts index 117058c1..6430be5d 100644 --- a/packages/sdk/src/chains/index.ts +++ b/packages/sdk/src/chains/index.ts @@ -9,6 +9,13 @@ export { unregisterChain, useChain, } from './registry'; +export { + ensurePrimitivesSeeded, + getPrimitiveRegistry, + preflightPluginPrimitives, + registerPluginPrimitives, + validatePluginPrimitiveRequirements, +} from './primitives'; export type { ChainKey, @@ -18,6 +25,10 @@ export type { ChainRegistryResolveOptions, DeviceContext, DeviceId, + PluginPrimitives, + PrimitiveDefinition, + PrimitiveKind, + PrimitiveRequirement, } from '@gridplus/chain-core'; export type { SdkDeviceContext } from './context'; diff --git a/packages/sdk/src/chains/primitives.ts b/packages/sdk/src/chains/primitives.ts new file mode 100644 index 00000000..136e5d72 --- /dev/null +++ b/packages/sdk/src/chains/primitives.ts @@ -0,0 +1,154 @@ +import { + createPrimitiveRegistry, + type ChainPlugin, + type PrimitiveDefinition, + type PrimitiveRequirement, +} from '@gridplus/chain-core'; +import { EXTERNAL } from '../constants'; + +type FirmwareVersionTuple = [number, number, number]; + +const registry = createPrimitiveRegistry(); +let seeded = false; + +const isFirmwareVersionTuple = ( + value: unknown, +): value is FirmwareVersionTuple => { + if (!Array.isArray(value) || value.length !== 3) return false; + return value.every( + (part) => + typeof part === 'number' && + Number.isInteger(part) && + Number.isFinite(part) && + part >= 0, + ); +}; + +const isPrimitiveRequirement = ( + requirement: unknown, +): requirement is PrimitiveRequirement => { + if (!requirement || typeof requirement !== 'object') return false; + const req = requirement as PrimitiveRequirement; + const kind = + req.kind === 'hash' || req.kind === 'curve' || req.kind === 'encoding'; + return ( + kind && + typeof req.name === 'string' && + req.name.trim().length > 0 && + isFirmwareVersionTuple(req.minFirmware) + ); +}; + +const compareFirmwareVersions = ( + current: FirmwareVersionTuple, + required: FirmwareVersionTuple, +): number => { + if (current[0] !== required[0]) return current[0] - required[0]; + if (current[1] !== required[1]) return current[1] - required[1]; + return current[2] - required[2]; +}; + +const getPluginPrimitiveDefinitions = ( + plugin: ChainPlugin, +): PrimitiveDefinition[] => { + const definitions = plugin.primitives?.definitions; + if (!definitions || !Array.isArray(definitions)) return []; + return definitions; +}; + +const getPluginPrimitiveRequirements = ( + plugin: ChainPlugin, +): PrimitiveRequirement[] => { + const requirements = plugin.primitives?.requirements; + if (!requirements || !Array.isArray(requirements)) return []; + return requirements as PrimitiveRequirement[]; +}; + +const validatePluginRequirementShape = (plugin: ChainPlugin): void => { + const requirements = plugin.primitives?.requirements; + if (!requirements) return; + if (!Array.isArray(requirements)) { + throw new Error( + `Chain "${plugin.chainId}" has invalid primitive requirements: expected an array.`, + ); + } + requirements.forEach((requirement, index) => { + if (!isPrimitiveRequirement(requirement)) { + throw new Error( + `Chain "${plugin.chainId}" has invalid primitive requirement at index ${index}.`, + ); + } + }); +}; + +export function getPrimitiveRegistry() { + return registry; +} + +export function ensurePrimitivesSeeded(): void { + if (seeded) return; + const definitions: PrimitiveDefinition[] = []; + + Object.entries(EXTERNAL.SIGNING.HASHES).forEach(([name, code]) => { + definitions.push({ kind: 'hash', name, code }); + }); + Object.entries(EXTERNAL.SIGNING.CURVES).forEach(([name, code]) => { + definitions.push({ kind: 'curve', name, code }); + }); + Object.entries(EXTERNAL.SIGNING.ENCODINGS).forEach(([name, code]) => { + definitions.push({ kind: 'encoding', name, code }); + }); + + registry.register(definitions); + seeded = true; +} + +export function preflightPluginPrimitives(plugin: ChainPlugin): void { + validatePluginRequirementShape(plugin); + ensurePrimitivesSeeded(); + + const definitions = getPluginPrimitiveDefinitions(plugin); + if (definitions.length === 0) return; + registry.preflight(definitions); +} + +export function registerPluginPrimitives(plugin: ChainPlugin): void { + validatePluginRequirementShape(plugin); + ensurePrimitivesSeeded(); + + const definitions = getPluginPrimitiveDefinitions(plugin); + if (definitions.length === 0) return; + registry.register(definitions); +} + +export function validatePluginPrimitiveRequirements( + plugin: ChainPlugin, + fwVersion: FirmwareVersionTuple, +): void { + validatePluginRequirementShape(plugin); + ensurePrimitivesSeeded(); + + const requirements = getPluginPrimitiveRequirements(plugin); + if (requirements.length === 0) return; + + requirements.forEach((requirement) => { + if (!registry.has(requirement.kind, requirement.name)) { + throw new Error( + `Chain "${plugin.chainId}" requires ${requirement.kind}:${requirement.name}, but it is not registered.`, + ); + } + + if (compareFirmwareVersions(fwVersion, requirement.minFirmware) < 0) { + throw new Error( + `Chain "${plugin.chainId}" requires ${requirement.kind}:${requirement.name} (min firmware ${requirement.minFirmware.join( + '.', + )}), but device is running ${fwVersion.join('.')}. Please update firmware.`, + ); + } + }); +} + +export function resetPrimitiveRegistry(): void { + registry.reset(); + seeded = false; +} diff --git a/packages/sdk/src/chains/registry.ts b/packages/sdk/src/chains/registry.ts index 61951fcb..dac258ba 100644 --- a/packages/sdk/src/chains/registry.ts +++ b/packages/sdk/src/chains/registry.ts @@ -5,6 +5,12 @@ import { } from '@gridplus/chain-core'; import { createDeviceContext, type SdkDeviceContext } from './context'; import { discoverAndRegisterChains as discoverChains } from './discovery'; +import { + ensurePrimitivesSeeded, + preflightPluginPrimitives, + registerPluginPrimitives, + validatePluginPrimitiveRequirements, +} from './primitives'; type ConfigureChainRuntimeOptions = { autoRegisterChains?: boolean; @@ -53,9 +59,38 @@ const stringifyAdapterOptions = (options: unknown): string => { } }; +type FirmwareVersionTuple = [number, number, number]; + +type FirmwareVersionSource = { + getFwVersion?: () => { + major?: unknown; + minor?: unknown; + fix?: unknown; + }; +}; + +const normalizeFirmwarePart = (value: unknown): number => { + if (typeof value !== 'number' || !Number.isFinite(value)) return 0; + return Math.max(0, Math.trunc(value)); +}; + +const getFirmwareVersion = (client: unknown): FirmwareVersionTuple => { + const maybeClient = client as FirmwareVersionSource; + if (typeof maybeClient?.getFwVersion !== 'function') { + return [0, 0, 0]; + } + + const fw = maybeClient.getFwVersion(); + return [ + normalizeFirmwarePart(fw?.major), + normalizeFirmwarePart(fw?.minor), + normalizeFirmwarePart(fw?.fix), + ]; +}; + const registerDiscoveredPlugin = (plugin: ChainPlugin): boolean => { if (registry.has(plugin.chainId, plugin.device)) return false; - registry.register(plugin as ChainPlugin); + registerChainPlugin(plugin); return true; }; @@ -88,7 +123,20 @@ export function getDefaultDevice(): DeviceId { } export function registerChainPlugin(plugin: ChainPlugin): void { - registry.register(plugin as ChainPlugin); + ensurePrimitivesSeeded(); + preflightPluginPrimitives(plugin); + + let chainRegistered = false; + try { + registry.register(plugin as ChainPlugin); + chainRegistered = true; + registerPluginPrimitives(plugin); + } catch (err) { + if (chainRegistered) { + registry.unregister(plugin.chainId, plugin.device); + } + throw err; + } } export function unregisterChain(chainId: string, device?: DeviceId): boolean { @@ -151,6 +199,14 @@ export async function useChain( // Signer is created once per (chain, device) generation and reused by adapters. if (!cached || cached.generation !== cacheGeneration) { const context = createDeviceContext(); + if (resolved.primitives?.requirements?.length) { + const client = await context.getClient(); + const fwVersion = getFirmwareVersion(client); + validatePluginPrimitiveRequirements( + resolved as ChainPlugin, + fwVersion, + ); + } const signer = await resolved.createSigner(context); cached = { generation: cacheGeneration, diff --git a/packages/types/src/firmware.ts b/packages/types/src/firmware.ts index 527211da..ba509e1c 100644 --- a/packages/types/src/firmware.ts +++ b/packages/types/src/firmware.ts @@ -26,11 +26,13 @@ export interface GenericSigningData { KECCAK256: typeof LatticeSignHash.keccak256; SHA256: typeof LatticeSignHash.sha256; SHA512HALF?: typeof LatticeSignHash.sha512half; + [key: string]: number | undefined; }; curveTypes: { SECP256K1: typeof LatticeSignCurve.secp256k1; ED25519: typeof LatticeSignCurve.ed25519; BLS12_381_G2: typeof LatticeSignCurve.bls12_381; + [key: string]: number | undefined; }; encodingTypes: { NONE: typeof LatticeSignEncoding.none; @@ -38,6 +40,7 @@ export interface GenericSigningData { COSMOS?: typeof LatticeSignEncoding.cosmos; EVM?: typeof LatticeSignEncoding.evm; XRP?: typeof LatticeSignEncoding.xrp; + [key: string]: number | undefined; }; } From deb4cb92309abf8d0ae9c30f2f9872526ce0b4b3 Mon Sep 17 00:00:00 2001 From: baha Date: Wed, 25 Feb 2026 16:49:09 +0300 Subject: [PATCH 2/6] fix: lint --- packages/chains/cosmos/src/devices/lattice.ts | 13 ++++--------- packages/chains/solana/src/devices/lattice.ts | 13 ++++--------- packages/chains/xrp/src/devices/lattice.ts | 4 +--- .../__test__/unit/chainRuntimePrimitives.test.ts | 4 +++- packages/sdk/src/__test__/unit/context.test.ts | 6 +++--- packages/sdk/src/__test__/unit/primitives.test.ts | 6 +++++- 6 files changed, 20 insertions(+), 26 deletions(-) diff --git a/packages/chains/cosmos/src/devices/lattice.ts b/packages/chains/cosmos/src/devices/lattice.ts index 2407bbd8..6aa90533 100644 --- a/packages/chains/cosmos/src/devices/lattice.ts +++ b/packages/chains/cosmos/src/devices/lattice.ts @@ -27,9 +27,7 @@ type LatticeCosmosContext = DeviceContext & { }; }; -function getLatticeCosmosContext( - context: DeviceContext, -): LatticeCosmosContext { +function getLatticeCosmosContext(context: DeviceContext): LatticeCosmosContext { const typed = context as LatticeCosmosContext; const constants = typed.constants; const hasNumber = (value: unknown): value is number => @@ -37,9 +35,7 @@ function getLatticeCosmosContext( if (typeof typed.resolvePrimitive !== 'function') { throw new Error('Lattice Cosmos signer requires primitive resolver'); } - if ( - !hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.SECP256K1_PUB) - ) { + if (!hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.SECP256K1_PUB)) { throw new Error('Lattice Cosmos signer requires EXTERNAL constants'); } return typed; @@ -48,9 +44,8 @@ function getLatticeCosmosContext( export function createLatticeCosmosSigner( context: DeviceContext, ): CosmosSigner { - const { queue, resolvePrimitive, constants } = getLatticeCosmosContext( - context, - ); + const { queue, resolvePrimitive, constants } = + getLatticeCosmosContext(context); const { EXTERNAL } = constants; const curveSecp256k1 = resolvePrimitive('curve', 'SECP256K1'); const hashSha256 = resolvePrimitive('hash', 'SHA256'); diff --git a/packages/chains/solana/src/devices/lattice.ts b/packages/chains/solana/src/devices/lattice.ts index c2682ddd..6cd720d5 100644 --- a/packages/chains/solana/src/devices/lattice.ts +++ b/packages/chains/solana/src/devices/lattice.ts @@ -28,9 +28,7 @@ type LatticeSolanaContext = DeviceContext & { }; }; -function getLatticeSolanaContext( - context: DeviceContext, -): LatticeSolanaContext { +function getLatticeSolanaContext(context: DeviceContext): LatticeSolanaContext { const typed = context as LatticeSolanaContext; const constants = typed.constants; const hasNumber = (value: unknown): value is number => @@ -38,9 +36,7 @@ function getLatticeSolanaContext( if (typeof typed.resolvePrimitive !== 'function') { throw new Error('Lattice Solana signer requires primitive resolver'); } - if ( - !hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.ED25519_PUB) - ) { + if (!hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.ED25519_PUB)) { throw new Error('Lattice Solana signer requires EXTERNAL constants'); } return typed; @@ -49,9 +45,8 @@ function getLatticeSolanaContext( export function createLatticeSolanaSigner( context: DeviceContext, ): SolanaSigner { - const { queue, resolvePrimitive, constants } = getLatticeSolanaContext( - context, - ); + const { queue, resolvePrimitive, constants } = + getLatticeSolanaContext(context); const { EXTERNAL } = constants; const curveEd25519 = resolvePrimitive('curve', 'ED25519'); const hashNone = resolvePrimitive('hash', 'NONE'); diff --git a/packages/chains/xrp/src/devices/lattice.ts b/packages/chains/xrp/src/devices/lattice.ts index 2f5d8a13..2ba15fda 100644 --- a/packages/chains/xrp/src/devices/lattice.ts +++ b/packages/chains/xrp/src/devices/lattice.ts @@ -32,9 +32,7 @@ type LatticeXrpContext = DeviceContext & { }; }; -function getLatticeXrpContext( - context: DeviceContext, -): LatticeXrpContext { +function getLatticeXrpContext(context: DeviceContext): LatticeXrpContext { const typed = context as LatticeXrpContext; const constants = typed.constants; const hasNumber = (value: unknown): value is number => diff --git a/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts b/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts index 63206306..7b34d3aa 100644 --- a/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts +++ b/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts @@ -137,7 +137,9 @@ describe('chain runtime primitive integration', () => { }) as any, ); - await expect(useChain('req-chain')).rejects.toThrow('Please update firmware'); + await expect(useChain('req-chain')).rejects.toThrow( + 'Please update firmware', + ); }); test('useChain succeeds when firmware satisfies requirements', async () => { diff --git a/packages/sdk/src/__test__/unit/context.test.ts b/packages/sdk/src/__test__/unit/context.test.ts index 79dd2848..f028f635 100644 --- a/packages/sdk/src/__test__/unit/context.test.ts +++ b/packages/sdk/src/__test__/unit/context.test.ts @@ -15,8 +15,8 @@ describe('chain context primitive resolution', () => { test('resolvePrimitive throws for unknown primitive', () => { const context = createDeviceContext(); - expect(() => - context.resolvePrimitive('encoding', 'UNKNOWN_CHAIN'), - ).toThrow('Primitive not found'); + expect(() => context.resolvePrimitive('encoding', 'UNKNOWN_CHAIN')).toThrow( + 'Primitive not found', + ); }); }); diff --git a/packages/sdk/src/__test__/unit/primitives.test.ts b/packages/sdk/src/__test__/unit/primitives.test.ts index f606bce6..acd6bcc9 100644 --- a/packages/sdk/src/__test__/unit/primitives.test.ts +++ b/packages/sdk/src/__test__/unit/primitives.test.ts @@ -130,7 +130,11 @@ describe('sdk primitives module', () => { test('validatePluginPrimitiveRequirements throws for missing primitive mapping', () => { const plugin = buildPlugin('custom-chain', { requirements: [ - { kind: 'encoding', name: 'UNREGISTERED_CHAIN', minFirmware: [0, 20, 0] }, + { + kind: 'encoding', + name: 'UNREGISTERED_CHAIN', + minFirmware: [0, 20, 0], + }, ], }); From d308cf86e28441fb6f576dd6ab0bb4a3ce470d76 Mon Sep 17 00:00:00 2001 From: baha Date: Wed, 25 Feb 2026 23:51:51 +0300 Subject: [PATCH 3/6] chore: apply final lint and wiring updates for primitive registry --- packages/chains/chain-core/src/index.ts | 44 ++++++++++++ packages/chains/evm/src/devices/lattice.ts | 78 ++++++++++------------ packages/sdk/src/chains/index.ts | 1 - packages/sdk/src/chains/primitives.ts | 13 +--- packages/sdk/src/chains/registry.ts | 30 +-------- 5 files changed, 81 insertions(+), 85 deletions(-) diff --git a/packages/chains/chain-core/src/index.ts b/packages/chains/chain-core/src/index.ts index b7066af9..77117248 100644 --- a/packages/chains/chain-core/src/index.ts +++ b/packages/chains/chain-core/src/index.ts @@ -274,6 +274,50 @@ export { type PrimitiveRegistry, } from './primitiveRegistry'; +// --------------------------------------------------------------------------- +// Firmware version utilities +// --------------------------------------------------------------------------- + +export type FirmwareVersionTuple = [number, number, number]; + +const normalizeFirmwarePart = (value: unknown): number => { + if (typeof value !== 'number' || !Number.isFinite(value)) return 0; + return Math.max(0, Math.trunc(value)); +}; + +export const getFirmwareVersion = (client: unknown): FirmwareVersionTuple => { + const maybeClient = client as { + getFwVersion?: () => { + major?: unknown; + minor?: unknown; + fix?: unknown; + }; + }; + if (typeof maybeClient?.getFwVersion !== 'function') { + return [0, 0, 0]; + } + const fw = maybeClient.getFwVersion(); + return [ + normalizeFirmwarePart(fw?.major), + normalizeFirmwarePart(fw?.minor), + normalizeFirmwarePart(fw?.fix), + ]; +}; + +export const compareFirmwareVersions = ( + current: FirmwareVersionTuple, + required: FirmwareVersionTuple, +): number => { + if (current[0] !== required[0]) return current[0] - required[0]; + if (current[1] !== required[1]) return current[1] - required[1]; + return current[2] - required[2]; +}; + +export const isAtLeastFirmware = ( + current: FirmwareVersionTuple, + minimum: FirmwareVersionTuple, +): boolean => compareFirmwareVersions(current, minimum) >= 0; + // --------------------------------------------------------------------------- // Shared chain utilities // --------------------------------------------------------------------------- diff --git a/packages/chains/evm/src/devices/lattice.ts b/packages/chains/evm/src/devices/lattice.ts index ad88eb57..cb6031f0 100644 --- a/packages/chains/evm/src/devices/lattice.ts +++ b/packages/chains/evm/src/devices/lattice.ts @@ -1,11 +1,14 @@ -import type { - Address, - ChainPlugin, - DerivationPath, - DeviceContext, - PrimitiveKind, - PublicKey, - SignResult, +import { + getFirmwareVersion, + isAtLeastFirmware, + type Address, + type ChainPlugin, + type DerivationPath, + type DeviceContext, + type FirmwareVersionTuple, + type PrimitiveKind, + type PublicKey, + type SignResult, } from '@gridplus/chain-core'; import { Hash } from 'ox'; import { @@ -90,54 +93,32 @@ function normalizeRawEvmTx(tx: EvmRawTransaction): Hex | Buffer { return Buffer.from(tx); } +type EvmEncodingCodes = { + evm: number; + eip7702Auth: number; + eip7702AuthList: number; +}; + function getEvmEncodingType( tx: TransactionSerializable, - resolvePrimitive: LatticeEvmContext['resolvePrimitive'], + encodings: EvmEncodingCodes, ): number { if ((tx as any).type === 'eip7702') { const eip7702 = tx as TransactionSerializableEIP7702; const hasAuthList = eip7702.authorizationList && eip7702.authorizationList.length > 0; - return hasAuthList - ? resolvePrimitive('encoding', 'EIP7702_AUTH_LIST') - : resolvePrimitive('encoding', 'EIP7702_AUTH'); + return hasAuthList ? encodings.eip7702AuthList : encodings.eip7702Auth; } - return resolvePrimitive('encoding', 'EVM'); + return encodings.evm; } -const EIP7702_MIN_FIRMWARE: [number, number, number] = [0, 18, 0]; - -const getFirmwareVersion = (client: unknown): [number, number, number] => { - const maybeClient = client as { - getFwVersion?: () => { major?: unknown; minor?: unknown; fix?: unknown }; - }; - if (typeof maybeClient?.getFwVersion !== 'function') return [0, 0, 0]; - const fw = maybeClient.getFwVersion(); - const normalize = (value: unknown): number => - typeof value === 'number' && Number.isFinite(value) - ? Math.max(0, Math.trunc(value)) - : 0; - return [normalize(fw?.major), normalize(fw?.minor), normalize(fw?.fix)]; -}; - -const isAtLeastFirmware = ( - current: [number, number, number], - minimum: [number, number, number], -): boolean => { - if (current[0] !== minimum[0]) return current[0] > minimum[0]; - if (current[1] !== minimum[1]) return current[1] > minimum[1]; - return current[2] >= minimum[2]; -}; +const EIP7702_MIN_FIRMWARE: FirmwareVersionTuple = [0, 18, 0]; const assertEip7702FirmwareSupport = async ( context: DeviceContext, - resolvePrimitive: LatticeEvmContext['resolvePrimitive'], + eip7702Encodings: Set, encodingType: number, ): Promise => { - const eip7702Encodings = new Set([ - resolvePrimitive('encoding', 'EIP7702_AUTH'), - resolvePrimitive('encoding', 'EIP7702_AUTH_LIST'), - ]); if (!eip7702Encodings.has(encodingType)) return; const client = await context.getClient(); const fwVersion = getFirmwareVersion(client); @@ -157,6 +138,15 @@ export function createLatticeEvmSigner( const { EXTERNAL, CURRENCIES } = latticeContext.constants; const curveSecp256k1 = resolvePrimitive('curve', 'SECP256K1'); const hashKeccak256 = resolvePrimitive('hash', 'KECCAK256'); + const encodings: EvmEncodingCodes = { + evm: resolvePrimitive('encoding', 'EVM'), + eip7702Auth: resolvePrimitive('encoding', 'EIP7702_AUTH'), + eip7702AuthList: resolvePrimitive('encoding', 'EIP7702_AUTH_LIST'), + }; + const eip7702Encodings = new Set([ + encodings.eip7702Auth, + encodings.eip7702AuthList, + ]); return { getAddress: async (path: DerivationPath): Promise
=> { @@ -204,14 +194,14 @@ export function createLatticeEvmSigner( : serializeTransaction(request.payload as TransactionSerializable); const encodingType = isRaw - ? resolvePrimitive('encoding', 'EVM') + ? encodings.evm : getEvmEncodingType( request.payload as TransactionSerializable, - resolvePrimitive, + encodings, ); await assertEip7702FirmwareSupport( latticeContext, - resolvePrimitive, + eip7702Encodings, encodingType, ); diff --git a/packages/sdk/src/chains/index.ts b/packages/sdk/src/chains/index.ts index 6430be5d..f14dc574 100644 --- a/packages/sdk/src/chains/index.ts +++ b/packages/sdk/src/chains/index.ts @@ -11,7 +11,6 @@ export { } from './registry'; export { ensurePrimitivesSeeded, - getPrimitiveRegistry, preflightPluginPrimitives, registerPluginPrimitives, validatePluginPrimitiveRequirements, diff --git a/packages/sdk/src/chains/primitives.ts b/packages/sdk/src/chains/primitives.ts index 136e5d72..16b4ee40 100644 --- a/packages/sdk/src/chains/primitives.ts +++ b/packages/sdk/src/chains/primitives.ts @@ -1,13 +1,13 @@ import { + compareFirmwareVersions, createPrimitiveRegistry, type ChainPlugin, + type FirmwareVersionTuple, type PrimitiveDefinition, type PrimitiveRequirement, } from '@gridplus/chain-core'; import { EXTERNAL } from '../constants'; -type FirmwareVersionTuple = [number, number, number]; - const registry = createPrimitiveRegistry(); let seeded = false; @@ -39,15 +39,6 @@ const isPrimitiveRequirement = ( ); }; -const compareFirmwareVersions = ( - current: FirmwareVersionTuple, - required: FirmwareVersionTuple, -): number => { - if (current[0] !== required[0]) return current[0] - required[0]; - if (current[1] !== required[1]) return current[1] - required[1]; - return current[2] - required[2]; -}; - const getPluginPrimitiveDefinitions = ( plugin: ChainPlugin, ): PrimitiveDefinition[] => { diff --git a/packages/sdk/src/chains/registry.ts b/packages/sdk/src/chains/registry.ts index dac258ba..b6095f11 100644 --- a/packages/sdk/src/chains/registry.ts +++ b/packages/sdk/src/chains/registry.ts @@ -1,5 +1,6 @@ import { createChainRegistry, + getFirmwareVersion, type ChainPlugin, type DeviceId, } from '@gridplus/chain-core'; @@ -59,35 +60,6 @@ const stringifyAdapterOptions = (options: unknown): string => { } }; -type FirmwareVersionTuple = [number, number, number]; - -type FirmwareVersionSource = { - getFwVersion?: () => { - major?: unknown; - minor?: unknown; - fix?: unknown; - }; -}; - -const normalizeFirmwarePart = (value: unknown): number => { - if (typeof value !== 'number' || !Number.isFinite(value)) return 0; - return Math.max(0, Math.trunc(value)); -}; - -const getFirmwareVersion = (client: unknown): FirmwareVersionTuple => { - const maybeClient = client as FirmwareVersionSource; - if (typeof maybeClient?.getFwVersion !== 'function') { - return [0, 0, 0]; - } - - const fw = maybeClient.getFwVersion(); - return [ - normalizeFirmwarePart(fw?.major), - normalizeFirmwarePart(fw?.minor), - normalizeFirmwarePart(fw?.fix), - ]; -}; - const registerDiscoveredPlugin = (plugin: ChainPlugin): boolean => { if (registry.has(plugin.chainId, plugin.device)) return false; registerChainPlugin(plugin); From b06f89de6100eed959238f25044298d353cf2f2d Mon Sep 17 00:00:00 2001 From: baha Date: Thu, 26 Feb 2026 00:35:18 +0300 Subject: [PATCH 4/6] fix(sdk): preserve DeviceContext compatibility and clean up plugin primitives on unregister Add lattice signer fallback to EXTERNAL.SIGNING when resolvePrimitive is absent, and remove plugin-owned primitive definitions during unregisterChain to prevent stale conflicts. --- packages/chains/cosmos/src/devices/lattice.ts | 59 +++++++++-- packages/chains/evm/src/devices/lattice.ts | 99 ++++++++++++------- packages/chains/solana/src/devices/lattice.ts | 59 +++++++++-- packages/chains/xrp/src/devices/lattice.ts | 59 +++++++++-- .../unit/chainRuntimePrimitives.test.ts | 21 ++++ .../unit/latticeSignerPrimitives.test.ts | 52 +++++++++- packages/sdk/src/chains/primitives.ts | 84 +++++++++++++--- packages/sdk/src/chains/registry.ts | 8 +- 8 files changed, 359 insertions(+), 82 deletions(-) diff --git a/packages/chains/cosmos/src/devices/lattice.ts b/packages/chains/cosmos/src/devices/lattice.ts index 6aa90533..d2bc9f61 100644 --- a/packages/chains/cosmos/src/devices/lattice.ts +++ b/packages/chains/cosmos/src/devices/lattice.ts @@ -16,29 +16,70 @@ import { } from '../chain'; import { buildSigResultFromRsv, compressSecp256k1Pubkey } from './shared'; -type LatticeCosmosContext = DeviceContext & { - resolvePrimitive: (kind: PrimitiveKind, name: string) => number; +type PrimitiveCodeMaps = { + HASHES?: Record; + CURVES?: Record; + ENCODINGS?: Record; +}; + +type LatticeCosmosContextInput = DeviceContext & { + resolvePrimitive?: (kind: PrimitiveKind, name: string) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { SECP256K1_PUB: number; }; + SIGNING?: PrimitiveCodeMaps; }; }; }; +type LatticeCosmosContext = DeviceContext & { + resolvePrimitive: (kind: PrimitiveKind, name: string) => number; + constants: LatticeCosmosContextInput['constants']; +}; + +const hasNumber = (value: unknown): value is number => + typeof value === 'number' && Number.isFinite(value); + +const getPrimitiveFromConstants = ( + signing: PrimitiveCodeMaps | undefined, + kind: PrimitiveKind, + name: string, +): number | undefined => { + const byKind: Record | undefined> = { + hash: signing?.HASHES, + curve: signing?.CURVES, + encoding: signing?.ENCODINGS, + }; + const code = byKind[kind]?.[name]; + return hasNumber(code) ? code : undefined; +}; + function getLatticeCosmosContext(context: DeviceContext): LatticeCosmosContext { - const typed = context as LatticeCosmosContext; + const typed = context as LatticeCosmosContextInput; const constants = typed.constants; - const hasNumber = (value: unknown): value is number => - typeof value === 'number' && Number.isFinite(value); - if (typeof typed.resolvePrimitive !== 'function') { - throw new Error('Lattice Cosmos signer requires primitive resolver'); - } + const resolvePrimitive = (kind: PrimitiveKind, name: string): number => { + if (typeof typed.resolvePrimitive === 'function') { + return typed.resolvePrimitive(kind, name); + } + const fromConstants = getPrimitiveFromConstants( + constants?.EXTERNAL?.SIGNING, + kind, + name, + ); + if (fromConstants !== undefined) return fromConstants; + throw new Error( + `Lattice Cosmos signer requires resolvePrimitive() or EXTERNAL.SIGNING mapping for ${kind}:${name}.`, + ); + }; if (!hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.SECP256K1_PUB)) { throw new Error('Lattice Cosmos signer requires EXTERNAL constants'); } - return typed; + return { + ...typed, + resolvePrimitive, + }; } export function createLatticeCosmosSigner( diff --git a/packages/chains/evm/src/devices/lattice.ts b/packages/chains/evm/src/devices/lattice.ts index cb6031f0..860d1674 100644 --- a/packages/chains/evm/src/devices/lattice.ts +++ b/packages/chains/evm/src/devices/lattice.ts @@ -36,13 +36,20 @@ export type LatticeEvmSignerOptions = { fetchEvmDecoder?: boolean; }; -type LatticeEvmContext = DeviceContext & { - resolvePrimitive: (kind: PrimitiveKind, name: string) => number; +type PrimitiveCodeMaps = { + HASHES?: Record; + CURVES?: Record; + ENCODINGS?: Record; +}; + +type LatticeEvmContextInput = DeviceContext & { + resolvePrimitive?: (kind: PrimitiveKind, name: string) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { SECP256K1_PUB: number; }; + SIGNING?: PrimitiveCodeMaps; }; CURRENCIES: { ETH_MSG: string; @@ -57,14 +64,46 @@ type LatticeEvmContext = DeviceContext & { }; }; +type LatticeEvmContext = DeviceContext & { + resolvePrimitive: (kind: PrimitiveKind, name: string) => number; + constants: LatticeEvmContextInput['constants']; + services?: LatticeEvmContextInput['services']; +}; + +const hasNumber = (value: unknown): value is number => + typeof value === 'number' && Number.isFinite(value); + +const getPrimitiveFromConstants = ( + signing: PrimitiveCodeMaps | undefined, + kind: PrimitiveKind, + name: string, +): number | undefined => { + const byKind: Record | undefined> = { + hash: signing?.HASHES, + curve: signing?.CURVES, + encoding: signing?.ENCODINGS, + }; + const code = byKind[kind]?.[name]; + return hasNumber(code) ? code : undefined; +}; + function getLatticeEvmContext(context: DeviceContext): LatticeEvmContext { - const typed = context as LatticeEvmContext; + const typed = context as LatticeEvmContextInput; const constants = typed.constants; - const hasNumber = (value: unknown): value is number => - typeof value === 'number' && Number.isFinite(value); - if (typeof typed.resolvePrimitive !== 'function') { - throw new Error('Lattice EVM signer requires primitive resolver'); - } + const resolvePrimitive = (kind: PrimitiveKind, name: string): number => { + if (typeof typed.resolvePrimitive === 'function') { + return typed.resolvePrimitive(kind, name); + } + const fromConstants = getPrimitiveFromConstants( + constants?.EXTERNAL?.SIGNING, + kind, + name, + ); + if (fromConstants !== undefined) return fromConstants; + throw new Error( + `Lattice EVM signer requires resolvePrimitive() or EXTERNAL.SIGNING mapping for ${kind}:${name}.`, + ); + }; if ( !hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.SECP256K1_PUB) || !constants?.CURRENCIES?.ETH_MSG @@ -73,7 +112,10 @@ function getLatticeEvmContext(context: DeviceContext): LatticeEvmContext { 'Lattice EVM signer requires EXTERNAL and CURRENCIES constants', ); } - return typed; + return { + ...typed, + resolvePrimitive, + }; } function isRawEvmTx( @@ -93,33 +135,26 @@ function normalizeRawEvmTx(tx: EvmRawTransaction): Hex | Buffer { return Buffer.from(tx); } -type EvmEncodingCodes = { - evm: number; - eip7702Auth: number; - eip7702AuthList: number; -}; - function getEvmEncodingType( tx: TransactionSerializable, - encodings: EvmEncodingCodes, + resolvePrimitive: (kind: PrimitiveKind, name: string) => number, ): number { if ((tx as any).type === 'eip7702') { const eip7702 = tx as TransactionSerializableEIP7702; const hasAuthList = eip7702.authorizationList && eip7702.authorizationList.length > 0; - return hasAuthList ? encodings.eip7702AuthList : encodings.eip7702Auth; + return hasAuthList + ? resolvePrimitive('encoding', 'EIP7702_AUTH_LIST') + : resolvePrimitive('encoding', 'EIP7702_AUTH'); } - return encodings.evm; + return resolvePrimitive('encoding', 'EVM'); } const EIP7702_MIN_FIRMWARE: FirmwareVersionTuple = [0, 18, 0]; const assertEip7702FirmwareSupport = async ( context: DeviceContext, - eip7702Encodings: Set, - encodingType: number, ): Promise => { - if (!eip7702Encodings.has(encodingType)) return; const client = await context.getClient(); const fwVersion = getFirmwareVersion(client); if (!isAtLeastFirmware(fwVersion, EIP7702_MIN_FIRMWARE)) { @@ -138,15 +173,7 @@ export function createLatticeEvmSigner( const { EXTERNAL, CURRENCIES } = latticeContext.constants; const curveSecp256k1 = resolvePrimitive('curve', 'SECP256K1'); const hashKeccak256 = resolvePrimitive('hash', 'KECCAK256'); - const encodings: EvmEncodingCodes = { - evm: resolvePrimitive('encoding', 'EVM'), - eip7702Auth: resolvePrimitive('encoding', 'EIP7702_AUTH'), - eip7702AuthList: resolvePrimitive('encoding', 'EIP7702_AUTH_LIST'), - }; - const eip7702Encodings = new Set([ - encodings.eip7702Auth, - encodings.eip7702AuthList, - ]); + const encodingEvm = resolvePrimitive('encoding', 'EVM'); return { getAddress: async (path: DerivationPath): Promise
=> { @@ -194,16 +221,14 @@ export function createLatticeEvmSigner( : serializeTransaction(request.payload as TransactionSerializable); const encodingType = isRaw - ? encodings.evm + ? encodingEvm : getEvmEncodingType( request.payload as TransactionSerializable, - encodings, + resolvePrimitive, ); - await assertEip7702FirmwareSupport( - latticeContext, - eip7702Encodings, - encodingType, - ); + if (!isRaw && (request.payload as any).type === 'eip7702') { + await assertEip7702FirmwareSupport(latticeContext); + } let decoder: Buffer | undefined; const fetchDecoder = services?.fetchDecoder; diff --git a/packages/chains/solana/src/devices/lattice.ts b/packages/chains/solana/src/devices/lattice.ts index 6cd720d5..7917ca40 100644 --- a/packages/chains/solana/src/devices/lattice.ts +++ b/packages/chains/solana/src/devices/lattice.ts @@ -17,29 +17,70 @@ import { } from '../chain'; import { buildSigResultFromRsv, toBuffer } from './shared'; -type LatticeSolanaContext = DeviceContext & { - resolvePrimitive: (kind: PrimitiveKind, name: string) => number; +type PrimitiveCodeMaps = { + HASHES?: Record; + CURVES?: Record; + ENCODINGS?: Record; +}; + +type LatticeSolanaContextInput = DeviceContext & { + resolvePrimitive?: (kind: PrimitiveKind, name: string) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { ED25519_PUB: number; }; + SIGNING?: PrimitiveCodeMaps; }; }; }; +type LatticeSolanaContext = DeviceContext & { + resolvePrimitive: (kind: PrimitiveKind, name: string) => number; + constants: LatticeSolanaContextInput['constants']; +}; + +const hasNumber = (value: unknown): value is number => + typeof value === 'number' && Number.isFinite(value); + +const getPrimitiveFromConstants = ( + signing: PrimitiveCodeMaps | undefined, + kind: PrimitiveKind, + name: string, +): number | undefined => { + const byKind: Record | undefined> = { + hash: signing?.HASHES, + curve: signing?.CURVES, + encoding: signing?.ENCODINGS, + }; + const code = byKind[kind]?.[name]; + return hasNumber(code) ? code : undefined; +}; + function getLatticeSolanaContext(context: DeviceContext): LatticeSolanaContext { - const typed = context as LatticeSolanaContext; + const typed = context as LatticeSolanaContextInput; const constants = typed.constants; - const hasNumber = (value: unknown): value is number => - typeof value === 'number' && Number.isFinite(value); - if (typeof typed.resolvePrimitive !== 'function') { - throw new Error('Lattice Solana signer requires primitive resolver'); - } + const resolvePrimitive = (kind: PrimitiveKind, name: string): number => { + if (typeof typed.resolvePrimitive === 'function') { + return typed.resolvePrimitive(kind, name); + } + const fromConstants = getPrimitiveFromConstants( + constants?.EXTERNAL?.SIGNING, + kind, + name, + ); + if (fromConstants !== undefined) return fromConstants; + throw new Error( + `Lattice Solana signer requires resolvePrimitive() or EXTERNAL.SIGNING mapping for ${kind}:${name}.`, + ); + }; if (!hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.ED25519_PUB)) { throw new Error('Lattice Solana signer requires EXTERNAL constants'); } - return typed; + return { + ...typed, + resolvePrimitive, + }; } export function createLatticeSolanaSigner( diff --git a/packages/chains/xrp/src/devices/lattice.ts b/packages/chains/xrp/src/devices/lattice.ts index 2ba15fda..196e7760 100644 --- a/packages/chains/xrp/src/devices/lattice.ts +++ b/packages/chains/xrp/src/devices/lattice.ts @@ -21,31 +21,72 @@ import { toBuffer, } from './shared'; -type LatticeXrpContext = DeviceContext & { - resolvePrimitive: (kind: PrimitiveKind, name: string) => number; +type PrimitiveCodeMaps = { + HASHES?: Record; + CURVES?: Record; + ENCODINGS?: Record; +}; + +type LatticeXrpContextInput = DeviceContext & { + resolvePrimitive?: (kind: PrimitiveKind, name: string) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { SECP256K1_PUB: number; }; + SIGNING?: PrimitiveCodeMaps; }; }; }; +type LatticeXrpContext = DeviceContext & { + resolvePrimitive: (kind: PrimitiveKind, name: string) => number; + constants: LatticeXrpContextInput['constants']; +}; + +const hasNumber = (value: unknown): value is number => + typeof value === 'number' && Number.isFinite(value); + +const getPrimitiveFromConstants = ( + signing: PrimitiveCodeMaps | undefined, + kind: PrimitiveKind, + name: string, +): number | undefined => { + const byKind: Record | undefined> = { + hash: signing?.HASHES, + curve: signing?.CURVES, + encoding: signing?.ENCODINGS, + }; + const code = byKind[kind]?.[name]; + return hasNumber(code) ? code : undefined; +}; + function getLatticeXrpContext(context: DeviceContext): LatticeXrpContext { - const typed = context as LatticeXrpContext; + const typed = context as LatticeXrpContextInput; const constants = typed.constants; - const hasNumber = (value: unknown): value is number => - typeof value === 'number' && Number.isFinite(value); - if (typeof typed.resolvePrimitive !== 'function') { - throw new Error('Lattice XRP signer requires primitive resolver'); - } + const resolvePrimitive = (kind: PrimitiveKind, name: string): number => { + if (typeof typed.resolvePrimitive === 'function') { + return typed.resolvePrimitive(kind, name); + } + const fromConstants = getPrimitiveFromConstants( + constants?.EXTERNAL?.SIGNING, + kind, + name, + ); + if (fromConstants !== undefined) return fromConstants; + throw new Error( + `Lattice XRP signer requires resolvePrimitive() or EXTERNAL.SIGNING mapping for ${kind}:${name}.`, + ); + }; if (!hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.SECP256K1_PUB)) { throw new Error('Lattice XRP signer requires EXTERNAL constants'); } - return typed; + return { + ...typed, + resolvePrimitive, + }; } export function createLatticeXrpSigner(context: DeviceContext): XrpSigner { diff --git a/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts b/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts index 7b34d3aa..271a5954 100644 --- a/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts +++ b/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts @@ -115,6 +115,27 @@ describe('chain runtime primitive integration', () => { expect(primitiveRegistry.resolve('encoding', 'DUP_B')).toBeUndefined(); }); + test('unregisterChain removes plugin-owned primitive definitions', () => { + registerChainPlugin( + buildPlugin('chain-a', { + definitions: [{ kind: 'encoding', name: 'REPLACE_ME', code: 301 }], + }), + ); + + const primitiveRegistry = getPrimitiveRegistry(); + expect(primitiveRegistry.resolve('encoding', 'REPLACE_ME')).toBe(301); + + expect(unregisterChain('chain-a', 'lattice')).toBe(true); + expect(primitiveRegistry.resolve('encoding', 'REPLACE_ME')).toBeUndefined(); + + registerChainPlugin( + buildPlugin('chain-a', { + definitions: [{ kind: 'encoding', name: 'REPLACE_ME', code: 302 }], + }), + ); + expect(primitiveRegistry.resolve('encoding', 'REPLACE_ME')).toBe(302); + }); + test('useChain enforces primitive minFirmware requirements', async () => { configureChainRuntime({ autoRegisterChains: false, diff --git a/packages/sdk/src/__test__/unit/latticeSignerPrimitives.test.ts b/packages/sdk/src/__test__/unit/latticeSignerPrimitives.test.ts index 4bf5f8a6..4c3f96e8 100644 --- a/packages/sdk/src/__test__/unit/latticeSignerPrimitives.test.ts +++ b/packages/sdk/src/__test__/unit/latticeSignerPrimitives.test.ts @@ -9,13 +9,33 @@ type PrimitiveMap = Record; type MockContextOptions = { primitives: PrimitiveMap; firmware?: [number, number, number]; + includeResolver?: boolean; }; const primitiveKey = (kind: PrimitiveKind, name: string): string => `${kind}:${name}`; +const buildSigningConstants = (primitives: PrimitiveMap) => { + const signing = { + HASHES: {} as Record, + CURVES: {} as Record, + ENCODINGS: {} as Record, + }; + + Object.entries(primitives).forEach(([key, code]) => { + const [kind, name] = key.split(':'); + if (!name) return; + if (kind === 'hash') signing.HASHES[name] = code; + if (kind === 'curve') signing.CURVES[name] = code; + if (kind === 'encoding') signing.ENCODINGS[name] = code; + }); + + return signing; +}; + const buildMockContext = (options: MockContextOptions) => { const firmware = options.firmware ?? [1, 0, 0]; + const includeResolver = options.includeResolver ?? true; const signCalls: Array = []; const client = { @@ -40,23 +60,26 @@ const buildMockContext = (options: MockContextOptions) => { return code; }); - const context = { + const context: any = { queue: async (fn: (client: unknown) => Promise) => fn(client), getClient: async () => client, - resolvePrimitive, constants: { EXTERNAL: { GET_ADDR_FLAGS: { SECP256K1_PUB: 1, ED25519_PUB: 2, }, + SIGNING: buildSigningConstants(options.primitives), }, CURRENCIES: { ETH_MSG: 'ETH_MSG', }, }, services: {}, - } as any; + }; + if (includeResolver) { + context.resolvePrimitive = resolvePrimitive; + } return { context, @@ -182,6 +205,29 @@ describe('lattice signer primitive resolution', () => { expect(signCalls[0].data.encodingType).toBe(33); }); + test('evm signer falls back to EXTERNAL.SIGNING when resolvePrimitive is missing', async () => { + const { context, signCalls } = buildMockContext({ + primitives: { + 'curve:SECP256K1': 61, + 'hash:KECCAK256': 62, + 'encoding:EVM': 63, + }, + includeResolver: false, + }); + const signer = createLatticeEvmSigner(context); + + await signer.sign({ + kind: 'transaction', + payload: '0x01', + options: { path: [44, 60, 0] }, + }); + + expect(signCalls).toHaveLength(1); + expect(signCalls[0].data.curveType).toBe(61); + expect(signCalls[0].data.hashType).toBe(62); + expect(signCalls[0].data.encodingType).toBe(63); + }); + test('evm EIP-7702 signing fails below minimum firmware', async () => { const { context, client } = buildMockContext({ primitives: { diff --git a/packages/sdk/src/chains/primitives.ts b/packages/sdk/src/chains/primitives.ts index 16b4ee40..07e5b6dd 100644 --- a/packages/sdk/src/chains/primitives.ts +++ b/packages/sdk/src/chains/primitives.ts @@ -1,7 +1,9 @@ import { compareFirmwareVersions, createPrimitiveRegistry, + toChainKey, type ChainPlugin, + type DeviceId, type FirmwareVersionTuple, type PrimitiveDefinition, type PrimitiveRequirement, @@ -10,6 +12,48 @@ import { EXTERNAL } from '../constants'; const registry = createPrimitiveRegistry(); let seeded = false; +const pluginDefinitionsByKey = new Map(); + +const getBuiltinPrimitiveDefinitions = (): PrimitiveDefinition[] => { + const definitions: PrimitiveDefinition[] = []; + + Object.entries(EXTERNAL.SIGNING.HASHES).forEach(([name, code]) => { + definitions.push({ kind: 'hash', name, code }); + }); + Object.entries(EXTERNAL.SIGNING.CURVES).forEach(([name, code]) => { + definitions.push({ kind: 'curve', name, code }); + }); + Object.entries(EXTERNAL.SIGNING.ENCODINGS).forEach(([name, code]) => { + definitions.push({ kind: 'encoding', name, code }); + }); + + return definitions; +}; + +const cloneDefinitions = ( + definitions: PrimitiveDefinition[], +): PrimitiveDefinition[] => + definitions.map((definition) => ({ + kind: definition.kind, + name: definition.name, + code: definition.code, + })); + +const toPluginPrimitiveKey = (chainId: string, device: DeviceId): string => + toChainKey(chainId, device); + +const rebuildRegistryFromTrackedPrimitives = (): void => { + registry.reset(); + seeded = false; + ensurePrimitivesSeeded(); + + const sortedKeys = [...pluginDefinitionsByKey.keys()].sort(); + sortedKeys.forEach((key) => { + const definitions = pluginDefinitionsByKey.get(key); + if (!definitions || definitions.length === 0) return; + registry.register(definitions); + }); +}; const isFirmwareVersionTuple = ( value: unknown, @@ -78,19 +122,7 @@ export function getPrimitiveRegistry() { export function ensurePrimitivesSeeded(): void { if (seeded) return; - const definitions: PrimitiveDefinition[] = []; - - Object.entries(EXTERNAL.SIGNING.HASHES).forEach(([name, code]) => { - definitions.push({ kind: 'hash', name, code }); - }); - Object.entries(EXTERNAL.SIGNING.CURVES).forEach(([name, code]) => { - definitions.push({ kind: 'curve', name, code }); - }); - Object.entries(EXTERNAL.SIGNING.ENCODINGS).forEach(([name, code]) => { - definitions.push({ kind: 'encoding', name, code }); - }); - - registry.register(definitions); + registry.register(getBuiltinPrimitiveDefinitions()); seeded = true; } @@ -108,8 +140,31 @@ export function registerPluginPrimitives(plugin: ChainPlugin): void { ensurePrimitivesSeeded(); const definitions = getPluginPrimitiveDefinitions(plugin); - if (definitions.length === 0) return; + const pluginKey = toPluginPrimitiveKey(plugin.chainId, plugin.device); + if (definitions.length === 0) { + pluginDefinitionsByKey.delete(pluginKey); + return; + } registry.register(definitions); + pluginDefinitionsByKey.set(pluginKey, cloneDefinitions(definitions)); +} + +export function unregisterPluginPrimitives( + chainId: string, + device?: DeviceId, +): void { + if (!seeded) return; + + const keysToDelete = device + ? [toPluginPrimitiveKey(chainId, device)] + : [...pluginDefinitionsByKey.keys()].filter((key) => + key.startsWith(`${chainId}:`), + ); + + if (keysToDelete.length === 0) return; + + keysToDelete.forEach((key) => pluginDefinitionsByKey.delete(key)); + rebuildRegistryFromTrackedPrimitives(); } export function validatePluginPrimitiveRequirements( @@ -141,5 +196,6 @@ export function validatePluginPrimitiveRequirements( export function resetPrimitiveRegistry(): void { registry.reset(); + pluginDefinitionsByKey.clear(); seeded = false; } diff --git a/packages/sdk/src/chains/registry.ts b/packages/sdk/src/chains/registry.ts index b6095f11..15e118e0 100644 --- a/packages/sdk/src/chains/registry.ts +++ b/packages/sdk/src/chains/registry.ts @@ -10,6 +10,7 @@ import { ensurePrimitivesSeeded, preflightPluginPrimitives, registerPluginPrimitives, + unregisterPluginPrimitives, validatePluginPrimitiveRequirements, } from './primitives'; @@ -106,6 +107,7 @@ export function registerChainPlugin(plugin: ChainPlugin): void { } catch (err) { if (chainRegistered) { registry.unregister(plugin.chainId, plugin.device); + unregisterPluginPrimitives(plugin.chainId, plugin.device); } throw err; } @@ -114,7 +116,11 @@ export function registerChainPlugin(plugin: ChainPlugin): void { export function unregisterChain(chainId: string, device?: DeviceId): boolean { cacheGeneration += 1; cache.clear(); - return registry.unregister(chainId, device); + const unregistered = registry.unregister(chainId, device); + if (unregistered) { + unregisterPluginPrimitives(chainId, device); + } + return unregistered; } export function listChains(): ChainPlugin[] { From bed9999e756715303e2fac790dd1d07e7abc893a Mon Sep 17 00:00:00 2001 From: baha Date: Wed, 4 Mar 2026 10:01:28 +0300 Subject: [PATCH 5/6] refactor(chains): rename primitives to signing components Replace Primitive* types and registry APIs with SigningComponent* across chain-core and sdk. Migrate lattice signers/context to resolveSigningComponent with EXTERNAL.SIGNING fallback to preserve DeviceContext compatibility. Rename primitive-focused modules/tests and keep transactional plugin register/unregister cleanup for plugin-owned signing components. --- packages/chains/chain-core/src/index.ts | 26 +- .../chain-core/src/primitiveRegistry.ts | 220 ---------------- .../src/signingComponentRegistry.ts | 240 ++++++++++++++++++ packages/chains/cosmos/src/devices/lattice.ts | 40 +-- packages/chains/evm/src/devices/lattice.ts | 50 ++-- packages/chains/solana/src/devices/lattice.ts | 40 +-- packages/chains/xrp/src/devices/lattice.ts | 40 +-- ... => chainRuntimeSigningComponents.test.ts} | 46 ++-- .../sdk/src/__test__/unit/context.test.ts | 28 +- ...=> latticeSignerSigningComponents.test.ts} | 66 ++--- .../__test__/unit/setupChainPlugins.test.ts | 16 +- ... => signingComponentRegistry.core.test.ts} | 42 +-- ...ives.test.ts => signingComponents.test.ts} | 74 +++--- packages/sdk/src/api/setup.ts | 4 +- packages/sdk/src/chains/context.ts | 23 +- packages/sdk/src/chains/index.ts | 18 +- packages/sdk/src/chains/registry.ts | 26 +- .../{primitives.ts => signingComponents.ts} | 117 +++++---- packages/types/src/firmware.ts | 3 - 19 files changed, 588 insertions(+), 531 deletions(-) delete mode 100644 packages/chains/chain-core/src/primitiveRegistry.ts create mode 100644 packages/chains/chain-core/src/signingComponentRegistry.ts rename packages/sdk/src/__test__/unit/{chainRuntimePrimitives.test.ts => chainRuntimeSigningComponents.test.ts} (77%) rename packages/sdk/src/__test__/unit/{latticeSignerPrimitives.test.ts => latticeSignerSigningComponents.test.ts} (79%) rename packages/sdk/src/__test__/unit/{primitiveRegistry.core.test.ts => signingComponentRegistry.core.test.ts} (69%) rename packages/sdk/src/__test__/unit/{primitives.test.ts => signingComponents.test.ts} (59%) rename packages/sdk/src/chains/{primitives.ts => signingComponents.ts} (53%) diff --git a/packages/chains/chain-core/src/index.ts b/packages/chains/chain-core/src/index.ts index 77117248..0cbb1d04 100644 --- a/packages/chains/chain-core/src/index.ts +++ b/packages/chains/chain-core/src/index.ts @@ -33,23 +33,23 @@ export type ChainCapabilities = { getXpub?: boolean; }; -export type PrimitiveKind = 'hash' | 'curve' | 'encoding'; +export type SigningComponentKind = 'hash' | 'curve' | 'encoding'; -export type PrimitiveDefinition = { - kind: PrimitiveKind; +export type SigningComponentDefinition = { + kind: SigningComponentKind; name: string; code: number; }; -export type PrimitiveRequirement = { - kind: PrimitiveKind; +export type SigningComponentRequirement = { + kind: SigningComponentKind; name: string; minFirmware: [number, number, number]; }; -export type PluginPrimitives = { - definitions?: PrimitiveDefinition[]; - requirements?: PrimitiveRequirement[]; +export type ChainSigningSuite = { + definitions?: SigningComponentDefinition[]; + requirements?: SigningComponentRequirement[]; }; export type GetAddressParams = { @@ -146,7 +146,7 @@ export type ChainPlugin< signer: TSigner, options?: TOptions, ) => Promise | TAdapter; - primitives?: PluginPrimitives; + signingSuite?: ChainSigningSuite; }; export type ChainRegistryResolveOptions = { @@ -269,10 +269,10 @@ export function createChainRegistry( } export { - createPrimitiveRegistry, - PrimitiveConflictError, - type PrimitiveRegistry, -} from './primitiveRegistry'; + createSigningComponentRegistry, + SigningComponentConflictError, + type SigningComponentRegistry, +} from './signingComponentRegistry'; // --------------------------------------------------------------------------- // Firmware version utilities diff --git a/packages/chains/chain-core/src/primitiveRegistry.ts b/packages/chains/chain-core/src/primitiveRegistry.ts deleted file mode 100644 index ca22640a..00000000 --- a/packages/chains/chain-core/src/primitiveRegistry.ts +++ /dev/null @@ -1,220 +0,0 @@ -import type { PrimitiveDefinition, PrimitiveKind } from './index'; - -const PRIMITIVE_KINDS: PrimitiveKind[] = ['hash', 'curve', 'encoding']; - -type PrimitiveDefinitionMap = { - [K in PrimitiveKind]: Map; -}; - -type PrimitiveReverseDefinitionMap = { - [K in PrimitiveKind]: Map; -}; - -const createNameMaps = (): PrimitiveDefinitionMap => ({ - hash: new Map(), - curve: new Map(), - encoding: new Map(), -}); - -const createCodeMaps = (): PrimitiveReverseDefinitionMap => ({ - hash: new Map(), - curve: new Map(), - encoding: new Map(), -}); - -const normalizePrimitiveKind = (kind: unknown): PrimitiveKind => { - if (kind === 'hash' || kind === 'curve' || kind === 'encoding') { - return kind; - } - throw new Error(`Invalid primitive kind: ${String(kind)}`); -}; - -const normalizePrimitiveName = (name: unknown): string => { - if (typeof name !== 'string') { - throw new Error(`Invalid primitive name: ${String(name)}`); - } - const normalized = name.trim().toUpperCase(); - if (!normalized) { - throw new Error('Primitive name cannot be empty'); - } - return normalized; -}; - -const normalizePrimitiveCode = (code: unknown): number => { - if ( - typeof code !== 'number' || - !Number.isFinite(code) || - !Number.isInteger(code) || - code < 0 - ) { - throw new Error(`Invalid primitive code: ${String(code)}`); - } - return code; -}; - -const normalizeDefinition = (definition: PrimitiveDefinition) => { - const kind = normalizePrimitiveKind(definition.kind); - const name = normalizePrimitiveName(definition.name); - const code = normalizePrimitiveCode(definition.code); - return { kind, name, code }; -}; - -const sortDefinitions = ( - definitions: PrimitiveDefinition[], -): PrimitiveDefinition[] => { - return [...definitions].sort((a, b) => { - if (a.kind !== b.kind) return a.kind.localeCompare(b.kind); - if (a.name !== b.name) return a.name.localeCompare(b.name); - return a.code - b.code; - }); -}; - -const normalizeDefinitions = ( - definitions: PrimitiveDefinition[], -): PrimitiveDefinition[] => { - if (!Array.isArray(definitions)) { - throw new Error('Primitive definitions must be an array'); - } - return definitions.map(normalizeDefinition); -}; - -export class PrimitiveConflictError extends Error { - public readonly kind: PrimitiveKind; - public readonly primitiveName: string; - public readonly code: number; - - constructor(message: string, def: PrimitiveDefinition) { - super(message); - this.name = 'PrimitiveConflictError'; - this.kind = def.kind; - this.primitiveName = def.name; - this.code = def.code; - } -} - -export type PrimitiveRegistry = { - register: (definitions: PrimitiveDefinition[]) => void; - preflight: (definitions: PrimitiveDefinition[]) => void; - resolve: (kind: PrimitiveKind, name: string) => number | undefined; - resolveOrThrow: (kind: PrimitiveKind, name: string) => number; - reverseResolve: (kind: PrimitiveKind, code: number) => string | undefined; - has: (kind: PrimitiveKind, name: string) => boolean; - list: (kind?: PrimitiveKind) => PrimitiveDefinition[]; - reset: () => void; -}; - -export function createPrimitiveRegistry(): PrimitiveRegistry { - const nameToCode = createNameMaps(); - const codeToName = createCodeMaps(); - - const checkConflict = ( - stagedNameToCode: PrimitiveDefinitionMap, - stagedCodeToName: PrimitiveReverseDefinitionMap, - def: PrimitiveDefinition, - ) => { - const existingCode = stagedNameToCode[def.kind].get(def.name); - if (existingCode !== undefined && existingCode !== def.code) { - throw new PrimitiveConflictError( - `Primitive conflict for ${def.kind}:${def.name}. Existing code=${existingCode}, new code=${def.code}.`, - def, - ); - } - - const existingName = stagedCodeToName[def.kind].get(def.code); - if (existingName !== undefined && existingName !== def.name) { - throw new PrimitiveConflictError( - `Primitive conflict for ${def.kind} code=${def.code}. Existing name=${existingName}, new name=${def.name}.`, - def, - ); - } - }; - - const preflight = (definitions: PrimitiveDefinition[]) => { - const normalized = normalizeDefinitions(definitions); - const stagedNameToCode = createNameMaps(); - const stagedCodeToName = createCodeMaps(); - - for (const kind of PRIMITIVE_KINDS) { - nameToCode[kind].forEach((code, name) => { - stagedNameToCode[kind].set(name, code); - }); - codeToName[kind].forEach((name, code) => { - stagedCodeToName[kind].set(code, name); - }); - } - - for (const def of normalized) { - checkConflict(stagedNameToCode, stagedCodeToName, def); - stagedNameToCode[def.kind].set(def.name, def.code); - stagedCodeToName[def.kind].set(def.code, def.name); - } - }; - - const register = (definitions: PrimitiveDefinition[]) => { - const normalized = normalizeDefinitions(definitions); - preflight(normalized); - - for (const def of normalized) { - nameToCode[def.kind].set(def.name, def.code); - codeToName[def.kind].set(def.code, def.name); - } - }; - - const resolve = (kind: PrimitiveKind, name: string): number | undefined => { - return nameToCode[kind].get(normalizePrimitiveName(name)); - }; - - const resolveOrThrow = (kind: PrimitiveKind, name: string): number => { - const code = resolve(kind, name); - if (code === undefined) { - throw new Error(`Primitive not found: ${kind}:${name}`); - } - return code; - }; - - const reverseResolve = ( - kind: PrimitiveKind, - code: number, - ): string | undefined => { - return codeToName[kind].get(normalizePrimitiveCode(code)); - }; - - const has = (kind: PrimitiveKind, name: string): boolean => { - return resolve(kind, name) !== undefined; - }; - - const list = (kind?: PrimitiveKind): PrimitiveDefinition[] => { - const definitions: PrimitiveDefinition[] = []; - const kinds = kind ? [kind] : PRIMITIVE_KINDS; - - for (const currentKind of kinds) { - nameToCode[currentKind].forEach((code, name) => { - definitions.push({ - kind: currentKind, - name, - code, - }); - }); - } - - return sortDefinitions(definitions); - }; - - const reset = () => { - for (const kind of PRIMITIVE_KINDS) { - nameToCode[kind].clear(); - codeToName[kind].clear(); - } - }; - - return { - register, - preflight, - resolve, - resolveOrThrow, - reverseResolve, - has, - list, - reset, - }; -} diff --git a/packages/chains/chain-core/src/signingComponentRegistry.ts b/packages/chains/chain-core/src/signingComponentRegistry.ts new file mode 100644 index 00000000..98c1bb93 --- /dev/null +++ b/packages/chains/chain-core/src/signingComponentRegistry.ts @@ -0,0 +1,240 @@ +import type { SigningComponentDefinition, SigningComponentKind } from './index'; + +const SIGNING_COMPONENT_KINDS: SigningComponentKind[] = [ + 'hash', + 'curve', + 'encoding', +]; + +type SigningComponentDefinitionMap = { + [K in SigningComponentKind]: Map; +}; + +type SigningComponentReverseDefinitionMap = { + [K in SigningComponentKind]: Map; +}; + +const createNameMaps = (): SigningComponentDefinitionMap => ({ + hash: new Map(), + curve: new Map(), + encoding: new Map(), +}); + +const createCodeMaps = (): SigningComponentReverseDefinitionMap => ({ + hash: new Map(), + curve: new Map(), + encoding: new Map(), +}); + +const normalizeSigningComponentKind = ( + kind: unknown, +): SigningComponentKind => { + if (kind === 'hash' || kind === 'curve' || kind === 'encoding') { + return kind; + } + throw new Error(`Invalid signing component kind: ${String(kind)}`); +}; + +const normalizeSigningComponentName = (name: unknown): string => { + if (typeof name !== 'string') { + throw new Error(`Invalid signing component name: ${String(name)}`); + } + const normalized = name.trim().toUpperCase(); + if (!normalized) { + throw new Error('Signing component name cannot be empty'); + } + return normalized; +}; + +const normalizeSigningComponentCode = (code: unknown): number => { + if ( + typeof code !== 'number' || + !Number.isFinite(code) || + !Number.isInteger(code) || + code < 0 + ) { + throw new Error(`Invalid signing component code: ${String(code)}`); + } + return code; +}; + +const normalizeDefinition = (definition: SigningComponentDefinition) => { + const kind = normalizeSigningComponentKind(definition.kind); + const name = normalizeSigningComponentName(definition.name); + const code = normalizeSigningComponentCode(definition.code); + return { kind, name, code }; +}; + +const sortDefinitions = ( + definitions: SigningComponentDefinition[], +): SigningComponentDefinition[] => { + return [...definitions].sort((a, b) => { + if (a.kind !== b.kind) return a.kind.localeCompare(b.kind); + if (a.name !== b.name) return a.name.localeCompare(b.name); + return a.code - b.code; + }); +}; + +const normalizeDefinitions = ( + definitions: SigningComponentDefinition[], +): SigningComponentDefinition[] => { + if (!Array.isArray(definitions)) { + throw new Error('Signing component definitions must be an array'); + } + return definitions.map(normalizeDefinition); +}; + +export class SigningComponentConflictError extends Error { + public readonly kind: SigningComponentKind; + public readonly componentName: string; + public readonly code: number; + + constructor(message: string, def: SigningComponentDefinition) { + super(message); + this.name = 'SigningComponentConflictError'; + this.kind = def.kind; + this.componentName = def.name; + this.code = def.code; + } +} + +export type SigningComponentRegistry = { + register: (definitions: SigningComponentDefinition[]) => void; + preflight: (definitions: SigningComponentDefinition[]) => void; + resolve: ( + kind: SigningComponentKind, + name: string, + ) => number | undefined; + resolveOrThrow: (kind: SigningComponentKind, name: string) => number; + reverseResolve: ( + kind: SigningComponentKind, + code: number, + ) => string | undefined; + has: (kind: SigningComponentKind, name: string) => boolean; + list: (kind?: SigningComponentKind) => SigningComponentDefinition[]; + reset: () => void; +}; + +export function createSigningComponentRegistry(): SigningComponentRegistry { + const nameToCode = createNameMaps(); + const codeToName = createCodeMaps(); + + const checkConflict = ( + stagedNameToCode: SigningComponentDefinitionMap, + stagedCodeToName: SigningComponentReverseDefinitionMap, + def: SigningComponentDefinition, + ) => { + const existingCode = stagedNameToCode[def.kind].get(def.name); + if (existingCode !== undefined && existingCode !== def.code) { + throw new SigningComponentConflictError( + `Signing component conflict for ${def.kind}:${def.name}. Existing code=${existingCode}, new code=${def.code}.`, + def, + ); + } + + const existingName = stagedCodeToName[def.kind].get(def.code); + if (existingName !== undefined && existingName !== def.name) { + throw new SigningComponentConflictError( + `Signing component conflict for ${def.kind} code=${def.code}. Existing name=${existingName}, new name=${def.name}.`, + def, + ); + } + }; + + const preflight = (definitions: SigningComponentDefinition[]) => { + const normalized = normalizeDefinitions(definitions); + const stagedNameToCode = createNameMaps(); + const stagedCodeToName = createCodeMaps(); + + for (const kind of SIGNING_COMPONENT_KINDS) { + nameToCode[kind].forEach((code, name) => { + stagedNameToCode[kind].set(name, code); + }); + codeToName[kind].forEach((name, code) => { + stagedCodeToName[kind].set(code, name); + }); + } + + for (const def of normalized) { + checkConflict(stagedNameToCode, stagedCodeToName, def); + stagedNameToCode[def.kind].set(def.name, def.code); + stagedCodeToName[def.kind].set(def.code, def.name); + } + }; + + const register = (definitions: SigningComponentDefinition[]) => { + const normalized = normalizeDefinitions(definitions); + preflight(normalized); + + for (const def of normalized) { + nameToCode[def.kind].set(def.name, def.code); + codeToName[def.kind].set(def.code, def.name); + } + }; + + const resolve = ( + kind: SigningComponentKind, + name: string, + ): number | undefined => { + return nameToCode[kind].get(normalizeSigningComponentName(name)); + }; + + const resolveOrThrow = ( + kind: SigningComponentKind, + name: string, + ): number => { + const code = resolve(kind, name); + if (code === undefined) { + throw new Error(`Signing component not found: ${kind}:${name}`); + } + return code; + }; + + const reverseResolve = ( + kind: SigningComponentKind, + code: number, + ): string | undefined => { + return codeToName[kind].get(normalizeSigningComponentCode(code)); + }; + + const has = (kind: SigningComponentKind, name: string): boolean => { + return resolve(kind, name) !== undefined; + }; + + const list = ( + kind?: SigningComponentKind, + ): SigningComponentDefinition[] => { + const definitions: SigningComponentDefinition[] = []; + const kinds = kind ? [kind] : SIGNING_COMPONENT_KINDS; + + for (const currentKind of kinds) { + nameToCode[currentKind].forEach((code, name) => { + definitions.push({ + kind: currentKind, + name, + code, + }); + }); + } + + return sortDefinitions(definitions); + }; + + const reset = () => { + for (const kind of SIGNING_COMPONENT_KINDS) { + nameToCode[kind].clear(); + codeToName[kind].clear(); + } + }; + + return { + register, + preflight, + resolve, + resolveOrThrow, + reverseResolve, + has, + list, + reset, + }; +} diff --git a/packages/chains/cosmos/src/devices/lattice.ts b/packages/chains/cosmos/src/devices/lattice.ts index d2bc9f61..75e2eeb2 100644 --- a/packages/chains/cosmos/src/devices/lattice.ts +++ b/packages/chains/cosmos/src/devices/lattice.ts @@ -3,7 +3,7 @@ import type { ChainPlugin, DerivationPath, DeviceContext, - PrimitiveKind, + SigningComponentKind, PublicKey, SignResult, } from '@gridplus/chain-core'; @@ -16,38 +16,38 @@ import { } from '../chain'; import { buildSigResultFromRsv, compressSecp256k1Pubkey } from './shared'; -type PrimitiveCodeMaps = { +type SigningComponentCodes = { HASHES?: Record; CURVES?: Record; ENCODINGS?: Record; }; type LatticeCosmosContextInput = DeviceContext & { - resolvePrimitive?: (kind: PrimitiveKind, name: string) => number; + resolveSigningComponent?: (kind: SigningComponentKind, name: string) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { SECP256K1_PUB: number; }; - SIGNING?: PrimitiveCodeMaps; + SIGNING?: SigningComponentCodes; }; }; }; type LatticeCosmosContext = DeviceContext & { - resolvePrimitive: (kind: PrimitiveKind, name: string) => number; + resolveSigningComponent: (kind: SigningComponentKind, name: string) => number; constants: LatticeCosmosContextInput['constants']; }; const hasNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value); -const getPrimitiveFromConstants = ( - signing: PrimitiveCodeMaps | undefined, - kind: PrimitiveKind, +const getSigningComponentFromConstants = ( + signing: SigningComponentCodes | undefined, + kind: SigningComponentKind, name: string, ): number | undefined => { - const byKind: Record | undefined> = { + const byKind: Record | undefined> = { hash: signing?.HASHES, curve: signing?.CURVES, encoding: signing?.ENCODINGS, @@ -59,18 +59,18 @@ const getPrimitiveFromConstants = ( function getLatticeCosmosContext(context: DeviceContext): LatticeCosmosContext { const typed = context as LatticeCosmosContextInput; const constants = typed.constants; - const resolvePrimitive = (kind: PrimitiveKind, name: string): number => { - if (typeof typed.resolvePrimitive === 'function') { - return typed.resolvePrimitive(kind, name); + const resolveSigningComponent = (kind: SigningComponentKind, name: string): number => { + if (typeof typed.resolveSigningComponent === 'function') { + return typed.resolveSigningComponent(kind, name); } - const fromConstants = getPrimitiveFromConstants( + const fromConstants = getSigningComponentFromConstants( constants?.EXTERNAL?.SIGNING, kind, name, ); if (fromConstants !== undefined) return fromConstants; throw new Error( - `Lattice Cosmos signer requires resolvePrimitive() or EXTERNAL.SIGNING mapping for ${kind}:${name}.`, + `Lattice Cosmos signer requires resolveSigningComponent() or EXTERNAL.SIGNING mapping for ${kind}:${name}.`, ); }; if (!hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.SECP256K1_PUB)) { @@ -78,19 +78,19 @@ function getLatticeCosmosContext(context: DeviceContext): LatticeCosmosContext { } return { ...typed, - resolvePrimitive, + resolveSigningComponent, }; } export function createLatticeCosmosSigner( context: DeviceContext, ): CosmosSigner { - const { queue, resolvePrimitive, constants } = + const { queue, resolveSigningComponent, constants } = getLatticeCosmosContext(context); const { EXTERNAL } = constants; - const curveSecp256k1 = resolvePrimitive('curve', 'SECP256K1'); - const hashSha256 = resolvePrimitive('hash', 'SHA256'); - const encodingCosmos = resolvePrimitive('encoding', 'COSMOS'); + const curveSecp256k1 = resolveSigningComponent('curve', 'SECP256K1'); + const hashSha256 = resolveSigningComponent('hash', 'SHA256'); + const encodingCosmos = resolveSigningComponent('encoding', 'COSMOS'); return { getAddress: async (path: DerivationPath): Promise
=> { @@ -174,7 +174,7 @@ export const latticePlugin: ChainPlugin< device: 'lattice', module: cosmos, createSigner: createLatticeCosmosSigner, - primitives: { + signingSuite: { requirements: [ { kind: 'curve', name: 'SECP256K1', minFirmware: [0, 14, 0] }, { kind: 'hash', name: 'SHA256', minFirmware: [0, 14, 0] }, diff --git a/packages/chains/evm/src/devices/lattice.ts b/packages/chains/evm/src/devices/lattice.ts index 860d1674..81e1e4c0 100644 --- a/packages/chains/evm/src/devices/lattice.ts +++ b/packages/chains/evm/src/devices/lattice.ts @@ -6,7 +6,7 @@ import { type DerivationPath, type DeviceContext, type FirmwareVersionTuple, - type PrimitiveKind, + type SigningComponentKind, type PublicKey, type SignResult, } from '@gridplus/chain-core'; @@ -36,20 +36,20 @@ export type LatticeEvmSignerOptions = { fetchEvmDecoder?: boolean; }; -type PrimitiveCodeMaps = { +type SigningComponentCodes = { HASHES?: Record; CURVES?: Record; ENCODINGS?: Record; }; type LatticeEvmContextInput = DeviceContext & { - resolvePrimitive?: (kind: PrimitiveKind, name: string) => number; + resolveSigningComponent?: (kind: SigningComponentKind, name: string) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { SECP256K1_PUB: number; }; - SIGNING?: PrimitiveCodeMaps; + SIGNING?: SigningComponentCodes; }; CURRENCIES: { ETH_MSG: string; @@ -65,7 +65,7 @@ type LatticeEvmContextInput = DeviceContext & { }; type LatticeEvmContext = DeviceContext & { - resolvePrimitive: (kind: PrimitiveKind, name: string) => number; + resolveSigningComponent: (kind: SigningComponentKind, name: string) => number; constants: LatticeEvmContextInput['constants']; services?: LatticeEvmContextInput['services']; }; @@ -73,12 +73,12 @@ type LatticeEvmContext = DeviceContext & { const hasNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value); -const getPrimitiveFromConstants = ( - signing: PrimitiveCodeMaps | undefined, - kind: PrimitiveKind, +const getSigningComponentFromConstants = ( + signing: SigningComponentCodes | undefined, + kind: SigningComponentKind, name: string, ): number | undefined => { - const byKind: Record | undefined> = { + const byKind: Record | undefined> = { hash: signing?.HASHES, curve: signing?.CURVES, encoding: signing?.ENCODINGS, @@ -90,18 +90,18 @@ const getPrimitiveFromConstants = ( function getLatticeEvmContext(context: DeviceContext): LatticeEvmContext { const typed = context as LatticeEvmContextInput; const constants = typed.constants; - const resolvePrimitive = (kind: PrimitiveKind, name: string): number => { - if (typeof typed.resolvePrimitive === 'function') { - return typed.resolvePrimitive(kind, name); + const resolveSigningComponent = (kind: SigningComponentKind, name: string): number => { + if (typeof typed.resolveSigningComponent === 'function') { + return typed.resolveSigningComponent(kind, name); } - const fromConstants = getPrimitiveFromConstants( + const fromConstants = getSigningComponentFromConstants( constants?.EXTERNAL?.SIGNING, kind, name, ); if (fromConstants !== undefined) return fromConstants; throw new Error( - `Lattice EVM signer requires resolvePrimitive() or EXTERNAL.SIGNING mapping for ${kind}:${name}.`, + `Lattice EVM signer requires resolveSigningComponent() or EXTERNAL.SIGNING mapping for ${kind}:${name}.`, ); }; if ( @@ -114,7 +114,7 @@ function getLatticeEvmContext(context: DeviceContext): LatticeEvmContext { } return { ...typed, - resolvePrimitive, + resolveSigningComponent, }; } @@ -137,17 +137,17 @@ function normalizeRawEvmTx(tx: EvmRawTransaction): Hex | Buffer { function getEvmEncodingType( tx: TransactionSerializable, - resolvePrimitive: (kind: PrimitiveKind, name: string) => number, + resolveSigningComponent: (kind: SigningComponentKind, name: string) => number, ): number { if ((tx as any).type === 'eip7702') { const eip7702 = tx as TransactionSerializableEIP7702; const hasAuthList = eip7702.authorizationList && eip7702.authorizationList.length > 0; return hasAuthList - ? resolvePrimitive('encoding', 'EIP7702_AUTH_LIST') - : resolvePrimitive('encoding', 'EIP7702_AUTH'); + ? resolveSigningComponent('encoding', 'EIP7702_AUTH_LIST') + : resolveSigningComponent('encoding', 'EIP7702_AUTH'); } - return resolvePrimitive('encoding', 'EVM'); + return resolveSigningComponent('encoding', 'EVM'); } const EIP7702_MIN_FIRMWARE: FirmwareVersionTuple = [0, 18, 0]; @@ -169,11 +169,11 @@ export function createLatticeEvmSigner( options: LatticeEvmSignerOptions = {}, ): EvmSigner { const latticeContext = getLatticeEvmContext(context); - const { queue, services, resolvePrimitive } = latticeContext; + const { queue, services, resolveSigningComponent } = latticeContext; const { EXTERNAL, CURRENCIES } = latticeContext.constants; - const curveSecp256k1 = resolvePrimitive('curve', 'SECP256K1'); - const hashKeccak256 = resolvePrimitive('hash', 'KECCAK256'); - const encodingEvm = resolvePrimitive('encoding', 'EVM'); + const curveSecp256k1 = resolveSigningComponent('curve', 'SECP256K1'); + const hashKeccak256 = resolveSigningComponent('hash', 'KECCAK256'); + const encodingEvm = resolveSigningComponent('encoding', 'EVM'); return { getAddress: async (path: DerivationPath): Promise
=> { @@ -224,7 +224,7 @@ export function createLatticeEvmSigner( ? encodingEvm : getEvmEncodingType( request.payload as TransactionSerializable, - resolvePrimitive, + resolveSigningComponent, ); if (!isRaw && (request.payload as any).type === 'eip7702') { await assertEip7702FirmwareSupport(latticeContext); @@ -346,7 +346,7 @@ export const latticePlugin: ChainPlugin< device: 'lattice', module: evm, createSigner: (context) => createLatticeEvmSigner(context), - primitives: { + signingSuite: { requirements: [ { kind: 'curve', name: 'SECP256K1', minFirmware: [0, 14, 0] }, { kind: 'hash', name: 'KECCAK256', minFirmware: [0, 14, 0] }, diff --git a/packages/chains/solana/src/devices/lattice.ts b/packages/chains/solana/src/devices/lattice.ts index 7917ca40..5e7abeba 100644 --- a/packages/chains/solana/src/devices/lattice.ts +++ b/packages/chains/solana/src/devices/lattice.ts @@ -3,7 +3,7 @@ import type { ChainPlugin, DerivationPath, DeviceContext, - PrimitiveKind, + SigningComponentKind, PublicKey, SignResult, } from '@gridplus/chain-core'; @@ -17,38 +17,38 @@ import { } from '../chain'; import { buildSigResultFromRsv, toBuffer } from './shared'; -type PrimitiveCodeMaps = { +type SigningComponentCodes = { HASHES?: Record; CURVES?: Record; ENCODINGS?: Record; }; type LatticeSolanaContextInput = DeviceContext & { - resolvePrimitive?: (kind: PrimitiveKind, name: string) => number; + resolveSigningComponent?: (kind: SigningComponentKind, name: string) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { ED25519_PUB: number; }; - SIGNING?: PrimitiveCodeMaps; + SIGNING?: SigningComponentCodes; }; }; }; type LatticeSolanaContext = DeviceContext & { - resolvePrimitive: (kind: PrimitiveKind, name: string) => number; + resolveSigningComponent: (kind: SigningComponentKind, name: string) => number; constants: LatticeSolanaContextInput['constants']; }; const hasNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value); -const getPrimitiveFromConstants = ( - signing: PrimitiveCodeMaps | undefined, - kind: PrimitiveKind, +const getSigningComponentFromConstants = ( + signing: SigningComponentCodes | undefined, + kind: SigningComponentKind, name: string, ): number | undefined => { - const byKind: Record | undefined> = { + const byKind: Record | undefined> = { hash: signing?.HASHES, curve: signing?.CURVES, encoding: signing?.ENCODINGS, @@ -60,18 +60,18 @@ const getPrimitiveFromConstants = ( function getLatticeSolanaContext(context: DeviceContext): LatticeSolanaContext { const typed = context as LatticeSolanaContextInput; const constants = typed.constants; - const resolvePrimitive = (kind: PrimitiveKind, name: string): number => { - if (typeof typed.resolvePrimitive === 'function') { - return typed.resolvePrimitive(kind, name); + const resolveSigningComponent = (kind: SigningComponentKind, name: string): number => { + if (typeof typed.resolveSigningComponent === 'function') { + return typed.resolveSigningComponent(kind, name); } - const fromConstants = getPrimitiveFromConstants( + const fromConstants = getSigningComponentFromConstants( constants?.EXTERNAL?.SIGNING, kind, name, ); if (fromConstants !== undefined) return fromConstants; throw new Error( - `Lattice Solana signer requires resolvePrimitive() or EXTERNAL.SIGNING mapping for ${kind}:${name}.`, + `Lattice Solana signer requires resolveSigningComponent() or EXTERNAL.SIGNING mapping for ${kind}:${name}.`, ); }; if (!hasNumber(constants?.EXTERNAL?.GET_ADDR_FLAGS?.ED25519_PUB)) { @@ -79,19 +79,19 @@ function getLatticeSolanaContext(context: DeviceContext): LatticeSolanaContext { } return { ...typed, - resolvePrimitive, + resolveSigningComponent, }; } export function createLatticeSolanaSigner( context: DeviceContext, ): SolanaSigner { - const { queue, resolvePrimitive, constants } = + const { queue, resolveSigningComponent, constants } = getLatticeSolanaContext(context); const { EXTERNAL } = constants; - const curveEd25519 = resolvePrimitive('curve', 'ED25519'); - const hashNone = resolvePrimitive('hash', 'NONE'); - const encodingSolana = resolvePrimitive('encoding', 'SOLANA'); + const curveEd25519 = resolveSigningComponent('curve', 'ED25519'); + const hashNone = resolveSigningComponent('hash', 'NONE'); + const encodingSolana = resolveSigningComponent('encoding', 'SOLANA'); const getPublicKey = async (path: DerivationPath): Promise => { const res = (await queue((client: any) => @@ -163,7 +163,7 @@ export const latticePlugin: ChainPlugin< device: 'lattice', module: solana, createSigner: createLatticeSolanaSigner, - primitives: { + signingSuite: { requirements: [ { kind: 'curve', name: 'ED25519', minFirmware: [0, 14, 0] }, { kind: 'hash', name: 'NONE', minFirmware: [0, 14, 0] }, diff --git a/packages/chains/xrp/src/devices/lattice.ts b/packages/chains/xrp/src/devices/lattice.ts index 196e7760..268ab8a8 100644 --- a/packages/chains/xrp/src/devices/lattice.ts +++ b/packages/chains/xrp/src/devices/lattice.ts @@ -3,7 +3,7 @@ import type { ChainPlugin, DerivationPath, DeviceContext, - PrimitiveKind, + SigningComponentKind, PublicKey, SignResult, } from '@gridplus/chain-core'; @@ -21,38 +21,38 @@ import { toBuffer, } from './shared'; -type PrimitiveCodeMaps = { +type SigningComponentCodes = { HASHES?: Record; CURVES?: Record; ENCODINGS?: Record; }; type LatticeXrpContextInput = DeviceContext & { - resolvePrimitive?: (kind: PrimitiveKind, name: string) => number; + resolveSigningComponent?: (kind: SigningComponentKind, name: string) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { SECP256K1_PUB: number; }; - SIGNING?: PrimitiveCodeMaps; + SIGNING?: SigningComponentCodes; }; }; }; type LatticeXrpContext = DeviceContext & { - resolvePrimitive: (kind: PrimitiveKind, name: string) => number; + resolveSigningComponent: (kind: SigningComponentKind, name: string) => number; constants: LatticeXrpContextInput['constants']; }; const hasNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value); -const getPrimitiveFromConstants = ( - signing: PrimitiveCodeMaps | undefined, - kind: PrimitiveKind, +const getSigningComponentFromConstants = ( + signing: SigningComponentCodes | undefined, + kind: SigningComponentKind, name: string, ): number | undefined => { - const byKind: Record | undefined> = { + const byKind: Record | undefined> = { hash: signing?.HASHES, curve: signing?.CURVES, encoding: signing?.ENCODINGS, @@ -64,18 +64,18 @@ const getPrimitiveFromConstants = ( function getLatticeXrpContext(context: DeviceContext): LatticeXrpContext { const typed = context as LatticeXrpContextInput; const constants = typed.constants; - const resolvePrimitive = (kind: PrimitiveKind, name: string): number => { - if (typeof typed.resolvePrimitive === 'function') { - return typed.resolvePrimitive(kind, name); + const resolveSigningComponent = (kind: SigningComponentKind, name: string): number => { + if (typeof typed.resolveSigningComponent === 'function') { + return typed.resolveSigningComponent(kind, name); } - const fromConstants = getPrimitiveFromConstants( + const fromConstants = getSigningComponentFromConstants( constants?.EXTERNAL?.SIGNING, kind, name, ); if (fromConstants !== undefined) return fromConstants; throw new Error( - `Lattice XRP signer requires resolvePrimitive() or EXTERNAL.SIGNING mapping for ${kind}:${name}.`, + `Lattice XRP signer requires resolveSigningComponent() or EXTERNAL.SIGNING mapping for ${kind}:${name}.`, ); }; @@ -85,16 +85,16 @@ function getLatticeXrpContext(context: DeviceContext): LatticeXrpContext { return { ...typed, - resolvePrimitive, + resolveSigningComponent, }; } export function createLatticeXrpSigner(context: DeviceContext): XrpSigner { - const { queue, resolvePrimitive, constants } = getLatticeXrpContext(context); + const { queue, resolveSigningComponent, constants } = getLatticeXrpContext(context); const { EXTERNAL } = constants; - const curveSecp256k1 = resolvePrimitive('curve', 'SECP256K1'); - const hashSha512Half = resolvePrimitive('hash', 'SHA512HALF'); - const encodingXrp = resolvePrimitive('encoding', 'XRP'); + const curveSecp256k1 = resolveSigningComponent('curve', 'SECP256K1'); + const hashSha512Half = resolveSigningComponent('hash', 'SHA512HALF'); + const encodingXrp = resolveSigningComponent('encoding', 'XRP'); const getPublicKey = async ( path: DerivationPath, @@ -181,7 +181,7 @@ export const latticePlugin: ChainPlugin< device: 'lattice', module: xrp, createSigner: createLatticeXrpSigner, - primitives: { + signingSuite: { requirements: [ { kind: 'curve', name: 'SECP256K1', minFirmware: [0, 14, 0] }, { kind: 'hash', name: 'SHA512HALF', minFirmware: [0, 18, 10] }, diff --git a/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts b/packages/sdk/src/__test__/unit/chainRuntimeSigningComponents.test.ts similarity index 77% rename from packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts rename to packages/sdk/src/__test__/unit/chainRuntimeSigningComponents.test.ts index 271a5954..ba3b7891 100644 --- a/packages/sdk/src/__test__/unit/chainRuntimePrimitives.test.ts +++ b/packages/sdk/src/__test__/unit/chainRuntimeSigningComponents.test.ts @@ -2,7 +2,7 @@ import type { ChainAdapter, ChainModule, ChainPlugin, - PluginPrimitives, + ChainSigningSuite, Signer, } from '@gridplus/chain-core'; import { setLoadClient } from '../../api/state'; @@ -14,9 +14,9 @@ import { useChain, } from '../../chains'; import { - getPrimitiveRegistry, - resetPrimitiveRegistry, -} from '../../chains/primitives'; + getSigningComponentRegistry, + resetSigningComponentRegistry, +} from '../../chains/signingComponents'; const mockSigner: Signer = { getAddress: async () => 'mock', @@ -33,7 +33,7 @@ const mockAdapter: ChainAdapter = { const buildPlugin = ( chainId: string, - primitives?: PluginPrimitives, + signingSuite?: ChainSigningSuite, ): ChainPlugin => { const module: ChainModule = { id: chainId, @@ -57,17 +57,17 @@ const buildPlugin = ( device: 'lattice', module, createSigner: async () => mockSigner, - primitives, + signingSuite, }; }; -describe('chain runtime primitive integration', () => { +describe('chain runtime signing component integration', () => { afterEach(() => { unregisterChain('chain-a', 'lattice'); unregisterChain('chain-b', 'lattice'); unregisterChain('dup-chain', 'lattice'); unregisterChain('req-chain', 'lattice'); - resetPrimitiveRegistry(); + resetSigningComponentRegistry(); configureChainRuntime({ autoRegisterChains: true, defaultDevice: 'lattice', @@ -76,7 +76,7 @@ describe('chain runtime primitive integration', () => { setLoadClient(async () => undefined); }); - test('primitive conflict does not register conflicting chain', () => { + test('signing component conflict does not register conflicting chain', () => { registerChainPlugin( buildPlugin('chain-a', { definitions: [{ kind: 'encoding', name: 'TESTCHAIN', code: 99 }], @@ -95,7 +95,7 @@ describe('chain runtime primitive integration', () => { expect(getChain('chain-b', 'lattice')).toBeUndefined(); }); - test('chain registration failure does not leave new primitive definitions', () => { + test('chain registration failure does not leave new signing component definitions', () => { registerChainPlugin( buildPlugin('dup-chain', { definitions: [{ kind: 'encoding', name: 'DUP_A', code: 201 }], @@ -110,33 +110,41 @@ describe('chain runtime primitive integration', () => { ), ).toThrow('already registered'); - const primitiveRegistry = getPrimitiveRegistry(); - expect(primitiveRegistry.resolve('encoding', 'DUP_A')).toBe(201); - expect(primitiveRegistry.resolve('encoding', 'DUP_B')).toBeUndefined(); + const signingComponentRegistry = getSigningComponentRegistry(); + expect(signingComponentRegistry.resolve('encoding', 'DUP_A')).toBe(201); + expect( + signingComponentRegistry.resolve('encoding', 'DUP_B'), + ).toBeUndefined(); }); - test('unregisterChain removes plugin-owned primitive definitions', () => { + test('unregisterChain removes plugin-owned signing component definitions', () => { registerChainPlugin( buildPlugin('chain-a', { definitions: [{ kind: 'encoding', name: 'REPLACE_ME', code: 301 }], }), ); - const primitiveRegistry = getPrimitiveRegistry(); - expect(primitiveRegistry.resolve('encoding', 'REPLACE_ME')).toBe(301); + const signingComponentRegistry = getSigningComponentRegistry(); + expect(signingComponentRegistry.resolve('encoding', 'REPLACE_ME')).toBe( + 301, + ); expect(unregisterChain('chain-a', 'lattice')).toBe(true); - expect(primitiveRegistry.resolve('encoding', 'REPLACE_ME')).toBeUndefined(); + expect( + signingComponentRegistry.resolve('encoding', 'REPLACE_ME'), + ).toBeUndefined(); registerChainPlugin( buildPlugin('chain-a', { definitions: [{ kind: 'encoding', name: 'REPLACE_ME', code: 302 }], }), ); - expect(primitiveRegistry.resolve('encoding', 'REPLACE_ME')).toBe(302); + expect(signingComponentRegistry.resolve('encoding', 'REPLACE_ME')).toBe( + 302, + ); }); - test('useChain enforces primitive minFirmware requirements', async () => { + test('useChain enforces signing component minFirmware requirements', async () => { configureChainRuntime({ autoRegisterChains: false, defaultDevice: 'lattice', diff --git a/packages/sdk/src/__test__/unit/context.test.ts b/packages/sdk/src/__test__/unit/context.test.ts index f028f635..205bd112 100644 --- a/packages/sdk/src/__test__/unit/context.test.ts +++ b/packages/sdk/src/__test__/unit/context.test.ts @@ -1,22 +1,28 @@ import { createDeviceContext } from '../../chains/context'; -import { resetPrimitiveRegistry } from '../../chains/primitives'; +import { resetSigningComponentRegistry } from '../../chains/signingComponents'; -describe('chain context primitive resolution', () => { +describe('chain context signing component resolution', () => { afterEach(() => { - resetPrimitiveRegistry(); + resetSigningComponentRegistry(); }); - test('resolvePrimitive resolves seeded builtins', () => { + test('resolveSigningComponent resolves seeded builtins', () => { const context = createDeviceContext(); - expect(context.resolvePrimitive('hash', 'KECCAK256')).toBeDefined(); - expect(context.resolvePrimitive('curve', 'SECP256K1')).toBeDefined(); - expect(context.resolvePrimitive('encoding', 'EVM')).toBeDefined(); + expect( + context.resolveSigningComponent('hash', 'KECCAK256'), + ).toBeDefined(); + expect( + context.resolveSigningComponent('curve', 'SECP256K1'), + ).toBeDefined(); + expect( + context.resolveSigningComponent('encoding', 'EVM'), + ).toBeDefined(); }); - test('resolvePrimitive throws for unknown primitive', () => { + test('resolveSigningComponent throws for unknown signing component', () => { const context = createDeviceContext(); - expect(() => context.resolvePrimitive('encoding', 'UNKNOWN_CHAIN')).toThrow( - 'Primitive not found', - ); + expect(() => + context.resolveSigningComponent('encoding', 'UNKNOWN_CHAIN'), + ).toThrow('Signing component not found'); }); }); diff --git a/packages/sdk/src/__test__/unit/latticeSignerPrimitives.test.ts b/packages/sdk/src/__test__/unit/latticeSignerSigningComponents.test.ts similarity index 79% rename from packages/sdk/src/__test__/unit/latticeSignerPrimitives.test.ts rename to packages/sdk/src/__test__/unit/latticeSignerSigningComponents.test.ts index 4c3f96e8..a63d36f7 100644 --- a/packages/sdk/src/__test__/unit/latticeSignerPrimitives.test.ts +++ b/packages/sdk/src/__test__/unit/latticeSignerSigningComponents.test.ts @@ -2,27 +2,29 @@ import { createLatticeCosmosSigner } from '@gridplus/cosmos'; import { createLatticeEvmSigner } from '@gridplus/evm'; import { createLatticeSolanaSigner } from '@gridplus/solana'; import { createLatticeXrpSigner } from '@gridplus/xrp'; -import type { PrimitiveKind } from '@gridplus/chain-core'; +import type { SigningComponentKind } from '@gridplus/chain-core'; -type PrimitiveMap = Record; +type SigningComponentMap = Record; type MockContextOptions = { - primitives: PrimitiveMap; + signingComponents: SigningComponentMap; firmware?: [number, number, number]; includeResolver?: boolean; }; -const primitiveKey = (kind: PrimitiveKind, name: string): string => - `${kind}:${name}`; +const signingComponentKey = ( + kind: SigningComponentKind, + name: string, +): string => `${kind}:${name}`; -const buildSigningConstants = (primitives: PrimitiveMap) => { +const buildSigningConstants = (signingComponents: SigningComponentMap) => { const signing = { HASHES: {} as Record, CURVES: {} as Record, ENCODINGS: {} as Record, }; - Object.entries(primitives).forEach(([key, code]) => { + Object.entries(signingComponents).forEach(([key, code]) => { const [kind, name] = key.split(':'); if (!name) return; if (kind === 'hash') signing.HASHES[name] = code; @@ -51,14 +53,16 @@ const buildMockContext = (options: MockContextOptions) => { getAddresses: vi.fn(async () => []), }; - const resolvePrimitive = vi.fn((kind: PrimitiveKind, name: string) => { - const key = primitiveKey(kind, name); - const code = options.primitives[key]; - if (code === undefined) { - throw new Error(`Missing primitive mapping for ${key}`); - } - return code; - }); + const resolveSigningComponent = vi.fn( + (kind: SigningComponentKind, name: string) => { + const key = signingComponentKey(kind, name); + const code = options.signingComponents[key]; + if (code === undefined) { + throw new Error(`Missing signing component mapping for ${key}`); + } + return code; + }, + ); const context: any = { queue: async (fn: (client: unknown) => Promise) => fn(client), @@ -69,7 +73,7 @@ const buildMockContext = (options: MockContextOptions) => { SECP256K1_PUB: 1, ED25519_PUB: 2, }, - SIGNING: buildSigningConstants(options.primitives), + SIGNING: buildSigningConstants(options.signingComponents), }, CURRENCIES: { ETH_MSG: 'ETH_MSG', @@ -78,14 +82,14 @@ const buildMockContext = (options: MockContextOptions) => { services: {}, }; if (includeResolver) { - context.resolvePrimitive = resolvePrimitive; + context.resolveSigningComponent = resolveSigningComponent; } return { context, client, signCalls, - resolvePrimitive, + resolveSigningComponent, }; }; @@ -114,10 +118,10 @@ const buildEip7702AuthListTx = () => ({ ], }); -describe('lattice signer primitive resolution', () => { - test('evm signer uses resolved primitive codes in sign payload', async () => { +describe('lattice signer signing component resolution', () => { + test('evm signer uses resolved signing component codes in sign payload', async () => { const { context, signCalls } = buildMockContext({ - primitives: { + signingComponents: { 'curve:SECP256K1': 91, 'hash:KECCAK256': 92, 'encoding:EVM': 93, @@ -139,9 +143,9 @@ describe('lattice signer primitive resolution', () => { expect(signCalls[0].data.encodingType).toBe(93); }); - test('solana signer uses resolved primitive codes in sign payload', async () => { + test('solana signer uses resolved signing component codes in sign payload', async () => { const { context, signCalls } = buildMockContext({ - primitives: { + signingComponents: { 'curve:ED25519': 11, 'hash:NONE': 12, 'encoding:SOLANA': 13, @@ -161,9 +165,9 @@ describe('lattice signer primitive resolution', () => { expect(signCalls[0].data.encodingType).toBe(13); }); - test('cosmos signer uses resolved primitive codes in sign payload', async () => { + test('cosmos signer uses resolved signing component codes in sign payload', async () => { const { context, signCalls } = buildMockContext({ - primitives: { + signingComponents: { 'curve:SECP256K1': 21, 'hash:SHA256': 22, 'encoding:COSMOS': 23, @@ -183,9 +187,9 @@ describe('lattice signer primitive resolution', () => { expect(signCalls[0].data.encodingType).toBe(23); }); - test('xrp signer uses resolved primitive codes in sign payload', async () => { + test('xrp signer uses resolved signing component codes in sign payload', async () => { const { context, signCalls } = buildMockContext({ - primitives: { + signingComponents: { 'curve:SECP256K1': 31, 'hash:SHA512HALF': 32, 'encoding:XRP': 33, @@ -205,9 +209,9 @@ describe('lattice signer primitive resolution', () => { expect(signCalls[0].data.encodingType).toBe(33); }); - test('evm signer falls back to EXTERNAL.SIGNING when resolvePrimitive is missing', async () => { + test('evm signer falls back to EXTERNAL.SIGNING when resolveSigningComponent is missing', async () => { const { context, signCalls } = buildMockContext({ - primitives: { + signingComponents: { 'curve:SECP256K1': 61, 'hash:KECCAK256': 62, 'encoding:EVM': 63, @@ -230,7 +234,7 @@ describe('lattice signer primitive resolution', () => { test('evm EIP-7702 signing fails below minimum firmware', async () => { const { context, client } = buildMockContext({ - primitives: { + signingComponents: { 'curve:SECP256K1': 41, 'hash:KECCAK256': 42, 'encoding:EVM': 43, @@ -253,7 +257,7 @@ describe('lattice signer primitive resolution', () => { test('evm EIP-7702 signing succeeds at minimum firmware and uses EIP-7702 encoding', async () => { const { context, signCalls } = buildMockContext({ - primitives: { + signingComponents: { 'curve:SECP256K1': 51, 'hash:KECCAK256': 52, 'encoding:EVM': 53, diff --git a/packages/sdk/src/__test__/unit/setupChainPlugins.test.ts b/packages/sdk/src/__test__/unit/setupChainPlugins.test.ts index 22218dbc..99f80208 100644 --- a/packages/sdk/src/__test__/unit/setupChainPlugins.test.ts +++ b/packages/sdk/src/__test__/unit/setupChainPlugins.test.ts @@ -2,7 +2,7 @@ import type { ChainAdapter, ChainModule, ChainPlugin, - PluginPrimitives, + ChainSigningSuite, Signer, } from '@gridplus/chain-core'; import { setup } from '../../api/setup'; @@ -12,9 +12,9 @@ import { DEFAULT_CHAIN_PLUGINS, } from '../../chains/defaultManifest'; import { - getPrimitiveRegistry, - resetPrimitiveRegistry, -} from '../../chains/primitives'; + getSigningComponentRegistry, + resetSigningComponentRegistry, +} from '../../chains/signingComponents'; const mockSigner: Signer = { getAddress: async () => 'custom', @@ -32,7 +32,7 @@ const mockAdapter: ChainAdapter = { const buildPlugin = ( chainId: string, device: string, - primitives?: PluginPrimitives, + signingSuite?: ChainSigningSuite, ): ChainPlugin => { const module: ChainModule = { id: chainId, @@ -56,7 +56,7 @@ const buildPlugin = ( device, module, createSigner: async () => mockSigner, - primitives, + signingSuite, }; }; @@ -68,7 +68,7 @@ const setupParamsBase = { describe('setup chainPlugins', () => { afterEach(() => { - resetPrimitiveRegistry(); + resetSigningComponentRegistry(); unregisterChain('unit-test-chain', 'unit-test-device'); DEFAULT_CHAIN_PLUGIN_KEYS.forEach((key) => { const [chainId, device] = key.split(':'); @@ -142,7 +142,7 @@ describe('setup chainPlugins', () => { registerChainPlugin(customPlugin); - const registry = getPrimitiveRegistry(); + const registry = getSigningComponentRegistry(); expect(registry.resolve('encoding', 'TESTCHAIN')).toBe(99); expect(registry.resolve('encoding', 'EVM')).toBeDefined(); expect(getChain('unit-test-chain', 'unit-test-device')).toBeDefined(); diff --git a/packages/sdk/src/__test__/unit/primitiveRegistry.core.test.ts b/packages/sdk/src/__test__/unit/signingComponentRegistry.core.test.ts similarity index 69% rename from packages/sdk/src/__test__/unit/primitiveRegistry.core.test.ts rename to packages/sdk/src/__test__/unit/signingComponentRegistry.core.test.ts index a2fa8b23..89209641 100644 --- a/packages/sdk/src/__test__/unit/primitiveRegistry.core.test.ts +++ b/packages/sdk/src/__test__/unit/signingComponentRegistry.core.test.ts @@ -1,12 +1,12 @@ import { - PrimitiveConflictError, - createPrimitiveRegistry, - type PrimitiveDefinition, + SigningComponentConflictError, + createSigningComponentRegistry, + type SigningComponentDefinition, } from '@gridplus/chain-core'; -describe('primitive registry core', () => { +describe('signing component registry core', () => { test('registers and resolves by name and code', () => { - const registry = createPrimitiveRegistry(); + const registry = createSigningComponentRegistry(); registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); expect(registry.resolve('hash', 'SHA256')).toBe(2); @@ -15,7 +15,7 @@ describe('primitive registry core', () => { }); test('ignores exact duplicates', () => { - const registry = createPrimitiveRegistry(); + const registry = createSigningComponentRegistry(); registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); @@ -24,25 +24,25 @@ describe('primitive registry core', () => { }); test('throws on name collision', () => { - const registry = createPrimitiveRegistry(); + const registry = createSigningComponentRegistry(); registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); expect(() => registry.register([{ kind: 'hash', name: 'SHA256', code: 99 }]), - ).toThrow(PrimitiveConflictError); + ).toThrow(SigningComponentConflictError); }); test('throws on code collision', () => { - const registry = createPrimitiveRegistry(); + const registry = createSigningComponentRegistry(); registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); expect(() => registry.register([{ kind: 'hash', name: 'BLAKE2B', code: 2 }]), - ).toThrow(PrimitiveConflictError); + ).toThrow(SigningComponentConflictError); }); test('namespaces collisions by kind', () => { - const registry = createPrimitiveRegistry(); + const registry = createSigningComponentRegistry(); registry.register([ { kind: 'hash', name: 'SHA256', code: 2 }, { kind: 'encoding', name: 'SHA256', code: 2 }, @@ -53,38 +53,40 @@ describe('primitive registry core', () => { }); test('preflight catches conflicts without mutation', () => { - const registry = createPrimitiveRegistry(); + const registry = createSigningComponentRegistry(); registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); expect(() => registry.preflight([{ kind: 'hash', name: 'SHA256', code: 77 }]), - ).toThrow(PrimitiveConflictError); + ).toThrow(SigningComponentConflictError); expect(registry.resolve('hash', 'SHA256')).toBe(2); expect(registry.resolve('hash', 'BLAKE2B')).toBeUndefined(); }); test('register is atomic for a batch', () => { - const registry = createPrimitiveRegistry(); + const registry = createSigningComponentRegistry(); registry.register([{ kind: 'hash', name: 'SHA256', code: 2 }]); - const batch: PrimitiveDefinition[] = [ + const batch: SigningComponentDefinition[] = [ { kind: 'hash', name: 'BLAKE2B', code: 4 }, { kind: 'hash', name: 'SHA256', code: 99 }, ]; - expect(() => registry.register(batch)).toThrow(PrimitiveConflictError); + expect(() => registry.register(batch)).toThrow( + SigningComponentConflictError, + ); expect(registry.resolve('hash', 'BLAKE2B')).toBeUndefined(); }); - test('resolveOrThrow throws for missing primitive', () => { - const registry = createPrimitiveRegistry(); + test('resolveOrThrow throws for missing signing component', () => { + const registry = createSigningComponentRegistry(); expect(() => registry.resolveOrThrow('hash', 'MISSING')).toThrow( - 'Primitive not found', + 'Signing component not found', ); }); test('reset clears registered mappings', () => { - const registry = createPrimitiveRegistry(); + const registry = createSigningComponentRegistry(); registry.register([{ kind: 'curve', name: 'SECP256K1', code: 0 }]); registry.reset(); diff --git a/packages/sdk/src/__test__/unit/primitives.test.ts b/packages/sdk/src/__test__/unit/signingComponents.test.ts similarity index 59% rename from packages/sdk/src/__test__/unit/primitives.test.ts rename to packages/sdk/src/__test__/unit/signingComponents.test.ts index acd6bcc9..2ffc920e 100644 --- a/packages/sdk/src/__test__/unit/primitives.test.ts +++ b/packages/sdk/src/__test__/unit/signingComponents.test.ts @@ -2,16 +2,16 @@ import type { ChainAdapter, ChainModule, ChainPlugin, - PluginPrimitives, + ChainSigningSuite, Signer, } from '@gridplus/chain-core'; import { - ensurePrimitivesSeeded, - getPrimitiveRegistry, - registerPluginPrimitives, - resetPrimitiveRegistry, - validatePluginPrimitiveRequirements, -} from '../../chains/primitives'; + ensureSigningComponentsSeeded, + getSigningComponentRegistry, + registerPluginSigningComponents, + resetSigningComponentRegistry, + validatePluginSigningRequirements, +} from '../../chains/signingComponents'; const mockSigner: Signer = { getAddress: async () => 'mock', @@ -28,7 +28,7 @@ const mockAdapter: ChainAdapter = { const buildPlugin = ( chainId: string, - primitives?: PluginPrimitives, + signingSuite?: ChainSigningSuite, ): ChainPlugin => { const module: ChainModule = { id: chainId, @@ -52,26 +52,26 @@ const buildPlugin = ( device: 'lattice', module, createSigner: async () => mockSigner, - primitives, + signingSuite, }; }; -describe('sdk primitives module', () => { +describe('sdk signing components module', () => { afterEach(() => { - resetPrimitiveRegistry(); + resetSigningComponentRegistry(); }); - test('ensurePrimitivesSeeded is idempotent and registers builtins', () => { - ensurePrimitivesSeeded(); - ensurePrimitivesSeeded(); + test('ensureSigningComponentsSeeded is idempotent and registers builtins', () => { + ensureSigningComponentsSeeded(); + ensureSigningComponentsSeeded(); - const registry = getPrimitiveRegistry(); + const registry = getSigningComponentRegistry(); expect(registry.resolve('hash', 'KECCAK256')).toBeDefined(); expect(registry.resolve('curve', 'SECP256K1')).toBeDefined(); expect(registry.resolve('encoding', 'EVM')).toBeDefined(); }); - test('registerPluginPrimitives registers custom definitions', () => { + test('registerPluginSigningComponents registers custom definitions', () => { const plugin = buildPlugin('testchain', { definitions: [{ kind: 'encoding', name: 'TESTCHAIN', code: 99 }], requirements: [ @@ -79,12 +79,14 @@ describe('sdk primitives module', () => { ], }); - registerPluginPrimitives(plugin); - expect(getPrimitiveRegistry().resolve('encoding', 'TESTCHAIN')).toBe(99); + registerPluginSigningComponents(plugin); + expect( + getSigningComponentRegistry().resolve('encoding', 'TESTCHAIN'), + ).toBe(99); }); - test('registerPluginPrimitives fails on conflicts without partial commit', () => { - registerPluginPrimitives( + test('registerPluginSigningComponents fails on conflicts without partial commit', () => { + registerPluginSigningComponents( buildPlugin('chain-a', { definitions: [{ kind: 'encoding', name: 'TESTCHAIN', code: 99 }], }), @@ -97,12 +99,14 @@ describe('sdk primitives module', () => { ], }); - expect(() => registerPluginPrimitives(conflicting)).toThrow(); - expect(getPrimitiveRegistry().resolve('hash', 'BLAKE2B')).toBeUndefined(); + expect(() => registerPluginSigningComponents(conflicting)).toThrow(); + expect( + getSigningComponentRegistry().resolve('hash', 'BLAKE2B'), + ).toBeUndefined(); }); - test('validatePluginPrimitiveRequirements passes when firmware requirement is met', () => { - ensurePrimitivesSeeded(); + test('validatePluginSigningRequirements passes when firmware requirement is met', () => { + ensureSigningComponentsSeeded(); const plugin = buildPlugin('cosmos-like', { requirements: [ { kind: 'encoding', name: 'COSMOS', minFirmware: [0, 18, 10] }, @@ -110,12 +114,12 @@ describe('sdk primitives module', () => { }); expect(() => - validatePluginPrimitiveRequirements(plugin, [0, 19, 0]), + validatePluginSigningRequirements(plugin, [0, 19, 0]), ).not.toThrow(); }); - test('validatePluginPrimitiveRequirements throws when firmware requirement is unmet', () => { - ensurePrimitivesSeeded(); + test('validatePluginSigningRequirements throws when firmware requirement is unmet', () => { + ensureSigningComponentsSeeded(); const plugin = buildPlugin('cosmos-like', { requirements: [ { kind: 'encoding', name: 'COSMOS', minFirmware: [0, 18, 10] }, @@ -123,11 +127,11 @@ describe('sdk primitives module', () => { }); expect(() => - validatePluginPrimitiveRequirements(plugin, [0, 18, 9]), + validatePluginSigningRequirements(plugin, [0, 18, 9]), ).toThrow('Please update firmware'); }); - test('validatePluginPrimitiveRequirements throws for missing primitive mapping', () => { + test('validatePluginSigningRequirements throws for missing signing component mapping', () => { const plugin = buildPlugin('custom-chain', { requirements: [ { @@ -139,21 +143,21 @@ describe('sdk primitives module', () => { }); expect(() => - validatePluginPrimitiveRequirements(plugin, [0, 20, 0]), + validatePluginSigningRequirements(plugin, [0, 20, 0]), ).toThrow('not registered'); }); test('rejects invalid requirement shape (fail-closed)', () => { const plugin = buildPlugin('invalid') as ChainPlugin; - (plugin as any).primitives = { + (plugin as any).signingSuite = { requirements: [{ kind: 'hash', name: 'SHA256' }], }; - expect(() => registerPluginPrimitives(plugin)).toThrow( - 'invalid primitive requirement', + expect(() => registerPluginSigningComponents(plugin)).toThrow( + 'invalid signing component requirement', ); expect(() => - validatePluginPrimitiveRequirements(plugin, [9, 9, 9]), - ).toThrow('invalid primitive requirement'); + validatePluginSigningRequirements(plugin, [9, 9, 9]), + ).toThrow('invalid signing component requirement'); }); }); diff --git a/packages/sdk/src/api/setup.ts b/packages/sdk/src/api/setup.ts index 8d9ac4ae..bfcd6c30 100644 --- a/packages/sdk/src/api/setup.ts +++ b/packages/sdk/src/api/setup.ts @@ -7,7 +7,7 @@ import { import { configureChainRuntime, discoverAndRegisterChains, - ensurePrimitivesSeeded, + ensureSigningComponentsSeeded, getChain, registerChainPlugin, unregisterChain, @@ -100,7 +100,7 @@ export const setup = async (params: SetupParameters): Promise => { if (!params.setStoredClient) throw new Error('Client data setter required'); setSaveClient(buildSaveClientFn(params.setStoredClient)); - ensurePrimitivesSeeded(); + ensureSigningComponentsSeeded(); configureChainRuntime({ autoRegisterChains: params.autoRegisterChains ?? true, defaultDevice: params.defaultDevice ?? 'lattice', diff --git a/packages/sdk/src/chains/context.ts b/packages/sdk/src/chains/context.ts index 1b938d3d..8cc81d2e 100644 --- a/packages/sdk/src/chains/context.ts +++ b/packages/sdk/src/chains/context.ts @@ -1,9 +1,15 @@ -import type { DeviceContext, PrimitiveKind } from '@gridplus/chain-core'; +import type { + DeviceContext, + SigningComponentKind, +} from '@gridplus/chain-core'; import { CURRENCIES } from '@gridplus/types'; import { getClient, queue } from '../api/utilities'; import { EXTERNAL } from '../constants'; import { fetchDecoder } from '../functions/fetchDecoder'; -import { ensurePrimitivesSeeded, getPrimitiveRegistry } from './primitives'; +import { + ensureSigningComponentsSeeded, + getSigningComponentRegistry, +} from './signingComponents'; export type SdkDeviceContext = DeviceContext & { constants: { @@ -13,10 +19,13 @@ export type SdkDeviceContext = DeviceContext & { services: { fetchDecoder: typeof fetchDecoder; }; - resolvePrimitive: (kind: PrimitiveKind, name: string) => number; + resolveSigningComponent: ( + kind: SigningComponentKind, + name: string, + ) => number; }; -// Bridges SDK runtime primitives into the generic chain-core DeviceContext shape. +// Bridges SDK runtime signing components into the generic chain-core DeviceContext shape. export const createDeviceContext = (): SdkDeviceContext => ({ queue, getClient, @@ -27,8 +36,8 @@ export const createDeviceContext = (): SdkDeviceContext => ({ services: { fetchDecoder, }, - resolvePrimitive: (kind, name) => { - ensurePrimitivesSeeded(); - return getPrimitiveRegistry().resolveOrThrow(kind, name); + resolveSigningComponent: (kind, name) => { + ensureSigningComponentsSeeded(); + return getSigningComponentRegistry().resolveOrThrow(kind, name); }, }); diff --git a/packages/sdk/src/chains/index.ts b/packages/sdk/src/chains/index.ts index f14dc574..cae33b66 100644 --- a/packages/sdk/src/chains/index.ts +++ b/packages/sdk/src/chains/index.ts @@ -10,11 +10,11 @@ export { useChain, } from './registry'; export { - ensurePrimitivesSeeded, - preflightPluginPrimitives, - registerPluginPrimitives, - validatePluginPrimitiveRequirements, -} from './primitives'; + ensureSigningComponentsSeeded, + preflightPluginSigningComponents, + registerPluginSigningComponents, + validatePluginSigningRequirements, +} from './signingComponents'; export type { ChainKey, @@ -22,12 +22,12 @@ export type { ChainRegistry, ChainRegistryOptions, ChainRegistryResolveOptions, + ChainSigningSuite, DeviceContext, DeviceId, - PluginPrimitives, - PrimitiveDefinition, - PrimitiveKind, - PrimitiveRequirement, + SigningComponentDefinition, + SigningComponentKind, + SigningComponentRequirement, } from '@gridplus/chain-core'; export type { SdkDeviceContext } from './context'; diff --git a/packages/sdk/src/chains/registry.ts b/packages/sdk/src/chains/registry.ts index 15e118e0..39d941fa 100644 --- a/packages/sdk/src/chains/registry.ts +++ b/packages/sdk/src/chains/registry.ts @@ -7,12 +7,12 @@ import { import { createDeviceContext, type SdkDeviceContext } from './context'; import { discoverAndRegisterChains as discoverChains } from './discovery'; import { - ensurePrimitivesSeeded, - preflightPluginPrimitives, - registerPluginPrimitives, - unregisterPluginPrimitives, - validatePluginPrimitiveRequirements, -} from './primitives'; + ensureSigningComponentsSeeded, + preflightPluginSigningComponents, + registerPluginSigningComponents, + unregisterPluginSigningComponents, + validatePluginSigningRequirements, +} from './signingComponents'; type ConfigureChainRuntimeOptions = { autoRegisterChains?: boolean; @@ -96,18 +96,18 @@ export function getDefaultDevice(): DeviceId { } export function registerChainPlugin(plugin: ChainPlugin): void { - ensurePrimitivesSeeded(); - preflightPluginPrimitives(plugin); + ensureSigningComponentsSeeded(); + preflightPluginSigningComponents(plugin); let chainRegistered = false; try { registry.register(plugin as ChainPlugin); chainRegistered = true; - registerPluginPrimitives(plugin); + registerPluginSigningComponents(plugin); } catch (err) { if (chainRegistered) { registry.unregister(plugin.chainId, plugin.device); - unregisterPluginPrimitives(plugin.chainId, plugin.device); + unregisterPluginSigningComponents(plugin.chainId, plugin.device); } throw err; } @@ -118,7 +118,7 @@ export function unregisterChain(chainId: string, device?: DeviceId): boolean { cache.clear(); const unregistered = registry.unregister(chainId, device); if (unregistered) { - unregisterPluginPrimitives(chainId, device); + unregisterPluginSigningComponents(chainId, device); } return unregistered; } @@ -177,10 +177,10 @@ export async function useChain( // Signer is created once per (chain, device) generation and reused by adapters. if (!cached || cached.generation !== cacheGeneration) { const context = createDeviceContext(); - if (resolved.primitives?.requirements?.length) { + if (resolved.signingSuite?.requirements?.length) { const client = await context.getClient(); const fwVersion = getFirmwareVersion(client); - validatePluginPrimitiveRequirements( + validatePluginSigningRequirements( resolved as ChainPlugin, fwVersion, ); diff --git a/packages/sdk/src/chains/primitives.ts b/packages/sdk/src/chains/signingComponents.ts similarity index 53% rename from packages/sdk/src/chains/primitives.ts rename to packages/sdk/src/chains/signingComponents.ts index 07e5b6dd..988d82f8 100644 --- a/packages/sdk/src/chains/primitives.ts +++ b/packages/sdk/src/chains/signingComponents.ts @@ -1,51 +1,54 @@ import { compareFirmwareVersions, - createPrimitiveRegistry, + createSigningComponentRegistry, toChainKey, type ChainPlugin, type DeviceId, type FirmwareVersionTuple, - type PrimitiveDefinition, - type PrimitiveRequirement, + type SigningComponentDefinition, + type SigningComponentRequirement, } from '@gridplus/chain-core'; import { EXTERNAL } from '../constants'; -const registry = createPrimitiveRegistry(); +const registry = createSigningComponentRegistry(); let seeded = false; -const pluginDefinitionsByKey = new Map(); +const pluginDefinitionsByKey = new Map(); -const getBuiltinPrimitiveDefinitions = (): PrimitiveDefinition[] => { - const definitions: PrimitiveDefinition[] = []; +const getBuiltinSigningComponentDefinitions = + (): SigningComponentDefinition[] => { + const definitions: SigningComponentDefinition[] = []; - Object.entries(EXTERNAL.SIGNING.HASHES).forEach(([name, code]) => { - definitions.push({ kind: 'hash', name, code }); - }); - Object.entries(EXTERNAL.SIGNING.CURVES).forEach(([name, code]) => { - definitions.push({ kind: 'curve', name, code }); - }); - Object.entries(EXTERNAL.SIGNING.ENCODINGS).forEach(([name, code]) => { - definitions.push({ kind: 'encoding', name, code }); - }); + Object.entries(EXTERNAL.SIGNING.HASHES).forEach(([name, code]) => { + definitions.push({ kind: 'hash', name, code }); + }); + Object.entries(EXTERNAL.SIGNING.CURVES).forEach(([name, code]) => { + definitions.push({ kind: 'curve', name, code }); + }); + Object.entries(EXTERNAL.SIGNING.ENCODINGS).forEach(([name, code]) => { + definitions.push({ kind: 'encoding', name, code }); + }); - return definitions; -}; + return definitions; + }; const cloneDefinitions = ( - definitions: PrimitiveDefinition[], -): PrimitiveDefinition[] => + definitions: SigningComponentDefinition[], +): SigningComponentDefinition[] => definitions.map((definition) => ({ kind: definition.kind, name: definition.name, code: definition.code, })); -const toPluginPrimitiveKey = (chainId: string, device: DeviceId): string => - toChainKey(chainId, device); +const toPluginSigningComponentKey = ( + chainId: string, + device: DeviceId, +): string => toChainKey(chainId, device); -const rebuildRegistryFromTrackedPrimitives = (): void => { +const rebuildRegistryFromTrackedComponents = (): void => { registry.reset(); seeded = false; - ensurePrimitivesSeeded(); + ensureSigningComponentsSeeded(); const sortedKeys = [...pluginDefinitionsByKey.keys()].sort(); sortedKeys.forEach((key) => { @@ -68,11 +71,11 @@ const isFirmwareVersionTuple = ( ); }; -const isPrimitiveRequirement = ( +const isSigningComponentRequirement = ( requirement: unknown, -): requirement is PrimitiveRequirement => { +): requirement is SigningComponentRequirement => { if (!requirement || typeof requirement !== 'object') return false; - const req = requirement as PrimitiveRequirement; + const req = requirement as SigningComponentRequirement; const kind = req.kind === 'hash' || req.kind === 'curve' || req.kind === 'encoding'; return ( @@ -83,64 +86,68 @@ const isPrimitiveRequirement = ( ); }; -const getPluginPrimitiveDefinitions = ( +const getPluginSigningComponentDefinitions = ( plugin: ChainPlugin, -): PrimitiveDefinition[] => { - const definitions = plugin.primitives?.definitions; +): SigningComponentDefinition[] => { + const definitions = plugin.signingSuite?.definitions; if (!definitions || !Array.isArray(definitions)) return []; return definitions; }; -const getPluginPrimitiveRequirements = ( +const getPluginSigningComponentRequirements = ( plugin: ChainPlugin, -): PrimitiveRequirement[] => { - const requirements = plugin.primitives?.requirements; +): SigningComponentRequirement[] => { + const requirements = plugin.signingSuite?.requirements; if (!requirements || !Array.isArray(requirements)) return []; - return requirements as PrimitiveRequirement[]; + return requirements as SigningComponentRequirement[]; }; const validatePluginRequirementShape = (plugin: ChainPlugin): void => { - const requirements = plugin.primitives?.requirements; + const requirements = plugin.signingSuite?.requirements; if (!requirements) return; if (!Array.isArray(requirements)) { throw new Error( - `Chain "${plugin.chainId}" has invalid primitive requirements: expected an array.`, + `Chain "${plugin.chainId}" has invalid signing component requirements: expected an array.`, ); } requirements.forEach((requirement, index) => { - if (!isPrimitiveRequirement(requirement)) { + if (!isSigningComponentRequirement(requirement)) { throw new Error( - `Chain "${plugin.chainId}" has invalid primitive requirement at index ${index}.`, + `Chain "${plugin.chainId}" has invalid signing component requirement at index ${index}.`, ); } }); }; -export function getPrimitiveRegistry() { +export function getSigningComponentRegistry() { return registry; } -export function ensurePrimitivesSeeded(): void { +export function ensureSigningComponentsSeeded(): void { if (seeded) return; - registry.register(getBuiltinPrimitiveDefinitions()); + registry.register(getBuiltinSigningComponentDefinitions()); seeded = true; } -export function preflightPluginPrimitives(plugin: ChainPlugin): void { +export function preflightPluginSigningComponents( + plugin: ChainPlugin, +): void { validatePluginRequirementShape(plugin); - ensurePrimitivesSeeded(); + ensureSigningComponentsSeeded(); - const definitions = getPluginPrimitiveDefinitions(plugin); + const definitions = getPluginSigningComponentDefinitions(plugin); if (definitions.length === 0) return; registry.preflight(definitions); } -export function registerPluginPrimitives(plugin: ChainPlugin): void { +export function registerPluginSigningComponents( + plugin: ChainPlugin, +): void { validatePluginRequirementShape(plugin); - ensurePrimitivesSeeded(); + ensureSigningComponentsSeeded(); - const definitions = getPluginPrimitiveDefinitions(plugin); - const pluginKey = toPluginPrimitiveKey(plugin.chainId, plugin.device); + const definitions = getPluginSigningComponentDefinitions(plugin); + const pluginKey = toPluginSigningComponentKey(plugin.chainId, plugin.device); if (definitions.length === 0) { pluginDefinitionsByKey.delete(pluginKey); return; @@ -149,14 +156,14 @@ export function registerPluginPrimitives(plugin: ChainPlugin): void { pluginDefinitionsByKey.set(pluginKey, cloneDefinitions(definitions)); } -export function unregisterPluginPrimitives( +export function unregisterPluginSigningComponents( chainId: string, device?: DeviceId, ): void { if (!seeded) return; const keysToDelete = device - ? [toPluginPrimitiveKey(chainId, device)] + ? [toPluginSigningComponentKey(chainId, device)] : [...pluginDefinitionsByKey.keys()].filter((key) => key.startsWith(`${chainId}:`), ); @@ -164,17 +171,17 @@ export function unregisterPluginPrimitives( if (keysToDelete.length === 0) return; keysToDelete.forEach((key) => pluginDefinitionsByKey.delete(key)); - rebuildRegistryFromTrackedPrimitives(); + rebuildRegistryFromTrackedComponents(); } -export function validatePluginPrimitiveRequirements( +export function validatePluginSigningRequirements( plugin: ChainPlugin, fwVersion: FirmwareVersionTuple, ): void { validatePluginRequirementShape(plugin); - ensurePrimitivesSeeded(); + ensureSigningComponentsSeeded(); - const requirements = getPluginPrimitiveRequirements(plugin); + const requirements = getPluginSigningComponentRequirements(plugin); if (requirements.length === 0) return; requirements.forEach((requirement) => { @@ -194,7 +201,7 @@ export function validatePluginPrimitiveRequirements( }); } -export function resetPrimitiveRegistry(): void { +export function resetSigningComponentRegistry(): void { registry.reset(); pluginDefinitionsByKey.clear(); seeded = false; diff --git a/packages/types/src/firmware.ts b/packages/types/src/firmware.ts index ba509e1c..527211da 100644 --- a/packages/types/src/firmware.ts +++ b/packages/types/src/firmware.ts @@ -26,13 +26,11 @@ export interface GenericSigningData { KECCAK256: typeof LatticeSignHash.keccak256; SHA256: typeof LatticeSignHash.sha256; SHA512HALF?: typeof LatticeSignHash.sha512half; - [key: string]: number | undefined; }; curveTypes: { SECP256K1: typeof LatticeSignCurve.secp256k1; ED25519: typeof LatticeSignCurve.ed25519; BLS12_381_G2: typeof LatticeSignCurve.bls12_381; - [key: string]: number | undefined; }; encodingTypes: { NONE: typeof LatticeSignEncoding.none; @@ -40,7 +38,6 @@ export interface GenericSigningData { COSMOS?: typeof LatticeSignEncoding.cosmos; EVM?: typeof LatticeSignEncoding.evm; XRP?: typeof LatticeSignEncoding.xrp; - [key: string]: number | undefined; }; } From ef3f2ad99044f1f2983386eb56fb3f97939bf667 Mon Sep 17 00:00:00 2001 From: baha Date: Wed, 4 Mar 2026 23:05:56 +0300 Subject: [PATCH 6/6] fix: lint --- .../src/signingComponentRegistry.ts | 18 ++++---------- packages/chains/cosmos/src/devices/lattice.ts | 15 +++++++++--- packages/chains/evm/src/devices/lattice.ts | 15 +++++++++--- packages/chains/solana/src/devices/lattice.ts | 15 +++++++++--- packages/chains/xrp/src/devices/lattice.ts | 18 ++++++++++---- .../sdk/src/__test__/unit/context.test.ts | 12 +++------- .../__test__/unit/signingComponents.test.ts | 24 +++++++++---------- packages/sdk/src/chains/context.ts | 10 ++------ 8 files changed, 71 insertions(+), 56 deletions(-) diff --git a/packages/chains/chain-core/src/signingComponentRegistry.ts b/packages/chains/chain-core/src/signingComponentRegistry.ts index 98c1bb93..53e38023 100644 --- a/packages/chains/chain-core/src/signingComponentRegistry.ts +++ b/packages/chains/chain-core/src/signingComponentRegistry.ts @@ -26,9 +26,7 @@ const createCodeMaps = (): SigningComponentReverseDefinitionMap => ({ encoding: new Map(), }); -const normalizeSigningComponentKind = ( - kind: unknown, -): SigningComponentKind => { +const normalizeSigningComponentKind = (kind: unknown): SigningComponentKind => { if (kind === 'hash' || kind === 'curve' || kind === 'encoding') { return kind; } @@ -101,10 +99,7 @@ export class SigningComponentConflictError extends Error { export type SigningComponentRegistry = { register: (definitions: SigningComponentDefinition[]) => void; preflight: (definitions: SigningComponentDefinition[]) => void; - resolve: ( - kind: SigningComponentKind, - name: string, - ) => number | undefined; + resolve: (kind: SigningComponentKind, name: string) => number | undefined; resolveOrThrow: (kind: SigningComponentKind, name: string) => number; reverseResolve: ( kind: SigningComponentKind, @@ -179,10 +174,7 @@ export function createSigningComponentRegistry(): SigningComponentRegistry { return nameToCode[kind].get(normalizeSigningComponentName(name)); }; - const resolveOrThrow = ( - kind: SigningComponentKind, - name: string, - ): number => { + const resolveOrThrow = (kind: SigningComponentKind, name: string): number => { const code = resolve(kind, name); if (code === undefined) { throw new Error(`Signing component not found: ${kind}:${name}`); @@ -201,9 +193,7 @@ export function createSigningComponentRegistry(): SigningComponentRegistry { return resolve(kind, name) !== undefined; }; - const list = ( - kind?: SigningComponentKind, - ): SigningComponentDefinition[] => { + const list = (kind?: SigningComponentKind): SigningComponentDefinition[] => { const definitions: SigningComponentDefinition[] = []; const kinds = kind ? [kind] : SIGNING_COMPONENT_KINDS; diff --git a/packages/chains/cosmos/src/devices/lattice.ts b/packages/chains/cosmos/src/devices/lattice.ts index 75e2eeb2..dac94f6b 100644 --- a/packages/chains/cosmos/src/devices/lattice.ts +++ b/packages/chains/cosmos/src/devices/lattice.ts @@ -23,7 +23,10 @@ type SigningComponentCodes = { }; type LatticeCosmosContextInput = DeviceContext & { - resolveSigningComponent?: (kind: SigningComponentKind, name: string) => number; + resolveSigningComponent?: ( + kind: SigningComponentKind, + name: string, + ) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { @@ -47,7 +50,10 @@ const getSigningComponentFromConstants = ( kind: SigningComponentKind, name: string, ): number | undefined => { - const byKind: Record | undefined> = { + const byKind: Record< + SigningComponentKind, + Record | undefined + > = { hash: signing?.HASHES, curve: signing?.CURVES, encoding: signing?.ENCODINGS, @@ -59,7 +65,10 @@ const getSigningComponentFromConstants = ( function getLatticeCosmosContext(context: DeviceContext): LatticeCosmosContext { const typed = context as LatticeCosmosContextInput; const constants = typed.constants; - const resolveSigningComponent = (kind: SigningComponentKind, name: string): number => { + const resolveSigningComponent = ( + kind: SigningComponentKind, + name: string, + ): number => { if (typeof typed.resolveSigningComponent === 'function') { return typed.resolveSigningComponent(kind, name); } diff --git a/packages/chains/evm/src/devices/lattice.ts b/packages/chains/evm/src/devices/lattice.ts index 81e1e4c0..e12da7cb 100644 --- a/packages/chains/evm/src/devices/lattice.ts +++ b/packages/chains/evm/src/devices/lattice.ts @@ -43,7 +43,10 @@ type SigningComponentCodes = { }; type LatticeEvmContextInput = DeviceContext & { - resolveSigningComponent?: (kind: SigningComponentKind, name: string) => number; + resolveSigningComponent?: ( + kind: SigningComponentKind, + name: string, + ) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { @@ -78,7 +81,10 @@ const getSigningComponentFromConstants = ( kind: SigningComponentKind, name: string, ): number | undefined => { - const byKind: Record | undefined> = { + const byKind: Record< + SigningComponentKind, + Record | undefined + > = { hash: signing?.HASHES, curve: signing?.CURVES, encoding: signing?.ENCODINGS, @@ -90,7 +96,10 @@ const getSigningComponentFromConstants = ( function getLatticeEvmContext(context: DeviceContext): LatticeEvmContext { const typed = context as LatticeEvmContextInput; const constants = typed.constants; - const resolveSigningComponent = (kind: SigningComponentKind, name: string): number => { + const resolveSigningComponent = ( + kind: SigningComponentKind, + name: string, + ): number => { if (typeof typed.resolveSigningComponent === 'function') { return typed.resolveSigningComponent(kind, name); } diff --git a/packages/chains/solana/src/devices/lattice.ts b/packages/chains/solana/src/devices/lattice.ts index 5e7abeba..0d42b35a 100644 --- a/packages/chains/solana/src/devices/lattice.ts +++ b/packages/chains/solana/src/devices/lattice.ts @@ -24,7 +24,10 @@ type SigningComponentCodes = { }; type LatticeSolanaContextInput = DeviceContext & { - resolveSigningComponent?: (kind: SigningComponentKind, name: string) => number; + resolveSigningComponent?: ( + kind: SigningComponentKind, + name: string, + ) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { @@ -48,7 +51,10 @@ const getSigningComponentFromConstants = ( kind: SigningComponentKind, name: string, ): number | undefined => { - const byKind: Record | undefined> = { + const byKind: Record< + SigningComponentKind, + Record | undefined + > = { hash: signing?.HASHES, curve: signing?.CURVES, encoding: signing?.ENCODINGS, @@ -60,7 +66,10 @@ const getSigningComponentFromConstants = ( function getLatticeSolanaContext(context: DeviceContext): LatticeSolanaContext { const typed = context as LatticeSolanaContextInput; const constants = typed.constants; - const resolveSigningComponent = (kind: SigningComponentKind, name: string): number => { + const resolveSigningComponent = ( + kind: SigningComponentKind, + name: string, + ): number => { if (typeof typed.resolveSigningComponent === 'function') { return typed.resolveSigningComponent(kind, name); } diff --git a/packages/chains/xrp/src/devices/lattice.ts b/packages/chains/xrp/src/devices/lattice.ts index 268ab8a8..4dd4aba0 100644 --- a/packages/chains/xrp/src/devices/lattice.ts +++ b/packages/chains/xrp/src/devices/lattice.ts @@ -28,7 +28,10 @@ type SigningComponentCodes = { }; type LatticeXrpContextInput = DeviceContext & { - resolveSigningComponent?: (kind: SigningComponentKind, name: string) => number; + resolveSigningComponent?: ( + kind: SigningComponentKind, + name: string, + ) => number; constants: { EXTERNAL: { GET_ADDR_FLAGS: { @@ -52,7 +55,10 @@ const getSigningComponentFromConstants = ( kind: SigningComponentKind, name: string, ): number | undefined => { - const byKind: Record | undefined> = { + const byKind: Record< + SigningComponentKind, + Record | undefined + > = { hash: signing?.HASHES, curve: signing?.CURVES, encoding: signing?.ENCODINGS, @@ -64,7 +70,10 @@ const getSigningComponentFromConstants = ( function getLatticeXrpContext(context: DeviceContext): LatticeXrpContext { const typed = context as LatticeXrpContextInput; const constants = typed.constants; - const resolveSigningComponent = (kind: SigningComponentKind, name: string): number => { + const resolveSigningComponent = ( + kind: SigningComponentKind, + name: string, + ): number => { if (typeof typed.resolveSigningComponent === 'function') { return typed.resolveSigningComponent(kind, name); } @@ -90,7 +99,8 @@ function getLatticeXrpContext(context: DeviceContext): LatticeXrpContext { } export function createLatticeXrpSigner(context: DeviceContext): XrpSigner { - const { queue, resolveSigningComponent, constants } = getLatticeXrpContext(context); + const { queue, resolveSigningComponent, constants } = + getLatticeXrpContext(context); const { EXTERNAL } = constants; const curveSecp256k1 = resolveSigningComponent('curve', 'SECP256K1'); const hashSha512Half = resolveSigningComponent('hash', 'SHA512HALF'); diff --git a/packages/sdk/src/__test__/unit/context.test.ts b/packages/sdk/src/__test__/unit/context.test.ts index 205bd112..8f71d259 100644 --- a/packages/sdk/src/__test__/unit/context.test.ts +++ b/packages/sdk/src/__test__/unit/context.test.ts @@ -8,15 +8,9 @@ describe('chain context signing component resolution', () => { test('resolveSigningComponent resolves seeded builtins', () => { const context = createDeviceContext(); - expect( - context.resolveSigningComponent('hash', 'KECCAK256'), - ).toBeDefined(); - expect( - context.resolveSigningComponent('curve', 'SECP256K1'), - ).toBeDefined(); - expect( - context.resolveSigningComponent('encoding', 'EVM'), - ).toBeDefined(); + expect(context.resolveSigningComponent('hash', 'KECCAK256')).toBeDefined(); + expect(context.resolveSigningComponent('curve', 'SECP256K1')).toBeDefined(); + expect(context.resolveSigningComponent('encoding', 'EVM')).toBeDefined(); }); test('resolveSigningComponent throws for unknown signing component', () => { diff --git a/packages/sdk/src/__test__/unit/signingComponents.test.ts b/packages/sdk/src/__test__/unit/signingComponents.test.ts index 2ffc920e..3bc3e995 100644 --- a/packages/sdk/src/__test__/unit/signingComponents.test.ts +++ b/packages/sdk/src/__test__/unit/signingComponents.test.ts @@ -80,9 +80,9 @@ describe('sdk signing components module', () => { }); registerPluginSigningComponents(plugin); - expect( - getSigningComponentRegistry().resolve('encoding', 'TESTCHAIN'), - ).toBe(99); + expect(getSigningComponentRegistry().resolve('encoding', 'TESTCHAIN')).toBe( + 99, + ); }); test('registerPluginSigningComponents fails on conflicts without partial commit', () => { @@ -126,9 +126,9 @@ describe('sdk signing components module', () => { ], }); - expect(() => - validatePluginSigningRequirements(plugin, [0, 18, 9]), - ).toThrow('Please update firmware'); + expect(() => validatePluginSigningRequirements(plugin, [0, 18, 9])).toThrow( + 'Please update firmware', + ); }); test('validatePluginSigningRequirements throws for missing signing component mapping', () => { @@ -142,9 +142,9 @@ describe('sdk signing components module', () => { ], }); - expect(() => - validatePluginSigningRequirements(plugin, [0, 20, 0]), - ).toThrow('not registered'); + expect(() => validatePluginSigningRequirements(plugin, [0, 20, 0])).toThrow( + 'not registered', + ); }); test('rejects invalid requirement shape (fail-closed)', () => { @@ -156,8 +156,8 @@ describe('sdk signing components module', () => { expect(() => registerPluginSigningComponents(plugin)).toThrow( 'invalid signing component requirement', ); - expect(() => - validatePluginSigningRequirements(plugin, [9, 9, 9]), - ).toThrow('invalid signing component requirement'); + expect(() => validatePluginSigningRequirements(plugin, [9, 9, 9])).toThrow( + 'invalid signing component requirement', + ); }); }); diff --git a/packages/sdk/src/chains/context.ts b/packages/sdk/src/chains/context.ts index 8cc81d2e..78d7d139 100644 --- a/packages/sdk/src/chains/context.ts +++ b/packages/sdk/src/chains/context.ts @@ -1,7 +1,4 @@ -import type { - DeviceContext, - SigningComponentKind, -} from '@gridplus/chain-core'; +import type { DeviceContext, SigningComponentKind } from '@gridplus/chain-core'; import { CURRENCIES } from '@gridplus/types'; import { getClient, queue } from '../api/utilities'; import { EXTERNAL } from '../constants'; @@ -19,10 +16,7 @@ export type SdkDeviceContext = DeviceContext & { services: { fetchDecoder: typeof fetchDecoder; }; - resolveSigningComponent: ( - kind: SigningComponentKind, - name: string, - ) => number; + resolveSigningComponent: (kind: SigningComponentKind, name: string) => number; }; // Bridges SDK runtime signing components into the generic chain-core DeviceContext shape.