From df86435847f77b14b94dfb6577eabf9958297ec5 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Wed, 16 Sep 2026 17:54:09 -0500 Subject: [PATCH 1/5] feat(sdk-core): consume slug_name's canonical string carrier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A slug field now arrives and leaves as its canonical spelling, matching the depot's ABI builtin (wire-sysio#619). The two row unwrappers and the two {value} writers are gone, and chainSlugValue/reserveSlugValue no longer read a digit-only string as a decimal — "12345678" is a valid slug whose packed value is nothing like it, so that branch silently mis-decoded. ChainsSlugName and ReservSlugName stop being Struct subclasses and become ABISerializableObject types modelled on Name. A Struct cannot express this: the object decoder dispatches on type.fields before consulting a class's own from(), so a struct-shaped slug can only ever be written {value}. Wire bytes are unchanged — a struct of one uint64 packs as that uint64. Regenerates SysioContractTypes against the generator's new slug_name builtin mapping; the diff is slug-only. Change-Id: Id1e57c61100d20daa53d6275ed39c886a02e04d8 --- .../src/contracts/sysio/chains/Actions.ts | 8 +- .../src/contracts/sysio/chains/Client.ts | 8 +- .../src/contracts/sysio/chains/Slug.ts | 23 +- .../src/contracts/sysio/chains/Structs.ts | 67 +++++- .../src/contracts/sysio/reserv/Actions.ts | 20 +- .../src/contracts/sysio/reserv/Client.ts | 18 +- .../src/contracts/sysio/reserv/Slug.ts | 26 +-- .../src/contracts/sysio/reserv/Structs.ts | 67 +++++- .../src/contracts/sysio/tokens/Client.ts | 12 +- .../src/contracts/sysio/uwrit/Actions.ts | 8 +- .../src/contracts/sysio/uwrit/Client.ts | 22 +- .../sdk-core/src/types/SysioContractTypes.ts | 206 +++++++++--------- .../contracts/sysio/chains/Actions.test.ts | 11 +- .../contracts/sysio/chains/Client.test.ts | 6 +- .../tests/contracts/sysio/chains/Slug.test.ts | 19 +- .../contracts/sysio/reserv/Client.test.ts | 12 +- .../contracts/sysio/tokens/Client.test.ts | 14 +- .../contracts/sysio/uwrit/Client.test.ts | 28 ++- 18 files changed, 335 insertions(+), 240 deletions(-) diff --git a/packages/sdk-core/src/contracts/sysio/chains/Actions.ts b/packages/sdk-core/src/contracts/sysio/chains/Actions.ts index 5f52ea33..7e3d2441 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Actions.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Actions.ts @@ -4,7 +4,7 @@ import { buildContractAction } from "../../Contract.js" import { DEFAULT_CHAINS_CONTRACT, MAX_EXTERNAL_CHAIN_ID } from "./Constants.js" import { descriptor } from "./Descriptor.js" -import { chainSlugData } from "./Slug.js" +import { chainSlugString } from "./Slug.js" import type { ChainOutpostAddresses, ChainRegistration, @@ -44,7 +44,7 @@ export function createRegisterChainActionData( ): SysioContracts.SysioChainsRegchainAction { return { kind: registration.kind, - code: chainSlugData(registration.code), + code: chainSlugString(registration.code), external_chain_id: assertExternalChainId(registration.externalChainId), name: registration.name, description: registration.description, @@ -69,7 +69,7 @@ export function createSetOutpostActionData( code: CreateSetOutpostActionOptions["code"], outpost: CreateSetOutpostActionOptions["outpost"] ): SysioContracts.SysioChainsSetoutpostAction { - return { code: chainSlugData(code), outpost: outpostAddressData(outpost) } + return { code: chainSlugString(code), outpost: outpostAddressData(outpost) } } /** Creates an unsigned privileged `sysio.chains::setoutpost` action. */ @@ -88,7 +88,7 @@ export function createSetOutpostAction( export function createActivateChainActionData( code: CreateActivateChainActionOptions["code"] ): SysioContracts.SysioChainsActivchainAction { - return { code: chainSlugData(code) } + return { code: chainSlugString(code) } } /** Creates an unsigned privileged `sysio.chains::activchain` action. */ diff --git a/packages/sdk-core/src/contracts/sysio/chains/Client.ts b/packages/sdk-core/src/contracts/sysio/chains/Client.ts index 167f5638..fef30a4c 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Client.ts @@ -45,15 +45,11 @@ function chainKindValue( return Number(mapped) as SysioContracts.SysioChainsChainkind } -function rowSlugValue(value: SysioContracts.SysioChainsSlugNameType): number { - return chainSlugValue(value.value) -} - /** Normalizes a generated chain registry row into application-friendly values. */ export function normalizeChainRow( row: SysioContracts.SysioChainsChainRowType ): ChainRecord { - const codeValue = rowSlugValue(row.code) + const codeValue = chainSlugValue(row.code) return { code: chainSlugString(codeValue), @@ -156,7 +152,7 @@ export class ChainsClient { 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) + row = rows.find(candidate => chainSlugValue(candidate.code) === codeValue) return row ? normalizeChainRow(row) : null } diff --git a/packages/sdk-core/src/contracts/sysio/chains/Slug.ts b/packages/sdk-core/src/contracts/sysio/chains/Slug.ts index bd5b1def..926d1017 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Slug.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Slug.ts @@ -1,16 +1,16 @@ import { SlugName } from "../../../SlugName.js" -import type * as SysioContracts from "../../../types/SysioContractTypes.js" import type { ChainSlugName } from "./Types.js" -/** Converts a friendly chain slug or packed value to its safe numeric form. */ +/** + * Converts a friendly chain slug or packed value to its safe numeric form. + * + * A string is ALWAYS parsed as a slug, never as a decimal: the slug alphabet + * contains digits, so `"12345678"` is a legitimate code whose packed value is + * nothing like 12345678. Pass a `number` to supply an already-packed value. + */ export function chainSlugValue(value: ChainSlugName): number { - const numericString = typeof value === "string" && /^[0-9]+$/.test(value), - packed = numericString - ? Number(value) - : typeof value === "string" - ? SlugName.from(value) - : Number(value) + const packed = typeof value === "string" ? SlugName.from(value) : Number(value) if (!Number.isSafeInteger(packed) || packed <= 0) { throw new Error( @@ -21,13 +21,6 @@ export function chainSlugValue(value: ChainSlugName): number { return packed } -/** Converts a friendly chain slug to generated `slug_name` action data. */ -export function chainSlugData( - value: ChainSlugName -): SysioContracts.SysioChainsSlugNameType { - return { value: chainSlugValue(value) } -} - /** Returns the display form of a packed chain slug. */ export function chainSlugString(value: ChainSlugName): string { return SlugName.toString(chainSlugValue(value)) diff --git a/packages/sdk-core/src/contracts/sysio/chains/Structs.ts b/packages/sdk-core/src/contracts/sysio/chains/Structs.ts index ab5b6eb3..5f4c8929 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Structs.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Structs.ts @@ -1,11 +1,68 @@ +import { ABIDecoder } from "../../../serializer/Decoder.js" +import { ABIEncoder } from "../../../serializer/Encoder.js" +import { ABISerializableObject } from "../../../serializer/Serializable.js" import { Int32, UInt32, UInt64 } from "../../../chain/Integer.js" import { Struct } from "../../../chain/Struct.js" +import { SlugName } from "../../../SlugName.js" +import { isInstanceOf } from "../../../Utils.js" -/** Runtime serializer for the Wire `slug_name` wrapper used by `sysio.chains`. */ -@Struct.type("slug_name") -export class ChainsSlugName extends Struct { - /** Packed eight-character chain code. */ - @Struct.field("uint64") declare value: UInt64 +/** + * Runtime serializer for the Wire `slug_name` chain type used by `sysio.chains`. + * + * Modelled on `Name`, not on `Struct`: `slug_name` is an ABI BUILTIN on the + * chain, so it has one wire form (a packed `uint64` — unchanged, the bytes are + * what the depot stores) and one JSON form (the canonical spelling, the only + * carrier the builtin emits or accepts). A `Struct` subclass cannot express + * that — the object decoder dispatches on `type.fields` before consulting a + * class's own `from`, so a struct-shaped slug can only ever be written as + * `{ value }`. + */ +export class ChainsSlugName implements ABISerializableObject { + static abiName = "slug_name" + + /** Packed eight-symbol code — the wire form. */ + value: UInt64 + + /** Builds from the canonical spelling, an already-packed value, or itself. */ + static from(value: ChainsSlugName | UInt64 | string | number): ChainsSlugName { + if (isInstanceOf(value, ChainsSlugName)) return value + if (isInstanceOf(value, UInt64)) return new ChainsSlugName(value) + + return new ChainsSlugName( + UInt64.from(typeof value === "string" ? SlugName.from(value) : value) + ) + } + + static fromABI(decoder: ABIDecoder) { + return new ChainsSlugName(UInt64.fromABI(decoder)) + } + + static abiDefault() { + return new this(UInt64.from(0)) + } + + constructor(value: UInt64) { + this.value = value + } + + /** Return true if this slug is equal to the passed slug. */ + equals(other: ChainsSlugName | UInt64 | string | number): boolean { + return this.value.equals(ChainsSlugName.from(other).value) + } + + /** The canonical spelling — `""` for the zero sentinel. */ + toString(): string { + return SlugName.toString(Number(this.value)) + } + + toABI(encoder: ABIEncoder) { + this.value.toABI(encoder) + } + + /** @internal */ + toJSON() { + return this.toString() + } } /** diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Actions.ts b/packages/sdk-core/src/contracts/sysio/reserv/Actions.ts index 6f7703fe..40f99470 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/Actions.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/Actions.ts @@ -6,7 +6,7 @@ import { SysioContractName } from "../../../types/SysioContractTypes.js" import { assertEncodedAction, getSysioContract } from "../Client.js" import { DEFAULT_RESERV_CONTRACT } from "./Constants.js" -import { reserveSlugData } from "./Slug.js" +import { reserveSlugString } from "./Slug.js" import type { MatchReserveOptions, ReserveQuoteOptions } from "./Types.js" /** Any value with a string form — an amount as a number, string, bigint, or a BN/Decimal-like object. */ @@ -23,9 +23,9 @@ export function matchReserveActionData( options: MatchReserveOptions ): SysioContracts.SysioReservMatchreserveAction { return { - chain_code: reserveSlugData(options.chainCode), - token_code: reserveSlugData(options.tokenCode), - reserve_code: reserveSlugData(options.reserveCode), + chain_code: reserveSlugString(options.chainCode), + token_code: reserveSlugString(options.tokenCode), + reserve_code: reserveSlugString(options.reserveCode), matcher: Name.from(options.matcher).toString(), wire_amount: amountString(options.wireAmount) } @@ -54,13 +54,13 @@ export function swapQuoteActionData( options: ReserveQuoteOptions ): SysioContracts.SysioReservSwapquoteAction { return { - from_chain_code: reserveSlugData(options.from.chainCode), - from_token_code: reserveSlugData(options.from.tokenCode), - from_reserve_code: reserveSlugData(options.from.reserveCode), + from_chain_code: reserveSlugString(options.from.chainCode), + from_token_code: reserveSlugString(options.from.tokenCode), + from_reserve_code: reserveSlugString(options.from.reserveCode), from_amount: amountString(options.fromAmount), - to_chain_code: reserveSlugData(options.to.chainCode), - to_token_code: reserveSlugData(options.to.tokenCode), - to_reserve_code: reserveSlugData(options.to.reserveCode) + to_chain_code: reserveSlugString(options.to.chainCode), + to_token_code: reserveSlugString(options.to.tokenCode), + to_reserve_code: reserveSlugString(options.to.reserveCode) } } diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Client.ts b/packages/sdk-core/src/contracts/sysio/reserv/Client.ts index a425e7f2..0600b973 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/Client.ts @@ -23,11 +23,7 @@ import { DEFAULT_RESERV_CONTRACT, DEFAULT_RESERVE_QUERY_LIMIT } from "./Constants.js" -import { - reserveRowSlugValue, - reserveSlugString, - reserveSlugValue -} from "./Slug.js" +import { reserveSlugString, reserveSlugValue } from "./Slug.js" import type { ListReservesOptions, PushMatchReserveOptions, @@ -74,9 +70,9 @@ function optionalAccountString(value: NameType): string { export function normalizeReserveRow( row: SysioContracts.SysioReservReserveRowType ): ReserveRecord { - const chainCodeValue = reserveRowSlugValue(row.chain_code), - tokenCodeValue = reserveRowSlugValue(row.token_code), - reserveCodeValue = reserveRowSlugValue(row.reserve_code) + const chainCodeValue = reserveSlugValue(row.chain_code), + tokenCodeValue = reserveSlugValue(row.token_code), + reserveCodeValue = reserveSlugValue(row.reserve_code) return { chainCode: reserveSlugString(chainCodeValue), @@ -186,11 +182,11 @@ export class ReserveClient { const status = enumValue(SysioReservReservestatus, row.status), chainMatches = options.chainCode == null || - reserveRowSlugValue(row.chain_code) === + reserveSlugValue(row.chain_code) === reserveSlugValue(options.chainCode), tokenMatches = options.tokenCode == null || - reserveRowSlugValue(row.token_code) === + reserveSlugValue(row.token_code) === reserveSlugValue(options.tokenCode), statusMatches = options.status == null || status === options.status, ownerMatches = !owner || optionalAccountString(row.owner) === owner, @@ -235,7 +231,7 @@ export class ReserveClient { limit: Number.MAX_SAFE_INTEGER }), row = rows.find( - candidate => reserveRowSlugValue(candidate.reserve_code) === reserveCode + candidate => reserveSlugValue(candidate.reserve_code) === reserveCode ) return row ? normalizeReserveRow(row) : null diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Slug.ts b/packages/sdk-core/src/contracts/sysio/reserv/Slug.ts index 62e34e47..7e2b9d0e 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/Slug.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/Slug.ts @@ -1,9 +1,14 @@ import { SlugName } from "../../../SlugName.js" -import type * as SysioContracts from "../../../types/SysioContractTypes.js" import type { ReserveSlugName } from "./Types.js" -/** Converts a friendly slug string or packed value to its safe numeric form. */ +/** + * Converts a friendly slug string or packed value to its safe numeric form. + * + * A string is ALWAYS parsed as a slug, never as a decimal: the slug alphabet + * contains digits, so `"12345678"` is a legitimate code whose packed value is + * nothing like 12345678. Pass a `number` to supply an already-packed value. + */ export function reserveSlugValue(value: ReserveSlugName): number { const packed = typeof value === "string" ? SlugName.from(value) : Number(value) @@ -17,23 +22,6 @@ export function reserveSlugValue(value: ReserveSlugName): number { return packed } -/** Converts a generated row slug, including JSON-serialized uint64 strings. */ -export function reserveRowSlugValue( - value: SysioContracts.SysioReservSlugNameType -): number { - const packed = value.value - return typeof packed === "string" && /^[0-9]+$/.test(packed) - ? reserveSlugValue(Number(packed)) - : reserveSlugValue(packed) -} - -/** Converts a friendly reserve slug to generated `slug_name` action data. */ -export function reserveSlugData( - value: ReserveSlugName -): SysioContracts.SysioReservSlugNameType { - return { value: reserveSlugValue(value) } -} - /** Returns the display form of a packed reserve slug. */ export function reserveSlugString(value: ReserveSlugName): string { return SlugName.toString(reserveSlugValue(value)) diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Structs.ts b/packages/sdk-core/src/contracts/sysio/reserv/Structs.ts index 2597afdb..158e263a 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/Structs.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/Structs.ts @@ -1,12 +1,69 @@ +import { ABIDecoder } from "../../../serializer/Decoder.js" +import { ABIEncoder } from "../../../serializer/Encoder.js" +import { ABISerializableObject } from "../../../serializer/Serializable.js" import { UInt64 } from "../../../chain/Integer.js" import { Name } from "../../../chain/Name.js" import { Struct } from "../../../chain/Struct.js" +import { SlugName } from "../../../SlugName.js" +import { isInstanceOf } from "../../../Utils.js" -/** Runtime serializer for the Wire `slug_name` wrapper used by `sysio.reserv`. */ -@Struct.type("slug_name") -export class ReservSlugName extends Struct { - /** Packed eight-character reserve slug. */ - @Struct.field("uint64") declare value: UInt64 +/** + * Runtime serializer for the Wire `slug_name` chain type used by `sysio.reserv`. + * + * Modelled on `Name`, not on `Struct`: `slug_name` is an ABI BUILTIN on the + * chain, so it has one wire form (a packed `uint64` — unchanged, the bytes are + * what the depot stores) and one JSON form (the canonical spelling, the only + * carrier the builtin emits or accepts). A `Struct` subclass cannot express + * that — the object decoder dispatches on `type.fields` before consulting a + * class's own `from`, so a struct-shaped slug can only ever be written as + * `{ value }`. + */ +export class ReservSlugName implements ABISerializableObject { + static abiName = "slug_name" + + /** Packed eight-symbol code — the wire form. */ + value: UInt64 + + /** Builds from the canonical spelling, an already-packed value, or itself. */ + static from(value: ReservSlugName | UInt64 | string | number): ReservSlugName { + if (isInstanceOf(value, ReservSlugName)) return value + if (isInstanceOf(value, UInt64)) return new ReservSlugName(value) + + return new ReservSlugName( + UInt64.from(typeof value === "string" ? SlugName.from(value) : value) + ) + } + + static fromABI(decoder: ABIDecoder) { + return new ReservSlugName(UInt64.fromABI(decoder)) + } + + static abiDefault() { + return new this(UInt64.from(0)) + } + + constructor(value: UInt64) { + this.value = value + } + + /** Return true if this slug is equal to the passed slug. */ + equals(other: ReservSlugName | UInt64 | string | number): boolean { + return this.value.equals(ReservSlugName.from(other).value) + } + + /** The canonical spelling — `""` for the zero sentinel. */ + toString(): string { + return SlugName.toString(Number(this.value)) + } + + toABI(encoder: ABIEncoder) { + this.value.toABI(encoder) + } + + /** @internal */ + toJSON() { + return this.toString() + } } /** Runtime serializer for `sysio.reserv::matchreserve`. */ diff --git a/packages/sdk-core/src/contracts/sysio/tokens/Client.ts b/packages/sdk-core/src/contracts/sysio/tokens/Client.ts index e8bc6c7d..df024a60 100644 --- a/packages/sdk-core/src/contracts/sysio/tokens/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/tokens/Client.ts @@ -5,11 +5,7 @@ import { } from "../../../types/SysioContractTypes.js" import type * as SysioContracts from "../../../types/SysioContractTypes.js" import { getSysioContract, type SysioContractClient } from "../Client.js" -import { - reserveRowSlugValue, - reserveSlugString, - reserveSlugValue -} from "../reserv/Slug.js" +import { reserveSlugString, reserveSlugValue } from "../reserv/Slug.js" import { DEFAULT_TOKENS_CONTRACT, @@ -44,7 +40,7 @@ function enumValue>( export function normalizeTokenRow( row: SysioContracts.SysioTokensTokenRowType ): TokenRecord { - const codeValue = reserveRowSlugValue(row.code) + const codeValue = reserveSlugValue(row.code) return { code: reserveSlugString(codeValue), @@ -69,8 +65,8 @@ export function normalizeTokenRow( export function normalizeChainTokenRow( row: SysioContracts.SysioTokensChainTokenRowType ): ChainTokenRecord { - const chainCodeValue = reserveRowSlugValue(row.chain_code), - tokenCodeValue = reserveRowSlugValue(row.token_code) + const chainCodeValue = reserveSlugValue(row.chain_code), + tokenCodeValue = reserveSlugValue(row.token_code) return { chainCode: reserveSlugString(chainCodeValue), diff --git a/packages/sdk-core/src/contracts/sysio/uwrit/Actions.ts b/packages/sdk-core/src/contracts/sysio/uwrit/Actions.ts index 3793d7cf..03e087fd 100644 --- a/packages/sdk-core/src/contracts/sysio/uwrit/Actions.ts +++ b/packages/sdk-core/src/contracts/sysio/uwrit/Actions.ts @@ -1,6 +1,6 @@ import { Name } from "../../../chain/Name.js" import type * as SysioContracts from "../../../types/SysioContractTypes.js" -import { reserveSlugData } from "../reserv/Slug.js" +import { reserveSlugString } from "../reserv/Slug.js" import type { SwapFromWireOptions } from "./Types.js" @@ -11,9 +11,9 @@ export function swapFromWireActionData( 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), + dst_chain_code: reserveSlugString(options.destination.chainCode), + dst_token_code: reserveSlugString(options.destination.tokenCode), + dst_reserve_code: reserveSlugString(options.destination.reserveCode), target_amount: options.targetAmount.toString(), target_tolerance_bps: options.targetToleranceBps, recipient_kind: options.recipientKind, diff --git a/packages/sdk-core/src/contracts/sysio/uwrit/Client.ts b/packages/sdk-core/src/contracts/sysio/uwrit/Client.ts index 6b879f1b..bbbb9733 100644 --- a/packages/sdk-core/src/contracts/sysio/uwrit/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/uwrit/Client.ts @@ -8,11 +8,7 @@ import { } from "../../../types/SysioContractTypes.js" import type * as SysioContracts from "../../../types/SysioContractTypes.js" import { getSysioContract, type SysioContractClient } from "../Client.js" -import { - reserveRowSlugValue, - reserveSlugString, - reserveSlugValue -} from "../reserv/Slug.js" +import { reserveSlugString, reserveSlugValue } from "../reserv/Slug.js" import { swapFromWireActionData } from "./Actions.js" import { @@ -30,10 +26,14 @@ import type { UnderwritingRequestRecord } from "./Types.js" +/** + * The three slug fields that identify a reserve leg on a `uwreqs` row. Typed by + * indexed access off the generated row so a codegen change propagates here. + */ interface ReserveIdentitySlugs { - chain: SysioContracts.SysioUwritSlugNameType - token: SysioContracts.SysioUwritSlugNameType - reserve: SysioContracts.SysioUwritSlugNameType + chain: SysioContracts.SysioUwritUwRequestTType["src_chain_code"] + token: SysioContracts.SysioUwritUwRequestTType["src_token_code"] + reserve: SysioContracts.SysioUwritUwRequestTType["src_reserve_code"] } function bigintValue(value: number | string): bigint { @@ -58,9 +58,9 @@ function identity( reserve: ReserveIdentitySlugs["reserve"] ) { return { - chainCode: reserveSlugString(reserveRowSlugValue(chain)), - tokenCode: reserveSlugString(reserveRowSlugValue(token)), - reserveCode: reserveSlugString(reserveRowSlugValue(reserve)) + chainCode: reserveSlugString(chain), + tokenCode: reserveSlugString(token), + reserveCode: reserveSlugString(reserve) } } diff --git a/packages/sdk-core/src/types/SysioContractTypes.ts b/packages/sdk-core/src/types/SysioContractTypes.ts index 22841332..191d78df 100644 --- a/packages/sdk-core/src/types/SysioContractTypes.ts +++ b/packages/sdk-core/src/types/SysioContractTypes.ts @@ -298,17 +298,17 @@ export enum SysioChainsChainkind { /** sysio.chains::activchain (action) */ export interface SysioChainsActivchainAction { - code: SysioChainsSlugNameType + code: string } /** sysio.chains::chain_key (type) */ export interface SysioChainsChainKeyType { - code: SysioChainsSlugNameType + code: string } /** sysio.chains::chain_row (type) */ export interface SysioChainsChainRowType { - code: SysioChainsSlugNameType + code: string kind: SysioChainsChainkind | keyof typeof SysioChainsChainkind external_chain_id: number name: string @@ -331,7 +331,7 @@ export interface SysioChainsOutpostAddrsType { /** sysio.chains::regchain (action) */ export interface SysioChainsRegchainAction { kind: SysioChainsChainkind | keyof typeof SysioChainsChainkind - code: SysioChainsSlugNameType + code: string external_chain_id: number name: string description: string @@ -340,7 +340,7 @@ export interface SysioChainsRegchainAction { /** sysio.chains::setoutpost (action) */ export interface SysioChainsSetoutpostAction { - code: SysioChainsSlugNameType + code: string outpost: SysioChainsOutpostAddrsType } @@ -1786,14 +1786,14 @@ export interface SysioOpregTokenamountType { /** sysio.opreg::available (action) */ export interface SysioOpregAvailableAction { account: string - chain_code: SysioOpregSlugNameType - token_code: SysioOpregSlugNameType + chain_code: string + token_code: string } /** sysio.opreg::balance_entry (type) */ export interface SysioOpregBalanceEntryType { - chain_code: SysioOpregSlugNameType - token_code: SysioOpregSlugNameType + chain_code: string + token_code: string balance: number | string last_updated_ms: number | string } @@ -1806,8 +1806,8 @@ export interface SysioOpregCancelwtdwAction { /** sysio.opreg::chain_min_bond (type) */ export interface SysioOpregChainMinBondType { - chain_code: SysioOpregSlugNameType - token_code: SysioOpregSlugNameType + chain_code: string + token_code: string min_bond: number | string config_timestamp_ms: number | string } @@ -1840,8 +1840,8 @@ export interface SysioOpregDepositAction { /** sysio.opreg::depositinle (action) */ export interface SysioOpregDepositinleAction { account: string - chain_code: SysioOpregSlugNameType - token_code: SysioOpregSlugNameType + chain_code: string + token_code: string amount: number | string actor_chain: SysioOpregChainkind | keyof typeof SysioOpregChainkind actor_address: string @@ -1935,8 +1935,8 @@ export interface SysioOpregRegoperatorAction { /** sysio.opreg::releaselock (action) */ export interface SysioOpregReleaselockAction { account: string - chain_code: SysioOpregSlugNameType - token_code: SysioOpregSlugNameType + chain_code: string + token_code: string amount: number | string } @@ -2013,8 +2013,8 @@ export interface SysioOpregWithdrawKeyType { export interface SysioOpregWithdrawRequestType { request_id: number | string account: string - chain_code: SysioOpregSlugNameType - token_code: SysioOpregSlugNameType + chain_code: string + token_code: string amount: number | string eligible_at_epoch: number requested_at_epoch: number @@ -2023,8 +2023,8 @@ export interface SysioOpregWithdrawRequestType { /** sysio.opreg::withdrawinle (action) */ export interface SysioOpregWithdrawinleAction { account: string - chain_code: SysioOpregSlugNameType - token_code: SysioOpregSlugNameType + chain_code: string + token_code: string amount: number | string } @@ -2087,9 +2087,9 @@ export interface SysioReservChainaddressType { /** sysio.reserv::applyfromwire (action) */ export interface SysioReservApplyfromwireAction { - dst_chain_code: SysioReservSlugNameType - dst_token_code: SysioReservSlugNameType - dst_reserve_code: SysioReservSlugNameType + dst_chain_code: string + dst_token_code: string + dst_reserve_code: string wire_in: number | string dst_amount: number | string underwriter: string @@ -2097,22 +2097,22 @@ export interface SysioReservApplyfromwireAction { /** sysio.reserv::applyswap (action) */ export interface SysioReservApplyswapAction { - src_chain_code: SysioReservSlugNameType - src_token_code: SysioReservSlugNameType - src_reserve_code: SysioReservSlugNameType + src_chain_code: string + src_token_code: string + src_reserve_code: string src_amount: number | string - dst_chain_code: SysioReservSlugNameType - dst_token_code: SysioReservSlugNameType - dst_reserve_code: SysioReservSlugNameType + dst_chain_code: string + dst_token_code: string + dst_reserve_code: string dst_amount: number | string underwriter: string } /** sysio.reserv::claimrsvfee (action) */ export interface SysioReservClaimrsvfeeAction { - chain_code: SysioReservSlugNameType - token_code: SysioReservSlugNameType - reserve_code: SysioReservSlugNameType + chain_code: string + token_code: string + reserve_code: string } /** sysio.reserv::claimuwfee (action) */ @@ -2127,9 +2127,9 @@ export interface SysioReservClaimwireAction { /** sysio.reserv::debit (action) */ export interface SysioReservDebitAction { - chain_code: SysioReservSlugNameType - token_code: SysioReservSlugNameType - reserve_code: SysioReservSlugNameType + chain_code: string + token_code: string + reserve_code: string amount: number | string } @@ -2140,27 +2140,27 @@ export interface SysioReservDrainrewardsAction { /** sysio.reserv::matchreserve (action) */ export interface SysioReservMatchreserveAction { - chain_code: SysioReservSlugNameType - token_code: SysioReservSlugNameType - reserve_code: SysioReservSlugNameType + chain_code: string + token_code: string + reserve_code: string matcher: string wire_amount: number | string } /** sysio.reserv::oncnclrsv (action) */ export interface SysioReservOncnclrsvAction { - chain_code: SysioReservSlugNameType - token_code: SysioReservSlugNameType - reserve_code: SysioReservSlugNameType + chain_code: string + token_code: string + reserve_code: string creator_chain_kind: SysioReservChainkind | keyof typeof SysioReservChainkind creator_chain_addr: string } /** sysio.reserv::oncrtreserve (action) */ export interface SysioReservOncrtreserveAction { - chain_code: SysioReservSlugNameType - token_code: SysioReservSlugNameType - reserve_code: SysioReservSlugNameType + chain_code: string + token_code: string + reserve_code: string name: string description: string external_token_amount: number | string @@ -2175,9 +2175,9 @@ export interface SysioReservOncrtreserveAction { /** sysio.reserv::paywire (action) */ export interface SysioReservPaywireAction { - src_chain_code: SysioReservSlugNameType - src_token_code: SysioReservSlugNameType - src_reserve_code: SysioReservSlugNameType + src_chain_code: string + src_token_code: string + src_reserve_code: string src_amount: number | string recipient: string wire_out: number | string @@ -2193,9 +2193,9 @@ export interface SysioReservRefundwireAction { /** sysio.reserv::regreserve (action) */ export interface SysioReservRegreserveAction { - chain_code: SysioReservSlugNameType - token_code: SysioReservSlugNameType - reserve_code: SysioReservSlugNameType + chain_code: string + token_code: string + reserve_code: string name: string description: string initial_chain_amount: number | string @@ -2213,16 +2213,16 @@ export interface SysioReservReserveConfigType { /** sysio.reserv::reserve_key (type) */ export interface SysioReservReserveKeyType { - chain_code: SysioReservSlugNameType - token_code: SysioReservSlugNameType - reserve_code: SysioReservSlugNameType + chain_code: string + token_code: string + reserve_code: string } /** sysio.reserv::reserve_row (type) */ export interface SysioReservReserveRowType { - chain_code: SysioReservSlugNameType - token_code: SysioReservSlugNameType - reserve_code: SysioReservSlugNameType + chain_code: string + token_code: string + reserve_code: string name: string description: string status: SysioReservReservestatus | keyof typeof SysioReservReservestatus @@ -2256,9 +2256,9 @@ export interface SysioReservRewardsBucketType { /** sysio.reserv::rsvfeebal (action) */ export interface SysioReservRsvfeebalAction { - chain_code: SysioReservSlugNameType - token_code: SysioReservSlugNameType - reserve_code: SysioReservSlugNameType + chain_code: string + token_code: string + reserve_code: string } /** sysio.reserv::setconfig (action) */ @@ -2268,9 +2268,9 @@ export interface SysioReservSetconfigAction { /** sysio.reserv::setrsvfee (action) */ export interface SysioReservSetrsvfeeAction { - chain_code: SysioReservSlugNameType - token_code: SysioReservSlugNameType - reserve_code: SysioReservSlugNameType + chain_code: string + token_code: string + reserve_code: string owner_fee_bps: number } @@ -2281,13 +2281,13 @@ export interface SysioReservSlugNameType { /** sysio.reserv::swapquote (action) */ export interface SysioReservSwapquoteAction { - from_chain_code: SysioReservSlugNameType - from_token_code: SysioReservSlugNameType - from_reserve_code: SysioReservSlugNameType + from_chain_code: string + from_token_code: string + from_reserve_code: string from_amount: number | string - to_chain_code: SysioReservSlugNameType - to_token_code: SysioReservSlugNameType - to_reserve_code: SysioReservSlugNameType + to_chain_code: string + to_token_code: string + to_reserve_code: string } /** sysio.reserv::sweepclaims (action) */ @@ -3806,25 +3806,25 @@ export interface SysioTokensChainaddressType { /** sysio.tokens::activctok (action) */ export interface SysioTokensActivctokAction { - chain_code: SysioTokensSlugNameType - token_code: SysioTokensSlugNameType + chain_code: string + token_code: string } /** sysio.tokens::activtoken (action) */ export interface SysioTokensActivtokenAction { - code: SysioTokensSlugNameType + code: string } /** sysio.tokens::chain_token_key (type) */ export interface SysioTokensChainTokenKeyType { - chain_code: SysioTokensSlugNameType - token_code: SysioTokensSlugNameType + chain_code: string + token_code: string } /** sysio.tokens::chain_token_row (type) */ export interface SysioTokensChainTokenRowType { - chain_code: SysioTokensSlugNameType - token_code: SysioTokensSlugNameType + chain_code: string + token_code: string contract_addr: string is_native: boolean active: boolean @@ -3834,8 +3834,8 @@ export interface SysioTokensChainTokenRowType { /** sysio.tokens::regctok (action) */ export interface SysioTokensRegctokAction { - chain_code: SysioTokensSlugNameType - token_code: SysioTokensSlugNameType + chain_code: string + token_code: string contract_addr: string is_native: boolean } @@ -3843,7 +3843,7 @@ export interface SysioTokensRegctokAction { /** sysio.tokens::regtoken (action) */ export interface SysioTokensRegtokenAction { kind: SysioTokensTokenkind | keyof typeof SysioTokensTokenkind - code: SysioTokensSlugNameType + code: string symbol_name: string description: string precision: number @@ -3857,12 +3857,12 @@ export interface SysioTokensSlugNameType { /** sysio.tokens::token_key (type) */ export interface SysioTokensTokenKeyType { - code: SysioTokensSlugNameType + code: string } /** sysio.tokens::token_row (type) */ export interface SysioTokensTokenRowType { - code: SysioTokensSlugNameType + code: string kind: SysioTokensTokenkind | keyof typeof SysioTokensTokenkind symbol_name: string description: string @@ -3992,9 +3992,9 @@ export interface SysioUwritFromwireQType { id: number | string user: string wire_amount: number | string - dst_chain_code: SysioUwritSlugNameType - dst_token_code: SysioUwritSlugNameType - dst_reserve_code: SysioUwritSlugNameType + dst_chain_code: string + dst_token_code: string + dst_reserve_code: string target_amount: number | string variance_tolerance_bps: number recipient_kind: SysioUwritChainkind | keyof typeof SysioUwritChainkind @@ -4024,9 +4024,9 @@ export interface SysioUwritLockEntryType { lock_id: number | string uwreq_id: number | string underwriter: string - chain_code: SysioUwritSlugNameType - token_code: SysioUwritSlugNameType - reserve_code: SysioUwritSlugNameType + chain_code: string + token_code: string + reserve_code: string amount: number | string created_at_ms: number | string expires_at_ms: number | string @@ -4041,16 +4041,16 @@ export interface SysioUwritLockKeyType { /** sysio.uwrit::lock_sum (type) */ export interface SysioUwritLockSumType { underwriter: string - chain_code: SysioUwritSlugNameType - token_code: SysioUwritSlugNameType + chain_code: string + token_code: string amount: number | string } /** sysio.uwrit::lock_sum_key (type) */ export interface SysioUwritLockSumKeyType { underwriter: string - chain_code: SysioUwritSlugNameType - token_code: SysioUwritSlugNameType + chain_code: string + token_code: string } /** sysio.uwrit::pruneuwreqs (action) */ @@ -4063,9 +4063,9 @@ export interface SysioUwritRcrdcommitAction { uwreq_id: number | string underwriter: string chain_code: number | string - from_chain_code: SysioUwritSlugNameType - from_token_code: SysioUwritSlugNameType - reserve_code: SysioUwritSlugNameType + from_chain_code: string + from_token_code: string + reserve_code: string uic_bytes: string } @@ -4087,17 +4087,17 @@ export interface SysioUwritSlugNameType { /** sysio.uwrit::sumlocks (action) */ export interface SysioUwritSumlocksAction { underwriter: string - chain_code: SysioUwritSlugNameType - token_code: SysioUwritSlugNameType + chain_code: string + token_code: string } /** sysio.uwrit::swapfromwire (action) */ export interface SysioUwritSwapfromwireAction { user: string wire_amount: number | string - dst_chain_code: SysioUwritSlugNameType - dst_token_code: SysioUwritSlugNameType - dst_reserve_code: SysioUwritSlugNameType + dst_chain_code: string + dst_token_code: string + dst_reserve_code: string target_amount: number | string target_tolerance_bps: number recipient_kind: SysioUwritChainkind | keyof typeof SysioUwritChainkind @@ -4131,13 +4131,13 @@ export interface SysioUwritUwRequestTType { id: number | string type: SysioUwritAttestationtype | keyof typeof SysioUwritAttestationtype status: SysioUwritUnderwriterequeststatus | keyof typeof SysioUwritUnderwriterequeststatus - src_chain_code: SysioUwritSlugNameType - src_token_code: SysioUwritSlugNameType - src_reserve_code: SysioUwritSlugNameType + src_chain_code: string + src_token_code: string + src_reserve_code: string src_amount: number | string - dst_chain_code: SysioUwritSlugNameType - dst_token_code: SysioUwritSlugNameType - dst_reserve_code: SysioUwritSlugNameType + dst_chain_code: string + dst_token_code: string + dst_reserve_code: string dst_amount: number | string target_amount: number | string variance_tolerance_bps: number diff --git a/packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts b/packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts index e98b6345..2bd1a153 100644 --- a/packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts @@ -37,7 +37,7 @@ describe("sysio.chains action helpers", () => { test("creates generated register and activation action data", () => { expect(createRegisterChainActionData(REGISTRATION)).toEqual({ kind: SysioChainsChainkind.CHAIN_KIND_EVM, - code: { value: SlugName.from("POLYGON") }, + code: "POLYGON", external_chain_id: 137, name: "Polygon", description: "Polygon EVM outpost", @@ -46,7 +46,7 @@ describe("sysio.chains action helpers", () => { outpost: EMPTY_OUTPOST }) expect(createActivateChainActionData("POLYGON")).toEqual({ - code: { value: SlugName.from("POLYGON") } + code: "POLYGON" }) }) @@ -66,12 +66,16 @@ describe("sysio.chains action helpers", () => { expect(register.name.toString()).toBe("regchain") expect(register.authorization.map(String)).toEqual(["sysio.chains@active"]) expect(Number(registerData.kind)).toBe(SysioChainsChainkind.CHAIN_KIND_EVM) + // Both forms of the one type: the packed wire value, and the canonical + // spelling that is its only JSON carrier. expect(Number(registerData.code.value)).toBe(SlugName.from("POLYGON")) + expect(registerData.code.toJSON()).toBe("POLYGON") expect(Number(registerData.external_chain_id)).toBe(137) expect(registerData.name).toBe("Polygon") expect(registerData.description).toBe("Polygon EVM outpost") expect(registerData.outpost.opp_addr).toBe("") expect(Number(activateData.code.value)).toBe(SlugName.from("POLYGON")) + expect(activateData.code.toJSON()).toBe("POLYGON") }) test("carries every EVM contract role through regchain", () => { @@ -99,7 +103,7 @@ describe("sysio.chains action helpers", () => { expect( createSetOutpostActionData("SOLANA", { oppAddress: SVM_PROGRAM }) ).toEqual({ - code: { value: SlugName.from("SOLANA") }, + code: "SOLANA", outpost: { ...EMPTY_OUTPOST, opp_addr: SVM_PROGRAM } }) }) @@ -120,6 +124,7 @@ describe("sysio.chains action helpers", () => { expect(action.account.toString()).toBe("sysio.chains") expect(action.name.toString()).toBe("setoutpost") expect(Number(data.code.value)).toBe(SlugName.from("POLYGON")) + expect(data.code.toJSON()).toBe("POLYGON") expect(data.outpost.opp_addr).toBe(EVM_OPP) expect(data.outpost.opp_inbound_addr).toBe(EVM_INBOUND) expect(data.outpost.operator_registry_addr).toBe(EVM_OPREG) 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 6f1bfbe4..38036a17 100644 --- a/packages/sdk-core/tests/contracts/sysio/chains/Client.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/chains/Client.test.ts @@ -5,7 +5,7 @@ const { ChainsClient } = contracts.sysio.chains const CHAIN_ROWS = [ { - code: { value: String(SlugName.from("ETHEREUM")) }, + code: "ETHEREUM", kind: "CHAIN_KIND_EVM", external_chain_id: 31337, name: "Ethereum (anvil)", @@ -16,7 +16,7 @@ const CHAIN_ROWS = [ activated_at_ms: "1784345978000" }, { - code: { value: SlugName.from("SOLANA") }, + code: "SOLANA", kind: SysioChainsChainkind.CHAIN_KIND_SVM, external_chain_id: 0, name: "Solana", @@ -27,7 +27,7 @@ const CHAIN_ROWS = [ activated_at_ms: "0" }, { - code: { value: SlugName.from("WIRE") }, + code: "WIRE", kind: "CHAIN_KIND_WIRE", external_chain_id: 0, name: "Wire", diff --git a/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts b/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts index d88e5170..647f6de2 100644 --- a/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts @@ -1,18 +1,29 @@ import { contracts, SlugName } from "@wireio/sdk-core" -const { chainSlugData, chainSlugString, chainSlugValue } = - contracts.sysio.chains +const { chainSlugString, chainSlugValue } = contracts.sysio.chains describe("sysio.chains slug helpers", () => { test("round-trips friendly and packed chain codes", () => { const packed = SlugName.from("ETHEREUM") expect(chainSlugValue("ETHEREUM")).toBe(packed) - expect(chainSlugValue(String(packed))).toBe(packed) - expect(chainSlugData("ETHEREUM")).toEqual({ value: packed }) + expect(chainSlugValue(packed)).toBe(packed) + expect(chainSlugString("ETHEREUM")).toBe("ETHEREUM") expect(chainSlugString(packed)).toBe("ETHEREUM") }) + test("reads a digit-only code as a slug, never as its own decimal", () => { + // The slug alphabet contains digits, so "12345678" is a legitimate code + // whose packed value is nothing like 12345678. A decimal string is + // therefore not a second spelling of a packed value — it is a slug, or it + // is invalid. The packed form is passed as a number. + expect(chainSlugValue("12345678")).toBe(SlugName.from("12345678")) + expect(chainSlugValue("12345678")).not.toBe(12345678) + expect(() => chainSlugValue(String(SlugName.from("ETHEREUM")))).toThrow( + "longer than 8" + ) + }) + test("rejects empty, invalid, and unsafe values", () => { expect(() => chainSlugValue(0)).toThrow("non-zero safe integer") expect(() => chainSlugValue("ethereum")).toThrow("outside [A-Z0-9_]") 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 a52a6068..365fc96f 100644 --- a/packages/sdk-core/tests/contracts/sysio/reserv/Client.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/reserv/Client.test.ts @@ -1,15 +1,15 @@ import { UInt64 } from "@wireio/sdk-core/chain/Integer" import { Serializer } from "@wireio/sdk-core/serializer" -import { contracts, SlugName } from "@wireio/sdk-core" +import { contracts } from "@wireio/sdk-core" import { SysioReservReservestatus } from "@wireio/sdk-core/types/SysioContractTypes" const { ReserveClient } = contracts.sysio.reserv function reserveRow(overrides: Record = {}) { return { - chain_code: { value: String(SlugName.from("ETHEREUM")) }, - token_code: { value: SlugName.from("ETH") }, - reserve_code: { value: SlugName.from("PRIMARY") }, + chain_code: "ETHEREUM", + token_code: "ETH", + reserve_code: "PRIMARY", name: "Primary ETH", description: "Public ETH liquidity", status: "RESERVE_STATUS_PENDING", @@ -125,9 +125,7 @@ describe("ReserveClient", () => { getTableRows.mockImplementation(async (params: any) => ({ rows: [ reserveRow({ - reserve_code: { - value: SlugName.from(params.lower_bound ? "PRIMARY" : "OTHER") - } + reserve_code: params.lower_bound ? "PRIMARY" : "OTHER" }) ], more: !params.lower_bound, diff --git a/packages/sdk-core/tests/contracts/sysio/tokens/Client.test.ts b/packages/sdk-core/tests/contracts/sysio/tokens/Client.test.ts index f0de471f..e336774a 100644 --- a/packages/sdk-core/tests/contracts/sysio/tokens/Client.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/tokens/Client.test.ts @@ -1,4 +1,4 @@ -import { contracts, SlugName } from "@wireio/sdk-core" +import { contracts } from "@wireio/sdk-core" import { SysioTokensChainkind, SysioTokensTokenkind @@ -13,7 +13,7 @@ interface TableQuery { function tokenRow(overrides: Record = {}) { return { - code: { value: SlugName.from("ETH") }, + code: "ETH", kind: "TOKEN_KIND_NATIVE", symbol_name: "ETH", description: "Ether", @@ -28,8 +28,8 @@ function tokenRow(overrides: Record = {}) { function chainTokenRow(overrides: Record = {}) { return { - chain_code: { value: SlugName.from("ETHEREUM") }, - token_code: { value: SlugName.from("ETH") }, + chain_code: "ETHEREUM", + token_code: "ETH", contract_addr: "0x", is_native: true, active: true, @@ -70,14 +70,14 @@ describe("TokenRegistryClient", () => { test("normalizes packed slugs serialized as uint64 strings", () => { expect( normalizeTokenRow( - tokenRow({ code: { value: String(SlugName.from("ETH")) } }) as any + tokenRow({ code: "ETH" }) as any ) ).toMatchObject({ code: "ETH" }) expect( normalizeChainTokenRow( chainTokenRow({ - chain_code: { value: String(SlugName.from("ETHEREUM")) }, - token_code: { value: String(SlugName.from("ETH")) } + chain_code: "ETHEREUM", + token_code: "ETH" }) as any ) ).toMatchObject({ chainCode: "ETHEREUM", tokenCode: "ETH" }) diff --git a/packages/sdk-core/tests/contracts/sysio/uwrit/Client.test.ts b/packages/sdk-core/tests/contracts/sysio/uwrit/Client.test.ts index c832b1c3..90f176c6 100644 --- a/packages/sdk-core/tests/contracts/sysio/uwrit/Client.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/uwrit/Client.test.ts @@ -1,4 +1,4 @@ -import { contracts, SlugName } from "@wireio/sdk-core" +import { contracts } from "@wireio/sdk-core" import { SysioUwritChainkind, SysioUwritUnderwriterequeststatus @@ -16,20 +16,18 @@ interface TableQuery { table: string } -const slug = (value: string) => ({ value: String(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_chain_code: "ETHEREUM", + src_token_code: "ETH", + src_reserve_code: "PRIMARY", src_amount: "100", - dst_chain_code: slug("SOLANA"), - dst_token_code: slug("SOL"), - dst_reserve_code: slug("PRIMARY"), + dst_chain_code: "SOLANA", + dst_token_code: "SOL", + dst_reserve_code: "PRIMARY", dst_amount: "90", variance_tolerance_bps: 500, source_tx_id: "000000000000002a", @@ -50,9 +48,9 @@ function queueRow(overrides: Record = {}) { id: "8", user: "alice", wire_amount: "5000000000", - dst_chain_code: slug("SOLANA"), - dst_token_code: slug("SOL"), - dst_reserve_code: slug("PRIMARY"), + dst_chain_code: "SOLANA", + dst_token_code: "SOL", + dst_reserve_code: "PRIMARY", target_amount: "80", variance_tolerance_bps: 500, recipient_kind: "CHAIN_KIND_SVM", @@ -135,9 +133,9 @@ describe("UnderwritingClient", () => { test("decodes the synthetic WIRE request id and depositor account", () => { const row = requestRow({ - src_chain_code: slug("WIRE"), - src_token_code: slug("WIRE"), - src_reserve_code: slug("WIRE"), + src_chain_code: "WIRE", + src_token_code: "WIRE", + src_reserve_code: "WIRE", source_tx_id: "0700000000000080", depositor: "776972656e6f2e616263" }) From 637b20ca6da55f5969d272cb82d546a61d2927a8 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Thu, 17 Sep 2026 13:17:51 -0500 Subject: [PATCH 2/5] fix(sdk-core): mirror the chain's leading-letter rule in SlugName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A code must start with a letter, so SlugName.from refuses a digit- or underscore-leading spelling rather than packing one the depot will reject — the failure surfaces at the parse with its reason instead of at pushAction as an opaque ABI error. toString stays total: it is a reader, and a throwing renderer in a scan loop is what stalls a consumer. Change-Id: Ibfcfee56719a9800f1ab7e41719f1dd527062576 --- packages/sdk-core/src/SlugName.ts | 29 +++++++++++++-- .../src/contracts/sysio/chains/Slug.ts | 6 +-- .../src/contracts/sysio/reserv/Slug.ts | 6 +-- packages/sdk-core/tests/SlugName.test.ts | 37 ++++++++++++++++--- .../tests/contracts/sysio/chains/Slug.test.ts | 15 ++++---- 5 files changed, 70 insertions(+), 23 deletions(-) diff --git a/packages/sdk-core/src/SlugName.ts b/packages/sdk-core/src/SlugName.ts index 6a353d87..957c2cc3 100644 --- a/packages/sdk-core/src/SlugName.ts +++ b/packages/sdk-core/src/SlugName.ts @@ -1,3 +1,12 @@ +/** Every character a slug_name may contain — `slug_name_traits::alphabet`. */ +const SlugAlphabetPattern = /^[A-Z0-9_]+$/ + +/** + * The characters a slug_name may START with — + * `slug_name_traits::leading_alphabet`. + */ +const SlugLeadingAlphabetPattern = /^[A-Z]/ + /** * SlugName — TypeScript counterpart to the `sysio::slug_name` (contract) / * `fc::slug_name` (host) packed 8-byte type used as the primary key for @@ -12,7 +21,14 @@ * `A..Z`, slots 27..36 hold `0..9`, slot 37 holds `_`. Characters outside the * alphabet are a parse error. Inputs longer than 8 chars are a parse error. * - * Mirrors the encoding in `wire-sysio/contracts/sysio.opp.common/include/sysio.opp.common/slug_name.hpp`. + * A code must START with a letter. That is what makes the chain's string + * carrier unambiguous — no legal code can be spelled like a number, so a bare + * JSON string is always a code and never a decimal. Digits and `_` stay legal + * in every position after the first (`V1`, `USDC`, `TRAIL_`). + * + * Mirrors the encoding in `wire-sysio/contracts/sysio.opp.common/include/sysio.opp.common/slug_name.hpp`, + * and the leading rule in `fc::slug_name_traits::leading_alphabet` (host) / + * `sysio::slug_name_traits::leading_alphabet` (CDT). */ export class SlugName { /** @@ -23,8 +39,10 @@ export class SlugName { * slug_name occupies 48 bits, well above the 32-bit boundary but safely * under `Number.MAX_SAFE_INTEGER` (53 bits) — arithmetic stays exact. * - * @param s uppercase letters, digits, and underscore; ≤8 chars - * @throws on empty input, length > 8, or any out-of-alphabet character + * @param s uppercase letters, digits, and underscore; ≤8 chars, and the + * first character must be a letter ([A-Z]) + * @throws on empty input, length > 8, any out-of-alphabet character, or a + * first character that is not a letter */ static from(s: string): number { if (s.length === 0) { @@ -33,9 +51,12 @@ export class SlugName { if (s.length > 8) { throw new Error(`SlugName.from: '${s}' is longer than 8 chars`) } - if (!/^[A-Z0-9_]+$/.test(s)) { + if (!SlugAlphabetPattern.test(s)) { throw new Error(`SlugName.from: '${s}' has chars outside [A-Z0-9_]`) } + if (!SlugLeadingAlphabetPattern.test(s)) { + throw new Error(`SlugName.from: '${s}' must start with a letter ([A-Z])`) + } let v = 0 for (let i = 0; i < s.length; ++i) { const c = s.charCodeAt(i) diff --git a/packages/sdk-core/src/contracts/sysio/chains/Slug.ts b/packages/sdk-core/src/contracts/sysio/chains/Slug.ts index 926d1017..ba54bce1 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Slug.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Slug.ts @@ -5,9 +5,9 @@ import type { ChainSlugName } from "./Types.js" /** * Converts a friendly chain slug or packed value to its safe numeric form. * - * A string is ALWAYS parsed as a slug, never as a decimal: the slug alphabet - * contains digits, so `"12345678"` is a legitimate code whose packed value is - * nothing like 12345678. Pass a `number` to supply an already-packed value. + * A string is ALWAYS parsed as a slug, never as a decimal — and that is + * unambiguous because a code must start with a letter, so no legal spelling can + * be read as a number. Pass a `number` to supply an already-packed value. */ export function chainSlugValue(value: ChainSlugName): number { const packed = typeof value === "string" ? SlugName.from(value) : Number(value) diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Slug.ts b/packages/sdk-core/src/contracts/sysio/reserv/Slug.ts index 7e2b9d0e..aaba5639 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/Slug.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/Slug.ts @@ -5,9 +5,9 @@ import type { ReserveSlugName } from "./Types.js" /** * Converts a friendly slug string or packed value to its safe numeric form. * - * A string is ALWAYS parsed as a slug, never as a decimal: the slug alphabet - * contains digits, so `"12345678"` is a legitimate code whose packed value is - * nothing like 12345678. Pass a `number` to supply an already-packed value. + * A string is ALWAYS parsed as a slug, never as a decimal — and that is + * unambiguous because a code must start with a letter, so no legal spelling can + * be read as a number. Pass a `number` to supply an already-packed value. */ export function reserveSlugValue(value: ReserveSlugName): number { const packed = diff --git a/packages/sdk-core/tests/SlugName.test.ts b/packages/sdk-core/tests/SlugName.test.ts index 9f01edcc..ad0b6d24 100644 --- a/packages/sdk-core/tests/SlugName.test.ts +++ b/packages/sdk-core/tests/SlugName.test.ts @@ -14,12 +14,12 @@ describe("SlugName", () => { expect(SlugName.from("Z")).toBe(26 * SLOT0) }) - it("encodes digit '0' to slot 27", () => { - expect(SlugName.from("0")).toBe(27 * SLOT0) + it("encodes digit '0' to slot 27, behind a leading letter", () => { + expect(SlugName.from("A0")).toBe(1 * SLOT0 + 27 * SLOT1) }) - it("encodes '_' to slot 37", () => { - expect(SlugName.from("_")).toBe(37 * SLOT0) + it("encodes '_' to slot 37, behind a leading letter", () => { + expect(SlugName.from("A_")).toBe(1 * SLOT0 + 37 * SLOT1) }) it("encodes a multi-char string with 6-bit MSB-first packing", () => { @@ -54,6 +54,28 @@ describe("SlugName", () => { expect(() => SlugName.from("ETH!")).toThrow(/outside/) expect(() => SlugName.from("E-T")).toThrow(/outside/) }) + + it("rejects a code that does not start with a letter", () => { + // The rule that makes the chain's string carrier unambiguous: no legal + // code can be spelled like a number. Mirrors + // `fc::slug_name_traits::leading_alphabet`. + const leadingLetter = /must start with a letter/ + expect(() => SlugName.from("0")).toThrow(leadingLetter) + expect(() => SlugName.from("7")).toThrow(leadingLetter) + expect(() => SlugName.from("101")).toThrow(leadingLetter) + expect(() => SlugName.from("1E3")).toThrow(leadingLetter) + expect(() => SlugName.from("0X10")).toThrow(leadingLetter) + expect(() => SlugName.from("12345678")).toThrow(leadingLetter) + expect(() => SlugName.from("_")).toThrow(leadingLetter) + expect(() => SlugName.from("_LEAD")).toThrow(leadingLetter) + }) + + it("keeps digits and '_' legal after the first character", () => { + expect(SlugName.toString(SlugName.from("V1"))).toBe("V1") + expect(SlugName.toString(SlugName.from("TRAIL_"))).toBe("TRAIL_") + expect(SlugName.toString(SlugName.from("Z1234567"))).toBe("Z1234567") + expect(SlugName.toString(SlugName.from("Z_______"))).toBe("Z_______") + }) }) describe("toString", () => { @@ -68,7 +90,10 @@ describe("SlugName", () => { expect(SlugName.toString(26 * SLOT0)).toBe("Z") }) - it("decodes digit slots", () => { + // `toString` stays TOTAL on purpose: it is a READER. A throwing renderer in + // a scan loop is what stalls a consumer (wire-sysio #619 thread 3), so the + // leading rule is enforced on the WRITE path (`from`) only. + it("decodes digit slots even though such a value is not a writable code", () => { expect(SlugName.toString(27 * SLOT0)).toBe("0") expect(SlugName.toString(36 * SLOT0)).toBe("9") }) @@ -84,7 +109,7 @@ describe("SlugName", () => { }) describe("round-trip", () => { - const inputs = ["ETH", "WIRE", "SOL", "USDC", "LIQETH", "LIQSOL", "PRIMARY", "ETHEREUM", "SOLANA", "0", "_", "A", "Z9_"] + const inputs = ["ETH", "WIRE", "SOL", "USDC", "LIQETH", "LIQSOL", "PRIMARY", "ETHEREUM", "SOLANA", "A0", "A_", "A", "Z9_"] inputs.forEach(s => { it(`round-trips '${s}'`, () => { const encoded = SlugName.from(s) diff --git a/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts b/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts index 647f6de2..ff118dda 100644 --- a/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts @@ -12,16 +12,17 @@ describe("sysio.chains slug helpers", () => { expect(chainSlugString(packed)).toBe("ETHEREUM") }) - test("reads a digit-only code as a slug, never as its own decimal", () => { - // The slug alphabet contains digits, so "12345678" is a legitimate code - // whose packed value is nothing like 12345678. A decimal string is - // therefore not a second spelling of a packed value — it is a slug, or it - // is invalid. The packed form is passed as a number. - expect(chainSlugValue("12345678")).toBe(SlugName.from("12345678")) - expect(chainSlugValue("12345678")).not.toBe(12345678) + test("refuses a digit-leading string, so a decimal is never read as a slug", () => { + // A code must start with a letter, which is what makes the string carrier + // unambiguous: a decimal spelling can never also be a code. The packed form + // is passed as a number, never as its decimal string. + expect(() => chainSlugValue("12345678")).toThrow("must start with a letter") + expect(() => chainSlugValue("7")).toThrow("must start with a letter") expect(() => chainSlugValue(String(SlugName.from("ETHEREUM")))).toThrow( "longer than 8" ) + // Digits after the leading letter are ordinary. + expect(chainSlugValue("Z1234567")).toBe(SlugName.from("Z1234567")) }) test("rejects empty, invalid, and unsafe values", () => { From 9e012fef869cb397ada55dbc8a2b052c8bde12cf Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Tue, 22 Sep 2026 16:51:15 -0500 Subject: [PATCH 3/5] feat(sdk-core): accept the transitional { value } slug carrier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pre-builtin depot emits a slug_name as an object — the reflected struct is object-only — and the depot still accepts that shape inbound. A string-only reader cannot straddle the landing window, so #83 could not merge before the node change; with this it can. slugValue is now the one decoder for every carrier (string, number, bigint, or the { value } object) and both per-contract readers delegate to it instead of duplicating the body. Matches wire-tools' slugUtils, which already takes all three. Delete SlugNameObject once no depot emits the object form. Change-Id: Icfbe2e544e4416194ab12e2434e90cb795189c7a --- packages/sdk-core/src/SlugName.ts | 53 +++++++++++++++++++ .../src/contracts/sysio/chains/Slug.ts | 19 ++----- .../src/contracts/sysio/chains/Types.ts | 5 +- .../src/contracts/sysio/reserv/Slug.ts | 20 ++----- .../src/contracts/sysio/reserv/Types.ts | 5 +- packages/sdk-core/tests/SlugName.test.ts | 45 +++++++++++++++- 6 files changed, 111 insertions(+), 36 deletions(-) diff --git a/packages/sdk-core/src/SlugName.ts b/packages/sdk-core/src/SlugName.ts index 957c2cc3..bc05d420 100644 --- a/packages/sdk-core/src/SlugName.ts +++ b/packages/sdk-core/src/SlugName.ts @@ -1,3 +1,5 @@ +import { match, P } from "ts-pattern" + /** Every character a slug_name may contain — `slug_name_traits::alphabet`. */ const SlugAlphabetPattern = /^[A-Z0-9_]+$/ @@ -116,3 +118,54 @@ export class SlugName { return out } } + +/** + * The transitional object carrier for a packed slug_name. + * + * With the ABI builtin absent, a slug converts through + * `FC_REFLECT_TEMPLATE(basic_name, (value))` and is therefore + * object-only, so a pre-builtin depot emits this shape. The depot still + * ACCEPTS it (`fc::slug_name::from_variant`), which is what lets a READER + * straddle the landing window — a JSON writer cannot, since no value both + * spellings accept. + * + * Delete this once no depot emits the object form; the bare string is the one + * canonical carrier. + */ +export interface SlugNameObject { + /** The packed uint64. `fc::json` quotes it once it exceeds `0xffffffff`. */ + value: number | string +} + +/** Every carrier a slug_name cell can arrive in. */ +export type SlugNameValue = string | number | bigint | SlugNameObject + +/** + * The packed numeric value of a slug cell, whatever carrier it arrived in. + * + * A bare string is ALWAYS parsed as a slug, never as a decimal — unambiguous + * because a code must start with a letter, so no legal spelling can be read as + * a number. A number or bigint is an already-packed value. An object is the + * transitional {@link SlugNameObject} carrier. + * + * @param value the cell, in any carrier + * @param label names the slug in the failure message (`Chain`, `Reserve`) + * @throws if the result is not a non-zero safe integer, or a string is not a + * valid slug_name spelling + */ +export function slugValue(value: SlugNameValue, label: string): number { + const packed = match(value) + .with(P.string, spelling => SlugName.from(spelling)) + .with({ value: P.union(P.string, P.number) }, wrapped => + Number(wrapped.value) + ) + .otherwise(alreadyPacked => Number(alreadyPacked)) + + if (!Number.isSafeInteger(packed) || packed <= 0) { + throw new Error( + `${label} slug must be a non-zero safe integer or valid slug_name string.` + ) + } + + return packed +} diff --git a/packages/sdk-core/src/contracts/sysio/chains/Slug.ts b/packages/sdk-core/src/contracts/sysio/chains/Slug.ts index ba54bce1..9d92deb4 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Slug.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Slug.ts @@ -1,24 +1,13 @@ -import { SlugName } from "../../../SlugName.js" +import { SlugName, slugValue } from "../../../SlugName.js" import type { ChainSlugName } from "./Types.js" /** - * Converts a friendly chain slug or packed value to its safe numeric form. - * - * A string is ALWAYS parsed as a slug, never as a decimal — and that is - * unambiguous because a code must start with a letter, so no legal spelling can - * be read as a number. Pass a `number` to supply an already-packed value. + * Converts a chain slug cell to its safe numeric form, in any carrier — + * see {@link slugValue} for how each one is read. */ export function chainSlugValue(value: ChainSlugName): number { - const packed = typeof value === "string" ? SlugName.from(value) : Number(value) - - if (!Number.isSafeInteger(packed) || packed <= 0) { - throw new Error( - "Chain slug must be a non-zero safe integer or valid slug_name string." - ) - } - - return packed + return slugValue(value, "Chain") } /** Returns the display form of a packed chain slug. */ diff --git a/packages/sdk-core/src/contracts/sysio/chains/Types.ts b/packages/sdk-core/src/contracts/sysio/chains/Types.ts index 6b29dc26..c0093153 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Types.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Types.ts @@ -2,9 +2,10 @@ import type { NameType } from "../../../chain/Name.js" import type { ContractPermissionLevel } from "../../Contract.js" import type * as SysioContracts from "../../../types/SysioContractTypes.js" import type { APIClient } from "../../../api/Client.js" +import type { SlugNameValue } from "../../../SlugName.js" -/** Friendly string or packed numeric `slug_name` accepted by chain helpers. */ -export type ChainSlugName = string | number | bigint +/** Every carrier a chain `slug_name` may arrive in — see {@link SlugNameValue}. */ +export type ChainSlugName = SlugNameValue /** Configuration for `ChainsClient`. */ export interface ChainsClientOptions { diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Slug.ts b/packages/sdk-core/src/contracts/sysio/reserv/Slug.ts index aaba5639..d2709c31 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/Slug.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/Slug.ts @@ -1,25 +1,13 @@ -import { SlugName } from "../../../SlugName.js" +import { SlugName, slugValue } from "../../../SlugName.js" import type { ReserveSlugName } from "./Types.js" /** - * Converts a friendly slug string or packed value to its safe numeric form. - * - * A string is ALWAYS parsed as a slug, never as a decimal — and that is - * unambiguous because a code must start with a letter, so no legal spelling can - * be read as a number. Pass a `number` to supply an already-packed value. + * Converts a reserve slug cell to its safe numeric form, in any carrier — + * see {@link slugValue} for how each one is read. */ export function reserveSlugValue(value: ReserveSlugName): number { - const packed = - typeof value === "string" ? SlugName.from(value) : Number(value) - - if (!Number.isSafeInteger(packed) || packed <= 0) { - throw new Error( - "Reserve slug must be a non-zero safe integer or valid slug_name string." - ) - } - - return packed + return slugValue(value, "Reserve") } /** Returns the display form of a packed reserve slug. */ diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Types.ts b/packages/sdk-core/src/contracts/sysio/reserv/Types.ts index ed5b2658..f059d17e 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/Types.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/Types.ts @@ -3,9 +3,10 @@ import type { TransactionExtraOptions } from "../../../api/Types.js" import type { UInt64Type } from "../../../chain/Integer.js" import type { NameType } from "../../../chain/Name.js" import type * as SysioContracts from "../../../types/SysioContractTypes.js" +import type { SlugNameValue } from "../../../SlugName.js" -/** Friendly string or packed numeric `slug_name` accepted by reserve helpers. */ -export type ReserveSlugName = string | number | bigint +/** Every carrier a reserve `slug_name` may arrive in — see {@link SlugNameValue}. */ +export type ReserveSlugName = SlugNameValue /** Three-part identity that uniquely addresses one reserve. */ export interface ReserveIdentity { diff --git a/packages/sdk-core/tests/SlugName.test.ts b/packages/sdk-core/tests/SlugName.test.ts index ad0b6d24..a39275b6 100644 --- a/packages/sdk-core/tests/SlugName.test.ts +++ b/packages/sdk-core/tests/SlugName.test.ts @@ -1,4 +1,4 @@ -import { SlugName } from "@wireio/sdk-core/SlugName" +import { SlugName, slugValue } from "@wireio/sdk-core/SlugName" describe("SlugName", () => { describe("from", () => { @@ -126,4 +126,47 @@ describe("SlugName", () => { expect(SlugName.from("WIRE")).not.toBe(SlugName.from("ETH")) }) }) + + describe("slugValue", () => { + const packedEthereum = SlugName.from("ETHEREUM") + + it("parses a bare string as a SPELLING, never a decimal", () => { + expect(slugValue("ETHEREUM", "Chain")).toBe(packedEthereum) + }) + + it("passes an already-packed number through", () => { + expect(slugValue(packedEthereum, "Chain")).toBe(packedEthereum) + }) + + it("accepts a bigint, since a generated field may carry one", () => { + expect(slugValue(BigInt(packedEthereum), "Chain")).toBe(packedEthereum) + }) + + it("accepts the transitional { value } object carrier", () => { + // The shape a pre-builtin depot emits: FC_REFLECT_TEMPLATE makes the + // reflected struct object-only, so a reader must take both carriers. + expect(slugValue({ value: packedEthereum }, "Chain")).toBe(packedEthereum) + }) + + it("accepts the object carrier quoted, as fc::json emits it above 0xffffffff", () => { + expect(slugValue({ value: String(packedEthereum) }, "Chain")).toBe( + packedEthereum + ) + }) + + it("rejects a spelling that does not lead with a letter", () => { + expect(() => slugValue("7", "Chain")).toThrow(/must start with a letter/) + }) + + it("names the slug in the failure, so the caller knows which one", () => { + expect(() => slugValue(0, "Reserve")).toThrow(/^Reserve slug must be/) + expect(() => slugValue(0, "Chain")).toThrow(/^Chain slug must be/) + }) + + it("rejects an object carrying a non-numeric value", () => { + expect(() => slugValue({ value: "not-a-number" }, "Chain")).toThrow( + /must be a non-zero safe integer/ + ) + }) + }) }) From d9ed2328c3d4e7f481f550d6452f4fe8efac7b83 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Fri, 25 Sep 2026 08:57:07 -0500 Subject: [PATCH 4/5] chore(sdk-core): regenerate SysioContractTypes after rebasing onto master Generated from the wire-sysio#619 ABIs, which carry master's sysio.liq and sysio.swap contracts; slug_name fields are the builtin string carrier. Change-Id: Ia77e57d060a151de76b36d5bbfc4f74a8cd7d50b --- .../sdk-core/src/types/SysioContractTypes.ts | 60 +++++-------------- 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/packages/sdk-core/src/types/SysioContractTypes.ts b/packages/sdk-core/src/types/SysioContractTypes.ts index 191d78df..332c90c9 100644 --- a/packages/sdk-core/src/types/SysioContractTypes.ts +++ b/packages/sdk-core/src/types/SysioContractTypes.ts @@ -344,11 +344,6 @@ export interface SysioChainsSetoutpostAction { outpost: SysioChainsOutpostAddrsType } -/** sysio.chains::slug_name (type) */ -export interface SysioChainsSlugNameType { - value: number | string -} - /** sysio.chains - action + table surface for the typed contract client. */ export interface SysioChainsContract { actions: { @@ -1079,15 +1074,15 @@ export interface SysioLiqCloseAction { /** sysio.liq::create (action) */ export interface SysioLiqCreateAction { sym: string - chain_code: SysioLiqSlugNameType - token_code: SysioLiqSlugNameType + chain_code: string + token_code: string } /** sysio.liq::currency_stats (type) */ export interface SysioLiqCurrencyStatsType { supply: string - chain_code: SysioLiqSlugNameType - token_code: SysioLiqSlugNameType + chain_code: string + token_code: string pair_symbol: string } @@ -1114,8 +1109,8 @@ export interface SysioLiqImportdoneAction { /** sysio.liq::importsynd (action) */ export interface SysioLiqImportsyndAction { - chain_code: SysioLiqSlugNameType - token_code: SysioLiqSlugNameType + chain_code: string + token_code: string credits: SysioLiqImportCreditType[] } @@ -1139,26 +1134,26 @@ export interface SysioLiqLiqCountersType { /** sysio.liq::liq_cursor (type) */ export interface SysioLiqLiqCursorType { - chain_code: SysioLiqSlugNameType + chain_code: string last_sequence: number | string last_epoch: number | string } /** sysio.liq::mintsynd (action) */ export interface SysioLiqMintsyndAction { - chain_code: SysioLiqSlugNameType + chain_code: string sequence: number | string account: string - token_code: SysioLiqSlugNameType + token_code: string amount: number | string } /** sysio.liq::mintyield (action) */ export interface SysioLiqMintyieldAction { - chain_code: SysioLiqSlugNameType + chain_code: string sequence: number | string epoch: number | string - token_code: SysioLiqSlugNameType + token_code: string amount: number | string } @@ -1171,11 +1166,11 @@ export interface SysioLiqOpenAction { /** sysio.liq::park (action) */ export interface SysioLiqParkAction { - chain_code: SysioLiqSlugNameType + chain_code: string sequence: number | string chain_kind: SysioLiqChainkind | keyof typeof SysioLiqChainkind pubkey: string - token_code: SysioLiqSlugNameType + token_code: string amount: number | string } @@ -1211,8 +1206,8 @@ export interface SysioLiqRecreditAction { /** sysio.liq::regliqpool (action) */ export interface SysioLiqRegliqpoolAction { - chain_code: SysioLiqSlugNameType - token_code: SysioLiqSlugNameType + chain_code: string + token_code: string pair_symbol: string initial_chain_amount: number | string initial_wire_amount: number | string @@ -1228,11 +1223,6 @@ export interface SysioLiqSetkickerAction { bps: number } -/** sysio.liq::slug_name (type) */ -export interface SysioLiqSlugNameType { - value: number | string -} - /** sysio.liq::sweep (action) */ export interface SysioLiqSweepAction { account: string @@ -1972,11 +1962,6 @@ export interface SysioOpregSlashAction { reason: string } -/** sysio.opreg::slug_name (type) */ -export interface SysioOpregSlugNameType { - value: number | string -} - /** sysio.opreg::termcheck (action) */ export interface SysioOpregTermcheckAction { account: string @@ -2274,11 +2259,6 @@ export interface SysioReservSetrsvfeeAction { owner_fee_bps: number } -/** sysio.reserv::slug_name (type) */ -export interface SysioReservSlugNameType { - value: number | string -} - /** sysio.reserv::swapquote (action) */ export interface SysioReservSwapquoteAction { from_chain_code: string @@ -3850,11 +3830,6 @@ export interface SysioTokensRegtokenAction { address: SysioTokensChainaddressType } -/** sysio.tokens::slug_name (type) */ -export interface SysioTokensSlugNameType { - value: number | string -} - /** sysio.tokens::token_key (type) */ export interface SysioTokensTokenKeyType { code: string @@ -4079,11 +4054,6 @@ export interface SysioUwritSetconfigAction { uwreq_retention_epochs: number } -/** sysio.uwrit::slug_name (type) */ -export interface SysioUwritSlugNameType { - value: number | string -} - /** sysio.uwrit::sumlocks (action) */ export interface SysioUwritSumlocksAction { underwriter: string From 7420cd268e1f96da555061b2aeb20873e1a349a0 Mon Sep 17 00:00:00 2001 From: kevin Heifner Date: Fri, 25 Sep 2026 08:57:07 -0500 Subject: [PATCH 5/5] fix(sdk-core): parse a packed slug value strictly Number() coerces "", " 12 ", "0x10" and "1e3", and parseInt stops at the first non-digit, so neither rejects a malformed { value } carrier. Parse through UInt64.from, which accepts only a base-10 integer or a safe integer, and convert the slug structs with toNumber(), which throws past 53 bits instead of rounding. Change-Id: Iff04918124c7dce134becefa1cc9e1a53ebf9e1e --- packages/sdk-core/src/SlugName.ts | 37 ++++++++++++++++--- .../src/contracts/sysio/chains/Structs.ts | 2 +- .../src/contracts/sysio/reserv/Structs.ts | 2 +- packages/sdk-core/tests/SlugName.test.ts | 18 +++++++++ .../tests/contracts/sysio/chains/Slug.test.ts | 12 +++++- .../tests/contracts/sysio/reserv/Slug.test.ts | 13 +++++++ 6 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 packages/sdk-core/tests/contracts/sysio/reserv/Slug.test.ts diff --git a/packages/sdk-core/src/SlugName.ts b/packages/sdk-core/src/SlugName.ts index bc05d420..374b4ebb 100644 --- a/packages/sdk-core/src/SlugName.ts +++ b/packages/sdk-core/src/SlugName.ts @@ -1,5 +1,10 @@ +import { Either } from "@3fv/prelude-ts" import { match, P } from "ts-pattern" +import { NestedError } from "@wireio/shared" + +import { UInt64 } from "./chain/Integer.js" + /** Every character a slug_name may contain — `slug_name_traits::alphabet`. */ const SlugAlphabetPattern = /^[A-Z0-9_]+$/ @@ -157,15 +162,35 @@ export function slugValue(value: SlugNameValue, label: string): number { const packed = match(value) .with(P.string, spelling => SlugName.from(spelling)) .with({ value: P.union(P.string, P.number) }, wrapped => - Number(wrapped.value) + packedValue(wrapped.value, label) ) - .otherwise(alreadyPacked => Number(alreadyPacked)) + .otherwise(alreadyPacked => packedValue(alreadyPacked, label)) - if (!Number.isSafeInteger(packed) || packed <= 0) { - throw new Error( - `${label} slug must be a non-zero safe integer or valid slug_name string.` - ) + if (packed <= 0) { + throw new Error(slugValueMessage(label)) } return packed } + +/** The failure message every rejected slug carrier shares. */ +function slugValueMessage(label: string): string { + return `${label} slug must be a non-zero safe integer or valid slug_name string.` +} + +/** + * An already-packed slug value, parsed strictly. `UInt64.from` accepts only a + * base-10 integer string or a safe integer, so `""`, `" 12 "`, `"0x10"`, + * `"1e3"` and `"12abc"` are rejected rather than coerced the way `Number()` or + * `parseInt()` would; `toNumber()` rejects anything past 53 bits. + */ +function packedValue(packed: string | number | bigint, label: string): number { + return Either.try(() => UInt64.from(packed).toNumber()) + .ifLeft(cause => { + throw new NestedError(slugValueMessage(label), { + cause, + context: { packed: String(packed) } + }) + }) + .getOrThrow() +} diff --git a/packages/sdk-core/src/contracts/sysio/chains/Structs.ts b/packages/sdk-core/src/contracts/sysio/chains/Structs.ts index 5f4c8929..3bd2fddf 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Structs.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Structs.ts @@ -52,7 +52,7 @@ export class ChainsSlugName implements ABISerializableObject { /** The canonical spelling — `""` for the zero sentinel. */ toString(): string { - return SlugName.toString(Number(this.value)) + return SlugName.toString(this.value.toNumber()) } toABI(encoder: ABIEncoder) { diff --git a/packages/sdk-core/src/contracts/sysio/reserv/Structs.ts b/packages/sdk-core/src/contracts/sysio/reserv/Structs.ts index 158e263a..071bc1d0 100644 --- a/packages/sdk-core/src/contracts/sysio/reserv/Structs.ts +++ b/packages/sdk-core/src/contracts/sysio/reserv/Structs.ts @@ -53,7 +53,7 @@ export class ReservSlugName implements ABISerializableObject { /** The canonical spelling — `""` for the zero sentinel. */ toString(): string { - return SlugName.toString(Number(this.value)) + return SlugName.toString(this.value.toNumber()) } toABI(encoder: ABIEncoder) { diff --git a/packages/sdk-core/tests/SlugName.test.ts b/packages/sdk-core/tests/SlugName.test.ts index a39275b6..b25c5063 100644 --- a/packages/sdk-core/tests/SlugName.test.ts +++ b/packages/sdk-core/tests/SlugName.test.ts @@ -168,5 +168,23 @@ describe("SlugName", () => { /must be a non-zero safe integer/ ) }) + + it.each(["", " 12 ", "0x10", "1e3", "12abc", "-5", "1.5"])( + "rejects the loosely numeric carrier %j instead of coercing it", + loose => { + expect(() => slugValue({ value: loose }, "Chain")).toThrow( + /must be a non-zero safe integer/ + ) + } + ) + + it("rejects a packed value past 53 bits rather than rounding it", () => { + expect(() => + slugValue({ value: "18446744073709551615" }, "Chain") + ).toThrow(/must be a non-zero safe integer/) + expect(() => slugValue(BigInt(2) ** BigInt(60), "Chain")).toThrow( + /must be a non-zero safe integer/ + ) + }) }) }) diff --git a/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts b/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts index ff118dda..a0a5a930 100644 --- a/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/chains/Slug.test.ts @@ -1,6 +1,6 @@ -import { contracts, SlugName } from "@wireio/sdk-core" +import { contracts, SlugName, UInt64 } from "@wireio/sdk-core" -const { chainSlugString, chainSlugValue } = contracts.sysio.chains +const { ChainsSlugName, chainSlugString, chainSlugValue } = contracts.sysio.chains describe("sysio.chains slug helpers", () => { test("round-trips friendly and packed chain codes", () => { @@ -32,4 +32,12 @@ describe("sysio.chains slug helpers", () => { "non-zero safe integer" ) }) + + test("ChainsSlugName renders its canonical spelling and refuses an unsafe value", () => { + expect(ChainsSlugName.from("ETHEREUM").toString()).toBe("ETHEREUM") + expect(ChainsSlugName.abiDefault().toString()).toBe("") + // Past 53 bits the packed value cannot be a canonical slug; converting it + // throws instead of silently rounding to a different code. + expect(() => ChainsSlugName.from(UInt64.from("18446744073709551615")).toString()).toThrow() + }) }) diff --git a/packages/sdk-core/tests/contracts/sysio/reserv/Slug.test.ts b/packages/sdk-core/tests/contracts/sysio/reserv/Slug.test.ts new file mode 100644 index 00000000..52472204 --- /dev/null +++ b/packages/sdk-core/tests/contracts/sysio/reserv/Slug.test.ts @@ -0,0 +1,13 @@ +import { contracts, UInt64 } from "@wireio/sdk-core" + +const { ReservSlugName } = contracts.sysio.reserv + +describe("sysio.reserv slug struct", () => { + test("ReservSlugName renders its canonical spelling and refuses an unsafe value", () => { + expect(ReservSlugName.from("PRIMARY").toString()).toBe("PRIMARY") + expect(ReservSlugName.abiDefault().toString()).toBe("") + // Past 53 bits the packed value cannot be a canonical slug; converting it + // throws instead of silently rounding to a different code. + expect(() => ReservSlugName.from(UInt64.from("18446744073709551615")).toString()).toThrow() + }) +})