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
6 changes: 5 additions & 1 deletion BUNDLE_SIZE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ Every budget is the measured baseline **+ 15% headroom**, rounded up.
| `./chains/ckb` | CJS (`require`) | 128.76 KB | 148.1 KB |
| `./vault` | ESM (`import *`) | 1.93 KB | 2.3 KB |
| `./vault` | CJS (`require`) | 2.07 KB | 2.4 KB |
| `./chains/stellar` | ESM (`import *`) | 26.48 KB | 30.5 KB |
| `./chains/stellar` | ESM (`import *`) | 26.48 KB | 31 KB |
| `./chains/stellar` | CJS (`require`) | 34.29 KB | 39.5 KB |

> Stellar note: the original 20 KB-per-format budget predated any actual
> measurement (the baseline had never been populated). The current Stellar
> bundle exceeds 20 KB, so its budget was re-baselined on 2026-08-29 using
> the same baseline + 15% rule as the other entries. Entry paths and options
> are unchanged.
>
> On 2026-10-16 the Stellar ESM budget was raised from 30.5 KB to 31 KB for the
> cancellation and timeout support in the one-shot Horizon helpers (issue #235),
> which brought that entry to 30.68 KB.

## Checking Sizes (CI gate)

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ All notable changes to the Wraith Protocol SDK will be documented in this file.

### Added

- **Cancellation and Timeouts for the Remaining Stellar Network Helpers** (issue #235): `findStrictReceivePath()`, `buildMultisigStealthWithdraw()` and `submitOfflineStellarTransaction()` accept `signal` and `timeouts`, like the Horizon and RPC clients.
- Defaults: 10 s for the headers and 30 s in total for path finding and the account lookup; no headers timeout and 60 s in total for transaction submission, since Horizon answers a submission only once the transaction is in a ledger.
- New `RPCAbortedError` (`WRAITH/NETWORK/RPC_ABORTED`), exported from the package root, with the signal's `reason` on `cause`. A non-2xx status is an `RPCRequestError`, a timeout an `RPCTimeoutError` and an invalid response a `HorizonResponseError`. See [`docs/chains/stellar-request-timeouts.md`](./docs/chains/stellar-request-timeouts.md).
- **Supported Runtime and Peer Dependency Matrix** (issue #209): `compat/matrix.json` is now the single source of truth for the runtimes the SDK supports — Node.js, Bun, evergreen browsers and React Native — and for the dependency ranges it accepts (`@stellar/stellar-sdk`, `@solana/web3.js`, `viem`).
- [`COMPAT.md`](./COMPAT.md) is generated from that file and documents the unsupported combinations alongside the exact failure message each one produces.
- `pnpm test:compat` validates the matrix against `package.json` and `COMPAT.md`, imports every entry point on the running runtime, repeats that against a simulated React Native global scope, bundles every entry point for `platform: browser`, imports every entry point from an install with the optional peers removed, and asserts the npm tarball contains every file the `exports` map points at.
Expand Down Expand Up @@ -50,6 +53,7 @@ All notable changes to the Wraith Protocol SDK will be documented in this file.

### Changed

- **Stellar Helper Errors Use the Taxonomy** (issue #235): for a non-2xx Horizon status, `buildMultisigStealthWithdraw()` throws `RPCRequestError` instead of a plain `Error`, and an account record without its sequence, thresholds or signers throws `HorizonResponseError`. `submitOfflineStellarTransaction()` reports the full `/transactions` URL on `RPCRequestError`, throws `HorizonResponseError` for a success body that is not a JSON object, and gives up after 60 s by default; pass `{ timeouts: { requestMs: 0 } }` for the old behaviour.
- **Stellar Horizon and RPC Clients Time Out by Default** (issue #202): an attempt now fails after 10 s without response headers or 30 s in total, then retries or fails over, instead of waiting indefinitely. Pass `timeouts: { connectMs: 0, requestMs: 0 }` for the old behaviour, or longer values for slow calls such as Horizon transaction submission.
- **Stellar Chain Module Cryptographic Audit Fixes**: Applied all findings from independent cryptographic audit (issue #55). Breaking changes:
- `scanAnnouncements()` now skips candidates with zero derived scalars (cryptographically required, probability ~1 in 2^255).
Expand Down
48 changes: 48 additions & 0 deletions docs/chains/stellar-request-timeouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,51 @@ try {
```

`cause` is not enumerable and is left out of `toJSON()`, the same as on the wallet errors.

## One-shot Horizon helpers

`findStrictReceivePath()`, `buildMultisigStealthWithdraw()` (when it loads the account from
`horizonUrl`) and `submitOfflineStellarTransaction()` each make one Horizon request, with no
retries. They take the same `timeouts` shape plus an `AbortSignal`.

| Helper | Options | Default `timeouts` |
| :------------------------------------------------------- | :----------------------------------- | :--------------------------------------- |
| `findStrictReceivePath(options)` | `options.signal`, `options.timeouts` | `{ connectMs: 10000, requestMs: 30000 }` |
| `buildMultisigStealthWithdraw(options)` | `options.signal`, `options.timeouts` | `{ connectMs: 10000, requestMs: 30000 }` |
| `submitOfflineStellarTransaction(xdr, network, options)` | `options.signal`, `options.timeouts` | `{ connectMs: 0, requestMs: 60000 }` |

Horizon answers a submission only once the transaction is in a ledger or its own wait runs out,
so a submission has no separate headers timeout by default.

```ts
import {
findStrictReceivePath,
submitOfflineStellarTransaction,
} from '@wraith-protocol/sdk/chains/stellar';

const controller = new AbortController();

const { sourceAmount, path } = await findStrictReceivePath({
sendAsset,
receiveAsset,
destAmount: '100',
signal: controller.signal,
timeouts: { requestMs: 5_000 },
});

const result = await submitOfflineStellarTransaction(signedXdr, 'stellar', {
signal: controller.signal,
});
```

Every failure maps onto the error taxonomy:

| Failure | Error |
| :--------------------------------------------------------------- | :------------------------------------------------------------------------------------------- |
| The signal is aborted, before or during the request | `RPCAbortedError` (`WRAITH/NETWORK/RPC_ABORTED`), with the signal's `reason` on `cause` |
| A timeout fires | `RPCTimeoutError` (`WRAITH/NETWORK/RPC_TIMEOUT`), with `attempt: 1` |
| Horizon answers with a non-2xx status | `RPCRequestError` (`WRAITH/NETWORK/RPC_REQUEST`), with the body on `context.responseText` |
| A success response is not JSON, is oversized or fails validation | `HorizonResponseError` (`WRAITH/NETWORK/HORIZON_RESPONSE`), with the failing part on `field` |

A submission that times out or is aborted may still be applied. Look the transaction up by its
hash before submitting it again.
2 changes: 2 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ WraithError (Abstract Base)
│ ├── HorizonResponseError
│ ├── RPCRetryExhaustedError
│ ├── RPCTimeoutError
│ ├── RPCAbortedError
│ └── RetentionExceededError
├── WraithContractError
│ ├── NameNotFoundError
Expand Down Expand Up @@ -85,6 +86,7 @@ Thrown when HTTP queries to Wraith APIs, Horizon/Soroban endpoints, Solana clust
| `HorizonResponseError` | `"WRAITH/NETWORK/HORIZON_RESPONSE"` | `url`, `field`, `reason` | Thrown when Horizon answers 2xx but the body is oversized, malformed, or does not match the request. |
| `RPCRetryExhaustedError` | `"WRAITH/NETWORK/RPC_RETRY_EXHAUSTED"` | `url`, `attempts`, `lastError` | Thrown when all query retry strategies have timed out or failed. |
| `RPCTimeoutError` | `"WRAITH/NETWORK/RPC_TIMEOUT"` | `url`, `endpoint`, `attempt`, `phase`, `timeoutMs` | Thrown when a Horizon/Soroban attempt exceeds its connect or request timeout. |
| `RPCAbortedError` | `"WRAITH/NETWORK/RPC_ABORTED"` | `url` | Thrown when the caller's `AbortSignal` cancels a Horizon request; the signal's reason is on `cause`. |
| `RetentionExceededError` | `"WRAITH/NETWORK/RETENTION_EXCEEDED"` | `limit`, `actual` | Thrown when querying historical logs beyond maximum retention boundaries. |

When the Horizon or Soroban RPC client gives up, `RPCRetryExhaustedError.cause` holds the last attempt's error, such as an `RPCTimeoutError`. See [Stellar Horizon and RPC request timeouts](./chains/stellar-request-timeouts.md).
Expand Down
4 changes: 3 additions & 1 deletion docs/offline-signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ console.log('Transaction hash:', result.hash);
console.log('Ledger:', result.ledger);
```

The function resolves the Horizon URL from the chain deployment config (`getDeployment('stellar')`) and POSTs the XDR to `/transactions`. On failure it throws an `RPCRequestError` with the Horizon response body.
The function resolves the Horizon URL from the chain deployment config (`getDeployment('stellar')`) and POSTs the XDR to `/transactions`. On failure it throws an `RPCRequestError` with the Horizon response body on `context.responseText`.

A third argument takes an `AbortSignal` and timeouts: `submitOfflineStellarTransaction(signedXdr, 'stellar', { signal, timeouts })`. By default there is no headers timeout and the request gives up after 60 s, because Horizon answers only once the transaction is in a ledger. An aborted or timed-out submission rejects with `RPCAbortedError` or `RPCTimeoutError` and may still be applied, so look the transaction up by its hash before submitting it again.

## Stealth Address Compatibility

Expand Down
31 changes: 17 additions & 14 deletions docs/stellar-path-payment.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,15 @@ const { sourceAmount, path } = await findStrictReceivePath(options);

#### Parameters

| Field | Type | Required | Description |
| -------------- | -------- | -------- | -------------------------------------------------- |
| `sendAsset` | `Asset` | ✅ | Asset the sender is spending |
| `receiveAsset` | `Asset` | ✅ | Asset the stealth address should receive |
| `destAmount` | `string` | ✅ | Exact amount of `receiveAsset` desired |
| `horizonUrl` | `string` | ❌ | Custom Horizon URL (default: deployment's Horizon) |
| `chain` | `string` | ❌ | Chain deployment key (default: `"stellar"`) |
| Field | Type | Required | Description |
| -------------- | ----------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `sendAsset` | `Asset` | ✅ | Asset the sender is spending |
| `receiveAsset` | `Asset` | ✅ | Asset the stealth address should receive |
| `destAmount` | `string` | ✅ | Exact amount of `receiveAsset` desired |
| `horizonUrl` | `string` | ❌ | Custom Horizon URL (default: deployment's Horizon) |
| `chain` | `string` | ❌ | Chain deployment key (default: `"stellar"`) |
| `signal` | `AbortSignal` | ❌ | Cancels the Horizon request; the call then rejects with `RPCAbortedError` |
| `timeouts` | `RequestTimeouts` | ❌ | Connect and request timeouts (default: the Horizon client's 10 s for the headers, 30 s in total) |

#### Return value

Expand All @@ -103,7 +105,7 @@ Horizon returns every path in one `_embedded.records` page; this endpoint is not

The record with the smallest `source_amount` is returned. If any record fails a check, the whole response is rejected with `HorizonResponseError`.

`destAmount` must itself be a positive amount with at most 7 decimals; anything else throws before Horizon is called. The request uses the same timeouts as the SDK's Horizon client (10 s for the response headers, 30 s in total) and throws `RPCTimeoutError` when one fires.
`destAmount` must itself be a positive amount with at most 7 decimals; anything else throws before Horizon is called. By default the request uses the same timeouts as the SDK's Horizon client (10 s for the response headers, 30 s in total); `timeouts` overrides either one, and `RPCTimeoutError` is thrown when one fires. Aborting `signal`, before or during the request, rejects with `RPCAbortedError`, with the signal's `reason` on `cause`.

---

Expand Down Expand Up @@ -284,12 +286,13 @@ The recipient can claim the balance once they detect the announcement and derive

`findStrictReceivePath` throws:

| Error | When |
| ---------------------- | -------------------------------------------------------------------------------------------------------------- |
| `RPCRequestError` | Horizon answers with a non-2xx status (`statusCode` and the start of the body are on the error). |
| `HorizonResponseError` | The response is too large, is not JSON, or a record fails validation. `error.field` names the rejected value. |
| `RPCTimeoutError` | Horizon sends no headers within 10 s, or the whole request takes longer than 30 s. |
| `Error` | Horizon returns no path for the asset pair and amount, or `destAmount` is invalid (thrown before the request). |
| Error | When |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `RPCRequestError` | Horizon answers with a non-2xx status (`statusCode` and the start of the body are on the error). |
| `HorizonResponseError` | The response is too large, is not JSON, or a record fails validation. `error.field` names the rejected value. |
| `RPCTimeoutError` | Horizon sends no headers within 10 s, or the whole request takes longer than 30 s (or the limits in `timeouts`). |
| `RPCAbortedError` | `signal` aborts before or during the request. The signal's `reason` is on `cause`. |
| `Error` | Horizon returns no path for the asset pair and amount, or `destAmount` is invalid (thrown before the request). |

### No payment path found

Expand Down
12 changes: 9 additions & 3 deletions etc/sdk-stellar.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ export interface BuildBatchSendTxResult {
// @public
export function buildMultisigStealthWithdraw(options: BuildMultisigStealthWithdrawOptions): Promise<Transaction>;

// @public (undocumented)
export interface BuildMultisigStealthWithdrawOptions {
// @public
export interface BuildMultisigStealthWithdrawOptions extends HorizonCallOptions {
account?: Pick<Horizon.ServerApi.AccountRecord, 'sequence' | 'thresholds' | 'signers'>;
destination: string;
fee?: string;
Expand Down Expand Up @@ -382,7 +382,7 @@ export function fetchAnnouncementsStream(chain?: string, sorobanUrlOrOpts?: stri
export function findStrictReceivePath(options: FindStrictReceivePathOptions): Promise<StrictReceivePathResult>;

// @public
export interface FindStrictReceivePathOptions {
export interface FindStrictReceivePathOptions extends HorizonCallOptions {
chain?: string;
destAmount: string;
horizonUrl?: string;
Expand Down Expand Up @@ -450,6 +450,12 @@ export type HexString = `0x${string}`;
// @public
export function hexToBytes(hex: string): Uint8Array;

// @public
export interface HorizonCallOptions {
signal?: AbortSignal;
timeouts?: RequestTimeouts;
}

// @public (undocumented)
export interface HorizonClient {
get<T = unknown>(path: string, overrides?: HorizonRequestOptions): Promise<T>;
Expand Down
13 changes: 13 additions & 0 deletions etc/sdk.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,19 @@ export class RetentionExceededError extends WraithNetworkError {
describe(): string;
}

// @public
export class RPCAbortedError extends WraithNetworkError {
constructor(url: string, options?: {
cause?: unknown;
});
// (undocumented)
readonly code = "WRAITH/NETWORK/RPC_ABORTED";
// (undocumented)
describe(): string;
// (undocumented)
readonly url: string;
}

// @public (undocumented)
export class RPCRequestError extends WraithNetworkError {
constructor(url: string, statusCode: number, responseText?: string);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
"name": "Stellar ESM (import *)",
"path": "dist/chains/stellar/index.js",
"import": "*",
"limit": "30.5 KB"
"limit": "31 KB"
},
{
"name": "Stellar CJS (require)",
Expand Down
1 change: 1 addition & 0 deletions src/chains/stellar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,4 @@ export type {
export { createRpcClient } from './rpc';
export type { RpcClient, RpcClientConfig, RpcEndpoint, RpcRequestOptions } from './rpc';
export type { RequestTimeouts } from './timeouts';
export type { HorizonCallOptions } from './request';
Loading
Loading