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
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pnpm build # Build all packages via tsc -b
pnpm build:dev # Watch mode (incremental)
pnpm test # Build + jest (all packages)
pnpm format # Prettier on **/*.{ts,tsx,md}
pnpm lint # ESLint (eslint.config.mjs) — no-restricted-syntax bans + typescript-eslint
```

Per-package commands (run from package directory):
Expand Down Expand Up @@ -92,7 +93,7 @@ Assets (Rust runtime, Solidity templates, Handlebars templates) are embedded via

## Code Style & Approach

Enforced by Prettier (`.prettierrc.js`):
Enforced by Prettier (`.prettierrc.js`) + ESLint (`eslint.config.mjs`):
- **modern code** Use forEach, ... (spreads), map, filter, and reduce modern paradigms instead of for loops and other legacy style code
- **OPP & FP (functional programming)** is preferred over old-school if/else/switch and generally branching code.
- Use `Future` from `@3fv/prelude-ts` for async flows.
Expand All @@ -104,7 +105,7 @@ Enforced by Prettier (`.prettierrc.js`):
- No trailing commas
- 2-space indent, no tabs
- Arrow parens: avoid when possible
- No ESLint configured
- **ESLint is configured** (`eslint.config.mjs`, run via `pnpm lint` = `eslint .`) — a flat config **copied from `wire-tools-ts`** that encodes the same `no-restricted-syntax` bans (`BanSwitch`, `BanInlineIife`, `BanNullUnionReturn`, `BanInlineTypeLiteral`, `BanPickParameter`, `BanStringLiteralUnion`, `BanAsOptionAwait`, `BanMemberCoalesceDeclarator`) on top of `typescript-eslint`'s recommended set. The per-selector grandfather debt-file lists are **ratchets**: touching a listed file pays down its debt and deletes its entry — never add one.

## Code Quality Invariants

Expand Down
74 changes: 70 additions & 4 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,41 @@ import eslint from "@eslint/js"
import tseslint from "typescript-eslint"
import prettier from "eslint-config-prettier"

// prefer-lodash-identity.md (author law, 2026-07-21): an identity arrow `x => x`
// IS lodash `identity` — import { identity } from "lodash". A custom rule because
// no-restricted-syntax (esquery) cannot assert body-name === param-name.
const wireLocalPlugin = {
meta: { name: "wire-local" },
rules: {
"no-identity-arrow": {
meta: {
type: "problem",
docs: { description: "Use lodash `identity` instead of an `x => x` arrow." },
messages: {
useLodashIdentity:
"Use `identity` from lodash instead of an `x => x` arrow (prefer-lodash-identity.md)."
},
schema: []
},
create(context) {
return {
ArrowFunctionExpression(node) {
const [param] = node.params
if (
node.params.length === 1 &&
param.type === "Identifier" &&
node.body.type === "Identifier" &&
node.body.name === param.name
) {
context.report({ node, messageId: "useLodashIdentity" })
}
}
}
}
}
}
}

// STYLE.md "match() over switch always".
const BanSwitch = {
selector: "SwitchStatement",
Expand Down Expand Up @@ -98,6 +133,29 @@ const BanMemberCoalesceDeclarator = {
"Destructure with a default — `const { member: local = Default } = obj` — not `const local = obj.member ?? Default` (STYLE.md 'Destructuring over member-coalesce')."
}

// nested-error-preserve-cause.md (author law, 2026-07-21): NEVER rewrite a caught
// error into a bare Error that restrings it — the root cause + its stack + message
// are SWALLOWED. Wrap it: NestedError(message, { cause, context }). (a) the restring
// tell — a caught error's `.message`/`.stack` (or `toMessage(err)`) interpolated
// into a new Error.
const BanErrorMessageRestring = {
selector:
"NewExpression[callee.name=/^(Error|TypeError|RangeError|EvalError|SyntaxError|URIError)$/] :matches(CallExpression[callee.name='toMessage'], MemberExpression[property.name=/^(message|stack)$/][object.name=/^(err|error|e|ex|cause|reason)$/])",
message:
"Don't restring a caught error into a bare Error — use NestedError(message, { cause, context }) so the root cause + its stack survive (nested-error-preserve-cause.md)."
}
// (b) a bare (no-cause) native Error constructed inside an error handler that
// RECEIVES the error (Either.mapLeft/ifLeft/recoverWith, Promise.catch,
// Future.onFailure) — it discards the very error being handled. Throw a
// NestedError with { cause }. NOT Option's orElse/ifNone — those handle ABSENCE
// (a None is not an error), so a fresh Error there has no cause to preserve.
const BanBareErrorInHandler = {
selector:
"CallExpression[callee.property.name=/^(mapLeft|ifLeft|catch|recoverWith|onFailure)$/] NewExpression[callee.name=/^(Error|TypeError|RangeError|EvalError|SyntaxError|URIError)$/][arguments.length<2]",
message:
"A bare Error in an error handler (mapLeft/ifLeft/catch/recoverWith/onFailure) SWALLOWS the handled error — throw a NestedError with { cause } (nested-error-preserve-cause.md)."
}

// Pre-existing debt, grandfathered per file per ban — RATCHETS: touch a listed
// file → pay its debt → DELETE its entry. Never add an entry. Computed from
// the 2026-07-18 baseline lint run.
Expand Down Expand Up @@ -325,7 +383,9 @@ const AllBans = [
BanPickParameter,
BanStringLiteralUnion,
BanAsOptionAwait,
BanMemberCoalesceDeclarator
BanMemberCoalesceDeclarator,
BanErrorMessageRestring,
BanBareErrorInHandler
]
const DebtListsBySelector = [
[BanSwitch.selector, SwitchDebtFiles],
Expand Down Expand Up @@ -384,11 +444,15 @@ export default tseslint.config(
...tseslint.configs.recommended,
{
files: ["**/*.ts", "**/*.tsx"],
plugins: { "wire-local": wireLocalPlugin },
rules: {
// use-logging-framework.md: console is banned; the framework writes
// through (jest buffers console.*). Carve-outs below.
"no-console": "error",

// prefer-lodash-identity.md: an `x => x` arrow IS lodash `identity`.
"wire-local/no-identity-arrow": "error",

"no-restricted-syntax": ["error", ...AllBans],

// standard-names-not-invented.md: get-or-throw helpers are assert*,
Expand Down Expand Up @@ -432,9 +496,11 @@ export default tseslint.config(
// semantic structure — `interface FooConfig extends Required<FooOptions>
// {}` (STYLE.md three-layer options) and `{}` generic defaults.
"@typescript-eslint/no-empty-object-type": "off",
// `{ cause }` chaining is NOT a codified house law and enforcing it
// would force edits to untouched files. Off deliberately, not forgotten.
"preserve-caught-error": "off",
// nested-error-preserve-cause.md (author law, 2026-07-21): a catch clause
// that rethrows a bare Error SWALLOWS the original (its message + stack).
// Preserve it as `{ cause }` — pair with NestedError for the
// { cause, context } form.
"preserve-caught-error": "error",
"prefer-const": "error",
"no-var": "error"
}
Expand Down
11 changes: 6 additions & 5 deletions packages/sdk-core/src/api/Client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { NestedError } from "@wireio/shared"

