From 80f5adf26c3ac85b178c7f180dd6251f5454d9e5 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Tue, 21 Jul 2026 14:30:28 -0400 Subject: [PATCH 1/7] refactor(sdk-core): add generated sysio contract proxy --- CLAUDE.md | 2 + README.md | 2 +- eslint.config.mjs | 4 - packages/sdk-core/README.md | 42 +- .../sdk-core/src/contracts/sysio/Client.ts | 555 ++++++++++++++++++ .../src/contracts/sysio/authex/Actions.ts | 77 +-- .../src/contracts/sysio/authex/Client.ts | 31 +- .../sdk-core/src/contracts/sysio/index.ts | 58 +- .../src/contracts/sysio/msig/Actions.ts | 164 +++--- .../src/contracts/sysio/msig/Client.ts | 83 +-- .../src/contracts/sysio/reserv/Actions.ts | 42 +- .../src/contracts/sysio/reserv/Client.ts | 58 +- .../tests/contracts/sysio/Client.test.ts | 174 ++++++ 13 files changed, 996 insertions(+), 296 deletions(-) create mode 100644 packages/sdk-core/src/contracts/sysio/Client.ts create mode 100644 packages/sdk-core/tests/contracts/sysio/Client.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 68777bf..48d62ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -211,6 +211,8 @@ All generated or modified code **must** include JSDoc comments (`/** ... */`), c - `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. +- System-contract `prepare` prefers synchronous ABI encoding but must retain a typed `AnyAction` fallback so `APIClient` can resolve the deployed ABI. Never invent a default write authorization in the public SDK. - `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 - Path aliases in tsconfig base resolve to `src/` for dev, but published packages use `lib/` — jest module name maps handle this mismatch diff --git a/README.md b/README.md index 840df2e..dedfcf2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A monorepo containing shared TypeScript libraries for Wire applications, providi | [`@wireio/shared`](packages/shared/) | Core shared utilities (logging, guards, helpers) | [![npm](https://img.shields.io/npm/v/@wireio/shared)](https://www.npmjs.com/package/@wireio/shared) | | [`@wireio/shared-web`](packages/shared-web/) | Web-specific utilities | *private* | | [`@wireio/shared-node`](packages/shared-node/) | Node.js-specific utilities | *private* | -| [`@wireio/sdk-core`](packages/sdk-core/) | Wire blockchain SDK core types, primitives, signing helpers, and system contract workflows such as `sysio.msig` and `sysio.reserv` | [![npm](https://img.shields.io/npm/v/@wireio/sdk-core)](https://www.npmjs.com/package/@wireio/sdk-core) | +| [`@wireio/sdk-core`](packages/sdk-core/) | Wire blockchain SDK core types, primitives, signing helpers, generated `sysio` contract proxy, and domain workflows such as multisig and reserves | [![npm](https://img.shields.io/npm/v/@wireio/sdk-core)](https://www.npmjs.com/package/@wireio/sdk-core) | | [`@wireio/wallet-ext-sdk`](packages/wallet-ext-sdk/) | Client SDK for the Wire Wallet browser extension | [![npm](https://img.shields.io/npm/v/@wireio/wallet-ext-sdk)](https://www.npmjs.com/package/@wireio/wallet-ext-sdk) | | [`@wireio/wallet-browser-ext`](packages/wallet-browser-ext/) | Chrome extension developer wallet for Wire | *private* | diff --git a/eslint.config.mjs b/eslint.config.mjs index d1a1622..99d692a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -189,7 +189,6 @@ const InlineIifeDebtFiles = ["packages/wallet-browser-ext/src/inject/Provider.ts const NullUnionReturnDebtFiles = [ "packages/sdk-core/src/contracts/Contract.ts", "packages/sdk-core/src/contracts/sysio/msig/Capabilities.ts", - "packages/sdk-core/src/contracts/sysio/msig/Client.ts", "packages/sdk-core/src/contracts/sysio/msig/Hash.ts", "packages/sdk-core/src/contracts/sysio/msig/Proposal.ts", "packages/sdk-core/src/contracts/sysio/msig/Status.ts", @@ -219,7 +218,6 @@ const InlineTypeLiteralDebtFiles = [ "packages/sdk-core/src/common/Connect.ts", "packages/sdk-core/src/common/Types.ts", "packages/sdk-core/src/contracts/sysio/msig/Capabilities.ts", - "packages/sdk-core/src/contracts/sysio/msig/Client.ts", "packages/sdk-core/src/contracts/sysio/msig/Proposal.ts", "packages/sdk-core/src/contracts/sysio/msig/Status.ts", "packages/sdk-core/src/contracts/sysio/msig/Transaction.ts", @@ -261,7 +259,6 @@ const StringLiteralUnionDebtFiles = [ "packages/sdk-core/src/common/Logo.ts", "packages/sdk-core/src/contracts/Contract.ts", "packages/sdk-core/src/contracts/sysio/msig/Capabilities.ts", - "packages/sdk-core/src/contracts/sysio/msig/Client.ts", "packages/sdk-core/src/contracts/sysio/msig/Types.ts", "packages/shared/src/logging/context/LogContext.ts", "packages/wallet-browser-ext/src/popup/slices/WalletSlice.ts", @@ -332,7 +329,6 @@ const RuleDebt = [ files: [ "packages/sdk-core/src/api/Client.ts", "packages/sdk-core/src/chain/PrivateKey.ts", - "packages/sdk-core/src/contracts/sysio/msig/Client.ts", "packages/shared/src/node/logging/appenders/FileAppender.ts" ] }, diff --git a/packages/sdk-core/README.md b/packages/sdk-core/README.md index 1c6f1c0..203dcf5 100644 --- a/packages/sdk-core/README.md +++ b/packages/sdk-core/README.md @@ -19,22 +19,50 @@ const client = new contracts.sysio.msig.MsigClient({ client: api }) const detail = await client.getProposalDetail("alice", "upgrade1") ``` -System contract descriptors also feed a generic typed client factory. The current registry includes `sysio.msig`; future generated metadata can add the rest of the system contracts without changing the factory shape. +## Generated system-contract proxy + +`contracts.sysio.createClient({ client })` exposes every contract in the +generated `SysioContractDefinitions` registry. Contract, action, and table +members are typed from `SysioContractMapping`, validated at runtime, and cached +after their first access. Use either the concise root syntax or +`getSysioContract` when the contract name is dynamic. ```ts -const msig = contracts.sysio.createClient({ client: api, name: "msig" }) -const approve = msig.actions.approve( +import { SysioContracts } from "@wireio/sdk-core" + +const sysio = contracts.sysio.createClient({ client: api }) +const msig = sysio.msig +const epoch = sysio.getSysioContract(SysioContracts.SysioContractName.epoch) + +const approve = msig.actions.approve.prepare( { proposer: "alice", proposal_name: "upgrade1", level: { actor: "bob", permission: "active" }, proposal_hash: null }, - ["bob@active"] + { authorization: ["bob@active"] } ) -const proposals = await msig.tables.proposal.rows({ scope: "alice" }) +const proposals = await msig.tables.proposal.query({ scope: "alice" }) + +await epoch.actions.advance.invoke({}, { authorization: ["operator@active"] }) ``` +`prepare` returns an ABI-encoded `Action` when a local runtime codec or caller +ABI can encode the action. If synchronous encoding is unavailable or fails, it +returns the same generated data as a typed `AnyAction`; `APIClient` resolves the +deployed ABI when that payload is invoked or pushed. Authorization is empty by +default and must be supplied explicitly for writes. The legacy named factory and +callable action shorthand remain available for encoded descriptor-backed +contracts; the old `descriptors` export is retained as a deprecated migration +surface. + +The `AuthexClient`, `MsigClient`, and `ReserveClient` classes remain the public +domain facades for proof creation, proposal compatibility, reserve +normalization, and other workflow behavior. Each exposes `contractClient` for +lower-level generated action and table access without duplicating transport +logic. + The multisig module supports: - unsigned action builders for `propose`, `approve`, `unapprove`, `cancel`, `exec`, `invalidate`, and read-only `getproposal` @@ -103,10 +131,6 @@ const action = reserves.buildMatchReserveAction({ }) ``` -The generic descriptor registry also accepts -`contracts.sysio.createClient({ client: api, name: "reserv" })` for typed public -action and table access. - ## Install ```sh diff --git a/packages/sdk-core/src/contracts/sysio/Client.ts b/packages/sdk-core/src/contracts/sysio/Client.ts new file mode 100644 index 0000000..08adc4e --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/Client.ts @@ -0,0 +1,555 @@ +import type { APIClient } from "../../api/Client.js" +import type { TransactionExtraOptions } from "../../api/Types.js" +import type { + GetTableRowsResponse, + TableIndexType +} from "../../api/v1/Types.js" +import type { ABIDef } from "../../chain/Abi.js" +import { Action, type AnyAction } from "../../chain/Action.js" +import type { NameType } from "../../chain/Name.js" +import { + PermissionLevel, + type PermissionLevelType +} from "../../chain/PermissionLevel.js" +import { + SysioContractDefinitions, + SysioContractName, + type SysioContractMapping +} from "../../types/SysioContractTypes.js" +import type { + ContractActionDescriptor, + ContractBuildActionOptions, + ContractPermissionLevel, + ContractTableRowsOptions, + ContractTableScopesOptions +} from "../Contract.js" +import { + descriptor as authexDescriptor, + type SysioAuthexActionData +} from "./authex/Descriptor.js" +import { + descriptor as msigDescriptor, + type SysioMsigActionData +} from "./msig/Descriptor.js" +import { + descriptor as reservDescriptor, + type SysioReservActionData +} from "./reserv/Descriptor.js" + +const GetSysioContractMember = "getSysioContract" +const PromiseThenMember = "then" + +enum SysioMemberKind { + action = "action", + table = "table" +} + +/** Generated action and table surface for one system contract. */ +export type SysioContract = + SysioContractMapping[Name] + +/** Generated action names for one system contract. */ +export type SysioActionName = Extract< + keyof SysioContract["actions"], + string +> + +/** Generated action data for one system contract action. */ +export type SysioActionData< + Name extends SysioContractName, + ActionName extends SysioActionName +> = Name extends keyof RuntimeActionDataMapping + ? ActionName extends keyof RuntimeActionDataMapping[Name] + ? RuntimeActionDataMapping[Name][ActionName] + : SysioContract["actions"][ActionName] + : SysioContract["actions"][ActionName] + +/** Generated table names for one system contract. */ +export type SysioTableName = Extract< + keyof SysioContract["tables"], + string +> + +/** Generated table row for one system contract table. */ +export type SysioTableRow< + Name extends SysioContractName, + TableName extends SysioTableName +> = SysioContract["tables"][TableName] + +/** Options shared by typed system-contract action preparation and invocation. */ +export interface SysioActionOptions extends ContractBuildActionOptions { + /** Permission levels authorizing the action. Empty for read-only actions. */ + authorization?: ContractPermissionLevel[] +} + +/** Options for invoking a typed system-contract action. */ +export interface SysioActionInvocationOptions extends SysioActionOptions { + /** Optional signed-transaction behavior such as waiting for finality. */ + pushOptions?: TransactionExtraOptions +} + +/** Unencoded action payload retained when synchronous ABI encoding is unavailable. */ +export interface SysioActionPayload< + Name extends SysioContractName, + ActionName extends SysioActionName +> extends AnyAction { + /** Generated short system-contract name. */ + readonly contract: Name + /** On-chain account hosting the contract. */ + readonly account: NameType + /** ABI action name. */ + readonly name: ActionName + /** Normalized action authorization. */ + readonly authorization: PermissionLevelType[] + /** Generated ABI action data. */ + readonly data: SysioActionData +} + +/** Prepared action: ABI encoded when possible, otherwise ready for APIClient ABI resolution. */ +export type SysioPreparedAction< + Name extends SysioContractName, + ActionName extends SysioActionName +> = Action | SysioActionPayload + +/** Typed prepare/invoke interface for one generated system-contract action. */ +export interface SysioActionInvoker< + Name extends SysioContractName, + ActionName extends SysioActionName +> { + /** Backward-compatible shorthand for {@link SysioActionInvoker.prepare}. */ + ( + data: SysioActionData, + authorization: ContractPermissionLevel[], + options?: ContractBuildActionOptions + ): Action + + /** Prepares an unsigned action, preferring synchronous ABI encoding. */ + prepare( + data: SysioActionData, + options?: SysioActionOptions + ): SysioPreparedAction + + /** Prepares and pushes the action through the configured signed API client. */ + invoke( + data: SysioActionData, + options?: SysioActionInvocationOptions + ): Promise>> +} + +/** Typed query interface for one generated system-contract table. */ +export interface SysioTableQuery< + Name extends SysioContractName, + TableName extends SysioTableName +> { + /** Reads table rows with the complete chain table-query option surface. */ + query( + options?: ContractTableRowsOptions + ): Promise>> + + /** Backward-compatible alias for {@link SysioTableQuery.query}. */ + rows( + options?: ContractTableRowsOptions + ): Promise>> + + /** Reads the first matching table row. */ + first( + options?: ContractTableRowsOptions + ): Promise> + + /** Lists scopes containing rows for this table. */ + scopes(options?: ContractTableScopesOptions): Promise +} + +/** Typed action invokers keyed by one contract's generated action names. */ +export type SysioActionInvokers = { + readonly [ActionName in SysioActionName]: SysioActionInvoker< + Name, + ActionName + > +} + +/** Typed table queries keyed by one contract's generated table names. */ +export type SysioTableQueries = { + readonly [TableName in SysioTableName]: SysioTableQuery +} + +/** Generated typed action/table proxy for one system contract. */ +export interface SysioContractClient { + /** Generated short system-contract name. */ + readonly name: Name + /** On-chain account hosting the contract. */ + readonly account: NameType + /** Lazily resolved typed action invokers. */ + readonly actions: SysioActionInvokers + /** Lazily resolved typed table queries. */ + readonly tables: SysioTableQueries +} + +/** Per-contract account overrides for nonstandard system-contract deployments. */ +export type SysioContractAccounts = Partial> + +/** String values accepted by the backward-compatible named client factory. */ +export type SysioContractNameInput = `${SysioContractName}` + +/** Generated enum member corresponding to a named factory input. */ +export type SysioContractNameFromInput = + Extract + +/** Options for creating the root system-contract proxy. */ +export interface SysioClientOptions { + /** Chain API client used for reads and signed action invocation. */ + client: APIClient + /** Optional account overrides keyed by generated system-contract name. */ + contracts?: SysioContractAccounts +} + +/** Options for directly resolving one system-contract proxy. */ +export interface GetSysioContractOptions { + /** Optional API client; action preparation works without one. */ + client?: APIClient + /** Optional on-chain account override. */ + contract?: NameType +} + +/** Backward-compatible options for creating one named system-contract client. */ +export interface SystemContractClientOptions< + Name extends SysioContractNameInput +> extends SysioClientOptions { + /** Generated system-contract name. */ + name: Name + /** Optional on-chain account override. */ + contract?: NameType +} + +/** Root proxy methods shared with the sister-repository Wire client. */ +export interface SysioClientMethods { + /** Resolves one generated typed system-contract client. */ + getSysioContract( + name: Name + ): SysioContractClient +} + +/** Root proxy exposing every generated system contract by its short name. */ +export type SysioClient = SysioClientMethods & { + readonly [Name in SysioContractName]: SysioContractClient +} + +interface RuntimeActionDescriptor { + serialize: ContractActionDescriptor["serialize"] +} + +interface RuntimeContractDescriptor { + actions: Record +} + +interface RuntimeActionDataMapping { + [SysioContractName.authex]: SysioAuthexActionData + [SysioContractName.msig]: SysioMsigActionData + [SysioContractName.reserv]: SysioReservActionData +} + +/** @deprecated Use the generated proxy; retained for descriptor consumers. */ +export const descriptors = { + [SysioContractName.authex]: authexDescriptor, + [SysioContractName.msig]: msigDescriptor, + [SysioContractName.reserv]: reservDescriptor +} as const + +/** @deprecated Use {@link SysioContractNameInput}. */ +export type SystemContractName = `${keyof typeof descriptors}` + +/** @deprecated Use generated action and table maps through the proxy. */ +export type SystemContractDescriptor = + (typeof descriptors)[Extract] + +/** @deprecated Use {@link SysioContractClient}. */ +export type SystemContractClient = + SysioContractClient> + +const RuntimeDescriptors: Partial< + Record +> = descriptors as unknown as Partial< + Record +> + +/** Returns true when a reflected root-proxy member is a generated contract name. */ +function isSysioContractName(value: string): value is SysioContractName { + return Object.prototype.hasOwnProperty.call(SysioContractDefinitions, value) +} + +/** Throws when a read or invocation is attempted without an API client. */ +function assertClient(client: APIClient): APIClient { + if (!client) { + throw new Error( + "An APIClient is required to query or invoke a system contract." + ) + } + return client +} + +/** Normalizes string and object permission inputs for action payloads. */ +function normalizeAuthorization( + authorization: ContractPermissionLevel[] = [] +): PermissionLevel[] { + return authorization.map(value => PermissionLevel.from(value)) +} + +/** Returns the optional runtime action descriptor used for synchronous encoding. */ +function getRuntimeActionDescriptor( + name: SysioContractName, + actionName: string +): RuntimeActionDescriptor { + return RuntimeDescriptors[name]?.actions[actionName] +} + +/** Prefers an encoded Action and deliberately retains the raw payload on encoding failure. */ +function prepareAction< + Name extends SysioContractName, + ActionName extends SysioActionName +>( + payload: SysioActionPayload, + abi?: ABIDef +): SysioPreparedAction { + try { + const descriptor = getRuntimeActionDescriptor( + payload.contract, + String(payload.name) + ), + data = descriptor?.serialize + ? descriptor.serialize(payload.data) + : payload.data + + return Action.from({ ...payload, data }, abi) + } catch { + // Raw AnyAction payloads are intentional: APIClient resolves the deployed + // ABI during push, keeping generated contracts usable without hand codecs. + return payload + } +} + +/** Creates one lazy typed action invoker. */ +function createActionInvoker< + Name extends SysioContractName, + ActionName extends SysioActionName +>( + name: Name, + account: NameType, + actionName: ActionName, + client?: APIClient +): SysioActionInvoker { + const prepare = ( + data: SysioActionData, + options: SysioActionOptions = {} + ) => { + const contract = options.contract || account, + payload: SysioActionPayload = { + contract: name, + account: contract, + name: actionName, + authorization: normalizeAuthorization(options.authorization), + data + } + + return prepareAction(payload, options.abi) + }, + invoker = ( + data: SysioActionData, + authorization: ContractPermissionLevel[], + options: ContractBuildActionOptions = {} + ) => assertEncodedAction(prepare(data, { ...options, authorization })) + + invoker.prepare = prepare + invoker.invoke = async ( + data: SysioActionData, + options: SysioActionInvocationOptions = {} + ) => + assertClient(client).pushTransaction( + prepare(data, options), + options.pushOptions + ) + + return invoker +} + +/** Creates one lazy typed table query. */ +function createTableQuery< + Name extends SysioContractName, + TableName extends SysioTableName +>( + account: NameType, + tableName: TableName, + client?: APIClient +): SysioTableQuery { + const query = async ( + options: ContractTableRowsOptions = {} + ) => + assertClient(client).v1.chain.get_table_rows({ + code: account, + table: tableName, + scope: account, + json: true, + ...options + } as any) as Promise< + GetTableRowsResponse> + >, + first = async ( + options: ContractTableRowsOptions = {} + ) => { + const result = await query({ ...options, limit: options.limit || 1 }) + return result.rows[0] || null + }, + scopes = async (options: ContractTableScopesOptions = {}) => { + const result = await assertClient(client).v1.chain.get_table_by_scope({ + code: account, + table: tableName, + ...options + }) + return result.rows.map(row => row.scope.toString()) + } + + return { query, rows: query, first, scopes } +} + +/** Creates a guarded lazy member proxy for actions or tables. */ +function createMemberProxy( + contractName: SysioContractName, + kind: SysioMemberKind, + knownMembers: ReadonlyArray, + createMember: (member: string) => Member +): Record { + const members = new Map() + + return new Proxy({} as Record, { + get: (_target, property) => { + if (typeof property === "symbol" || property === PromiseThenMember) { + return null + } + + const member = String(property) + if (!knownMembers.includes(member)) { + throw new Error(`Unknown sysio.${contractName} ${kind}: ${member}`) + } + + const existing = members.get(member) + if (existing) return existing + + const created = createMember(member) + members.set(member, created) + return created + } + }) +} + +/** + * Resolves one generated system-contract proxy. + * + * Action preparation is available without an API client; reads and invocation + * assert that one was supplied. Runtime member access is restricted to the + * generated contract definition. + */ +export function getSysioContract( + name: Name, + options: GetSysioContractOptions = {} +): SysioContractClient { + if (!isSysioContractName(String(name))) { + throw new Error(`Unknown sysio contract: ${name}`) + } + + const definition = SysioContractDefinitions[name], + account = options.contract || definition.account, + actions = createMemberProxy( + name, + SysioMemberKind.action, + definition.actions, + actionName => + createActionInvoker( + name, + account, + actionName as SysioActionName, + options.client + ) + ), + tables = createMemberProxy( + name, + SysioMemberKind.table, + definition.tables, + tableName => + createTableQuery>( + account, + tableName as SysioTableName, + options.client + ) + ) + + return { + name, + account, + actions, + tables + } as unknown as SysioContractClient +} + +/** Creates the root proxy exposing all generated system contracts. */ +export function createSysioClient(options: SysioClientOptions): SysioClient { + const clients = new Map< + SysioContractName, + SysioContractClient + >(), + getContract = (name: Name) => { + const existing = clients.get(name) + if (existing) return existing as SysioContractClient + + const created = getSysioContract(name, { + client: options.client, + contract: options.contracts?.[name] + }) + clients.set(name, created as SysioContractClient) + return created + } + + return new Proxy({ getSysioContract: getContract } as SysioClient, { + get: (target, property) => { + if (typeof property === "symbol" || property === PromiseThenMember) { + return null + } + if (property === GetSysioContractMember) return target.getSysioContract + + const member = String(property) + if (!isSysioContractName(member)) { + throw new Error(`Unknown sysio contract: ${member}`) + } + return getContract(member) + } + }) +} + +/** Creates either the root proxy or one named proxy using the legacy factory form. */ +export function createClient(options: SysioClientOptions): SysioClient +export function createClient( + options: SystemContractClientOptions +): SysioContractClient> +export function createClient( + options: + | SysioClientOptions + | SystemContractClientOptions +): SysioClient | SysioContractClient { + if (!("name" in options)) return createSysioClient(options) + + return getSysioContract(options.name as SysioContractName, { + client: options.client, + contract: options.contract + }) +} + +/** Asserts that preparation produced an ABI-encoded action. */ +export function assertEncodedAction< + Name extends SysioContractName, + ActionName extends SysioActionName +>(action: SysioPreparedAction): Action { + if (!(action instanceof Action)) { + throw new Error( + "System-contract action could not be synchronously ABI encoded." + ) + } + return action +} diff --git a/packages/sdk-core/src/contracts/sysio/authex/Actions.ts b/packages/sdk-core/src/contracts/sysio/authex/Actions.ts index 1f30322..4cc070f 100644 --- a/packages/sdk-core/src/contracts/sysio/authex/Actions.ts +++ b/packages/sdk-core/src/contracts/sysio/authex/Actions.ts @@ -1,13 +1,13 @@ -import { buildContractAction } from "../../Contract.js" import type { Action } from "../../../chain/Action.js" import { Name } from "../../../chain/Name.js" import { PermissionLevel } from "../../../chain/PermissionLevel.js" import { PublicKey } from "../../../chain/PublicKey.js" import { Signature } from "../../../chain/Signature.js" import type * as SysioContracts from "../../../types/SysioContractTypes.js" +import { SysioContractName } from "../../../types/SysioContractTypes.js" +import { assertEncodedAction, getSysioContract } from "../Client.js" import { DEFAULT_AUTHEX_CONTRACT } from "./Constants.js" -import { descriptor as authexDescriptor } from "./Descriptor.js" import { AuthexClearLinks, AuthexCreateLink, @@ -41,17 +41,18 @@ export function buildCreateLinkAction( ): Action { const account = Name.from(options.account) - return buildContractAction({ - contract: options.contract || DEFAULT_AUTHEX_CONTRACT, - descriptor: authexDescriptor.actions.createlink, - authorization: [ - PermissionLevel.from({ - actor: account, - permission: options.permission || "active" - }) - ], - data: createLinkActionData(options) - }) + return assertEncodedAction( + getSysioContract(SysioContractName.authex, { + contract: options.contract || DEFAULT_AUTHEX_CONTRACT + }).actions.createlink.prepare(createLinkActionData(options), { + authorization: [ + PermissionLevel.from({ + actor: account, + permission: options.permission || "active" + }) + ] + }) + ) } /** Builds generated action data for `sysio.authex::recordlink`. */ @@ -69,17 +70,19 @@ export function recordLinkActionData( export function buildRecordLinkAction( options: BuildRecordLinkActionOptions ): Action { - return buildContractAction({ - contract: options.contract || DEFAULT_AUTHEX_CONTRACT, - descriptor: authexDescriptor.actions.recordlink, - authorization: [ - PermissionLevel.from({ - actor: options.contract || DEFAULT_AUTHEX_CONTRACT, - permission: "active" - }) - ], - data: recordLinkActionData(options) - }) + const contract = options.contract || DEFAULT_AUTHEX_CONTRACT + return assertEncodedAction( + getSysioContract(SysioContractName.authex, { + contract + }).actions.recordlink.prepare(recordLinkActionData(options), { + authorization: [ + PermissionLevel.from({ + actor: contract, + permission: "active" + }) + ] + }) + ) } /** Builds an unsigned testing-only `sysio.authex::clearlinks` action. */ @@ -88,17 +91,21 @@ export function buildClearLinksAction( ): Action { const contract = options.contract || DEFAULT_AUTHEX_CONTRACT - return buildContractAction({ - contract, - descriptor: authexDescriptor.actions.clearlinks, - authorization: [ - PermissionLevel.from({ - actor: contract, - permission: "active" - }) - ], - data: {} - }) + return assertEncodedAction( + getSysioContract(SysioContractName.authex, { + contract + }).actions.clearlinks.prepare( + {}, + { + authorization: [ + PermissionLevel.from({ + actor: contract, + permission: "active" + }) + ] + } + ) + ) } /** Runtime action data serializers keyed by `sysio.authex` action name. */ diff --git a/packages/sdk-core/src/contracts/sysio/authex/Client.ts b/packages/sdk-core/src/contracts/sysio/authex/Client.ts index 8cab575..8b729e2 100644 --- a/packages/sdk-core/src/contracts/sysio/authex/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/authex/Client.ts @@ -7,12 +7,12 @@ import { Checksum256 } from "../../../chain/Checksum.js" import { KeyType } from "../../../chain/KeyType.js" import { Name, type NameType } from "../../../chain/Name.js" import { PublicKey, type PublicKeyType } from "../../../chain/PublicKey.js" -import { SysioAuthexChainkind } from "../../../types/SysioContractTypes.js" import { - ContractClient, - createContractClient, - type ContractTableRowsOptions -} from "../../Contract.js" + SysioAuthexChainkind, + SysioContractName +} from "../../../types/SysioContractTypes.js" +import type { ContractTableRowsOptions } from "../../Contract.js" +import { getSysioContract, type SysioContractClient } from "../Client.js" import { buildClearLinksAction, @@ -24,11 +24,6 @@ import { AUTHEX_LINKS_BY_PUBLIC_KEY_INDEX, DEFAULT_AUTHEX_CONTRACT } from "./Constants.js" -import { - descriptor, - type SysioAuthexActionData, - type SysioAuthexTableRows -} from "./Descriptor.js" import { signCreateLink } from "./Signing.js" import type { AuthexClientOptions, @@ -96,19 +91,15 @@ export class AuthexClient { readonly contract: NameType /** Generic typed contract client for direct action/table access. */ - readonly contractClient: ContractClient< - SysioAuthexActionData, - SysioAuthexTableRows - > + readonly contractClient: SysioContractClient /** Creates an AuthEx client. */ constructor(config: AuthexClientOptions) { this.client = config.client this.contract = config.contract || DEFAULT_AUTHEX_CONTRACT - this.contractClient = createContractClient({ + this.contractClient = getSysioContract(SysioContractName.authex, { client: config.client, - contract: this.contract, - descriptor + contract: this.contract }) } @@ -182,7 +173,7 @@ export class AuthexClient { async listLinks( options: ContractTableRowsOptions = {} ): Promise { - const result = await this.contractClient.tables.links.rows({ + const result = await this.contractClient.tables.links.query({ limit: 100, ...options }) @@ -197,7 +188,7 @@ export class AuthexClient { ): Promise { const name = accountString(account), nameValue = Name.from(account).value.toString(), - result = await this.contractClient.tables.links.rows({ + result = await this.contractClient.tables.links.query({ index_name: AUTHEX_LINKS_BY_NAME_INDEX, lower_bound: jsonIndexBound(AUTHEX_LINKS_BY_NAME_INDEX, nameValue), limit: options.limit || 100 @@ -224,7 +215,7 @@ export class AuthexClient { ): Promise { const key = PublicKey.from(publicKey), hash = publicKeyHash(key), - result = await this.contractClient.tables.links.rows({ + result = await this.contractClient.tables.links.query({ index_name: AUTHEX_LINKS_BY_PUBLIC_KEY_INDEX, lower_bound: jsonIndexBound( AUTHEX_LINKS_BY_PUBLIC_KEY_INDEX, diff --git a/packages/sdk-core/src/contracts/sysio/index.ts b/packages/sdk-core/src/contracts/sysio/index.ts index e91e4de..c2fee81 100644 --- a/packages/sdk-core/src/contracts/sysio/index.ts +++ b/packages/sdk-core/src/contracts/sysio/index.ts @@ -1,61 +1,5 @@ -import type { APIClient } from "../../api/Client.js" -import type { NameType } from "../../chain/Name.js" -import { - ContractClient, - createContractClient, - 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 - -/** Friendly system contract names registered with this package. */ -export type SystemContractName = keyof typeof descriptors - -/** Descriptor registered for a friendly system contract name. */ -export type SystemContractDescriptor = - (typeof descriptors)[TName] - -/** Typed client derived from a registered system contract descriptor. */ -export type SystemContractClient = - SystemContractDescriptor extends ContractDescriptor< - infer TActions, - infer TTables - > - ? ContractClient - : never - -/** Options for creating a registered system contract client. */ -export interface SystemContractClientOptions { - /** Chain API client used for RPC reads. */ - client: APIClient - /** Friendly system contract name. */ - name: TName - /** Optional account override for deployments that do not use the default system account. */ - contract?: NameType -} - -/** Creates a typed client for a registered system contract descriptor. */ -export function createClient( - options: SystemContractClientOptions -): SystemContractClient { - return createContractClient({ - client: options.client, - contract: options.contract, - descriptor: descriptors[options.name] - } as any) as SystemContractClient -} +export * from "./Client.js" diff --git a/packages/sdk-core/src/contracts/sysio/msig/Actions.ts b/packages/sdk-core/src/contracts/sysio/msig/Actions.ts index d69d84b..bafc506 100644 --- a/packages/sdk-core/src/contracts/sysio/msig/Actions.ts +++ b/packages/sdk-core/src/contracts/sysio/msig/Actions.ts @@ -1,13 +1,17 @@ -import { buildContractAction } from "../../Contract.js" import type { Action } from "../../../chain/Action.js" import { Checksum256 } from "../../../chain/Checksum.js" -import { Name, NameType } from "../../../chain/Name.js" -import { PermissionLevel, PermissionLevelType } from "../../../chain/PermissionLevel.js" +import { Name, type NameType } from "../../../chain/Name.js" +import { + PermissionLevel, + type PermissionLevelType +} from "../../../chain/PermissionLevel.js" import { Transaction } from "../../../chain/Transaction.js" import type * as SysioContracts from "../../../types/SysioContractTypes.js" +import { SysioContractName } from "../../../types/SysioContractTypes.js" +import { assertEncodedAction, getSysioContract } from "../Client.js" import { DEFAULT_MSIG_CONTRACT } from "./Constants.js" -import { descriptor as msigDescriptor } from "./Descriptor.js" +import type { SysioMsigProposeActionData } from "./Descriptor.js" import { MsigApprove, MsigCancel, @@ -35,28 +39,28 @@ function permissionLevel(value: MsigPermissionLevel): PermissionLevel { export function buildProposeAction(options: BuildProposeActionOptions): Action { const proposer = Name.from(options.proposer) - return buildContractAction({ - contract: options.contract || DEFAULT_MSIG_CONTRACT, - descriptor: { - name: msigDescriptor.actions.propose.name, - // The generated SysioMsigProposeAction currently omits inherited - // transaction-header fields, so this builder keeps using the runtime - // Transaction serializer until generator metadata catches up. - serialize: null - }, - authorization: [ - PermissionLevel.from({ - actor: proposer, - permission: options.proposerPermission || "active" - }) - ], - data: MsigPropose.from({ - proposer, - proposal_name: options.proposalName, - requested: options.requested.map(permissionLevel), - trx: Transaction.from(options.transaction) + // The generated propose interface currently omits inherited transaction + // header fields. Keep the complete runtime struct until generation includes + // those fields, then let the proxy's msig codec encode it synchronously. + const data = MsigPropose.from({ + proposer, + proposal_name: options.proposalName, + requested: options.requested.map(permissionLevel), + trx: Transaction.from(options.transaction) + }) as unknown as SysioMsigProposeActionData + + return assertEncodedAction( + getSysioContract(SysioContractName.msig, { + contract: options.contract || DEFAULT_MSIG_CONTRACT + }).actions.propose.prepare(data, { + authorization: [ + PermissionLevel.from({ + actor: proposer, + permission: options.proposerPermission || "active" + }) + ] }) - }) + ) } /** Builds an unsigned `sysio.msig::approve` action. */ @@ -74,16 +78,17 @@ export function buildApproveAction(options: BuildApproveActionOptions): Action { : null } - return buildContractAction({ - contract: options.contract || DEFAULT_MSIG_CONTRACT, - descriptor: msigDescriptor.actions.approve, - authorization: [level], - data - }) + return assertEncodedAction( + getSysioContract(SysioContractName.msig, { + contract: options.contract || DEFAULT_MSIG_CONTRACT + }).actions.approve.prepare(data, { authorization: [level] }) + ) } /** Builds an unsigned `sysio.msig::unapprove` action. */ -export function buildUnapproveAction(options: BuildUnapproveActionOptions): Action { +export function buildUnapproveAction( + options: BuildUnapproveActionOptions +): Action { const level = permissionLevel(options.level), data: SysioContracts.SysioMsigUnapproveAction = { proposer: Name.from(options.proposer).toString(), @@ -94,12 +99,11 @@ export function buildUnapproveAction(options: BuildUnapproveActionOptions): Acti } } - return buildContractAction({ - contract: options.contract || DEFAULT_MSIG_CONTRACT, - descriptor: msigDescriptor.actions.unapprove, - authorization: [level], - data - }) + return assertEncodedAction( + getSysioContract(SysioContractName.msig, { + contract: options.contract || DEFAULT_MSIG_CONTRACT + }).actions.unapprove.prepare(data, { authorization: [level] }) + ) } /** Builds an unsigned `sysio.msig::cancel` action. */ @@ -110,17 +114,18 @@ export function buildCancelAction(options: BuildCancelActionOptions): Action { canceler: Name.from(options.canceler).toString() } - return buildContractAction({ - contract: options.contract || DEFAULT_MSIG_CONTRACT, - descriptor: msigDescriptor.actions.cancel, - authorization: [ - PermissionLevel.from({ - actor: options.canceler, - permission: options.cancelerPermission || "active" - }) - ], - data - }) + return assertEncodedAction( + getSysioContract(SysioContractName.msig, { + contract: options.contract || DEFAULT_MSIG_CONTRACT + }).actions.cancel.prepare(data, { + authorization: [ + PermissionLevel.from({ + actor: options.canceler, + permission: options.cancelerPermission || "active" + }) + ] + }) + ) } /** Builds an unsigned `sysio.msig::exec` action. */ @@ -131,36 +136,40 @@ export function buildExecAction(options: BuildExecActionOptions): Action { executer: Name.from(options.executer).toString() } - return buildContractAction({ - contract: options.contract || DEFAULT_MSIG_CONTRACT, - descriptor: msigDescriptor.actions.exec, - authorization: [ - PermissionLevel.from({ - actor: options.executer, - permission: options.executerPermission || "active" - }) - ], - data - }) + return assertEncodedAction( + getSysioContract(SysioContractName.msig, { + contract: options.contract || DEFAULT_MSIG_CONTRACT + }).actions.exec.prepare(data, { + authorization: [ + PermissionLevel.from({ + actor: options.executer, + permission: options.executerPermission || "active" + }) + ] + }) + ) } /** Builds an unsigned `sysio.msig::invalidate` action. */ -export function buildInvalidateAction(options: BuildInvalidateActionOptions): Action { +export function buildInvalidateAction( + options: BuildInvalidateActionOptions +): Action { const data: SysioContracts.SysioMsigInvalidateAction = { account: Name.from(options.account).toString() } - return buildContractAction({ - contract: options.contract || DEFAULT_MSIG_CONTRACT, - descriptor: msigDescriptor.actions.invalidate, - authorization: [ - PermissionLevel.from({ - actor: options.account, - permission: options.permission || "active" - }) - ], - data - }) + return assertEncodedAction( + getSysioContract(SysioContractName.msig, { + contract: options.contract || DEFAULT_MSIG_CONTRACT + }).actions.invalidate.prepare(data, { + authorization: [ + PermissionLevel.from({ + actor: options.account, + permission: options.permission || "active" + }) + ] + }) + ) } /** Builds an unsigned read-only `sysio.msig::getproposal` action. */ @@ -174,12 +183,11 @@ export function buildGetProposalAction( proposal_name: Name.from(proposalName).toString() } - return buildContractAction({ - contract, - descriptor: msigDescriptor.actions.getproposal, - authorization: [], - data - }) + return assertEncodedAction( + getSysioContract(SysioContractName.msig, { + contract + }).actions.getproposal.prepare(data) + ) } /** Runtime action data serializers keyed by `sysio.msig` action name. */ diff --git a/packages/sdk-core/src/contracts/sysio/msig/Client.ts b/packages/sdk-core/src/contracts/sysio/msig/Client.ts index d4f700e..b156dd3 100644 --- a/packages/sdk-core/src/contracts/sysio/msig/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/msig/Client.ts @@ -6,6 +6,8 @@ import { Bytes } from "../../../chain/Bytes.js" import { Name, NameType } from "../../../chain/Name.js" import { SignedTransaction } from "../../../chain/Transaction.js" import { Serializer } from "../../../serializer/index.js" +import { SysioContractName } from "../../../types/SysioContractTypes.js" +import { getSysioContract, type SysioContractClient } from "../Client.js" import { buildGetProposalAction } from "./Actions.js" import { detectMsigCapabilities } from "./Capabilities.js" @@ -27,16 +29,22 @@ import type { MsigCapabilities, MsigClientOptions, MsigReadStrategy, + MsigTableName, ProposalDetail } from "./Types.js" +interface MsigNamedRow { + proposal_name?: NameType + account?: NameType +} + function nameString(value: NameType): string { return Name.from(value).toString() } function rowNameEquals( - row: { proposal_name?: unknown; account?: unknown }, - field: "proposal_name" | "account", + row: MsigNamedRow, + field: keyof MsigNamedRow, value: NameType ): boolean { const rowValue = row[field] @@ -101,7 +109,9 @@ export function decodeReadOnlyProposalReturn(response: any): MsigProposal { trace?.return_value_data_hex if (!value) { - throw new Error("Unable to find getproposal return value in read-only transaction trace.") + throw new Error( + "Unable to find getproposal return value in read-only transaction trace." + ) } return typeof value === "string" @@ -117,13 +127,20 @@ export class MsigClient { /** Multisig contract account. */ readonly contract: NameType - private readonly configuredReadStrategy: MsigReadStrategy | "auto" + /** Generic typed contract proxy used by the higher-level workflow helpers. */ + readonly contractClient: SysioContractClient + + private readonly configuredReadStrategy: MsigClientOptions["readStrategy"] private capabilities: Promise | null = null /** Creates a multisig client. */ constructor(config: MsigClientOptions) { this.client = config.client this.contract = config.contract || DEFAULT_MSIG_CONTRACT + this.contractClient = getSysioContract(SysioContractName.msig, { + client: config.client, + contract: this.contract + }) this.configuredReadStrategy = config.readStrategy || "auto" } @@ -150,9 +167,7 @@ export class MsigClient { async listProposalScopes( options: ListProposalScopesOptions = {} ): Promise { - const result = await this.client.v1.chain.get_table_by_scope({ - code: this.contract, - table: "proposal", + return this.contractClient.tables.proposal.scopes({ ...(options.lowerBound ? { lower_bound: Name.from(options.lowerBound).toString() } : {}), @@ -161,8 +176,6 @@ export class MsigClient { : {}), limit: options.limit || 100 }) - - return result.rows.map((row: any) => row.scope.toString()) } /** Reads a proposal row or reassembled chunked proposal. */ @@ -173,7 +186,11 @@ export class MsigClient { const capabilities = await this.getCapabilities() return ( - await this.readProposalWithCapabilities(capabilities, proposer, proposalName) + await this.readProposalWithCapabilities( + capabilities, + proposer, + proposalName + ) ).proposal } @@ -205,14 +222,11 @@ export class MsigClient { options: ListProposalsOptions = {} ): Promise { const limit = options.limit || 100 - let rows: any[] = [] + let rows: any[] try { - const result = await this.client.v1.chain.get_table_rows({ - json: true, - code: this.contract, + const result = await this.contractClient.tables.proposal.query({ scope: nameString(proposer), - table: "proposal", ...(options.lowerBound ? { lower_bound: Name.from(options.lowerBound as NameType) } : {}), @@ -228,7 +242,11 @@ export class MsigClient { throw error } - rows = await this.getScopedRows(proposer, "proposal", Math.max(limit, 1000)) + rows = await this.getScopedRows( + proposer, + "proposal", + Math.max(limit, 1000) + ) } if (options.lowerBound || options.upperBound) { @@ -259,7 +277,7 @@ export class MsigClient { async getApprovals( proposer: NameType, proposalName: NameType - ): Promise { + ): Promise { const modern = await this.getExactScopedRow( proposer, "approvals2", @@ -292,7 +310,7 @@ export class MsigClient { } /** Reads an account invalidation row, when present. */ - async getInvalidation(account: NameType): Promise { + async getInvalidation(account: NameType): Promise { const rows = await this.getScopedRows(this.contract, "invals"), row = rows.find((candidate: any) => rowNameEquals(candidate, "account", account) @@ -310,7 +328,11 @@ export class MsigClient { return match(strategy) .with("read-only-getproposal", async () => { - const action = buildGetProposalAction(proposer, proposalName, this.contract) + const action = buildGetProposalAction( + proposer, + proposalName, + this.contract + ) try { const response = await this.sendReadOnlyAction(action) @@ -476,17 +498,14 @@ export class MsigClient { private async getExactScopedRow( scope: NameType, - table: NameType, + table: MsigTableName, proposalName: NameType - ): Promise { - let rows: any[] = [] + ): Promise { + let rows: any[] try { - const result = await this.client.v1.chain.get_table_rows({ - json: true, - code: this.contract, + const result = await this.contractClient.tables[table].query({ scope: nameString(scope), - table, lower_bound: Name.from(proposalName), limit: 1 }) @@ -496,15 +515,14 @@ export class MsigClient { if (!isKvBoundReadError(error)) { throw error } + rows = [] } const row = rows.find((candidate: any) => rowNameEquals(candidate, "proposal_name", proposalName) ) || - ( - await this.getScopedRows(scope, table) - ).find((candidate: any) => + (await this.getScopedRows(scope, table)).find((candidate: any) => rowNameEquals(candidate, "proposal_name", proposalName) ) || null @@ -514,14 +532,11 @@ export class MsigClient { private async getScopedRows( scope: NameType, - table: NameType, + table: MsigTableName, limit = 1000 ): Promise { - const result = await this.client.v1.chain.get_table_rows({ - json: true, - code: this.contract, + const result = await this.contractClient.tables[table].query({ scope: nameString(scope), - table, limit }) diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Actions.ts b/packages/sdk-core/src/contracts/sysio/reserv/Actions.ts index bb2ed78..6f7703f 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/Actions.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/Actions.ts @@ -1,11 +1,11 @@ -import { buildContractAction } from "../../Contract.js" import type { Action } from "../../../chain/Action.js" import { Name } from "../../../chain/Name.js" import { PermissionLevel } from "../../../chain/PermissionLevel.js" import type * as SysioContracts from "../../../types/SysioContractTypes.js" +import { SysioContractName } from "../../../types/SysioContractTypes.js" +import { assertEncodedAction, getSysioContract } from "../Client.js" import { DEFAULT_RESERV_CONTRACT } from "./Constants.js" -import { descriptor as reservDescriptor } from "./Descriptor.js" import { reserveSlugData } from "./Slug.js" import type { MatchReserveOptions, ReserveQuoteOptions } from "./Types.js" @@ -14,9 +14,7 @@ interface Stringifiable { toString(): string } -function amountString( - value: number | string | bigint | Stringifiable -): string { +function amountString(value: number | string | bigint | Stringifiable): string { return value.toString() } @@ -37,17 +35,18 @@ export function matchReserveActionData( export function buildMatchReserveAction(options: MatchReserveOptions): Action { const matcher = Name.from(options.matcher) - return buildContractAction({ - contract: options.contract || DEFAULT_RESERV_CONTRACT, - descriptor: reservDescriptor.actions.matchreserve, - authorization: [ - PermissionLevel.from({ - actor: matcher, - permission: options.permission || "active" - }) - ], - data: matchReserveActionData(options) - }) + return assertEncodedAction( + getSysioContract(SysioContractName.reserv, { + contract: options.contract || DEFAULT_RESERV_CONTRACT + }).actions.matchreserve.prepare(matchReserveActionData(options), { + authorization: [ + PermissionLevel.from({ + actor: matcher, + permission: options.permission || "active" + }) + ] + }) + ) } /** Builds generated action data for read-only `sysio.reserv::swapquote`. */ @@ -67,10 +66,9 @@ export function swapQuoteActionData( /** Builds an unsigned read-only `sysio.reserv::swapquote` action. */ export function buildSwapQuoteAction(options: ReserveQuoteOptions): Action { - return buildContractAction({ - contract: options.contract || DEFAULT_RESERV_CONTRACT, - descriptor: reservDescriptor.actions.swapquote, - authorization: [], - data: swapQuoteActionData(options) - }) + return assertEncodedAction( + getSysioContract(SysioContractName.reserv, { + contract: options.contract || DEFAULT_RESERV_CONTRACT + }).actions.swapquote.prepare(swapQuoteActionData(options)) + ) } diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Client.ts b/packages/sdk-core/src/contracts/sysio/reserv/Client.ts index 3decd53..999bcf0 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/Client.ts @@ -7,29 +7,19 @@ import { SignedTransaction } from "../../../chain/Transaction.js" import { Serializer } from "../../../serializer/index.js" import { SysioReservChainkind, - SysioReservReservestatus + SysioReservReservestatus, + SysioContractName } from "../../../types/SysioContractTypes.js" import type * as SysioContracts from "../../../types/SysioContractTypes.js" -import { - ContractClient, - createContractClient, - type ContractTableRowsOptions -} from "../../Contract.js" +import type { ContractTableRowsOptions } from "../../Contract.js" +import { getSysioContract, type SysioContractClient } from "../Client.js" import { buildMatchReserveAction, buildSwapQuoteAction } from "./Actions.js" import { DEFAULT_RESERV_CONTRACT, DEFAULT_RESERVE_QUERY_LIMIT } from "./Constants.js" -import { - descriptor, - type SysioReservActionData, - type SysioReservTableRows -} from "./Descriptor.js" -import { - reserveSlugString, - reserveSlugValue -} from "./Slug.js" +import { reserveSlugString, reserveSlugValue } from "./Slug.js" import type { ListReservesOptions, MatchReserveOptions, @@ -155,19 +145,15 @@ export class ReserveClient { readonly contract: NameType /** Generic typed client for direct public action and table access. */ - readonly contractClient: ContractClient< - SysioReservActionData, - SysioReservTableRows - > + readonly contractClient: SysioContractClient /** Creates a reserve client. */ constructor(config: ReserveClientOptions) { this.client = config.client this.contract = config.contract || DEFAULT_RESERV_CONTRACT - this.contractClient = createContractClient({ + this.contractClient = getSysioContract(SysioContractName.reserv, { client: config.client, - contract: this.contract, - descriptor + contract: this.contract }) } @@ -191,21 +177,21 @@ export class ReserveClient { limit: DEFAULT_RESERVE_QUERY_LIMIT, ...(lowerBound ? { lower_bound: lowerBound } : {}) }, - result = await this.contractClient.tables.reserves.rows(query), + result = await this.contractClient.tables.reserves.query(query), matches = result.rows.filter(row => { - const status = enumValue(SysioReservReservestatus, row.status), - chainMatches = - options.chainCode == null || - rowSlugValue(row.chain_code) === - reserveSlugValue(options.chainCode), - tokenMatches = - options.tokenCode == null || - rowSlugValue(row.token_code) === - reserveSlugValue(options.tokenCode), - statusMatches = options.status == null || status === options.status, - ownerMatches = !owner || optionalAccountString(row.owner) === owner, - privacyMatches = - options.isPrivate == null || row.is_private === options.isPrivate + const status = enumValue(SysioReservReservestatus, row.status), + chainMatches = + options.chainCode == null || + rowSlugValue(row.chain_code) === + reserveSlugValue(options.chainCode), + tokenMatches = + options.tokenCode == null || + rowSlugValue(row.token_code) === + reserveSlugValue(options.tokenCode), + statusMatches = options.status == null || status === options.status, + ownerMatches = !owner || optionalAccountString(row.owner) === owner, + privacyMatches = + options.isPrivate == null || row.is_private === options.isPrivate return ( chainMatches && diff --git a/packages/sdk-core/tests/contracts/sysio/Client.test.ts b/packages/sdk-core/tests/contracts/sysio/Client.test.ts new file mode 100644 index 0000000..9e5335c --- /dev/null +++ b/packages/sdk-core/tests/contracts/sysio/Client.test.ts @@ -0,0 +1,174 @@ +import { Action, contracts, SysioContracts } from "@wireio/sdk-core" + +const { SysioContractName } = SysioContracts + +/** Creates the minimal API surface exercised by the system-contract proxy. */ +function createMockApi(rows: unknown[] = []) { + return { + pushTransaction: jest.fn(async action => ({ action })), + v1: { + chain: { + get_table_rows: jest.fn(async () => ({ + rows, + more: false + })), + get_table_by_scope: jest.fn(async () => ({ + rows: [{ scope: "alice" }], + more: "" + })) + } + } + } as any +} + +describe("system contract proxy", () => { + test("resolves and caches generated contracts from the root proxy", () => { + const api = createMockApi(), + sysio = contracts.sysio.createClient({ client: api }) + + expect(sysio.msig).toBe(sysio.getSysioContract(SysioContractName.msig)) + expect(sysio.msig.name).toBe(SysioContractName.msig) + expect(sysio.msig.account).toBe("sysio.msig") + expect(Reflect.get(sysio, "then")).toBeNull() + }) + + test("rejects unknown contracts, actions, and tables", () => { + const sysio = contracts.sysio.createClient({ client: createMockApi() }) + + expect(() => Reflect.get(sysio, "bogus")).toThrow( + "Unknown sysio contract: bogus" + ) + expect(() => Reflect.get(sysio.epoch.actions, "bogus")).toThrow( + "Unknown sysio.epoch action: bogus" + ) + expect(() => Reflect.get(sysio.epoch.tables, "bogus")).toThrow( + "Unknown sysio.epoch table: bogus" + ) + }) + + test("prepares ABI-encoded actions when a runtime codec is available", () => { + const msig = contracts.sysio.getSysioContract(SysioContractName.msig), + prepared = msig.actions.approve.prepare( + { + proposer: "alice", + proposal_name: "upgrade1", + level: { actor: "bob", permission: "active" }, + proposal_hash: null + }, + { authorization: ["bob@active"] } + ) + + expect(prepared).toBeInstanceOf(Action) + expect((prepared as Action).authorization.map(String)).toEqual([ + "bob@active" + ]) + }) + + test("falls back to a typed AnyAction when synchronous encoding fails", () => { + const msig = contracts.sysio.getSysioContract(SysioContractName.msig), + data = {}, + prepared = msig.actions.approve.prepare(data as any) + + expect(prepared).not.toBeInstanceOf(Action) + expect(prepared).toMatchObject({ + contract: SysioContractName.msig, + account: "sysio.msig", + name: "approve", + authorization: [], + data + }) + }) + + test("prepares generated contracts without hand-written codecs", () => { + const epoch = contracts.sysio.getSysioContract(SysioContractName.epoch), + prepared = epoch.actions.advance.prepare({}) + + expect(prepared).not.toBeInstanceOf(Action) + expect(prepared).toMatchObject({ + contract: SysioContractName.epoch, + account: "sysio.epoch", + name: "advance", + authorization: [], + data: {} + }) + }) + + test("uses a caller ABI to encode generated actions without local codecs", () => { + const epoch = contracts.sysio.getSysioContract(SysioContractName.epoch), + prepared = epoch.actions.advance.prepare( + {}, + { + abi: { + version: "sysio::abi/1.2", + structs: [{ name: "advance", base: "", fields: [] }], + actions: [ + { + name: "advance", + type: "advance", + ricardian_contract: "" + } + ] + } + } + ) + + expect(prepared).toBeInstanceOf(Action) + expect((prepared as Action).data.hexString).toBe("") + }) + + test("invokes prepared actions through the configured API client", async () => { + const api = createMockApi(), + epoch = contracts.sysio.getSysioContract(SysioContractName.epoch, { + client: api + }) + + await epoch.actions.advance.invoke( + {}, + { authorization: [{ actor: "alice", permission: "active" }] } + ) + + const [prepared, pushOptions] = api.pushTransaction.mock.calls[0] + expect(prepared.account).toBe("sysio.epoch") + expect(prepared.name).toBe("advance") + expect(prepared.authorization.map(String)).toEqual(["alice@active"]) + expect(pushOptions).toBeUndefined() + }) + + test("queries generated tables with the complete RPC option surface", async () => { + const rows = [{ current_epoch_index: 7 }], + api = createMockApi(rows), + epoch = contracts.sysio.getSysioContract(SysioContractName.epoch, { + client: api, + contract: "custom.epoch" + }), + result = await epoch.tables.epochstate.query({ + scope: "custom.scope", + lower_bound: "5", + limit: 10, + reverse: true + }) + + expect(result.rows).toEqual(rows) + expect(api.v1.chain.get_table_rows).toHaveBeenCalledWith({ + code: "custom.epoch", + table: "epochstate", + scope: "custom.scope", + json: true, + lower_bound: "5", + limit: 10, + reverse: true + }) + }) + + test("requires an API client only for reads and invocation", async () => { + const epoch = contracts.sysio.getSysioContract(SysioContractName.epoch) + + expect(() => epoch.actions.advance.prepare({})).not.toThrow() + await expect(epoch.tables.epochstate.query()).rejects.toThrow( + "An APIClient is required to query or invoke a system contract." + ) + await expect(epoch.actions.advance.invoke({})).rejects.toThrow( + "An APIClient is required to query or invoke a system contract." + ) + }) +}) From b4d1ca2c23553da1f79cd7418e84ef0c0cec73fa Mon Sep 17 00:00:00 2001 From: joshglogau Date: Tue, 21 Jul 2026 14:43:15 -0400 Subject: [PATCH 2/7] refactor(sdk-core): streamline sysio domain facades --- CLAUDE.md | 1 + packages/sdk-core/README.md | 6 +- .../sdk-core/src/contracts/sysio/Client.ts | 72 ++-------- .../sdk-core/src/contracts/sysio/Codecs.ts | 75 +++++++++++ .../src/contracts/sysio/authex/Actions.ts | 71 +--------- .../src/contracts/sysio/authex/Client.ts | 39 +----- .../src/contracts/sysio/authex/Descriptor.ts | 59 --------- .../src/contracts/sysio/authex/Structs.ts | 40 ------ .../src/contracts/sysio/authex/Types.ts | 24 +--- .../src/contracts/sysio/authex/index.ts | 1 - .../src/contracts/sysio/msig/Actions.ts | 25 +--- .../src/contracts/sysio/msig/Descriptor.ts | 123 ------------------ .../src/contracts/sysio/msig/Types.ts | 21 +++ .../src/contracts/sysio/msig/index.ts | 1 - .../src/contracts/sysio/reserv/Client.ts | 29 ++--- .../src/contracts/sysio/reserv/Descriptor.ts | 64 --------- .../src/contracts/sysio/reserv/index.ts | 1 - .../sdk-core/tests/contracts/Contract.test.ts | 27 +++- .../tests/contracts/sysio/Client.test.ts | 16 +++ .../contracts/sysio/reserv/Client.test.ts | 8 +- 20 files changed, 176 insertions(+), 527 deletions(-) create mode 100644 packages/sdk-core/src/contracts/sysio/Codecs.ts delete mode 100644 packages/sdk-core/src/contracts/sysio/authex/Descriptor.ts delete mode 100644 packages/sdk-core/src/contracts/sysio/msig/Descriptor.ts delete mode 100644 packages/sdk-core/src/contracts/sysio/reserv/Descriptor.ts diff --git a/CLAUDE.md b/CLAUDE.md index 48d62ad..507f738 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -212,6 +212,7 @@ All generated or modified code **must** include JSDoc comments (`/** ... */`), c - `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. - System-contract `prepare` prefers synchronous ABI encoding but must retain a typed `AnyAction` fallback so `APIClient` can resolve the deployed ABI. Never invent a default write authorization in the public SDK. - `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 203dcf5..7130b32 100644 --- a/packages/sdk-core/README.md +++ b/packages/sdk-core/README.md @@ -53,9 +53,7 @@ ABI can encode the action. If synchronous encoding is unavailable or fails, it returns the same generated data as a typed `AnyAction`; `APIClient` resolves the deployed ABI when that payload is invoked or pushed. Authorization is empty by default and must be supplied explicitly for writes. The legacy named factory and -callable action shorthand remain available for encoded descriptor-backed -contracts; the old `descriptors` export is retained as a deprecated migration -surface. +callable action shorthand remain available for locally encoded actions. The `AuthexClient`, `MsigClient`, and `ReserveClient` classes remain the public domain facades for proof creation, proposal compatibility, reserve @@ -122,7 +120,7 @@ const pending = await reserves.listReserves({ status: SysioReservReservestatus.RESERVE_STATUS_PENDING }) -const action = reserves.buildMatchReserveAction({ +await reserves.pushMatchReserve({ chainCode: "ETHEREUM", tokenCode: "ETH", reserveCode: "PRIMARY", diff --git a/packages/sdk-core/src/contracts/sysio/Client.ts b/packages/sdk-core/src/contracts/sysio/Client.ts index 08adc4e..dedc553 100644 --- a/packages/sdk-core/src/contracts/sysio/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/Client.ts @@ -17,24 +17,13 @@ import { type SysioContractMapping } from "../../types/SysioContractTypes.js" import type { - ContractActionDescriptor, ContractBuildActionOptions, ContractPermissionLevel, ContractTableRowsOptions, ContractTableScopesOptions } from "../Contract.js" -import { - descriptor as authexDescriptor, - type SysioAuthexActionData -} from "./authex/Descriptor.js" -import { - descriptor as msigDescriptor, - type SysioMsigActionData -} from "./msig/Descriptor.js" -import { - descriptor as reservDescriptor, - type SysioReservActionData -} from "./reserv/Descriptor.js" +import { getSysioActionCodec, type SysioActionCodec } from "./Codecs.js" +import type { SysioMsigActionDataOverrides } from "./msig/Types.js" const GetSysioContractMember = "getSysioContract" const PromiseThenMember = "then" @@ -234,51 +223,17 @@ export type SysioClient = SysioClientMethods & { readonly [Name in SysioContractName]: SysioContractClient } -interface RuntimeActionDescriptor { - serialize: ContractActionDescriptor["serialize"] -} - -interface RuntimeContractDescriptor { - actions: Record -} - interface RuntimeActionDataMapping { - [SysioContractName.authex]: SysioAuthexActionData - [SysioContractName.msig]: SysioMsigActionData - [SysioContractName.reserv]: SysioReservActionData + [SysioContractName.msig]: SysioMsigActionDataOverrides } -/** @deprecated Use the generated proxy; retained for descriptor consumers. */ -export const descriptors = { - [SysioContractName.authex]: authexDescriptor, - [SysioContractName.msig]: msigDescriptor, - [SysioContractName.reserv]: reservDescriptor -} as const - -/** @deprecated Use {@link SysioContractNameInput}. */ -export type SystemContractName = `${keyof typeof descriptors}` - -/** @deprecated Use generated action and table maps through the proxy. */ -export type SystemContractDescriptor = - (typeof descriptors)[Extract] - -/** @deprecated Use {@link SysioContractClient}. */ -export type SystemContractClient = - SysioContractClient> - -const RuntimeDescriptors: Partial< - Record -> = descriptors as unknown as Partial< - Record -> - /** Returns true when a reflected root-proxy member is a generated contract name. */ function isSysioContractName(value: string): value is SysioContractName { return Object.prototype.hasOwnProperty.call(SysioContractDefinitions, value) } /** Throws when a read or invocation is attempted without an API client. */ -function assertClient(client: APIClient): APIClient { +function assertClient(client?: APIClient): APIClient { if (!client) { throw new Error( "An APIClient is required to query or invoke a system contract." @@ -294,14 +249,6 @@ function normalizeAuthorization( return authorization.map(value => PermissionLevel.from(value)) } -/** Returns the optional runtime action descriptor used for synchronous encoding. */ -function getRuntimeActionDescriptor( - name: SysioContractName, - actionName: string -): RuntimeActionDescriptor { - return RuntimeDescriptors[name]?.actions[actionName] -} - /** Prefers an encoded Action and deliberately retains the raw payload on encoding failure. */ function prepareAction< Name extends SysioContractName, @@ -311,13 +258,10 @@ function prepareAction< abi?: ABIDef ): SysioPreparedAction { try { - const descriptor = getRuntimeActionDescriptor( - payload.contract, - String(payload.name) - ), - data = descriptor?.serialize - ? descriptor.serialize(payload.data) - : payload.data + const codec = getSysioActionCodec(payload.contract, payload.name) as + | SysioActionCodec> + | undefined, + data = codec?.serialize ? codec.serialize(payload.data) : payload.data return Action.from({ ...payload, data }, abi) } catch { diff --git a/packages/sdk-core/src/contracts/sysio/Codecs.ts b/packages/sdk-core/src/contracts/sysio/Codecs.ts new file mode 100644 index 0000000..daf7099 --- /dev/null +++ b/packages/sdk-core/src/contracts/sysio/Codecs.ts @@ -0,0 +1,75 @@ +import type { SysioContractMapping } from "../../types/SysioContractTypes.js" +import { SysioContractName } from "../../types/SysioContractTypes.js" + +import { AuthexCreateLink } from "./authex/Structs.js" +import { + MsigApprove, + MsigCancel, + MsigExec, + MsigGetProposal, + MsigInvalidate, + MsigPropose, + MsigUnapprove +} from "./msig/Structs.js" +import { + ReservMatchReserve, + ReservRewardBalance, + ReservSwapQuote +} from "./reserv/Structs.js" + +/** Generated action names for one system contract. */ +type ActionName = Extract< + keyof SysioContractMapping[Name]["actions"], + string +> + +/** Optional synchronous serializer for generated action data. */ +export interface SysioActionCodec { + /** Converts generated JSON data to an ABI-serializable runtime value. */ + serialize(data: Data): unknown +} + +/** Optional runtime codecs keyed by generated action name. */ +export type SysioActionCodecs = Partial<{ + readonly [Action in ActionName]: SysioActionCodec< + SysioContractMapping[Name]["actions"][Action] + > +}> + +/** Optional runtime codecs keyed by generated system-contract name. */ +type SysioActionCodecMapping = Partial<{ + readonly [Name in SysioContractName]: SysioActionCodecs +}> + +const ActionCodecs: SysioActionCodecMapping = { + [SysioContractName.authex]: { + createlink: { serialize: data => AuthexCreateLink.from(data) } + }, + [SysioContractName.msig]: { + approve: { serialize: data => MsigApprove.from(data) }, + cancel: { serialize: data => MsigCancel.from(data) }, + exec: { serialize: data => MsigExec.from(data) }, + getproposal: { serialize: data => MsigGetProposal.from(data) }, + invalidate: { serialize: data => MsigInvalidate.from(data) }, + propose: { serialize: data => MsigPropose.from(data) }, + unapprove: { serialize: data => MsigUnapprove.from(data) } + }, + [SysioContractName.reserv]: { + matchreserve: { serialize: data => ReservMatchReserve.from(data) }, + rewardbal: { serialize: data => ReservRewardBalance.from(data) }, + swapquote: { serialize: data => ReservSwapQuote.from(data) } + } +} + +/** Returns the optional local codec for a generated system-contract action. */ +export function getSysioActionCodec< + Name extends SysioContractName, + Action extends ActionName +>( + name: Name, + action: Action +): SysioActionCodec | undefined { + return ActionCodecs[name]?.[action] as + | SysioActionCodec + | undefined +} diff --git a/packages/sdk-core/src/contracts/sysio/authex/Actions.ts b/packages/sdk-core/src/contracts/sysio/authex/Actions.ts index 4cc070f..65b3894 100644 --- a/packages/sdk-core/src/contracts/sysio/authex/Actions.ts +++ b/packages/sdk-core/src/contracts/sysio/authex/Actions.ts @@ -8,16 +8,7 @@ import { SysioContractName } from "../../../types/SysioContractTypes.js" import { assertEncodedAction, getSysioContract } from "../Client.js" import { DEFAULT_AUTHEX_CONTRACT } from "./Constants.js" -import { - AuthexClearLinks, - AuthexCreateLink, - AuthexRecordLink -} from "./Structs.js" -import type { - BuildClearLinksActionOptions, - BuildCreateLinkActionOptions, - BuildRecordLinkActionOptions -} from "./Types.js" +import type { BuildCreateLinkActionOptions } from "./Types.js" import { assertSupportedCreateLinkChainKind } from "./Signing.js" /** Builds generated action data for `sysio.authex::createlink`. */ @@ -54,63 +45,3 @@ export function buildCreateLinkAction( }) ) } - -/** Builds generated action data for `sysio.authex::recordlink`. */ -export function recordLinkActionData( - options: BuildRecordLinkActionOptions -): SysioContracts.SysioAuthexRecordlinkAction { - return { - account: Name.from(options.account).toString(), - chain_kind: assertSupportedCreateLinkChainKind(options.chainKind), - pub_key: PublicKey.from(options.publicKey).toString() - } -} - -/** Builds an unsigned trusted `sysio.authex::recordlink` action. */ -export function buildRecordLinkAction( - options: BuildRecordLinkActionOptions -): Action { - const contract = options.contract || DEFAULT_AUTHEX_CONTRACT - return assertEncodedAction( - getSysioContract(SysioContractName.authex, { - contract - }).actions.recordlink.prepare(recordLinkActionData(options), { - authorization: [ - PermissionLevel.from({ - actor: contract, - permission: "active" - }) - ] - }) - ) -} - -/** Builds an unsigned testing-only `sysio.authex::clearlinks` action. */ -export function buildClearLinksAction( - options: BuildClearLinksActionOptions = {} -): Action { - const contract = options.contract || DEFAULT_AUTHEX_CONTRACT - - return assertEncodedAction( - getSysioContract(SysioContractName.authex, { - contract - }).actions.clearlinks.prepare( - {}, - { - authorization: [ - PermissionLevel.from({ - actor: contract, - permission: "active" - }) - ] - } - ) - ) -} - -/** Runtime action data serializers keyed by `sysio.authex` action name. */ -export const authexActionDataTypes = { - createlink: AuthexCreateLink, - recordlink: AuthexRecordLink, - clearlinks: AuthexClearLinks -} diff --git a/packages/sdk-core/src/contracts/sysio/authex/Client.ts b/packages/sdk-core/src/contracts/sysio/authex/Client.ts index 8b729e2..7c9d424 100644 --- a/packages/sdk-core/src/contracts/sysio/authex/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/authex/Client.ts @@ -14,11 +14,7 @@ import { import type { ContractTableRowsOptions } from "../../Contract.js" import { getSysioContract, type SysioContractClient } from "../Client.js" -import { - buildClearLinksAction, - buildCreateLinkAction, - buildRecordLinkAction -} from "./Actions.js" +import { buildCreateLinkAction } from "./Actions.js" import { AUTHEX_LINKS_BY_NAME_INDEX, AUTHEX_LINKS_BY_PUBLIC_KEY_INDEX, @@ -27,9 +23,7 @@ import { import { signCreateLink } from "./Signing.js" import type { AuthexClientOptions, - BuildClearLinksActionOptions, BuildCreateLinkActionOptions, - BuildRecordLinkActionOptions, CreateLinkActionResult, CreateLinkWithSignerOptions, ListLinksOptions, @@ -129,13 +123,6 @@ export class AuthexClient { return { proof, action } } - /** Lowercase alias matching the contract action spelling. */ - async createlink( - options: CreateLinkWithSignerOptions - ): Promise { - return this.createLink(options) - } - /** Signs the external-wallet proof, builds the action, and pushes it with this client's signer. */ async pushCreateLink( options: PushCreateLinkOptions, @@ -145,30 +132,6 @@ export class AuthexClient { return this.client.pushTransaction(result.action, pushOptions) } - /** Lowercase push alias matching the contract action spelling. */ - async pushCreatelink( - options: PushCreateLinkOptions, - pushOptions: TransactionExtraOptions = options.pushOptions || {} - ): Promise>> { - return this.pushCreateLink(options, pushOptions) - } - - /** Builds an unsigned trusted `sysio.authex::recordlink` action. */ - buildRecordLinkAction(options: BuildRecordLinkActionOptions): Action { - return buildRecordLinkAction({ - contract: this.contract, - ...options - }) - } - - /** Builds an unsigned testing-only `sysio.authex::clearlinks` action. */ - buildClearLinksAction(options: BuildClearLinksActionOptions = {}): Action { - return buildClearLinksAction({ - contract: this.contract, - ...options - }) - } - /** Lists AuthEx link rows. */ async listLinks( options: ContractTableRowsOptions = {} diff --git a/packages/sdk-core/src/contracts/sysio/authex/Descriptor.ts b/packages/sdk-core/src/contracts/sysio/authex/Descriptor.ts deleted file mode 100644 index 93c8f98..0000000 --- a/packages/sdk-core/src/contracts/sysio/authex/Descriptor.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { ContractDescriptor } from "../../Contract.js" -import type * as SysioContracts from "../../../types/SysioContractTypes.js" - -import { DEFAULT_AUTHEX_CONTRACT } from "./Constants.js" -import { - AuthexClearLinks, - AuthexCreateLink, - AuthexRecordLink -} from "./Structs.js" - -/** Generated `sysio.authex` action data keyed by ABI action name. */ -export interface SysioAuthexActionData { - /** `sysio.authex::createlink` action data. */ - createlink: SysioContracts.SysioAuthexCreatelinkAction - /** `sysio.authex::recordlink` action data. */ - recordlink: SysioContracts.SysioAuthexRecordlinkAction - /** `sysio.authex::clearlinks` action data. */ - clearlinks: SysioContracts.SysioAuthexClearlinksAction -} - -/** Generated `sysio.authex` table rows keyed by ABI table name. */ -export interface SysioAuthexTableRows { - /** `sysio.authex::links` table row. */ - links: SysioContracts.SysioAuthexLinksSType -} - -/** - * Runtime descriptor for `sysio.authex`. - * - * The data interfaces come from generated `SysioContractTypes`; the serializers - * mirror the deployed ABI so callers can build typed actions without fetching - * an ABI before serialization. - */ -export const descriptor: ContractDescriptor< - SysioAuthexActionData, - SysioAuthexTableRows -> = { - account: DEFAULT_AUTHEX_CONTRACT, - actions: { - createlink: { - name: "createlink", - serialize: data => AuthexCreateLink.from(data) - }, - recordlink: { - name: "recordlink", - serialize: data => AuthexRecordLink.from(data) - }, - clearlinks: { - name: "clearlinks", - serialize: data => AuthexClearLinks.from(data) - } - }, - tables: { - links: { - name: "links", - rowType: null - } - } -} diff --git a/packages/sdk-core/src/contracts/sysio/authex/Structs.ts b/packages/sdk-core/src/contracts/sysio/authex/Structs.ts index bc7f6c1..37207c3 100644 --- a/packages/sdk-core/src/contracts/sysio/authex/Structs.ts +++ b/packages/sdk-core/src/contracts/sysio/authex/Structs.ts @@ -22,43 +22,3 @@ export class AuthexCreateLink extends Struct { /** Millisecond nonce used by the contract freshness check. */ @Struct.field("uint64") declare nonce: UInt64 } - -/** Runtime serializer for `sysio.authex::clearlinks`. */ -@Struct.type("clearlinks") -export class AuthexClearLinks extends Struct {} - -/** Runtime serializer for `sysio.authex::recordlink`. */ -@Struct.type("recordlink") -export class AuthexRecordLink extends Struct { - /** Wire account name being linked to the external key. */ - @Struct.field("name") declare account: Name - - /** External chain identifier from `sysio.authex::ChainKind`. */ - @Struct.field("int32") declare chain_kind: Int32 - - /** External chain public key in Wire public-key format. */ - @Struct.field(PublicKey) declare pub_key: PublicKey -} - -/** Runtime serializer for the `sysio.authex::links_key` table key. */ -@Struct.type("links_key") -export class AuthexLinksKey extends Struct { - /** Monotonic link row primary key. */ - @Struct.field("uint64") declare key: UInt64 -} - -/** Runtime serializer for `sysio.authex::links` table rows. */ -@Struct.type("links_s") -export class AuthexLinkRow extends Struct { - /** Monotonic link row primary key. */ - @Struct.field("uint64") declare key: UInt64 - - /** Wire account name associated with the external key. */ - @Struct.field("name") declare username: Name - - /** External chain identifier from `sysio.authex::ChainKind`. */ - @Struct.field("int32") declare chain_kind: Int32 - - /** External chain public key in Wire public-key format. */ - @Struct.field(PublicKey) declare pub_key: PublicKey -} diff --git a/packages/sdk-core/src/contracts/sysio/authex/Types.ts b/packages/sdk-core/src/contracts/sysio/authex/Types.ts index 531df41..6d4e7bc 100644 --- a/packages/sdk-core/src/contracts/sysio/authex/Types.ts +++ b/packages/sdk-core/src/contracts/sysio/authex/Types.ts @@ -41,24 +41,6 @@ export interface BuildCreateLinkActionOptions { contract?: NameType } -/** Options for building `sysio.authex::recordlink`. */ -export interface BuildRecordLinkActionOptions { - /** Wire account being linked. */ - account: NameType - /** External chain kind, currently EVM or SVM. */ - chainKind: AuthexChainKind - /** External-chain public key in Wire public-key format. */ - publicKey: PublicKeyType - /** AuthEx contract account override. Defaults to `sysio.authex`. */ - contract?: NameType -} - -/** Options for building `sysio.authex::clearlinks`. */ -export interface BuildClearLinksActionOptions { - /** AuthEx contract account override. Defaults to `sysio.authex`. */ - contract?: NameType -} - /** Data needed before asking an external wallet for its create-link proof. */ export interface PreparedCreateLink { /** Wire account being linked. */ @@ -94,8 +76,10 @@ export interface SignedCreateLinkProof extends PreparedCreateLink { } /** Options for signing and building `sysio.authex::createlink`. */ -export interface CreateLinkWithSignerOptions - extends Omit { +export interface CreateLinkWithSignerOptions extends Omit< + PrepareCreateLinkOptions, + "publicKey" +> { /** Signer for the external wallet being linked. */ signer: SignerProvider /** External-chain public key. Defaults to `signer.pubKey`. */ diff --git a/packages/sdk-core/src/contracts/sysio/authex/index.ts b/packages/sdk-core/src/contracts/sysio/authex/index.ts index 4eed770..f093ede 100644 --- a/packages/sdk-core/src/contracts/sysio/authex/index.ts +++ b/packages/sdk-core/src/contracts/sysio/authex/index.ts @@ -1,7 +1,6 @@ export * from "./Actions.js" export * from "./Client.js" export * from "./Constants.js" -export * from "./Descriptor.js" export * from "./Signing.js" export * from "./Structs.js" export * from "./Types.js" diff --git a/packages/sdk-core/src/contracts/sysio/msig/Actions.ts b/packages/sdk-core/src/contracts/sysio/msig/Actions.ts index bafc506..8162b83 100644 --- a/packages/sdk-core/src/contracts/sysio/msig/Actions.ts +++ b/packages/sdk-core/src/contracts/sysio/msig/Actions.ts @@ -11,16 +11,7 @@ import { SysioContractName } from "../../../types/SysioContractTypes.js" import { assertEncodedAction, getSysioContract } from "../Client.js" import { DEFAULT_MSIG_CONTRACT } from "./Constants.js" -import type { SysioMsigProposeActionData } from "./Descriptor.js" -import { - MsigApprove, - MsigCancel, - MsigExec, - MsigGetProposal, - MsigInvalidate, - MsigPropose, - MsigUnapprove -} from "./Structs.js" +import { MsigPropose } from "./Structs.js" import type { BuildApproveActionOptions, BuildCancelActionOptions, @@ -28,7 +19,8 @@ import type { BuildInvalidateActionOptions, BuildProposeActionOptions, BuildUnapproveActionOptions, - MsigPermissionLevel + MsigPermissionLevel, + SysioMsigProposeActionData } from "./Types.js" function permissionLevel(value: MsigPermissionLevel): PermissionLevel { @@ -189,14 +181,3 @@ export function buildGetProposalAction( }).actions.getproposal.prepare(data) ) } - -/** Runtime action data serializers keyed by `sysio.msig` action name. */ -export const msigActionDataTypes = { - propose: MsigPropose, - approve: MsigApprove, - unapprove: MsigUnapprove, - cancel: MsigCancel, - exec: MsigExec, - invalidate: MsigInvalidate, - getproposal: MsigGetProposal -} diff --git a/packages/sdk-core/src/contracts/sysio/msig/Descriptor.ts b/packages/sdk-core/src/contracts/sysio/msig/Descriptor.ts deleted file mode 100644 index 4e24ba1..0000000 --- a/packages/sdk-core/src/contracts/sysio/msig/Descriptor.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { ContractDescriptor } from "../../Contract.js" -import type * as SysioContracts from "../../../types/SysioContractTypes.js" - -import { DEFAULT_MSIG_CONTRACT } from "./Constants.js" -import { - MsigApprove, - MsigCancel, - MsigExec, - MsigGetProposal, - MsigInvalidate, - MsigPropose, - MsigUnapprove -} from "./Structs.js" - -/** Generated `sysio.msig` transaction body plus inherited transaction header. */ -export interface SysioMsigTransactionData - extends SysioContracts.SysioMsigTransactionHeaderType, - SysioContracts.SysioMsigTransactionType {} - -/** Generated `sysio.msig::propose` data with complete transaction fields. */ -export interface SysioMsigProposeActionData - extends Omit { - /** Proposed transaction including header and action arrays. */ - trx: SysioMsigTransactionData -} - -/** Generated `sysio.msig` action data keyed by ABI action name. */ -export interface SysioMsigActionData { - /** `sysio.msig::propose` action data. */ - propose: SysioMsigProposeActionData - /** `sysio.msig::approve` action data. */ - approve: SysioContracts.SysioMsigApproveAction - /** `sysio.msig::unapprove` action data. */ - unapprove: SysioContracts.SysioMsigUnapproveAction - /** `sysio.msig::cancel` action data. */ - cancel: SysioContracts.SysioMsigCancelAction - /** `sysio.msig::exec` action data. */ - exec: SysioContracts.SysioMsigExecAction - /** `sysio.msig::invalidate` action data. */ - invalidate: SysioContracts.SysioMsigInvalidateAction - /** `sysio.msig::getproposal` action data. */ - getproposal: SysioContracts.SysioMsigGetproposalAction -} - -/** Generated `sysio.msig` table rows keyed by ABI table name. */ -export interface SysioMsigTableRows { - /** `sysio.msig::proposal` table row. */ - proposal: SysioContracts.SysioMsigProposalType - /** `sysio.msig::approvals2` table row. */ - approvals2: SysioContracts.SysioMsigApprovalsInfoType - /** `sysio.msig::approvals` legacy table row. */ - approvals: SysioContracts.SysioMsigOldApprovalsInfoType - /** `sysio.msig::invals` table row. */ - invals: SysioContracts.SysioMsigInvalidationType - /** `sysio.msig::propchunks` chunked-v2 table row. */ - propchunks: SysioContracts.SysioMsigPropchunkType -} - -/** - * Runtime descriptor for `sysio.msig`. - * - * The type maps come from generated `SysioContractTypes`; the serializers are - * currently supplied by hand-written `Struct` classes. A future generator can - * emit this metadata directly for every system contract. - */ -export const descriptor: ContractDescriptor< - SysioMsigActionData, - SysioMsigTableRows -> = { - account: DEFAULT_MSIG_CONTRACT, - actions: { - propose: { - name: "propose", - serialize: data => MsigPropose.from(data) - }, - approve: { - name: "approve", - serialize: data => MsigApprove.from(data) - }, - unapprove: { - name: "unapprove", - serialize: data => MsigUnapprove.from(data) - }, - cancel: { - name: "cancel", - serialize: data => MsigCancel.from(data) - }, - exec: { - name: "exec", - serialize: data => MsigExec.from(data) - }, - invalidate: { - name: "invalidate", - serialize: data => MsigInvalidate.from(data) - }, - getproposal: { - name: "getproposal", - serialize: data => MsigGetProposal.from(data) - } - }, - tables: { - proposal: { - name: "proposal", - rowType: null - }, - approvals2: { - name: "approvals2", - rowType: null - }, - approvals: { - name: "approvals", - rowType: null - }, - invals: { - name: "invals", - rowType: null - }, - propchunks: { - name: "propchunks", - rowType: null - } - } -} diff --git a/packages/sdk-core/src/contracts/sysio/msig/Types.ts b/packages/sdk-core/src/contracts/sysio/msig/Types.ts index 55b6bd1..8af8137 100644 --- a/packages/sdk-core/src/contracts/sysio/msig/Types.ts +++ b/packages/sdk-core/src/contracts/sysio/msig/Types.ts @@ -18,6 +18,27 @@ import type { MsigProposal } from "./Structs.js" +/** Generated multisig transaction body plus inherited transaction header. */ +export interface SysioMsigTransactionData + extends + SysioContracts.SysioMsigTransactionHeaderType, + SysioContracts.SysioMsigTransactionType {} + +/** Complete `sysio.msig::propose` data until generation inherits header fields. */ +export interface SysioMsigProposeActionData extends Omit< + SysioContracts.SysioMsigProposeAction, + "trx" +> { + /** Proposed transaction including header and action arrays. */ + trx: SysioMsigTransactionData +} + +/** Runtime action-data corrections layered over the generated contract map. */ +export interface SysioMsigActionDataOverrides { + /** Complete propose data including inherited transaction-header fields. */ + propose: SysioMsigProposeActionData +} + /** Permission level input accepted by multisig action builders. */ export type MsigPermissionLevel = | PermissionLevelType diff --git a/packages/sdk-core/src/contracts/sysio/msig/index.ts b/packages/sdk-core/src/contracts/sysio/msig/index.ts index ce9fd46..d8ae3bc 100644 --- a/packages/sdk-core/src/contracts/sysio/msig/index.ts +++ b/packages/sdk-core/src/contracts/sysio/msig/index.ts @@ -1,7 +1,6 @@ export * from "./Actions.js" export * from "./Capabilities.js" export * from "./Constants.js" -export * from "./Descriptor.js" export * from "./Hash.js" export * from "./Client.js" export * from "./Proposal.js" diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Client.ts b/packages/sdk-core/src/contracts/sysio/reserv/Client.ts index 999bcf0..e612180 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/Client.ts @@ -14,7 +14,7 @@ import type * as SysioContracts from "../../../types/SysioContractTypes.js" import type { ContractTableRowsOptions } from "../../Contract.js" import { getSysioContract, type SysioContractClient } from "../Client.js" -import { buildMatchReserveAction, buildSwapQuoteAction } from "./Actions.js" +import { buildSwapQuoteAction, matchReserveActionData } from "./Actions.js" import { DEFAULT_RESERV_CONTRACT, DEFAULT_RESERVE_QUERY_LIMIT @@ -22,7 +22,6 @@ import { import { reserveSlugString, reserveSlugValue } from "./Slug.js" import type { ListReservesOptions, - MatchReserveOptions, PushMatchReserveOptions, ReserveClientOptions, ReserveIdentity, @@ -255,31 +254,29 @@ export class ReserveClient { } } - /** Builds an unsigned Wire action that funds and activates a pending reserve. */ - buildMatchReserveAction(options: MatchReserveOptions): Action { - return buildMatchReserveAction({ contract: this.contract, ...options }) - } - /** Builds and pushes a signed Wire transaction that activates a pending reserve. */ async pushMatchReserve( options: PushMatchReserveOptions, pushOptions: TransactionExtraOptions = options.pushOptions || {} ): Promise>> { - return this.client.pushTransaction( - this.buildMatchReserveAction(options), - pushOptions + return this.contractClient.actions.matchreserve.invoke( + matchReserveActionData(options), + { + authorization: [ + { + actor: options.matcher, + permission: options.permission || "active" + } + ], + pushOptions + } ) } - /** Builds an unsigned read-only quote action using the deployed reserve curve. */ - buildSwapQuoteAction(options: ReserveQuoteOptions): Action { - return buildSwapQuoteAction({ contract: this.contract, ...options }) - } - /** Reads the current on-chain quote for one reserve route. */ async getSwapQuote(options: ReserveQuoteOptions): Promise { const response = await this.sendReadOnlyAction( - this.buildSwapQuoteAction(options) + buildSwapQuoteAction({ contract: this.contract, ...options }) ) return decodeReserveUInt64Return(response) } diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Descriptor.ts b/packages/sdk-core/src/contracts/sysio/reserv/Descriptor.ts deleted file mode 100644 index 083fe3a..0000000 --- a/packages/sdk-core/src/contracts/sysio/reserv/Descriptor.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { ContractDescriptor } from "../../Contract.js" -import type * as SysioContracts from "../../../types/SysioContractTypes.js" - -import { DEFAULT_RESERV_CONTRACT } from "./Constants.js" -import { - ReservMatchReserve, - ReservRewardBalance, - ReservSwapQuote -} from "./Structs.js" - -/** User-facing `sysio.reserv` action data keyed by ABI action name. */ -export interface SysioReservActionData { - /** `sysio.reserv::matchreserve` action data. */ - matchreserve: SysioContracts.SysioReservMatchreserveAction - /** Read-only `sysio.reserv::swapquote` action data. */ - swapquote: SysioContracts.SysioReservSwapquoteAction - /** Read-only `sysio.reserv::rewardbal` action data. */ - rewardbal: SysioContracts.SysioReservRewardbalAction -} - -/** User-facing `sysio.reserv` table rows keyed by ABI table name. */ -export interface SysioReservTableRows { - /** `sysio.reserv::reserves` registry row. */ - reserves: SysioContracts.SysioReservReserveRowType - /** `sysio.reserv::rewardbkt` singleton row. */ - rewardbkt: SysioContracts.SysioReservRewardsBucketType -} - -/** - * Runtime descriptor for the public `sysio.reserv` integration surface. - * - * Internal OPP dispatch and accounting actions remain available through - * generated types without being promoted as normal user operations. - */ -export const descriptor: ContractDescriptor< - SysioReservActionData, - SysioReservTableRows -> = { - account: DEFAULT_RESERV_CONTRACT, - actions: { - matchreserve: { - name: "matchreserve", - serialize: data => ReservMatchReserve.from(data) - }, - swapquote: { - name: "swapquote", - serialize: data => ReservSwapQuote.from(data) - }, - rewardbal: { - name: "rewardbal", - serialize: data => ReservRewardBalance.from(data) - } - }, - tables: { - reserves: { - name: "reserves", - rowType: null - }, - rewardbkt: { - name: "rewardbkt", - rowType: null - } - } -} diff --git a/packages/sdk-core/src/contracts/sysio/reserv/index.ts b/packages/sdk-core/src/contracts/sysio/reserv/index.ts index 9195f9a..7f9cb04 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/index.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/index.ts @@ -1,7 +1,6 @@ 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/tests/contracts/Contract.test.ts b/packages/sdk-core/tests/contracts/Contract.test.ts index eda6e1f..0f1b2c9 100644 --- a/packages/sdk-core/tests/contracts/Contract.test.ts +++ b/packages/sdk-core/tests/contracts/Contract.test.ts @@ -1,5 +1,26 @@ import { contracts } from "@wireio/sdk-core" +interface ProposalContractActions {} + +interface ProposalContractTables { + proposal: null +} + +/** Minimal descriptor fixture for the standalone generic contract client. */ +const ProposalContractDescriptor: contracts.ContractDescriptor< + ProposalContractActions, + ProposalContractTables +> = { + account: "sysio.msig", + actions: {}, + tables: { + proposal: { + name: "proposal", + rowType: null + } + } +} + function mockApi(rows: unknown[] = []) { return { v1: { @@ -18,7 +39,7 @@ function mockApi(rows: unknown[] = []) { } describe("contract client factory", () => { - test("builds typed actions from a generated system contract descriptor", () => { + test("builds typed actions from the generated system contract proxy", () => { const api = mockApi(), msig = contracts.sysio.createClient({ client: api, @@ -79,7 +100,7 @@ describe("contract client factory", () => { expect(decoded.trx.actions).toEqual([]) }) - test("reads typed table rows from a descriptor table client", async () => { + test("reads typed table rows from a generated system contract proxy", async () => { const row = { proposal_name: "upgrade1", packed_transaction: "", @@ -116,7 +137,7 @@ describe("contract client factory", () => { const api = mockApi(), msig = contracts.createContractClient({ client: api, - descriptor: contracts.sysio.msig.descriptor + descriptor: ProposalContractDescriptor }), scopes = await msig.table("proposal").scopes() diff --git a/packages/sdk-core/tests/contracts/sysio/Client.test.ts b/packages/sdk-core/tests/contracts/sysio/Client.test.ts index 9e5335c..da65e3b 100644 --- a/packages/sdk-core/tests/contracts/sysio/Client.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/Client.test.ts @@ -64,6 +64,22 @@ describe("system contract proxy", () => { ]) }) + test("keeps non-workflow actions available through the generated proxy", () => { + const authex = contracts.sysio.getSysioContract(SysioContractName.authex), + prepared = authex.actions.clearlinks.prepare( + {}, + { authorization: ["sysio.authex@active"] } + ) + + expect(prepared).not.toBeInstanceOf(Action) + expect(prepared).toMatchObject({ + contract: SysioContractName.authex, + account: "sysio.authex", + name: "clearlinks", + data: {} + }) + }) + test("falls back to a typed AnyAction when synchronous encoding fails", () => { const msig = contracts.sysio.getSysioContract(SysioContractName.msig), data = {}, diff --git a/packages/sdk-core/tests/contracts/sysio/reserv/Client.test.ts b/packages/sdk-core/tests/contracts/sysio/reserv/Client.test.ts index 0196d3f..359a1c2 100644 --- a/packages/sdk-core/tests/contracts/sysio/reserv/Client.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/reserv/Client.test.ts @@ -38,7 +38,9 @@ function clientFixture(rows = [reserveRow()]) { : rows, more: false })), - pushTransaction = jest.fn(async () => ({ transaction_id: "reserve-trx" })), + pushTransaction = jest.fn(async (_action: any, _options?: any) => ({ + transaction_id: "reserve-trx" + })), sendReadOnlyTransaction = jest.fn(async () => ({ processed: { action_traces: [ @@ -155,6 +157,10 @@ describe("ReserveClient", () => { }) ).resolves.toEqual({ transaction_id: "reserve-trx" }) expect(pushTransaction).toHaveBeenCalledTimes(1) + const [action] = pushTransaction.mock.calls[0] + expect(action.account.toString()).toBe("sysio.reserv") + expect(action.name.toString()).toBe("matchreserve") + expect(action.authorization.map(String)).toEqual(["alice@active"]) }) test("decodes read-only swapquote and rewards values", async () => { From 4eef9362b8b33c5a151349e863ed727cd49a2b2a Mon Sep 17 00:00:00 2001 From: joshglogau Date: Tue, 21 Jul 2026 14:54:05 -0400 Subject: [PATCH 3/7] refactor(sdk-core): align sysio proxy with wire client --- packages/sdk-core/README.md | 8 +- .../sdk-core/src/contracts/sysio/Client.ts | 96 +++++------------- .../sdk-core/src/contracts/sysio/Codecs.ts | 7 +- .../src/contracts/sysio/reserv/Structs.ts | 4 - .../sdk-core/tests/contracts/Contract.test.ts | 98 ++++++------------- .../tests/contracts/sysio/Client.test.ts | 86 +++++++++++++++- 6 files changed, 142 insertions(+), 157 deletions(-) diff --git a/packages/sdk-core/README.md b/packages/sdk-core/README.md index 7130b32..4826da0 100644 --- a/packages/sdk-core/README.md +++ b/packages/sdk-core/README.md @@ -24,8 +24,9 @@ const detail = await client.getProposalDetail("alice", "upgrade1") `contracts.sysio.createClient({ client })` exposes every contract in the generated `SysioContractDefinitions` registry. Contract, action, and table members are typed from `SysioContractMapping`, validated at runtime, and cached -after their first access. Use either the concise root syntax or -`getSysioContract` when the contract name is dynamic. +after their first access. Its `actions..prepare/invoke` and +`tables..query` surface mirrors Wire Tools' `getSysioContract`. Use either +the concise root syntax or `getSysioContract` when the contract name is dynamic. ```ts import { SysioContracts } from "@wireio/sdk-core" @@ -52,8 +53,7 @@ await epoch.actions.advance.invoke({}, { authorization: ["operator@active"] }) ABI can encode the action. If synchronous encoding is unavailable or fails, it returns the same generated data as a typed `AnyAction`; `APIClient` resolves the deployed ABI when that payload is invoked or pushed. Authorization is empty by -default and must be supplied explicitly for writes. The legacy named factory and -callable action shorthand remain available for locally encoded actions. +default and must be supplied explicitly for writes. The `AuthexClient`, `MsigClient`, and `ReserveClient` classes remain the public domain facades for proof creation, proposal compatibility, reserve diff --git a/packages/sdk-core/src/contracts/sysio/Client.ts b/packages/sdk-core/src/contracts/sysio/Client.ts index dedc553..69e0a04 100644 --- a/packages/sdk-core/src/contracts/sysio/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/Client.ts @@ -105,13 +105,6 @@ export interface SysioActionInvoker< Name extends SysioContractName, ActionName extends SysioActionName > { - /** Backward-compatible shorthand for {@link SysioActionInvoker.prepare}. */ - ( - data: SysioActionData, - authorization: ContractPermissionLevel[], - options?: ContractBuildActionOptions - ): Action - /** Prepares an unsigned action, preferring synchronous ABI encoding. */ prepare( data: SysioActionData, @@ -135,7 +128,7 @@ export interface SysioTableQuery< options?: ContractTableRowsOptions ): Promise>> - /** Backward-compatible alias for {@link SysioTableQuery.query}. */ + /** Alias matching the generic contract client's `rows` terminology. */ rows( options?: ContractTableRowsOptions ): Promise>> @@ -177,13 +170,6 @@ export interface SysioContractClient { /** Per-contract account overrides for nonstandard system-contract deployments. */ export type SysioContractAccounts = Partial> -/** String values accepted by the backward-compatible named client factory. */ -export type SysioContractNameInput = `${SysioContractName}` - -/** Generated enum member corresponding to a named factory input. */ -export type SysioContractNameFromInput = - Extract - /** Options for creating the root system-contract proxy. */ export interface SysioClientOptions { /** Chain API client used for reads and signed action invocation. */ @@ -200,16 +186,6 @@ export interface GetSysioContractOptions { contract?: NameType } -/** Backward-compatible options for creating one named system-contract client. */ -export interface SystemContractClientOptions< - Name extends SysioContractNameInput -> extends SysioClientOptions { - /** Generated system-contract name. */ - name: Name - /** Optional on-chain account override. */ - contract?: NameType -} - /** Root proxy methods shared with the sister-repository Wire client. */ export interface SysioClientMethods { /** Resolves one generated typed system-contract client. */ @@ -282,37 +258,29 @@ function createActionInvoker< client?: APIClient ): SysioActionInvoker { const prepare = ( - data: SysioActionData, - options: SysioActionOptions = {} - ) => { - const contract = options.contract || account, - payload: SysioActionPayload = { - contract: name, - account: contract, - name: actionName, - authorization: normalizeAuthorization(options.authorization), - data - } - - return prepareAction(payload, options.abi) - }, - invoker = ( - data: SysioActionData, - authorization: ContractPermissionLevel[], - options: ContractBuildActionOptions = {} - ) => assertEncodedAction(prepare(data, { ...options, authorization })) - - invoker.prepare = prepare - invoker.invoke = async ( data: SysioActionData, - options: SysioActionInvocationOptions = {} - ) => - assertClient(client).pushTransaction( - prepare(data, options), - options.pushOptions - ) + options: SysioActionOptions = {} + ) => { + const contract = options.contract || account, + payload: SysioActionPayload = { + contract: name, + account: contract, + name: actionName, + authorization: normalizeAuthorization(options.authorization), + data + } - return invoker + return prepareAction(payload, options.abi) + } + + return { + prepare, + invoke: async (data, options = {}) => + assertClient(client).pushTransaction( + prepare(data, options), + options.pushOptions + ) + } } /** Creates one lazy typed table query. */ @@ -434,7 +402,7 @@ export function getSysioContract( } /** Creates the root proxy exposing all generated system contracts. */ -export function createSysioClient(options: SysioClientOptions): SysioClient { +export function createClient(options: SysioClientOptions): SysioClient { const clients = new Map< SysioContractName, SysioContractClient @@ -467,24 +435,6 @@ export function createSysioClient(options: SysioClientOptions): SysioClient { }) } -/** Creates either the root proxy or one named proxy using the legacy factory form. */ -export function createClient(options: SysioClientOptions): SysioClient -export function createClient( - options: SystemContractClientOptions -): SysioContractClient> -export function createClient( - options: - | SysioClientOptions - | SystemContractClientOptions -): SysioClient | SysioContractClient { - if (!("name" in options)) return createSysioClient(options) - - return getSysioContract(options.name as SysioContractName, { - client: options.client, - contract: options.contract - }) -} - /** Asserts that preparation produced an ABI-encoded action. */ export function assertEncodedAction< Name extends SysioContractName, diff --git a/packages/sdk-core/src/contracts/sysio/Codecs.ts b/packages/sdk-core/src/contracts/sysio/Codecs.ts index daf7099..bb03b62 100644 --- a/packages/sdk-core/src/contracts/sysio/Codecs.ts +++ b/packages/sdk-core/src/contracts/sysio/Codecs.ts @@ -11,11 +11,7 @@ import { MsigPropose, MsigUnapprove } from "./msig/Structs.js" -import { - ReservMatchReserve, - ReservRewardBalance, - ReservSwapQuote -} from "./reserv/Structs.js" +import { ReservMatchReserve, ReservSwapQuote } from "./reserv/Structs.js" /** Generated action names for one system contract. */ type ActionName = Extract< @@ -56,7 +52,6 @@ const ActionCodecs: SysioActionCodecMapping = { }, [SysioContractName.reserv]: { matchreserve: { serialize: data => ReservMatchReserve.from(data) }, - rewardbal: { serialize: data => ReservRewardBalance.from(data) }, swapquote: { serialize: data => ReservSwapQuote.from(data) } } } diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Structs.ts b/packages/sdk-core/src/contracts/sysio/reserv/Structs.ts index 3f17288..2597afd 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/Structs.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/Structs.ts @@ -52,7 +52,3 @@ export class ReservSwapQuote extends Struct { /** Destination reserve code. */ @Struct.field(ReservSlugName) declare to_reserve_code: ReservSlugName } - -/** Runtime serializer for read-only `sysio.reserv::rewardbal`. */ -@Struct.type("rewardbal") -export class ReservRewardBalance extends Struct {} diff --git a/packages/sdk-core/tests/contracts/Contract.test.ts b/packages/sdk-core/tests/contracts/Contract.test.ts index 0f1b2c9..0324eb3 100644 --- a/packages/sdk-core/tests/contracts/Contract.test.ts +++ b/packages/sdk-core/tests/contracts/Contract.test.ts @@ -1,18 +1,25 @@ -import { contracts } from "@wireio/sdk-core" +import { contracts, SysioContracts } from "@wireio/sdk-core" -interface ProposalContractActions {} +interface ProposalContractActions { + approve: SysioContracts.SysioMsigApproveAction +} interface ProposalContractTables { - proposal: null + proposal: SysioContracts.SysioMsigProposalType } -/** Minimal descriptor fixture for the standalone generic contract client. */ +/** Minimal fixture for the standalone descriptor-backed contract client. */ const ProposalContractDescriptor: contracts.ContractDescriptor< ProposalContractActions, ProposalContractTables > = { account: "sysio.msig", - actions: {}, + actions: { + approve: { + name: "approve", + serialize: data => contracts.sysio.msig.MsigApprove.from(data) + } + }, tables: { proposal: { name: "proposal", @@ -21,14 +28,12 @@ const ProposalContractDescriptor: contracts.ContractDescriptor< } } +/** Creates the API surface used by the generic contract client. */ function mockApi(rows: unknown[] = []) { return { v1: { chain: { - get_table_rows: jest.fn(async () => ({ - rows, - more: false - })), + get_table_rows: jest.fn(async () => ({ rows, more: false })), get_table_by_scope: jest.fn(async () => ({ rows: [{ scope: "alice" }], more: "" @@ -38,21 +43,17 @@ function mockApi(rows: unknown[] = []) { } as any } -describe("contract client factory", () => { - test("builds typed actions from the generated system contract proxy", () => { - const api = mockApi(), - msig = contracts.sysio.createClient({ - client: api, - name: "msig" +describe("generic contract client", () => { + test("builds encoded actions from a typed descriptor", () => { + const client = contracts.createContractClient({ + client: mockApi(), + descriptor: ProposalContractDescriptor }), - action = msig.actions.approve( + action = client.actions.approve( { proposer: "alice", proposal_name: "upgrade1", - level: { - actor: "bob", - permission: "active" - }, + level: { actor: "bob", permission: "active" }, proposal_hash: null }, ["bob@active"] @@ -64,44 +65,10 @@ describe("contract client factory", () => { expect(action.authorization.map(String)).toEqual(["bob@active"]) expect(decoded.proposer.toString()).toBe("alice") expect(decoded.proposal_name.toString()).toBe("upgrade1") - expect(decoded.level.toString()).toBe("bob@active") }) - test("builds typed propose actions with generated transaction header fields", () => { - const api = mockApi(), - msig = contracts.sysio.createClient({ - client: api, - name: "msig" - }), - action = msig.actions.propose( - { - proposer: "alice", - proposal_name: "upgrade1", - requested: [{ actor: "bob", permission: "active" }], - trx: { - expiration: "2026-01-01T00:00:00", - ref_block_num: 0, - ref_block_prefix: 0, - max_net_usage_words: 0, - max_cpu_usage_ms: 0, - delay_sec: 0, - context_free_actions: [], - actions: [], - transaction_extensions: [] - } - }, - ["alice@active"] - ), - decoded = action.decodeData(contracts.sysio.msig.MsigPropose) - - expect(action.name.toString()).toBe("propose") - expect(decoded.proposer.toString()).toBe("alice") - expect(decoded.requested.map(String)).toEqual(["bob@active"]) - expect(decoded.trx.actions).toEqual([]) - }) - - test("reads typed table rows from a generated system contract proxy", async () => { - const row = { + test("reads typed rows and the first matching row", async () => { + const row: SysioContracts.SysioMsigProposalType = { proposal_name: "upgrade1", packed_transaction: "", earliest_exec_time: null, @@ -110,17 +77,15 @@ describe("contract client factory", () => { trx_hash: null }, api = mockApi([row]), - msig = contracts.sysio.createClient({ + client = contracts.createContractClient({ client: api, - name: "msig" + descriptor: ProposalContractDescriptor }), - result = await msig.tables.proposal.rows({ + result = await client.tables.proposal.rows({ scope: "alice", limit: 5 }), - first = await msig.tables.proposal.first({ - scope: "alice" - }) + first = await client.tables.proposal.first({ scope: "alice" }) expect(result.rows).toEqual([row]) expect(first).toEqual(row) @@ -133,15 +98,14 @@ describe("contract client factory", () => { }) }) - test("lists scopes for descriptor table clients", async () => { + test("lists table scopes", async () => { const api = mockApi(), - msig = contracts.createContractClient({ + client = contracts.createContractClient({ client: api, descriptor: ProposalContractDescriptor - }), - scopes = await msig.table("proposal").scopes() + }) - expect(scopes).toEqual(["alice"]) + await expect(client.table("proposal").scopes()).resolves.toEqual(["alice"]) expect(api.v1.chain.get_table_by_scope).toHaveBeenCalledWith({ code: "sysio.msig", table: "proposal" diff --git a/packages/sdk-core/tests/contracts/sysio/Client.test.ts b/packages/sdk-core/tests/contracts/sysio/Client.test.ts index da65e3b..62c6d8a 100644 --- a/packages/sdk-core/tests/contracts/sysio/Client.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/Client.test.ts @@ -22,22 +22,45 @@ function createMockApi(rows: unknown[] = []) { } describe("system contract proxy", () => { - test("resolves and caches generated contracts from the root proxy", () => { + test("resolves and caches every generated contract and member", () => { const api = createMockApi(), sysio = contracts.sysio.createClient({ client: api }) + Object.values(SysioContractName).forEach(name => { + expect(sysio.getSysioContract(name).name).toBe(name) + }) + expect(sysio.msig).toBe(sysio.getSysioContract(SysioContractName.msig)) + expect(sysio.msig.actions.approve).toBe(sysio.msig.actions.approve) + expect(sysio.msig.tables.proposal).toBe(sysio.msig.tables.proposal) expect(sysio.msig.name).toBe(SysioContractName.msig) expect(sysio.msig.account).toBe("sysio.msig") + expect(sysio.system.account).toBe("sysio") expect(Reflect.get(sysio, "then")).toBeNull() }) + test("applies root-level account overrides consistently", () => { + const sysio = contracts.sysio.createClient({ + client: createMockApi(), + contracts: { [SysioContractName.epoch]: "custom.epoch" } + }), + prepared = sysio.epoch.actions.advance.prepare({}) + + expect(sysio.epoch.account).toBe("custom.epoch") + expect(prepared.account).toBe("custom.epoch") + }) + test("rejects unknown contracts, actions, and tables", () => { const sysio = contracts.sysio.createClient({ client: createMockApi() }) expect(() => Reflect.get(sysio, "bogus")).toThrow( "Unknown sysio contract: bogus" ) + expect(() => + contracts.sysio.getSysioContract( + "bogus" as SysioContracts.SysioContractName + ) + ).toThrow("Unknown sysio contract: bogus") expect(() => Reflect.get(sysio.epoch.actions, "bogus")).toThrow( "Unknown sysio.epoch action: bogus" ) @@ -64,6 +87,37 @@ describe("system contract proxy", () => { ]) }) + test("prepares complete msig transactions despite inherited ABI fields", () => { + const msig = contracts.sysio.getSysioContract(SysioContractName.msig), + prepared = msig.actions.propose.prepare( + { + proposer: "alice", + proposal_name: "upgrade1", + requested: [{ actor: "bob", permission: "active" }], + trx: { + expiration: "2026-01-01T00:00:00", + ref_block_num: 0, + ref_block_prefix: 0, + max_net_usage_words: 0, + max_cpu_usage_ms: 0, + delay_sec: 0, + context_free_actions: [], + actions: [], + transaction_extensions: [] + } + }, + { authorization: ["alice@active"] } + ) + + expect(prepared).toBeInstanceOf(Action) + const decoded = (prepared as Action).decodeData( + contracts.sysio.msig.MsigPropose + ) + expect(decoded.proposer.toString()).toBe("alice") + expect(decoded.requested.map(String)).toEqual(["bob@active"]) + expect(decoded.trx.actions).toEqual([]) + }) + test("keeps non-workflow actions available through the generated proxy", () => { const authex = contracts.sysio.getSysioContract(SysioContractName.authex), prepared = authex.actions.clearlinks.prepare( @@ -140,14 +194,17 @@ describe("system contract proxy", () => { await epoch.actions.advance.invoke( {}, - { authorization: [{ actor: "alice", permission: "active" }] } + { + authorization: [{ actor: "alice", permission: "active" }], + pushOptions: { wait_final: true } + } ) const [prepared, pushOptions] = api.pushTransaction.mock.calls[0] expect(prepared.account).toBe("sysio.epoch") expect(prepared.name).toBe("advance") expect(prepared.authorization.map(String)).toEqual(["alice@active"]) - expect(pushOptions).toBeUndefined() + expect(pushOptions).toEqual({ wait_final: true }) }) test("queries generated tables with the complete RPC option surface", async () => { @@ -176,6 +233,29 @@ describe("system contract proxy", () => { }) }) + test("supports table aliases, first-row reads, and scope discovery", async () => { + const row = { current_epoch_index: 7 }, + api = createMockApi([row]), + epoch = contracts.sysio.getSysioContract(SysioContractName.epoch, { + client: api + }) + + await expect(epoch.tables.epochstate.rows()).resolves.toMatchObject({ + rows: [row] + }) + await expect(epoch.tables.epochstate.first()).resolves.toEqual(row) + await expect(epoch.tables.epochstate.scopes()).resolves.toEqual(["alice"]) + expect(api.v1.chain.get_table_by_scope).toHaveBeenCalledWith({ + code: "sysio.epoch", + table: "epochstate" + }) + + const empty = contracts.sysio.getSysioContract(SysioContractName.epoch, { + client: createMockApi() + }) + await expect(empty.tables.epochstate.first()).resolves.toBeNull() + }) + test("requires an API client only for reads and invocation", async () => { const epoch = contracts.sysio.getSysioContract(SysioContractName.epoch) From 01f42f8230768ad516826495ea4ecf20c3ed1a67 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 22 Jul 2026 14:01:36 -0400 Subject: [PATCH 4/7] feat(sdk-core): add typed sysio.chains registry client --- packages/sdk-core/README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/sdk-core/README.md b/packages/sdk-core/README.md index 4826da0..62d10e2 100644 --- a/packages/sdk-core/README.md +++ b/packages/sdk-core/README.md @@ -83,8 +83,6 @@ 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`, `chains`, `msig`, and `reserv`. - ## Chain registry `contracts.sysio.chains` exposes the active Wire chain registry through the @@ -98,9 +96,6 @@ 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. From a650e22424aada4189bf0b406f925fbaba2f01a8 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 22 Jul 2026 15:58:31 -0400 Subject: [PATCH 5/7] feat(sdk-core): add on-chain token registry client Expose a typed sysio.tokens domain facade over the generated system-contract proxy. Normalize canonical token metadata and chain bindings, support active and chain-scoped discovery, join deployable assets, and cover registry reads with focused unit tests. --- .../sdk-core/src/contracts/sysio/index.ts | 1 + .../src/contracts/sysio/tokens/Client.ts | 170 ++++++++++++++++++ .../src/contracts/sysio/tokens/Constants.ts | 5 + .../src/contracts/sysio/tokens/Types.ts | 91 ++++++++++ .../src/contracts/sysio/tokens/index.ts | 3 + .../contracts/sysio/tokens/Client.test.ts | 95 ++++++++++ 6 files changed, 365 insertions(+) create mode 100644 packages/sdk-core/src/contracts/sysio/tokens/Client.ts create mode 100644 packages/sdk-core/src/contracts/sysio/tokens/Constants.ts create mode 100644 packages/sdk-core/src/contracts/sysio/tokens/Types.ts create mode 100644 packages/sdk-core/src/contracts/sysio/tokens/index.ts create mode 100644 packages/sdk-core/tests/contracts/sysio/tokens/Client.test.ts diff --git a/packages/sdk-core/src/contracts/sysio/index.ts b/packages/sdk-core/src/contracts/sysio/index.ts index c2fee81..9bdde25 100644 --- a/packages/sdk-core/src/contracts/sysio/index.ts +++ b/packages/sdk-core/src/contracts/sysio/index.ts @@ -2,4 +2,5 @@ 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 * 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/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() + }) +}) From 9713f83ba684a7ad06a10298d8b66310af7d680b Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 22 Jul 2026 15:58:41 -0400 Subject: [PATCH 6/7] feat(sdk-core): add reserve swap underwriting client Add a typed sysio.uwrit facade for WIRE-origin swap submission, queue inspection, normalized request history, and live protocol configuration. Export the canonical WIRE/WIRE/PRIMARY sentinel, document reserve-swap composition, and verify action authorization and lifecycle normalization in unit tests. --- packages/sdk-core/README.md | 40 +++ .../sdk-core/src/contracts/sysio/index.ts | 1 + .../src/contracts/sysio/uwrit/Actions.ts | 22 ++ .../src/contracts/sysio/uwrit/Client.ts | 257 ++++++++++++++++++ .../src/contracts/sysio/uwrit/Constants.ts | 12 + .../src/contracts/sysio/uwrit/Types.ts | 147 ++++++++++ .../src/contracts/sysio/uwrit/index.ts | 4 + .../contracts/sysio/uwrit/Client.test.ts | 171 ++++++++++++ 8 files changed, 654 insertions(+) create mode 100644 packages/sdk-core/src/contracts/sysio/uwrit/Actions.ts create mode 100644 packages/sdk-core/src/contracts/sysio/uwrit/Client.ts create mode 100644 packages/sdk-core/src/contracts/sysio/uwrit/Constants.ts create mode 100644 packages/sdk-core/src/contracts/sysio/uwrit/Types.ts create mode 100644 packages/sdk-core/src/contracts/sysio/uwrit/index.ts create mode 100644 packages/sdk-core/tests/contracts/sysio/uwrit/Client.test.ts diff --git a/packages/sdk-core/README.md b/packages/sdk-core/README.md index 62d10e2..05017a8 100644 --- a/packages/sdk-core/README.md +++ b/packages/sdk-core/README.md @@ -124,6 +124,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/index.ts b/packages/sdk-core/src/contracts/sysio/index.ts index 9bdde25..fa93c4e 100644 --- a/packages/sdk-core/src/contracts/sysio/index.ts +++ b/packages/sdk-core/src/contracts/sysio/index.ts @@ -3,4 +3,5 @@ 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/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/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"]) + }) +}) From 788baa41ff0582f5803c25fa64332542165bd921 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Wed, 22 Jul 2026 16:25:11 -0400 Subject: [PATCH 7/7] refactor(sdk-core): align chain registry with sysio proxy --- .../src/contracts/sysio/chains/Client.ts | 29 +++++++------------ .../contracts/sysio/chains/Client.test.ts | 9 +++--- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/sdk-core/src/contracts/sysio/chains/Client.ts b/packages/sdk-core/src/contracts/sysio/chains/Client.ts index 33ef569..4c6f205 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Client.ts @@ -1,13 +1,13 @@ 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" + 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, @@ -17,11 +17,6 @@ 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, @@ -84,19 +79,15 @@ export class ChainsClient { readonly contract: NameType /** Generic typed proxy for direct action and table access. */ - readonly contractClient: ContractClient< - SysioChainsActionData, - SysioChainsTableRows - > + readonly contractClient: SysioContractClient /** Creates a chain registry client. */ constructor(config: ChainsClientOptions) { this.client = config.client this.contract = config.contract || DEFAULT_CHAINS_CONTRACT - this.contractClient = createContractClient({ + this.contractClient = getSysioContract(SysioContractName.chains, { client: config.client, - contract: this.contract, - descriptor + contract: this.contract }) } @@ -156,7 +147,7 @@ export class ChainsClient { } /** Reads one registered chain by exact chain code, or null when absent. */ - async getChain(code: ChainSlugName): Promise { + 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) diff --git a/packages/sdk-core/tests/contracts/sysio/chains/Client.test.ts b/packages/sdk-core/tests/contracts/sysio/chains/Client.test.ts index fa04b58..6f1bfbe 100644 --- a/packages/sdk-core/tests/contracts/sysio/chains/Client.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/chains/Client.test.ts @@ -95,15 +95,14 @@ describe("ChainsClient", () => { await expect(client.getChain("MISSING")).resolves.toBeNull() }) - test("exposes the registered descriptor through the generic sysio proxy", async () => { + test("exposes chain tables through the root sysio proxy", async () => { const { client } = clientFixture(), proxy = contracts.sysio.createClient({ - client: client.client, - name: "chains" + client: client.client }), - result = await proxy.tables.chains.rows() + result = await proxy.chains.tables.chains.rows() - expect(proxy.contract.toString()).toBe("sysio.chains") + expect(proxy.chains.account.toString()).toBe("sysio.chains") expect(result.rows).toEqual(CHAIN_ROWS) })