From ff9210e005f3bb4e1764da5ba5731c243a55db57 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 22 Jul 2026 14:01:36 -0400 Subject: [PATCH] feat(sdk-core): add typed sysio.chains registry client --- CLAUDE.md | 1 + packages/sdk-core/README.md | 24 ++- .../src/contracts/sysio/chains/Actions.ts | 64 +++++++ .../src/contracts/sysio/chains/Client.ts | 166 ++++++++++++++++++ .../src/contracts/sysio/chains/Constants.ts | 8 + .../src/contracts/sysio/chains/Descriptor.ts | 43 +++++ .../src/contracts/sysio/chains/Slug.ts | 34 ++++ .../src/contracts/sysio/chains/Structs.ts | 35 ++++ .../src/contracts/sysio/chains/Types.ts | 87 +++++++++ .../src/contracts/sysio/chains/index.ts | 7 + .../sdk-core/src/contracts/sysio/index.ts | 3 + .../contracts/sysio/chains/Actions.test.ts | 72 ++++++++ .../contracts/sysio/chains/Client.test.ts | 134 ++++++++++++++ .../tests/contracts/sysio/chains/Slug.test.ts | 23 +++ 14 files changed, 700 insertions(+), 1 deletion(-) create mode 100644 packages/sdk-core/src/contracts/sysio/chains/Actions.ts create mode 100644 packages/sdk-core/src/contracts/sysio/chains/Client.ts create mode 100644 packages/sdk-core/src/contracts/sysio/chains/Constants.ts create mode 100644 packages/sdk-core/src/contracts/sysio/chains/Descriptor.ts create mode 100644 packages/sdk-core/src/contracts/sysio/chains/Slug.ts create mode 100644 packages/sdk-core/src/contracts/sysio/chains/Structs.ts create mode 100644 packages/sdk-core/src/contracts/sysio/chains/Types.ts create mode 100644 packages/sdk-core/src/contracts/sysio/chains/index.ts create mode 100644 packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts create mode 100644 packages/sdk-core/tests/contracts/sysio/chains/Client.test.ts create mode 100644 packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 6c0b526..68777bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -209,6 +209,7 @@ All generated or modified code **must** include JSDoc comments (`/** ... */`), c - The `fix-hybrid-output.mjs` script must run after every build of hybrid packages or ESM imports will break in Node - Keep `AccountObject.created` optional: valid system-account responses may omit it. - `packages/sdk-core/src/contracts/sysio/authex` owns `sysio.authex` action builders, link table reads, and create-link proof helpers. Consumers should not duplicate the `createlink` message/signature rules locally. +- `packages/sdk-core/src/contracts/sysio/chains` owns `sysio.chains` registry reads, chain-code normalization, lifecycle filtering, and privileged action construction. Treat registry rows as protocol identity/activation data; RPCs, explorers, icons, wallets, and application capability metadata remain consumer concerns. - AuthEx `links` reads use wire-sysio KV `index_name` queries with JSON-encoded bounds. Preserve KV row unwrapping, generated enum-name normalization, and compressed/uncompressed EM key equivalence. - `packages/sdk-core/src/contracts/sysio/reserv` owns public `sysio.reserv` registry reads, normalized rows, matching, rewards, and read-only quote helpers. External-chain reserve custody belongs in the ABI/IDL-owning chain SDK. - `wallet-browser-ext` uses a global shim to avoid `new Function()` restrictions in Chrome MV3 diff --git a/packages/sdk-core/README.md b/packages/sdk-core/README.md index 66fbb58..1c6f1c0 100644 --- a/packages/sdk-core/README.md +++ b/packages/sdk-core/README.md @@ -57,7 +57,29 @@ const links = await authex.getLinks("alice") Current wire-sysio nodes expose `links` as a KV table. `AuthexClient` uses the deployed named indexes and JSON bounds, unwraps KV rows through `ChainAPI`, normalizes generated enum-name responses, and treats compressed/uncompressed EM public-key renderings as the same external key. -The system contract descriptor registry includes `authex`, `msig`, and `reserv`. +The system contract descriptor registry includes `authex`, `chains`, `msig`, and `reserv`. + +## Chain registry + +`contracts.sysio.chains` exposes the active Wire chain registry through the +same typed proxy used by the other system-contract facades. `ChainsClient` +normalizes packed chain codes, enum-name responses, lifecycle timestamps, and +depot state while preserving the generated row for lower-level consumers. + +```ts +const chains = new contracts.sysio.chains.ChainsClient({ client: api }) +const activeOutposts = await chains.listChains({ + activeOnly: true, + includeDepot: false +}) + +const proxy = contracts.sysio.createClient({ client: api, name: "chains" }) +const rows = await proxy.tables.chains.rows() +``` + +The on-chain registry is authoritative for protocol identity and activation. +RPC URLs, explorers, icons, wallet adapters, and application capabilities stay +in the consuming application's runtime configuration. ## Reserves diff --git a/packages/sdk-core/src/contracts/sysio/chains/Actions.ts b/packages/sdk-core/src/contracts/sysio/chains/Actions.ts new file mode 100644 index 0000000..102a015 --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/chains/Actions.ts @@ -0,0 +1,64 @@ +import type { Action } from "../../../chain/Action.js" +import type * as SysioContracts from "../../../types/SysioContractTypes.js" +import { buildContractAction } from "../../Contract.js" + +import { DEFAULT_CHAINS_CONTRACT, MAX_EXTERNAL_CHAIN_ID } from "./Constants.js" +import { descriptor } from "./Descriptor.js" +import { chainSlugData } from "./Slug.js" +import type { + ChainRegistration, + CreateActivateChainActionOptions, + CreateRegisterChainActionOptions +} from "./Types.js" + +function assertExternalChainId(value: number): number { + if (!Number.isInteger(value) || value < 0 || value > MAX_EXTERNAL_CHAIN_ID) { + throw new Error("External chain ID must be an unsigned 32-bit integer.") + } + + return value +} + +/** Creates generated action data for `sysio.chains::regchain`. */ +export function createRegisterChainActionData( + registration: ChainRegistration +): SysioContracts.SysioChainsRegchainAction { + return { + kind: registration.kind, + code: chainSlugData(registration.code), + external_chain_id: assertExternalChainId(registration.externalChainId), + name: registration.name, + description: registration.description + } +} + +/** Creates an unsigned privileged `sysio.chains::regchain` action. */ +export function createRegisterChainAction( + options: CreateRegisterChainActionOptions +): Action { + return buildContractAction({ + contract: options.contract || DEFAULT_CHAINS_CONTRACT, + descriptor: descriptor.actions.regchain, + authorization: options.authorization, + data: createRegisterChainActionData(options.registration) + }) +} + +/** Creates generated action data for `sysio.chains::activchain`. */ +export function createActivateChainActionData( + code: CreateActivateChainActionOptions["code"] +): SysioContracts.SysioChainsActivchainAction { + return { code: chainSlugData(code) } +} + +/** Creates an unsigned privileged `sysio.chains::activchain` action. */ +export function createActivateChainAction( + options: CreateActivateChainActionOptions +): Action { + return buildContractAction({ + contract: options.contract || DEFAULT_CHAINS_CONTRACT, + descriptor: descriptor.actions.activchain, + authorization: options.authorization, + data: createActivateChainActionData(options.code) + }) +} diff --git a/packages/sdk-core/src/contracts/sysio/chains/Client.ts b/packages/sdk-core/src/contracts/sysio/chains/Client.ts new file mode 100644 index 0000000..33ef569 --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/chains/Client.ts @@ -0,0 +1,166 @@ +import type { APIClient } from "../../../api/Client.js" +import type { Action } from "../../../chain/Action.js" +import type { NameType } from "../../../chain/Name.js" +import { SysioChainsChainkind } from "../../../types/SysioContractTypes.js" +import type * as SysioContracts from "../../../types/SysioContractTypes.js" +import { + ContractClient, + createContractClient, + type ContractTableRowsOptions +} from "../../Contract.js" + +import { + createActivateChainAction, + createRegisterChainAction +} from "./Actions.js" +import { + DEFAULT_CHAINS_CONTRACT, + DEFAULT_CHAIN_QUERY_LIMIT +} from "./Constants.js" +import { + descriptor, + type SysioChainsActionData, + type SysioChainsTableRows +} from "./Descriptor.js" +import { chainSlugString, chainSlugValue } from "./Slug.js" +import type { + ChainRecord, + ChainSlugName, + ChainsClientOptions, + CreateActivateChainActionOptions, + CreateRegisterChainActionOptions, + ListChainsOptions +} from "./Types.js" + +function chainKindValue( + value: SysioContracts.SysioChainsChainRowType["kind"] +): SysioContracts.SysioChainsChainkind { + if (typeof value === "number") return value + + const serialized = String(value) + if (/^[0-9]+$/.test(serialized)) { + return Number(serialized) as SysioContracts.SysioChainsChainkind + } + + const mapped = SysioChainsChainkind[value] + if (mapped == null) { + throw new Error(`Unknown chain kind: ${serialized}`) + } + + return Number(mapped) as SysioContracts.SysioChainsChainkind +} + +function rowSlugValue(value: SysioContracts.SysioChainsSlugNameType): number { + return chainSlugValue(value.value) +} + +/** Normalizes a generated chain registry row into application-friendly values. */ +export function normalizeChainRow( + row: SysioContracts.SysioChainsChainRowType +): ChainRecord { + const codeValue = rowSlugValue(row.code) + + return { + code: chainSlugString(codeValue), + codeValue, + kind: chainKindValue(row.kind), + externalChainId: row.external_chain_id, + name: row.name, + description: row.description, + isDepot: row.is_depot, + active: row.active, + registeredAtMs: BigInt(row.registered_at_ms.toString()), + activatedAtMs: BigInt(row.activated_at_ms.toString()), + raw: row + } +} + +/** UI-neutral client for `sysio.chains` discovery and privileged action creation. */ +export class ChainsClient { + /** Chain API client used for RPC calls. */ + readonly client: APIClient + + /** Chain registry contract account. */ + readonly contract: NameType + + /** Generic typed proxy for direct action and table access. */ + readonly contractClient: ContractClient< + SysioChainsActionData, + SysioChainsTableRows + > + + /** Creates a chain registry client. */ + constructor(config: ChainsClientOptions) { + this.client = config.client + this.contract = config.contract || DEFAULT_CHAINS_CONTRACT + this.contractClient = createContractClient({ + client: config.client, + contract: this.contract, + descriptor + }) + } + + /** Creates an unsigned privileged chain registration action. */ + createRegisterChainAction( + options: Omit + ): Action { + return createRegisterChainAction({ ...options, contract: this.contract }) + } + + /** Creates an unsigned privileged chain activation action. */ + createActivateChainAction( + options: Omit + ): Action { + return createActivateChainAction({ ...options, contract: this.contract }) + } + + /** Lists raw generated chain rows, scanning additional KV pages as needed. */ + async listChainRows( + options: ListChainsOptions = {} + ): Promise { + const limit = Math.max( + Math.floor(options.limit || DEFAULT_CHAIN_QUERY_LIMIT), + 1 + ), + rows: SysioContracts.SysioChainsChainRowType[] = [] + let lowerBound: string | undefined + + do { + const query: ContractTableRowsOptions = { + limit: DEFAULT_CHAIN_QUERY_LIMIT, + ...(lowerBound ? { lower_bound: lowerBound } : {}) + }, + result = await this.contractClient.tables.chains.rows(query), + matches = result.rows.filter(row => { + const kindMatches = + options.kind == null || chainKindValue(row.kind) === options.kind, + activeMatches = !options.activeOnly || row.active, + depotMatches = options.includeDepot !== false || !row.is_depot + + return kindMatches && activeMatches && depotMatches + }) + + rows.push(...matches.slice(0, limit - rows.length)) + + const nextKey = result.more ? result.next_key : undefined + if (!nextKey || String(nextKey) === lowerBound) break + lowerBound = String(nextKey) + } while (rows.length < limit) + + return rows + } + + /** Lists normalized registered chains with optional lifecycle/family filtering. */ + async listChains(options: ListChainsOptions = {}): Promise { + return (await this.listChainRows(options)).map(normalizeChainRow) + } + + /** Reads one registered chain by exact chain code, or null when absent. */ + async getChain(code: ChainSlugName): Promise { + const codeValue = chainSlugValue(code), + rows = await this.listChainRows({ limit: Number.MAX_SAFE_INTEGER }), + row = rows.find(candidate => rowSlugValue(candidate.code) === codeValue) + + return row ? normalizeChainRow(row) : null + } +} diff --git a/packages/sdk-core/src/contracts/sysio/chains/Constants.ts b/packages/sdk-core/src/contracts/sysio/chains/Constants.ts new file mode 100644 index 0000000..76d0f9c --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/chains/Constants.ts @@ -0,0 +1,8 @@ +/** Default account that hosts the Wire chain registry. */ +export const DEFAULT_CHAINS_CONTRACT = "sysio.chains" + +/** Rows requested per page while scanning the chain registry. */ +export const DEFAULT_CHAIN_QUERY_LIMIT = 500 + +/** Largest external chain identifier accepted by the uint32 contract field. */ +export const MAX_EXTERNAL_CHAIN_ID = 0xffffffff diff --git a/packages/sdk-core/src/contracts/sysio/chains/Descriptor.ts b/packages/sdk-core/src/contracts/sysio/chains/Descriptor.ts new file mode 100644 index 0000000..5a9b464 --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/chains/Descriptor.ts @@ -0,0 +1,43 @@ +import type { ContractDescriptor } from "../../Contract.js" +import type * as SysioContracts from "../../../types/SysioContractTypes.js" + +import { DEFAULT_CHAINS_CONTRACT } from "./Constants.js" +import { ChainsActivateChain, ChainsRegisterChain } from "./Structs.js" + +/** Generated `sysio.chains` action data keyed by ABI action name. */ +export interface SysioChainsActionData { + /** `sysio.chains::regchain` action data. */ + regchain: SysioContracts.SysioChainsRegchainAction + /** `sysio.chains::activchain` action data. */ + activchain: SysioContracts.SysioChainsActivchainAction +} + +/** Generated `sysio.chains` table rows keyed by ABI table name. */ +export interface SysioChainsTableRows { + /** `sysio.chains::chains` registry row. */ + chains: SysioContracts.SysioChainsChainRowType +} + +/** Runtime descriptor for the public `sysio.chains` integration surface. */ +export const descriptor: ContractDescriptor< + SysioChainsActionData, + SysioChainsTableRows +> = { + account: DEFAULT_CHAINS_CONTRACT, + actions: { + regchain: { + name: "regchain", + serialize: data => ChainsRegisterChain.from(data) + }, + activchain: { + name: "activchain", + serialize: data => ChainsActivateChain.from(data) + } + }, + tables: { + chains: { + name: "chains", + rowType: null + } + } +} diff --git a/packages/sdk-core/src/contracts/sysio/chains/Slug.ts b/packages/sdk-core/src/contracts/sysio/chains/Slug.ts new file mode 100644 index 0000000..bd5b1de --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/chains/Slug.ts @@ -0,0 +1,34 @@ +import { SlugName } from "../../../SlugName.js" +import type * as SysioContracts from "../../../types/SysioContractTypes.js" + +import type { ChainSlugName } from "./Types.js" + +/** Converts a friendly chain slug or packed value to its safe numeric form. */ +export function chainSlugValue(value: ChainSlugName): number { + const numericString = typeof value === "string" && /^[0-9]+$/.test(value), + packed = numericString + ? Number(value) + : typeof value === "string" + ? SlugName.from(value) + : Number(value) + + if (!Number.isSafeInteger(packed) || packed <= 0) { + throw new Error( + "Chain slug must be a non-zero safe integer or valid slug_name string." + ) + } + + return packed +} + +/** Converts a friendly chain slug to generated `slug_name` action data. */ +export function chainSlugData( + value: ChainSlugName +): SysioContracts.SysioChainsSlugNameType { + return { value: chainSlugValue(value) } +} + +/** Returns the display form of a packed chain slug. */ +export function chainSlugString(value: ChainSlugName): string { + return SlugName.toString(chainSlugValue(value)) +} diff --git a/packages/sdk-core/src/contracts/sysio/chains/Structs.ts b/packages/sdk-core/src/contracts/sysio/chains/Structs.ts new file mode 100644 index 0000000..343dd5b --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/chains/Structs.ts @@ -0,0 +1,35 @@ +import { Int32, UInt32, UInt64 } from "../../../chain/Integer.js" +import { Struct } from "../../../chain/Struct.js" + +/** Runtime serializer for the Wire `slug_name` wrapper used by `sysio.chains`. */ +@Struct.type("slug_name") +export class ChainsSlugName extends Struct { + /** Packed eight-character chain code. */ + @Struct.field("uint64") declare value: UInt64 +} + +/** Runtime serializer for `sysio.chains::regchain`. */ +@Struct.type("regchain") +export class ChainsRegisterChain extends Struct { + /** VM/signing family from `ChainKind`. */ + @Struct.field("int32") declare kind: Int32 + + /** Stable protocol chain code. */ + @Struct.field(ChainsSlugName) declare code: ChainsSlugName + + /** External numeric chain identifier. */ + @Struct.field("uint32") declare external_chain_id: UInt32 + + /** Human-readable chain name. */ + @Struct.field("string") declare name: string + + /** Human-readable chain description. */ + @Struct.field("string") declare description: string +} + +/** Runtime serializer for `sysio.chains::activchain`. */ +@Struct.type("activchain") +export class ChainsActivateChain extends Struct { + /** Stable protocol chain code to activate. */ + @Struct.field(ChainsSlugName) declare code: ChainsSlugName +} diff --git a/packages/sdk-core/src/contracts/sysio/chains/Types.ts b/packages/sdk-core/src/contracts/sysio/chains/Types.ts new file mode 100644 index 0000000..199e7dc --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/chains/Types.ts @@ -0,0 +1,87 @@ +import type { NameType } from "../../../chain/Name.js" +import type { ContractPermissionLevel } from "../../Contract.js" +import type * as SysioContracts from "../../../types/SysioContractTypes.js" +import type { APIClient } from "../../../api/Client.js" + +/** Friendly string or packed numeric `slug_name` accepted by chain helpers. */ +export type ChainSlugName = string | number | bigint + +/** Configuration for `ChainsClient`. */ +export interface ChainsClientOptions { + /** Chain API client used for registry reads. */ + client: APIClient + /** Chain registry contract account. Defaults to `sysio.chains`. */ + contract?: NameType +} + +/** User-facing chain registration data. */ +export interface ChainRegistration { + /** VM/signing family used by the registered chain. */ + kind: SysioContracts.SysioChainsChainkind + /** Stable protocol chain code, such as `ETHEREUM` or `SOLANA`. */ + code: ChainSlugName + /** External numeric chain identifier recorded by the protocol. */ + externalChainId: number + /** Human-readable chain name. */ + name: string + /** Human-readable chain description. */ + description: string +} + +/** Options for creating an unsigned `regchain` action. */ +export interface CreateRegisterChainActionOptions { + /** Chain registration written by the privileged action. */ + registration: ChainRegistration + /** Permission levels authorizing the action. */ + authorization: ContractPermissionLevel[] + /** Chain registry contract account override. */ + contract?: NameType +} + +/** Options for creating an unsigned `activchain` action. */ +export interface CreateActivateChainActionOptions { + /** Stable protocol chain code to activate. */ + code: ChainSlugName + /** Permission levels authorizing the action. */ + authorization: ContractPermissionLevel[] + /** Chain registry contract account override. */ + contract?: NameType +} + +/** Filters applied while reading registered chains. */ +export interface ListChainsOptions { + /** Return only active registry rows. */ + activeOnly?: boolean + /** Include the Wire depot row. Defaults to true. */ + includeDepot?: boolean + /** Return only one VM/signing family. */ + kind?: SysioContracts.SysioChainsChainkind + /** Maximum matching rows to return. Defaults to 500. */ + limit?: number +} + +/** Application-friendly representation of one `sysio.chains` row. */ +export interface ChainRecord { + /** Stable decoded protocol chain code. */ + code: string + /** Packed numeric value of `code`. */ + codeValue: number + /** VM/signing family used by the chain. */ + kind: SysioContracts.SysioChainsChainkind + /** External numeric chain identifier recorded by the protocol. */ + externalChainId: number + /** Human-readable chain name. */ + name: string + /** Human-readable chain description. */ + description: string + /** Whether this row describes the Wire depot itself. */ + isDepot: boolean + /** Whether protocol routing has activated this chain. */ + active: boolean + /** Registration time in Unix milliseconds. */ + registeredAtMs: bigint + /** Activation time in Unix milliseconds, or zero before activation. */ + activatedAtMs: bigint + /** Original generated registry row. */ + raw: SysioContracts.SysioChainsChainRowType +} diff --git a/packages/sdk-core/src/contracts/sysio/chains/index.ts b/packages/sdk-core/src/contracts/sysio/chains/index.ts new file mode 100644 index 0000000..9195f9a --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/chains/index.ts @@ -0,0 +1,7 @@ +export * from "./Actions.js" +export * from "./Client.js" +export * from "./Constants.js" +export * from "./Descriptor.js" +export * from "./Slug.js" +export * from "./Structs.js" +export * from "./Types.js" diff --git a/packages/sdk-core/src/contracts/sysio/index.ts b/packages/sdk-core/src/contracts/sysio/index.ts index 625b14d..e91e4de 100644 --- a/packages/sdk-core/src/contracts/sysio/index.ts +++ b/packages/sdk-core/src/contracts/sysio/index.ts @@ -6,16 +6,19 @@ import { type ContractDescriptor } from "../Contract.js" import { descriptor as authexDescriptor } from "./authex/Descriptor.js" +import { descriptor as chainsDescriptor } from "./chains/Descriptor.js" import { descriptor as msigDescriptor } from "./msig/Descriptor.js" import { descriptor as reservDescriptor } from "./reserv/Descriptor.js" export * as authex from "./authex/index.js" +export * as chains from "./chains/index.js" export * as msig from "./msig/index.js" export * as reserv from "./reserv/index.js" /** System contract descriptors available to the generic client factory. */ export const descriptors = { authex: authexDescriptor, + chains: chainsDescriptor, msig: msigDescriptor, reserv: reservDescriptor } as const diff --git a/packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts b/packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts new file mode 100644 index 0000000..338ff64 --- /dev/null +++ b/packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts @@ -0,0 +1,72 @@ +import { contracts, SlugName } from "@wireio/sdk-core" +import { SysioChainsChainkind } from "@wireio/sdk-core/types/SysioContractTypes" + +const { + ChainsActivateChain, + ChainsRegisterChain, + createActivateChainAction, + createActivateChainActionData, + createRegisterChainAction, + createRegisterChainActionData +} = contracts.sysio.chains + +const REGISTRATION = { + kind: SysioChainsChainkind.CHAIN_KIND_EVM, + code: "POLYGON", + externalChainId: 137, + name: "Polygon", + description: "Polygon EVM outpost" +} + +describe("sysio.chains action helpers", () => { + test("creates generated register and activation action data", () => { + expect(createRegisterChainActionData(REGISTRATION)).toEqual({ + kind: SysioChainsChainkind.CHAIN_KIND_EVM, + code: { value: SlugName.from("POLYGON") }, + external_chain_id: 137, + name: "Polygon", + description: "Polygon EVM outpost" + }) + expect(createActivateChainActionData("POLYGON")).toEqual({ + code: { value: SlugName.from("POLYGON") } + }) + }) + + test("serializes privileged actions through the shared contract proxy", () => { + const register = createRegisterChainAction({ + registration: REGISTRATION, + authorization: ["sysio.chains@active"] + }), + activate = createActivateChainAction({ + code: "POLYGON", + authorization: ["sysio.chains@active"] + }), + registerData = register.decodeData(ChainsRegisterChain), + activateData = activate.decodeData(ChainsActivateChain) + + expect(register.account.toString()).toBe("sysio.chains") + expect(register.name.toString()).toBe("regchain") + expect(register.authorization.map(String)).toEqual(["sysio.chains@active"]) + expect(Number(registerData.kind)).toBe(SysioChainsChainkind.CHAIN_KIND_EVM) + expect(Number(registerData.code.value)).toBe(SlugName.from("POLYGON")) + expect(Number(registerData.external_chain_id)).toBe(137) + expect(registerData.name).toBe("Polygon") + expect(registerData.description).toBe("Polygon EVM outpost") + expect(Number(activateData.code.value)).toBe(SlugName.from("POLYGON")) + }) + + test("rejects external chain identifiers outside uint32", () => { + expect(() => + createRegisterChainActionData({ + ...REGISTRATION, + externalChainId: -1 + }) + ).toThrow("unsigned 32-bit integer") + expect(() => + createRegisterChainActionData({ + ...REGISTRATION, + externalChainId: 0x100000000 + }) + ).toThrow("unsigned 32-bit integer") + }) +}) diff --git a/packages/sdk-core/tests/contracts/sysio/chains/Client.test.ts b/packages/sdk-core/tests/contracts/sysio/chains/Client.test.ts new file mode 100644 index 0000000..fa04b58 --- /dev/null +++ b/packages/sdk-core/tests/contracts/sysio/chains/Client.test.ts @@ -0,0 +1,134 @@ +import { contracts, SlugName } from "@wireio/sdk-core" +import { SysioChainsChainkind } from "@wireio/sdk-core/types/SysioContractTypes" + +const { ChainsClient } = contracts.sysio.chains + +const CHAIN_ROWS = [ + { + code: { value: String(SlugName.from("ETHEREUM")) }, + kind: "CHAIN_KIND_EVM", + external_chain_id: 31337, + name: "Ethereum (anvil)", + description: "Local EVM outpost", + is_depot: false, + active: true, + registered_at_ms: "1784345978000", + activated_at_ms: "1784345978000" + }, + { + code: { value: SlugName.from("SOLANA") }, + kind: SysioChainsChainkind.CHAIN_KIND_SVM, + external_chain_id: 0, + name: "Solana", + description: "Local SVM outpost", + is_depot: false, + active: false, + registered_at_ms: "1784345979000", + activated_at_ms: "0" + }, + { + code: { value: SlugName.from("WIRE") }, + kind: "CHAIN_KIND_WIRE", + external_chain_id: 0, + name: "Wire", + description: "Depot", + is_depot: true, + active: true, + registered_at_ms: "1784345977000", + activated_at_ms: "1784345977000" + } +] + +function clientFixture() { + const getTableRows = jest.fn(async () => ({ + rows: CHAIN_ROWS, + more: false, + next_key: "" + })), + client = new ChainsClient({ + client: { + v1: { chain: { get_table_rows: getTableRows } } + } as any + }) + + return { client, getTableRows } +} + +describe("ChainsClient", () => { + test("normalizes active non-depot chains from generated rows", async () => { + const { client, getTableRows } = clientFixture(), + chains = await client.listChains({ + activeOnly: true, + includeDepot: false + }) + + expect(chains).toHaveLength(1) + expect(chains[0]).toMatchObject({ + code: "ETHEREUM", + codeValue: SlugName.from("ETHEREUM"), + kind: SysioChainsChainkind.CHAIN_KIND_EVM, + externalChainId: 31337, + active: true, + isDepot: false, + registeredAtMs: 1784345978000n + }) + expect(getTableRows).toHaveBeenCalledWith({ + code: "sysio.chains", + table: "chains", + scope: "sysio.chains", + json: true, + limit: 500 + }) + }) + + test("filters by chain kind and reads one exact chain", async () => { + const { client } = clientFixture() + + await expect( + client.listChains({ kind: SysioChainsChainkind.CHAIN_KIND_SVM }) + ).resolves.toEqual([ + expect.objectContaining({ code: "SOLANA", active: false }) + ]) + await expect(client.getChain("WIRE")).resolves.toEqual( + expect.objectContaining({ code: "WIRE", isDepot: true }) + ) + await expect(client.getChain("MISSING")).resolves.toBeNull() + }) + + test("exposes the registered descriptor through the generic sysio proxy", async () => { + const { client } = clientFixture(), + proxy = contracts.sysio.createClient({ + client: client.client, + name: "chains" + }), + result = await proxy.tables.chains.rows() + + expect(proxy.contract.toString()).toBe("sysio.chains") + expect(result.rows).toEqual(CHAIN_ROWS) + }) + + test("creates actions with the client contract override", () => { + const { client } = clientFixture(), + custom = new ChainsClient({ + client: client.client, + contract: "chain.admin" + }), + register = custom.createRegisterChainAction({ + registration: { + kind: SysioChainsChainkind.CHAIN_KIND_EVM, + code: "MONAD", + externalChainId: 143, + name: "Monad", + description: "Monad EVM outpost" + }, + authorization: ["chain.admin@active"] + }), + activate = custom.createActivateChainAction({ + code: "MONAD", + authorization: ["chain.admin@active"] + }) + + expect(register.account.toString()).toBe("chain.admin") + expect(activate.account.toString()).toBe("chain.admin") + }) +}) diff --git a/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts b/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts new file mode 100644 index 0000000..d88e517 --- /dev/null +++ b/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts @@ -0,0 +1,23 @@ +import { contracts, SlugName } from "@wireio/sdk-core" + +const { chainSlugData, chainSlugString, chainSlugValue } = + contracts.sysio.chains + +describe("sysio.chains slug helpers", () => { + test("round-trips friendly and packed chain codes", () => { + const packed = SlugName.from("ETHEREUM") + + expect(chainSlugValue("ETHEREUM")).toBe(packed) + expect(chainSlugValue(String(packed))).toBe(packed) + expect(chainSlugData("ETHEREUM")).toEqual({ value: packed }) + expect(chainSlugString(packed)).toBe("ETHEREUM") + }) + + test("rejects empty, invalid, and unsafe values", () => { + expect(() => chainSlugValue(0)).toThrow("non-zero safe integer") + expect(() => chainSlugValue("ethereum")).toThrow("outside [A-Z0-9_]") + expect(() => chainSlugValue(Number.MAX_SAFE_INTEGER + 1)).toThrow( + "non-zero safe integer" + ) + }) +})