import { APIProvider, FetchProvider, FetchProviderOptions } from "./Provider.js"
import {
ABISerializableConstructor,
Expand Down Expand Up @@ -258,7 +260,7 @@ export class APIClient {
const msg = details
.map((d: any) => d.message.replace(/Error:/g, "").trim())
.join(", ")
throw new Error(msg)
throw new NestedError(msg, { cause: e })
} else throw e
}
}
Expand Down Expand Up @@ -294,10 +296,9 @@ export class APIClient {
.trim()
)
.join(", ")
throw new Error(msg)
throw new NestedError(msg, { cause: e })
} else {
const msg = e.message.replace(/Error:/g, "") || e
throw new Error(msg)
throw new NestedError("pushTransaction failed", { cause: e })
}
}
}
Expand Down Expand Up @@ -325,7 +326,7 @@ export class APIClient {

const { msgBytes } = transaction.signingDigest(info.chain_id, keyType)
const sigBytes = await this.signer.sign(msgBytes).catch(err => {
throw new Error(err)
throw new NestedError("transaction signing failed", { cause: err })
})
const signature = Signature.fromRaw(sigBytes, keyType)
return SignedTransaction.from({ ...transaction, signatures: [signature] })
Expand Down
4 changes: 2 additions & 2 deletions packages/sdk-core/src/contracts/sysio/authex/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export class AuthexClient {
async getLink(
account: NameType,
chainKind: SysioContracts.SysioAuthexChainkind
): Promise<SysioContracts.SysioAuthexLinksSType | null> {
): Promise<SysioContracts.SysioAuthexLinksSType> {
const links = await this.getLinks(account)
return (
links.find(row => authExChainKindValue(row.chain_kind) === chainKind) ||
Expand All @@ -221,7 +221,7 @@ export class AuthexClient {
/** Reads the first link matching an external public key using the `bypubkey` index. */
async getLinkByPublicKey(
publicKey: PublicKeyType
): Promise<SysioContracts.SysioAuthexLinksSType | null> {
): Promise<SysioContracts.SysioAuthexLinksSType> {
const key = PublicKey.from(publicKey),
hash = publicKeyHash(key),
result = await this.contractClient.tables.links.rows<string>({
Expand Down
12 changes: 8 additions & 4 deletions packages/sdk-core/src/contracts/sysio/authex/Signing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,16 @@ export function createLinkSignatureFromRaw(
return new Signature(KeyType.ED, Bytes.from(signature96))
}

/** Options for {@link signCreateLink} — the create-link inputs plus the external signer. */
export interface SignCreateLinkOptions
extends Omit<PrepareCreateLinkOptions, "publicKey"> {
signer: SignerProvider
publicKey?: PublicKeyType
}

/** Signs the create-link proof with the supplied external-wallet signer. */
export async function signCreateLink(
options: Omit<PrepareCreateLinkOptions, "publicKey"> & {
signer: SignerProvider
publicKey?: PublicKeyType
}
options: SignCreateLinkOptions
): Promise<SignedCreateLinkProof> {
const prepared = prepareCreateLink({
account: options.account,
Expand Down
7 changes: 6 additions & 1 deletion packages/sdk-core/src/contracts/sysio/reserv/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ import { descriptor as reservDescriptor } from "./Descriptor.js"
import { reserveSlugData } 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. */
interface Stringifiable {
toString(): string
}

function amountString(
value: number | string | bigint | { toString(): string }
value: number | string | bigint | Stringifiable
): string {
return value.toString()
}
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-core/src/contracts/sysio/reserv/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export class ReserveClient {
}

/** Reads one reserve by its three-part identity, or null when absent. */
async getReserve(identity: ReserveIdentity): Promise<ReserveRecord | null> {
async getReserve(identity: ReserveIdentity): Promise<ReserveRecord> {
const reserveCode = reserveSlugValue(identity.reserveCode),
rows = await this.listReserveRows({
chainCode: identity.chainCode,
Expand Down
6 changes: 4 additions & 2 deletions packages/sdk-core/src/serializer/Builtins.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { identity } from "lodash"

import { ABIDecoder } from "./Decoder.js"
import { ABIEncoder } from "./Encoder.js"
import { ABIField, ABISerializableConstructor } from "./Serializable.js"
Expand Down Expand Up @@ -35,7 +37,7 @@ const StringType = {
fromABI: (decoder: ABIDecoder) => {
return decoder.readString()
},
from: (string: string): string => string,
from: identity<string>,
toABI: (string: string, encoder: ABIEncoder) => {
encoder.writeString(string)
}
Expand All @@ -47,7 +49,7 @@ const BoolType = {
fromABI: (decoder: ABIDecoder) => {
return decoder.readByte() !== BOOL_FALSE_BYTE
},
from: (value: boolean): boolean => value,
from: identity<boolean>,
toABI: (value: boolean, encoder: ABIEncoder) => {
encoder.writeByte(value === true ? BOOL_TRUE_BYTE : BOOL_FALSE_BYTE)
}
Expand Down
9 changes: 6 additions & 3 deletions packages/sdk-core/src/serializer/Decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* Antelope/SYSIO ABI Decoder
*/

import { NestedError } from "@wireio/shared"

import { ABI, ABIDef } from "../chain/Abi.js"
import { Bytes, BytesType } from "../chain/Bytes.js"
import { Variant } from "../chain/Variant.js"
Expand Down Expand Up @@ -128,9 +130,10 @@ export function abiDecode(
abi = synthesized.abi
customTypes.push(...synthesized.types)
} catch (error: any) {
throw Error(
`Unable to synthesize ABI for: ${typeName} (${error.message}). ` +
"To decode non-class types you need to pass the ABI definition manually."
throw new NestedError(
`Unable to synthesize ABI for: ${typeName}. ` +
"To decode non-class types you need to pass the ABI definition manually.",
{ cause: error }
)
}
}
Expand Down
Loading
Loading