diff --git a/CLAUDE.md b/CLAUDE.md index 446d926..c6b0b15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -208,6 +208,7 @@ All generated or modified code **must** include JSDoc comments (`/** ... */`), c - protoc-gen packages have `postinstall` scripts that trigger `pnpm dist` — this can be slow on first install - The `fix-hybrid-output.mjs` script must run after every build of hybrid packages or ESM imports will break in Node - `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/Client.ts` is the generic transport facade for every generated system contract. Keep action/table names sourced from `SysioContractDefinitions`; keep workflow behavior in the `authex`, `msig`, and `reserv` domain clients. - `packages/sdk-core/src/contracts/sysio/Codecs.ts` is the only hand-written system-action codec registry. Add entries only when synchronous action construction is required; generated actions without a codec must retain the `AnyAction` fallback. diff --git a/packages/sdk-core/README.md b/packages/sdk-core/README.md index 96581ce..6339c39 100644 --- a/packages/sdk-core/README.md +++ b/packages/sdk-core/README.md @@ -80,6 +80,25 @@ 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. +## 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 +}) +``` + +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 `contracts.sysio.reserv` provides normalized reserve registry reads, @@ -102,6 +121,46 @@ await reserves.pushMatchReserve({ }) ``` +## Reserve swaps + +Reserve swap integrations compose three on-chain sources instead of carrying a +parallel token or route catalog: + +- `contracts.sysio.tokens.TokenRegistryClient` reads canonical token metadata + and active chain deployments. +- `contracts.sysio.reserv.ReserveClient` discovers active liquidity and returns + live `swapquote` output for external or WIRE endpoints. +- `contracts.sysio.uwrit.UnderwritingClient` reads swap lifecycle state and + submits WIRE-origin swaps into the next-epoch queue. + +```ts +const tokens = new contracts.sysio.tokens.TokenRegistryClient({ client: api }) +const reserves = new contracts.sysio.reserv.ReserveClient({ client: api }) +const underwriting = new contracts.sysio.uwrit.UnderwritingClient({ client: api }) + +const assets = await tokens.listAssets() +const quote = await reserves.getSwapQuote({ + from: contracts.sysio.uwrit.WIRE_SWAP_ENDPOINT, + fromAmount: 10_000_000_000n, + to: { chainCode: "SOLANA", tokenCode: "SOL", reserveCode: "PRIMARY" } +}) + +await underwriting.pushSwapFromWire({ + user: "alice", + wireAmount: 10_000_000_000n, + destination: { chainCode: "SOLANA", tokenCode: "SOL", reserveCode: "PRIMARY" }, + targetAmount: quote, + targetToleranceBps: 500, + recipientKind: SysioUwritChainkind.CHAIN_KIND_SVM, + recipientAddress: "" +}) +``` + +External-origin swap submission remains in the chain SDK that owns the deployed +outpost ABI or IDL. A mined source transaction means the swap was submitted; +`uwreqs` remains the source of truth for relay, underwriting, settlement, and +revert status. + ## Install ```sh 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..4c6f205 --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/chains/Client.ts @@ -0,0 +1,157 @@ +import type { APIClient } from "../../../api/Client.js" +import type { Action } from "../../../chain/Action.js" +import type { NameType } from "../../../chain/Name.js" +import { + SysioChainsChainkind, + SysioContractName +} from "../../../types/SysioContractTypes.js" +import type * as SysioContracts from "../../../types/SysioContractTypes.js" +import type { ContractTableRowsOptions } from "../../Contract.js" +import { getSysioContract, type SysioContractClient } from "../Client.js" + +import { + createActivateChainAction, + createRegisterChainAction +} from "./Actions.js" +import { + DEFAULT_CHAINS_CONTRACT, + DEFAULT_CHAIN_QUERY_LIMIT +} from "./Constants.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: SysioContractClient + + /** Creates a chain registry client. */ + constructor(config: ChainsClientOptions) { + this.client = config.client + this.contract = config.contract || DEFAULT_CHAINS_CONTRACT + this.contractClient = getSysioContract(SysioContractName.chains, { + client: config.client, + contract: this.contract + }) + } + + /** 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 e832789..fa93c4e 100644 --- a/packages/sdk-core/src/contracts/sysio/index.ts +++ b/packages/sdk-core/src/contracts/sysio/index.ts @@ -1,4 +1,7 @@ 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" +export * as tokens from "./tokens/index.js" +export * as uwrit from "./uwrit/index.js" export * from "./Client.js" diff --git a/packages/sdk-core/src/contracts/sysio/tokens/Client.ts b/packages/sdk-core/src/contracts/sysio/tokens/Client.ts new file mode 100644 index 0000000..d70a95c --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/tokens/Client.ts @@ -0,0 +1,170 @@ +import { + SysioContractName, + SysioTokensChainkind, + SysioTokensTokenkind +} from "../../../types/SysioContractTypes.js" +import type * as SysioContracts from "../../../types/SysioContractTypes.js" +import { getSysioContract, type SysioContractClient } from "../Client.js" +import { reserveSlugString, reserveSlugValue } from "../reserv/Slug.js" + +import { + DEFAULT_TOKENS_CONTRACT, + DEFAULT_TOKEN_QUERY_LIMIT +} from "./Constants.js" +import type { + ChainTokenRecord, + ListChainTokensOptions, + ListTokenRegistryOptions, + RegisteredAsset, + TokenRecord, + TokenRegistryClientOptions +} from "./Types.js" + +interface SlugValue { + value: number | string +} + +function bigintValue(value: number | string): bigint { + return BigInt(value.toString()) +} + +function enumValue>( + enumType: T, + value: number | keyof T +): number { + if (typeof value === "number") return value + const mapped = enumType[value] + if (mapped == null) { + throw new Error(`Unknown token enum value: ${String(value)}`) + } + return Number(mapped) +} + +function slugValue(value: SlugValue): number { + return reserveSlugValue(value.value) +} + +/** Normalizes a generated `sysio.tokens::tokens` row. */ +export function normalizeTokenRow( + row: SysioContracts.SysioTokensTokenRowType +): TokenRecord { + const codeValue = slugValue(row.code) + + return { + code: reserveSlugString(codeValue), + codeValue, + kind: enumValue(SysioTokensTokenkind, row.kind) as SysioTokensTokenkind, + symbol: row.symbol_name, + description: row.description, + precision: row.precision, + addressKind: enumValue( + SysioTokensChainkind, + row.address.kind + ) as SysioTokensChainkind, + address: row.address.address, + active: row.active, + registeredAtMs: bigintValue(row.registered_at_ms), + activatedAtMs: bigintValue(row.activated_at_ms), + raw: row + } +} + +/** Normalizes a generated `sysio.tokens::chaintokens` row. */ +export function normalizeChainTokenRow( + row: SysioContracts.SysioTokensChainTokenRowType +): ChainTokenRecord { + const chainCodeValue = slugValue(row.chain_code), + tokenCodeValue = slugValue(row.token_code) + + return { + chainCode: reserveSlugString(chainCodeValue), + chainCodeValue, + tokenCode: reserveSlugString(tokenCodeValue), + tokenCodeValue, + contractAddress: row.contract_addr, + isNative: row.is_native, + active: row.active, + registeredAtMs: bigintValue(row.registered_at_ms), + activatedAtMs: bigintValue(row.activated_at_ms), + raw: row + } +} + +/** UI-neutral client for the on-chain token and chain-token registry. */ +export class TokenRegistryClient { + /** Generic typed client for direct public action and table access. */ + readonly contractClient: SysioContractClient + + /** Creates a token registry client. */ + constructor(config: TokenRegistryClientOptions) { + this.contractClient = getSysioContract(SysioContractName.tokens, { + client: config.client, + contract: config.contract || DEFAULT_TOKENS_CONTRACT + }) + } + + /** Lists normalized canonical token metadata. */ + async listTokens( + options: ListTokenRegistryOptions = {} + ): Promise { + const result = await this.contractClient.tables.tokens.query({ + limit: Math.max(Math.floor(options.limit || DEFAULT_TOKEN_QUERY_LIMIT), 1) + }) + + return result.rows + .map(normalizeTokenRow) + .filter(record => options.includeInactive || record.active) + } + + /** Lists normalized chain-token bindings with optional slug filters. */ + async listChainTokens( + options: ListChainTokensOptions = {} + ): Promise { + const result = await this.contractClient.tables.chaintokens.query({ + limit: Math.max( + Math.floor(options.limit || DEFAULT_TOKEN_QUERY_LIMIT), + 1 + ) + }), + chainCode = + options.chainCode == null ? null : reserveSlugValue(options.chainCode), + tokenCode = + options.tokenCode == null ? null : reserveSlugValue(options.tokenCode) + + return result.rows + .map(normalizeChainTokenRow) + .filter( + record => + (options.includeInactive || record.active) && + (chainCode == null || record.chainCodeValue === chainCode) && + (tokenCode == null || record.tokenCodeValue === tokenCode) + ) + } + + /** Joins active token metadata to active chain deployments. */ + async listAssets( + options: ListChainTokensOptions = {} + ): Promise { + const [tokens, chainTokens] = await Promise.all([ + this.listTokens(options), + this.listChainTokens(options) + ]), + byCode = new Map(tokens.map(token => [token.codeValue, token])) + + return chainTokens.flatMap(chainToken => { + const token = byCode.get(chainToken.tokenCodeValue) + return token ? [{ token, chainToken }] : [] + }) + } + + /** Reads one registered asset, or null when either registry row is absent. */ + async getAsset( + chainCode: string | number | bigint, + tokenCode: string | number | bigint + ): Promise { + const asset = ( + await this.listAssets({ chainCode, tokenCode, includeInactive: true }) + )[0] + return asset || null + } +} diff --git a/packages/sdk-core/src/contracts/sysio/tokens/Constants.ts b/packages/sdk-core/src/contracts/sysio/tokens/Constants.ts new file mode 100644 index 0000000..3dadc7e --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/tokens/Constants.ts @@ -0,0 +1,5 @@ +/** Default account hosting the Wire token registry. */ +export const DEFAULT_TOKENS_CONTRACT = "sysio.tokens" + +/** Default maximum rows read from either registry table. */ +export const DEFAULT_TOKEN_QUERY_LIMIT = 500 diff --git a/packages/sdk-core/src/contracts/sysio/tokens/Types.ts b/packages/sdk-core/src/contracts/sysio/tokens/Types.ts new file mode 100644 index 0000000..9a4397f --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/tokens/Types.ts @@ -0,0 +1,91 @@ +import type { APIClient } from "../../../api/Client.js" +import type { NameType } from "../../../chain/Name.js" +import type { + SysioTokensChainkind, + SysioTokensTokenkind +} from "../../../types/SysioContractTypes.js" +import type * as SysioContracts from "../../../types/SysioContractTypes.js" + +/** Configuration for the `sysio.tokens` registry client. */ +export interface TokenRegistryClientOptions { + /** Chain API client used for table reads. */ + client: APIClient + /** Registry contract override. Defaults to `sysio.tokens`. */ + contract?: NameType +} + +/** Filters shared by normalized token registry reads. */ +export interface ListTokenRegistryOptions { + /** Return inactive records when true. Defaults to active records only. */ + includeInactive?: boolean + /** Maximum rows returned. Defaults to 500. */ + limit?: number +} + +/** Filters for chain-specific token bindings. */ +export interface ListChainTokensOptions extends ListTokenRegistryOptions { + /** Optional chain slug filter. */ + chainCode?: string | number | bigint + /** Optional token slug filter. */ + tokenCode?: string | number | bigint +} + +/** Normalized token metadata registered on Wire. */ +export interface TokenRecord { + /** Friendly token slug. */ + code: string + /** Packed token slug. */ + codeValue: number + /** Canonical token classification. */ + kind: SysioTokensTokenkind + /** Display ticker or symbol. */ + symbol: string + /** Human-readable registry description. */ + description: string + /** Token precision used by its native representation. */ + precision: number + /** Chain family of the canonical token address. */ + addressKind: SysioTokensChainkind + /** Canonical address bytes returned by the chain API. */ + address: string + /** Whether the token can be used by protocol flows. */ + active: boolean + /** Registration timestamp in milliseconds. */ + registeredAtMs: bigint + /** Activation timestamp in milliseconds. */ + activatedAtMs: bigint + /** Original generated table row. */ + raw: SysioContracts.SysioTokensTokenRowType +} + +/** Normalized token deployment on one registered external chain. */ +export interface ChainTokenRecord { + /** Friendly external-chain slug. */ + chainCode: string + /** Packed external-chain slug. */ + chainCodeValue: number + /** Friendly token slug. */ + tokenCode: string + /** Packed token slug. */ + tokenCodeValue: number + /** Contract or mint address bytes returned by the chain API. */ + contractAddress: string + /** Whether the chain treats this asset as its native currency. */ + isNative: boolean + /** Whether this chain/token binding can be used by protocol flows. */ + active: boolean + /** Registration timestamp in milliseconds. */ + registeredAtMs: bigint + /** Activation timestamp in milliseconds. */ + activatedAtMs: bigint + /** Original generated table row. */ + raw: SysioContracts.SysioTokensChainTokenRowType +} + +/** Token metadata joined to one active chain deployment. */ +export interface RegisteredAsset { + /** Canonical token metadata. */ + token: TokenRecord + /** Chain-specific contract or native-asset binding. */ + chainToken: ChainTokenRecord +} diff --git a/packages/sdk-core/src/contracts/sysio/tokens/index.ts b/packages/sdk-core/src/contracts/sysio/tokens/index.ts new file mode 100644 index 0000000..321c241 --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/tokens/index.ts @@ -0,0 +1,3 @@ +export * from "./Client.js" +export * from "./Constants.js" +export * from "./Types.js" diff --git a/packages/sdk-core/src/contracts/sysio/uwrit/Actions.ts b/packages/sdk-core/src/contracts/sysio/uwrit/Actions.ts new file mode 100644 index 0000000..3793d7c --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/uwrit/Actions.ts @@ -0,0 +1,22 @@ +import { Name } from "../../../chain/Name.js" +import type * as SysioContracts from "../../../types/SysioContractTypes.js" +import { reserveSlugData } from "../reserv/Slug.js" + +import type { SwapFromWireOptions } from "./Types.js" + +/** Builds generated action data for `sysio.uwrit::swapfromwire`. */ +export function swapFromWireActionData( + options: SwapFromWireOptions +): SysioContracts.SysioUwritSwapfromwireAction { + return { + user: Name.from(options.user).toString(), + wire_amount: options.wireAmount.toString(), + dst_chain_code: reserveSlugData(options.destination.chainCode), + dst_token_code: reserveSlugData(options.destination.tokenCode), + dst_reserve_code: reserveSlugData(options.destination.reserveCode), + target_amount: options.targetAmount.toString(), + target_tolerance_bps: options.targetToleranceBps, + recipient_kind: options.recipientKind, + recipient_addr: options.recipientAddress + } +} diff --git a/packages/sdk-core/src/contracts/sysio/uwrit/Client.ts b/packages/sdk-core/src/contracts/sysio/uwrit/Client.ts new file mode 100644 index 0000000..94e681f --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/uwrit/Client.ts @@ -0,0 +1,257 @@ +import { Name } from "../../../chain/Name.js" +import { + SysioContractName, + SysioUwritAttestationtype, + SysioUwritChainkind, + SysioUwritUnderwriterequeststatus, + SysioUwritUnderwritestatus +} from "../../../types/SysioContractTypes.js" +import type * as SysioContracts from "../../../types/SysioContractTypes.js" +import { getSysioContract, type SysioContractClient } from "../Client.js" +import { reserveSlugString, reserveSlugValue } from "../reserv/Slug.js" + +import { swapFromWireActionData } from "./Actions.js" +import { + DEFAULT_UWRIT_CONTRACT, + DEFAULT_UWRIT_QUERY_LIMIT +} from "./Constants.js" +import type { + FromWireQueueRecord, + ListFromWireQueueOptions, + ListUnderwritingRequestsOptions, + SwapFromWireOptions, + UnderwritingClientOptions, + UnderwritingCommitRecord, + UnderwritingConfig, + UnderwritingRequestRecord +} from "./Types.js" + +interface SlugValue { + value: number | string +} + +interface ReserveIdentitySlugs { + chain: SlugValue + token: SlugValue + reserve: SlugValue +} + +function bigintValue(value: number | string): bigint { + return BigInt(value.toString()) +} + +function enumValue>( + enumType: T, + value: number | keyof T +): number { + if (typeof value === "number") return value + const mapped = enumType[value] + if (mapped == null) { + throw new Error(`Unknown underwriting enum value: ${String(value)}`) + } + return Number(mapped) +} + +function slugValue(value: SlugValue): number { + return reserveSlugValue(value.value) +} + +function identity( + chain: ReserveIdentitySlugs["chain"], + token: ReserveIdentitySlugs["token"], + reserve: ReserveIdentitySlugs["reserve"] +) { + return { + chainCode: reserveSlugString(slugValue(chain)), + tokenCode: reserveSlugString(slugValue(token)), + reserveCode: reserveSlugString(slugValue(reserve)) + } +} + +function normalizeCommit( + row: SysioContracts.SysioUwritCommitEntryType +): UnderwritingCommitRecord { + return { + underwriter: row.underwriter, + sourceReceivedAtMs: bigintValue(row.source_received_at_ms), + destinationReceivedAtMs: bigintValue(row.dest_received_at_ms), + status: enumValue( + SysioUwritUnderwritestatus, + row.status + ) as SysioUwritUnderwritestatus, + reason: row.reason, + raw: row + } +} + +/** Normalizes a generated `sysio.uwrit::uwreqs` row. */ +export function normalizeUnderwritingRequest( + row: SysioContracts.SysioUwritUwRequestTType +): UnderwritingRequestRecord { + return { + id: bigintValue(row.id), + type: enumValue( + SysioUwritAttestationtype, + row.type + ) as SysioUwritAttestationtype, + status: enumValue( + SysioUwritUnderwriterequeststatus, + row.status + ) as SysioUwritUnderwriterequeststatus, + source: identity( + row.src_chain_code, + row.src_token_code, + row.src_reserve_code + ), + sourceAmount: bigintValue(row.src_amount), + destination: identity( + row.dst_chain_code, + row.dst_token_code, + row.dst_reserve_code + ), + destinationAmount: bigintValue(row.dst_amount), + toleranceBps: row.variance_tolerance_bps, + sourceTransactionId: row.source_tx_id, + depositor: row.depositor, + commits: row.commits_by.map(normalizeCommit), + winner: row.winner, + settledAtMs: bigintValue(row.settled_at_ms), + expiresAtEpoch: row.expires_at_epoch, + raw: row + } +} + +/** Normalizes a generated `sysio.uwrit::fwqueue` row. */ +export function normalizeFromWireQueue( + row: SysioContracts.SysioUwritFromwireQType +): FromWireQueueRecord { + return { + id: bigintValue(row.id), + user: row.user, + wireAmount: bigintValue(row.wire_amount), + destination: identity( + row.dst_chain_code, + row.dst_token_code, + row.dst_reserve_code + ), + targetAmount: bigintValue(row.target_amount), + toleranceBps: row.variance_tolerance_bps, + recipientKind: enumValue( + SysioUwritChainkind, + row.recipient_kind + ) as SysioUwritChainkind, + recipientAddress: row.recipient_addr, + createdAtEpoch: row.created_at_epoch, + raw: row + } +} + +/** UI-neutral client for swap underwriting reads and WIRE-origin submission. */ +export class UnderwritingClient { + /** Generic typed client for direct public action and table access. */ + readonly contractClient: SysioContractClient + + /** Creates an underwriting client. */ + constructor(config: UnderwritingClientOptions) { + this.contractClient = getSysioContract(SysioContractName.uwrit, { + client: config.client, + contract: config.contract || DEFAULT_UWRIT_CONTRACT + }) + } + + /** Lists normalized protocol swap requests. */ + async listRequests( + options: ListUnderwritingRequestsOptions = {} + ): Promise { + const result = await this.contractClient.tables.uwreqs.query({ + limit: Math.max( + Math.floor(options.limit || DEFAULT_UWRIT_QUERY_LIMIT), + 1 + ) + }), + sourceChain = + options.sourceChainCode == null + ? null + : reserveSlugValue(options.sourceChainCode), + destinationChain = + options.destinationChainCode == null + ? null + : reserveSlugValue(options.destinationChainCode) + + return result.rows + .map(normalizeUnderwritingRequest) + .filter( + record => + (options.status == null || record.status === options.status) && + (sourceChain == null || + reserveSlugValue(record.source.chainCode) === sourceChain) && + (destinationChain == null || + reserveSlugValue(record.destination.chainCode) === destinationChain) + ) + } + + /** Reads one protocol swap request, or null when absent. */ + async getRequest( + id: number | string | bigint + ): Promise { + const result = await this.contractClient.tables.uwreqs.query({ + lower_bound: id.toString(), + upper_bound: id.toString(), + limit: 1 + }), + row = result.rows[0] + return row ? normalizeUnderwritingRequest(row) : null + } + + /** Lists WIRE-origin swaps waiting for the next epoch drain. */ + async listFromWireQueue( + options: ListFromWireQueueOptions = {} + ): Promise { + const result = await this.contractClient.tables.fwqueue.query({ + limit: Math.max( + Math.floor(options.limit || DEFAULT_UWRIT_QUERY_LIMIT), + 1 + ) + }), + user = options.user ? Name.from(options.user).toString() : null + + return result.rows + .map(normalizeFromWireQueue) + .filter(record => !user || record.user === user) + } + + /** Reads the active underwriting configuration, or null before initialization. */ + async getConfig(): Promise { + const row = await this.contractClient.tables.uwconfig.first() + return row + ? { + feeBps: row.fee_bps, + collateralLockDurationMs: bigintValue( + row.collateral_lock_duration_ms + ), + minimumFromWireAmount: bigintValue(row.min_fromwire_amount), + fromWireRevertFeeBps: row.fromwire_revert_fee_bps + } + : null + } + + /** Escrows WIRE and queues a signed WIRE-to-external swap. */ + async pushSwapFromWire( + options: SwapFromWireOptions + ): Promise< + Awaited> + > { + return this.contractClient.actions.swapfromwire.invoke( + swapFromWireActionData(options), + { + authorization: [ + { + actor: options.user, + permission: options.permission || "active" + } + ], + pushOptions: options.pushOptions + } + ) + } +} diff --git a/packages/sdk-core/src/contracts/sysio/uwrit/Constants.ts b/packages/sdk-core/src/contracts/sysio/uwrit/Constants.ts new file mode 100644 index 0000000..7c45dff --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/uwrit/Constants.ts @@ -0,0 +1,12 @@ +/** Default account hosting the underwriting coordinator. */ +export const DEFAULT_UWRIT_CONTRACT = "sysio.uwrit" + +/** Default maximum rows returned from underwriting tables. */ +export const DEFAULT_UWRIT_QUERY_LIMIT = 500 + +/** Canonical WIRE endpoint used by reserve quotes and underwriting requests. */ +export const WIRE_SWAP_ENDPOINT = Object.freeze({ + chainCode: "WIRE", + tokenCode: "WIRE", + reserveCode: "PRIMARY" +}) diff --git a/packages/sdk-core/src/contracts/sysio/uwrit/Types.ts b/packages/sdk-core/src/contracts/sysio/uwrit/Types.ts new file mode 100644 index 0000000..77306e6 --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/uwrit/Types.ts @@ -0,0 +1,147 @@ +import type { APIClient } from "../../../api/Client.js" +import type { TransactionExtraOptions } from "../../../api/Types.js" +import type { NameType } from "../../../chain/Name.js" +import type { + SysioUwritAttestationtype, + SysioUwritChainkind, + SysioUwritUnderwriterequeststatus, + SysioUwritUnderwritestatus +} from "../../../types/SysioContractTypes.js" +import type * as SysioContracts from "../../../types/SysioContractTypes.js" +import type { ReserveIdentity, ReserveSlugName } from "../reserv/Types.js" + +/** Configuration for `UnderwritingClient`. */ +export interface UnderwritingClientOptions { + /** Chain API client used for reads and signed pushes. */ + client: APIClient + /** Underwriting contract override. Defaults to `sysio.uwrit`. */ + contract?: NameType +} + +/** Filters for underwriting request history. */ +export interface ListUnderwritingRequestsOptions { + /** Optional lifecycle status. */ + status?: SysioUwritUnderwriterequeststatus + /** Optional source chain slug. */ + sourceChainCode?: ReserveSlugName + /** Optional destination chain slug. */ + destinationChainCode?: ReserveSlugName + /** Maximum rows returned. Defaults to 500. */ + limit?: number +} + +/** Filters for queued WIRE-origin swaps. */ +export interface ListFromWireQueueOptions { + /** Optional depositor account. */ + user?: NameType + /** Maximum rows returned. Defaults to 500. */ + limit?: number +} + +/** Input for `sysio.uwrit::swapfromwire`. */ +export interface SwapFromWireOptions { + /** Wire account escrowing WIRE into reserve custody. */ + user: NameType + /** WIRE input in nine-decimal base units. */ + wireAmount: number | string | bigint + /** Destination external reserve. */ + destination: ReserveIdentity + /** Current quoted destination amount in depot units. */ + targetAmount: number | string | bigint + /** Maximum accepted quote drift in basis points. */ + targetToleranceBps: number + /** Destination signing/address family. */ + recipientKind: SysioUwritChainkind | keyof typeof SysioUwritChainkind + /** Raw destination address bytes accepted by the deployed contract. */ + recipientAddress: string + /** Wire permission authorizing escrow. Defaults to `active`. */ + permission?: NameType + /** Optional push behavior such as waiting for finality. */ + pushOptions?: TransactionExtraOptions +} + +/** Normalized underwriter commitment for one swap leg. */ +export interface UnderwritingCommitRecord { + /** Underwriter account. */ + underwriter: string + /** Source outpost receipt time. */ + sourceReceivedAtMs: bigint + /** Destination outpost receipt time. */ + destinationReceivedAtMs: bigint + /** Current commitment lifecycle status. */ + status: SysioUwritUnderwritestatus + /** Protocol reason when disqualified or rejected. */ + reason: string + /** Original generated commitment. */ + raw: SysioContracts.SysioUwritCommitEntryType +} + +/** Normalized swap request coordinated by `sysio.uwrit`. */ +export interface UnderwritingRequestRecord { + /** Protocol request identifier. */ + id: bigint + /** Original cross-chain attestation type. */ + type: SysioUwritAttestationtype + /** Current request lifecycle status. */ + status: SysioUwritUnderwriterequeststatus + /** Source reserve identity. */ + source: ReserveIdentity + /** Source amount in depot units. */ + sourceAmount: bigint + /** Destination reserve identity. */ + destination: ReserveIdentity + /** Quoted destination amount in depot units. */ + destinationAmount: bigint + /** Accepted quote drift in basis points. */ + toleranceBps: number + /** Origin-chain transaction identifier bytes. */ + sourceTransactionId: string + /** Originating Wire account when known. */ + depositor: string + /** Underwriter commitments received for this request. */ + commits: UnderwritingCommitRecord[] + /** Winning underwriter account, empty while unresolved. */ + winner: string + /** Settlement timestamp in milliseconds. */ + settledAtMs: bigint + /** Epoch after which an unresolved request expires. */ + expiresAtEpoch: number + /** Original generated request row. */ + raw: SysioContracts.SysioUwritUwRequestTType +} + +/** Normalized WIRE-origin swap awaiting the next epoch drain. */ +export interface FromWireQueueRecord { + /** Queue identifier. */ + id: bigint + /** Wire depositor account. */ + user: string + /** Escrowed WIRE amount. */ + wireAmount: bigint + /** Destination external reserve. */ + destination: ReserveIdentity + /** Current quoted destination amount. */ + targetAmount: bigint + /** Accepted quote drift in basis points. */ + toleranceBps: number + /** Destination signing/address family. */ + recipientKind: SysioUwritChainkind + /** Raw destination recipient bytes. */ + recipientAddress: string + /** Epoch in which the queue entry was created. */ + createdAtEpoch: number + /** Original generated queue row. */ + raw: SysioContracts.SysioUwritFromwireQType +} + +/** Normalized underwriting configuration used for client preflight. */ +export interface UnderwritingConfig { + /** Protocol fee in basis points. */ + feeBps: number + /** Collateral lock duration in milliseconds. */ + collateralLockDurationMs: bigint + /** Minimum WIRE-origin input amount. */ + minimumFromWireAmount: bigint + /** Fee retained when a WIRE-origin swap reverts. */ + fromWireRevertFeeBps: number +} diff --git a/packages/sdk-core/src/contracts/sysio/uwrit/index.ts b/packages/sdk-core/src/contracts/sysio/uwrit/index.ts new file mode 100644 index 0000000..a74dae1 --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/uwrit/index.ts @@ -0,0 +1,4 @@ +export * from "./Actions.js" +export * from "./Client.js" +export * from "./Constants.js" +export * from "./Types.js" 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..6f1bfbe --- /dev/null +++ b/packages/sdk-core/tests/contracts/sysio/chains/Client.test.ts @@ -0,0 +1,133 @@ +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 chain tables through the root sysio proxy", async () => { + const { client } = clientFixture(), + proxy = contracts.sysio.createClient({ + client: client.client + }), + result = await proxy.chains.tables.chains.rows() + + expect(proxy.chains.account.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" + ) + }) +}) diff --git a/packages/sdk-core/tests/contracts/sysio/tokens/Client.test.ts b/packages/sdk-core/tests/contracts/sysio/tokens/Client.test.ts new file mode 100644 index 0000000..0c50ea2 --- /dev/null +++ b/packages/sdk-core/tests/contracts/sysio/tokens/Client.test.ts @@ -0,0 +1,95 @@ +import { contracts, SlugName } from "@wireio/sdk-core" +import { + SysioTokensChainkind, + SysioTokensTokenkind +} from "@wireio/sdk-core/types/SysioContractTypes" + +const { TokenRegistryClient, normalizeChainTokenRow, normalizeTokenRow } = + contracts.sysio.tokens + +interface TableQuery { + table: string +} + +function tokenRow(overrides: Record = {}) { + return { + code: { value: SlugName.from("ETH") }, + kind: "TOKEN_KIND_NATIVE", + symbol_name: "ETH", + description: "Ether", + precision: 18, + address: { kind: "CHAIN_KIND_EVM", address: "" }, + active: true, + registered_at_ms: "10", + activated_at_ms: "20", + ...overrides + } +} + +function chainTokenRow(overrides: Record = {}) { + return { + chain_code: { value: SlugName.from("ETHEREUM") }, + token_code: { value: SlugName.from("ETH") }, + contract_addr: "0x", + is_native: true, + active: true, + registered_at_ms: "30", + activated_at_ms: "40", + ...overrides + } +} + +function fixture() { + const getTableRows = jest.fn(async ({ table }: TableQuery) => ({ + rows: table === "tokens" ? [tokenRow()] : [chainTokenRow()], + more: false + })), + client = new TokenRegistryClient({ + client: { v1: { chain: { get_table_rows: getTableRows } } } as any + }) + + return { client, getTableRows } +} + +describe("TokenRegistryClient", () => { + test("normalizes token and chain-token rows", () => { + expect(normalizeTokenRow(tokenRow() as any)).toMatchObject({ + code: "ETH", + kind: SysioTokensTokenkind.TOKEN_KIND_NATIVE, + addressKind: SysioTokensChainkind.CHAIN_KIND_EVM, + registeredAtMs: 10n + }) + expect(normalizeChainTokenRow(chainTokenRow() as any)).toMatchObject({ + chainCode: "ETHEREUM", + tokenCode: "ETH", + isNative: true, + activatedAtMs: 40n + }) + }) + + test("joins active token metadata to chain deployments", async () => { + const { client, getTableRows } = fixture() + + await expect(client.listAssets()).resolves.toEqual([ + expect.objectContaining({ + token: expect.objectContaining({ code: "ETH" }), + chainToken: expect.objectContaining({ chainCode: "ETHEREUM" }) + }) + ]) + expect(getTableRows).toHaveBeenCalledWith( + expect.objectContaining({ code: "sysio.tokens", scope: "sysio.tokens" }) + ) + }) + + test("filters bindings and reads an exact asset", async () => { + const { client } = fixture() + + await expect( + client.listChainTokens({ chainCode: "SOLANA" }) + ).resolves.toEqual([]) + await expect(client.getAsset("ETHEREUM", "ETH")).resolves.toMatchObject({ + token: { symbol: "ETH" } + }) + await expect(client.getAsset("ETHEREUM", "SOL")).resolves.toBeNull() + }) +}) diff --git a/packages/sdk-core/tests/contracts/sysio/uwrit/Client.test.ts b/packages/sdk-core/tests/contracts/sysio/uwrit/Client.test.ts new file mode 100644 index 0000000..a1689d8 --- /dev/null +++ b/packages/sdk-core/tests/contracts/sysio/uwrit/Client.test.ts @@ -0,0 +1,171 @@ +import { contracts, SlugName } from "@wireio/sdk-core" +import { + SysioUwritChainkind, + SysioUwritUnderwriterequeststatus +} from "@wireio/sdk-core/types/SysioContractTypes" + +const { + UnderwritingClient, + WIRE_SWAP_ENDPOINT, + normalizeFromWireQueue, + normalizeUnderwritingRequest, + swapFromWireActionData +} = contracts.sysio.uwrit + +interface TableQuery { + table: string +} + +const slug = (value: string) => ({ value: SlugName.from(value) }) + +function requestRow(overrides: Record = {}) { + return { + id: "7", + type: "ATTESTATION_TYPE_SWAP_REQUEST", + status: "UNDERWRITE_REQUEST_STATUS_PENDING", + src_chain_code: slug("ETHEREUM"), + src_token_code: slug("ETH"), + src_reserve_code: slug("PRIMARY"), + src_amount: "100", + dst_chain_code: slug("SOLANA"), + dst_token_code: slug("SOL"), + dst_reserve_code: slug("PRIMARY"), + dst_amount: "90", + variance_tolerance_bps: 500, + source_tx_id: "abcd", + depositor: "alice", + commits_by: [], + winner: "", + committed_at_ms: "0", + settled_at_ms: "0", + expires_at_epoch: 12, + attestation_inbound_data: "", + attestation_outbound_data: "", + ...overrides + } +} + +function queueRow(overrides: Record = {}) { + return { + id: "8", + user: "alice", + wire_amount: "5000000000", + dst_chain_code: slug("SOLANA"), + dst_token_code: slug("SOL"), + dst_reserve_code: slug("PRIMARY"), + target_amount: "80", + variance_tolerance_bps: 500, + recipient_kind: "CHAIN_KIND_SVM", + recipient_addr: "1234", + created_at_epoch: 5, + ...overrides + } +} + +function fixture() { + const getTableRows = jest.fn(async ({ table }: TableQuery) => ({ + rows: + table === "uwreqs" + ? [requestRow()] + : table === "fwqueue" + ? [queueRow()] + : [ + { + fee_bps: 10, + collateral_lock_duration_ms: "60000", + min_fromwire_amount: "5000000000", + fromwire_revert_fee_bps: 25 + } + ], + more: false + })), + pushTransaction = jest.fn(async (_action: any, _options?: any) => ({ + transaction_id: "wire-swap" + })), + client = new UnderwritingClient({ + client: { + v1: { chain: { get_table_rows: getTableRows } }, + pushTransaction + } as any + }) + + return { client, getTableRows, pushTransaction } +} + +describe("UnderwritingClient", () => { + test("exposes the canonical WIRE sentinel and action data", () => { + expect(WIRE_SWAP_ENDPOINT).toEqual({ + chainCode: "WIRE", + tokenCode: "WIRE", + reserveCode: "PRIMARY" + }) + expect( + swapFromWireActionData({ + user: "alice", + wireAmount: 5n, + destination: { + chainCode: "SOLANA", + tokenCode: "SOL", + reserveCode: "PRIMARY" + }, + targetAmount: 4n, + targetToleranceBps: 500, + recipientKind: SysioUwritChainkind.CHAIN_KIND_SVM, + recipientAddress: "1234" + }) + ).toMatchObject({ user: "alice", wire_amount: "5", target_amount: "4" }) + }) + + test("normalizes request and WIRE queue rows", () => { + expect(normalizeUnderwritingRequest(requestRow() as any)).toMatchObject({ + id: 7n, + status: + SysioUwritUnderwriterequeststatus.UNDERWRITE_REQUEST_STATUS_PENDING, + source: { chainCode: "ETHEREUM", tokenCode: "ETH" }, + destination: { chainCode: "SOLANA", tokenCode: "SOL" } + }) + expect(normalizeFromWireQueue(queueRow() as any)).toMatchObject({ + user: "alice", + wireAmount: 5000000000n, + recipientKind: SysioUwritChainkind.CHAIN_KIND_SVM + }) + }) + + test("reads request, queue, and config state", async () => { + const { client } = fixture() + + await expect(client.getRequest(7)).resolves.toMatchObject({ id: 7n }) + await expect(client.listFromWireQueue({ user: "alice" })).resolves.toEqual([ + expect.objectContaining({ id: 8n }) + ]) + await expect(client.getConfig()).resolves.toEqual({ + feeBps: 10, + collateralLockDurationMs: 60000n, + minimumFromWireAmount: 5000000000n, + fromWireRevertFeeBps: 25 + }) + }) + + test("pushes a WIRE-origin swap with user authorization", async () => { + const { client, pushTransaction } = fixture() + + await client.pushSwapFromWire({ + user: "alice", + wireAmount: 5000000000n, + destination: { + chainCode: "SOLANA", + tokenCode: "SOL", + reserveCode: "PRIMARY" + }, + targetAmount: 80n, + targetToleranceBps: 500, + recipientKind: SysioUwritChainkind.CHAIN_KIND_SVM, + recipientAddress: "1234" + }) + + const [action] = pushTransaction.mock.calls[0] + expect(action.account.toString()).toBe("sysio.uwrit") + expect(action.name.toString()).toBe("swapfromwire") + expect(action.authorization.map(String)).toEqual(["alice@active"]) + }) +})