Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 103 additions & 4 deletions packages/sdk-core/src/SlugName.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
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_]+$/

/**
* 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
Expand All @@ -12,7 +28,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 {
/**
Expand All @@ -23,8 +46,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) {
Expand All @@ -33,9 +58,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)
Expand Down Expand Up @@ -95,3 +123,74 @@ 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<Traits>, (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 =>
packedValue(wrapped.value, label)
)
.otherwise(alreadyPacked => packedValue(alreadyPacked, label))

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()
}
8 changes: 4 additions & 4 deletions packages/sdk-core/src/contracts/sysio/chains/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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. */
Expand All @@ -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. */
Expand Down
8 changes: 2 additions & 6 deletions packages/sdk-core/src/contracts/sysio/chains/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -156,7 +152,7 @@ export class ChainsClient {
async getChain(code: ChainSlugName): Promise<ChainRecord> {
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
}
Expand Down
30 changes: 6 additions & 24 deletions packages/sdk-core/src/contracts/sysio/chains/Slug.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,13 @@
import { SlugName } from "../../../SlugName.js"
import type * as SysioContracts from "../../../types/SysioContractTypes.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. */
/**
* 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 numericString = typeof value === "string" && /^[0-9]+$/.test(value),
packed = numericString
? Number(value)
: typeof value === "string"
? SlugName.from(value)
: Number(value)

if (!Number.isSafeInteger(packed) || packed <= 0) {
throw new Error(
"Chain slug must be a non-zero safe integer or valid slug_name string."
)
}

return packed
}

/** Converts a friendly chain slug to generated `slug_name` action data. */
export function chainSlugData(
value: ChainSlugName
): SysioContracts.SysioChainsSlugNameType {
return { value: chainSlugValue(value) }
return slugValue(value, "Chain")
}

/** Returns the display form of a packed chain slug. */
Expand Down
67 changes: 62 additions & 5 deletions packages/sdk-core/src/contracts/sysio/chains/Structs.ts
Original file line number Diff line number Diff line change
@@ -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(this.value.toNumber())
}

toABI(encoder: ABIEncoder) {
this.value.toABI(encoder)
}

/** @internal */
toJSON() {
return this.toString()
}
}

/**
Expand Down
5 changes: 3 additions & 2 deletions packages/sdk-core/src/contracts/sysio/chains/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 10 additions & 10 deletions packages/sdk-core/src/contracts/sysio/reserv/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
}

Expand Down
Loading
Loading