@zkp2p/cash 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -54,6 +54,9 @@ const relayCaps = await cash.capabilities({ includeRelaySources: true });
54
54
  // 2. Estimate - idempotent, cacheable, no side effects. Includes rolling ETA.
55
55
  const est = await cash.estimate({ amount: usdc(500), currency: 'EUR' });
56
56
 
57
+ // Optional: raw demand + speed evidence per offered platform:currency pair.
58
+ const stats = await cash.fillStats();
59
+
57
60
  // 3. Execute.
58
61
  const { depositId } = await cash.cashout(
59
62
  {
@@ -101,19 +104,28 @@ console.log(routed.source?.transactions?.origin, routed.source?.transactions?.de
101
104
  binding rate resolves at the oracle when a buyer fills. Do not display or
102
105
  log it as a locked price.
103
106
  - **Do not invent an ETA.** Use `estimate().eta`: `{ seconds, label }` backed
104
- by rolling 30-day indexer data from zero-spread (`spreadBps: 0`) market-rate
105
- deposits in the same payout corridor, measured from deposit creation to first
106
- fill. Use `order.explain()` for live order state.
107
+ by the same rolling 30-day, intent-attributed pair sample as `fillStats()`,
108
+ measured from deposit creation to first fill. Use `order.explain()` for live
109
+ order state.
107
110
  - **Do not hardcode Relay source assets.** Use Relay SDK-backed EVM
108
111
  `capabilities({ includeRelaySources: true })` and `cashout({ source, ... })`.
109
112
  Destination is always Base USDC. Non-Base source chains require
110
113
  `sourceSigner`. Use `EXACT_INPUT` in high-level cash-out flows so `amount`
111
114
  remains source-token base units. `source.amount` is Relay's guaranteed
112
115
  minimum output and the exact Base USDC deposit amount.
116
+ - **Use a nonce-managed source signer for routed cashouts.** Relay routes
117
+ with more than one source-chain transaction (approve, then route) are
118
+ refused preflight with `SOURCE_NONCE_MANAGER_REQUIRED` on plain local
119
+ accounts - create the source signer with
120
+ `privateKeyToAccount(pk, { nonceManager })`. Browser (`json-rpc`) wallets
121
+ are unaffected.
113
122
  - **Persist source evidence.** A routed result carries `requestId`, flat
114
123
  `txHashes`, and chain-aware `transactions.origin` / `.destination` arrays.
115
124
  - **Never repeat a completed or uncertain route.** On `SOURCE_EXECUTION_FAILED`,
116
- inspect its Relay request and transaction recovery evidence. On
125
+ inspect its Relay request and transaction recovery evidence. A failed
126
+ approval-only route can sit in `relayStatus` `waiting` indefinitely - decide
127
+ from the recovery payload and origin transactions, never by waiting for a
128
+ terminal Relay status. On
117
129
  `SOURCE_ROUTE_COMPLETED_CASHOUT_FAILED`, retry without `source` using
118
130
  `BigInt(err.recovery.amount)`. On `SOURCE_CASHOUT_SUBMISSION_UNKNOWN`, inspect
119
131
  Base wallet activity and `orders(err.recovery.depositor)`. On
@@ -128,7 +140,8 @@ console.log(routed.source?.transactions?.origin, routed.source?.transactions?.de
128
140
  deposit. The tx receipt you hold is the truth. Retry; `watch()` absorbs
129
141
  this automatically.
130
142
  - **Unwind with `withdraw()` only.** It is state-aware (prunes expired
131
- intents first). Do not call escrow functions directly.
143
+ intents first). Do not call escrow functions directly. Partial withdrawals
144
+ increment `returnedAmount`; `totalAmount` is historical and never shrinks.
132
145
  - **Read fills as receipts.** `fiatOwed` is the buyer's obligation at the
133
146
  locked rate; after the proof, `fiatPaid`/`paymentId`/`releasedAmount` are
134
147
  the verified outcome. Reconcile against those, not your own math.
@@ -161,6 +174,7 @@ Every `CashError` carries `code`, `retryable`, `remediation`. Behavior:
161
174
  | `SOURCE_RECIPIENT_MISMATCH` | no | Route Base USDC to the cashout depositor |
162
175
  | `SOURCE_CAPABILITIES_FAILED` | yes | Retry discovery or fall back to Base USDC |
163
176
  | `SOURCE_QUOTE_FAILED` | yes | Refresh capabilities and request a new canonical Base-USDC quote |
177
+ | `SOURCE_NONCE_MANAGER_REQUIRED` | no | Preflight; recreate the source signer with viem's `nonceManager`, then quote again |
164
178
  | `SOURCE_EXECUTION_FAILED` | no | Inspect source transactions and Relay status before any retry |
165
179
  | `SOURCE_STATUS_FAILED` | yes | Retry only the status read |
166
180
  | `SOURCE_ROUTE_COMPLETED_CASHOUT_FAILED` | no | Do not route again; retry Base-only with `recovery.amount` |
package/README.md CHANGED
@@ -76,6 +76,7 @@ console.log(source?.transactions?.origin, source?.transactions?.destination);
76
76
  | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
77
77
  | `capabilities()` | Sync discovery: Base USDC destination/default source, platforms × currencies × payee hints × amount bounds |
78
78
  | `capabilities({ includeRelaySources: true })` | Async discovery: adds live Relay SDK EVM source chains/tokens |
79
+ | `fillStats()` | Raw 30-day fill counts and median first-fill time per `platform:currency` pair |
79
80
  | `quoteSource(input)` / `executeSourceQuote(quote, { signer })` | Relay SDK EVM source routing into Base USDC before cashout |
80
81
  | `relayStatus(requestId)` | Relay request status from the Relay SDK request path |
81
82
  | `estimate({ amount, currency })` | Base USDC oracle estimate plus simple recent-fill ETA |
@@ -97,6 +98,11 @@ must execute and confirm its Relay route before preparing the Base-USDC
97
98
  cashout. Every Peer Cash transaction, including approves, carries ERC-8021
98
99
  attribution: `peer-cash` first, your own `referrer` code(s) after it.
99
100
 
101
+ `capabilities()` presents Zelle as one platform. A cashout with
102
+ `receive.platform: 'zelle'` automatically attaches the generic Zelle method
103
+ and its Chase, Bank of America, and Citi buyer routes to the deposit; the payee
104
+ handle and public API remain bank-agnostic.
105
+
100
106
  The default/minimal flow is unchanged: pass Base USDC base units to
101
107
  `estimate()` and `cashout()`. For any other source asset, pass `source` to
102
108
  `cashout()` with a source-chain signer. The SDK settles the Base allowance,
@@ -107,6 +113,13 @@ The destination is always canonical Base USDC
107
113
  discovered and quoted by `@relayprotocol/relay-sdk`, not a static token
108
114
  allowlist.
109
115
 
116
+ Routes that submit more than one source-chain transaction (approve, then
117
+ route) require a nonce-managed source signer -
118
+ `privateKeyToAccount(pk, { nonceManager })` from viem. Without one the SDK
119
+ refuses the route preflight with `SOURCE_NONCE_MANAGER_REQUIRED` instead of
120
+ letting the route transaction reuse the approval's nonce and revert
121
+ mid-route. Browser wallets are unaffected.
122
+
110
123
  `capabilities()` tells you which platforms need a verified identity for a new
111
124
  payee registration (`requiresIdentityAttestation` - Wise and PayPal today).
112
125
  An already-registered Wise or PayPal handle can be reused with bare payee data.
@@ -120,6 +133,10 @@ they are available. A source-routed result includes both a flat
120
133
  `source.txHashes` list and chain-aware `source.transactions.origin` /
121
134
  `.destination` entries.
122
135
 
136
+ - `SOURCE_EXECUTION_FAILED` where only the approval landed: the Relay request
137
+ can stay in `relayStatus` `waiting` indefinitely. Decide from the error's
138
+ recovery payload and origin transactions, never by waiting for a terminal
139
+ Relay status.
123
140
  - `SOURCE_ROUTE_COMPLETED_CASHOUT_FAILED`: Relay completed, but the Base
124
141
  cashout was not created. Do not route again. Retry a Base-USDC-only
125
142
  `cashout()` with `BigInt(error.recovery.amount)`.
@@ -161,9 +178,12 @@ awaiting-buyer ──────────► matched ───────
161
178
  fills. `estimate()` says "approximately"; nothing in this API pretends to
162
179
  lock a price.
163
180
  - **ETA is historical.** `estimate().eta` is just `{ seconds, label }`, backed
164
- by rolling 30-day indexer data from zero-spread (`spreadBps: 0`) market-rate
165
- deposits in the same payout corridor, measured from deposit creation to first
166
- fulfilled fill.
181
+ by the same rolling 30-day, intent-attributed pair sampler as `fillStats()`,
182
+ measured from deposit creation to the first fulfilled fill through the pair.
183
+ - **Availability thresholds belong to the consumer.** `fillStats()` returns raw
184
+ evidence. A recommended gate is `fills >= 10 && medianFillSeconds <= 48h`.
185
+ Fail open to the full `capabilities()` catalog when stats are unavailable or
186
+ the gate would empty the offered catalog.
167
187
  - **Everything is resumable.** An order is reconstructed from the chain by
168
188
  `depositId` alone. Close the tab, switch devices, crash the process - then
169
189
  call `order(depositId)`.
@@ -189,6 +189,12 @@ var errors = {
189
189
  },
190
190
  { cause }
191
191
  ),
192
+ sourceNonceManagerRequired: (transactionCount) => new CashError({
193
+ code: "SOURCE_NONCE_MANAGER_REQUIRED",
194
+ message: `This Relay route submits ${transactionCount} source-chain transactions, but the local signer has no nonce manager; the route transaction would reuse the approval nonce and revert.`,
195
+ retryable: false,
196
+ remediation: `Create the source signer with viem's nonce manager - privateKeyToAccount(pk, { nonceManager }) - then request a fresh quote and retry. No transaction was submitted.`
197
+ }),
192
198
  sourceExecutionFailed: (cause, evidence) => new CashError(
193
199
  {
194
200
  code: "SOURCE_EXECUTION_FAILED",
@@ -357,6 +357,14 @@ interface CashCapabilities {
357
357
  }
358
358
  declare function buildCapabilities(environment: RuntimeEnv): CashCapabilities;
359
359
 
360
+ interface CashPairFillStats {
361
+ /** Fulfilled intents through this pair inside the rolling 30-day window. */
362
+ fills: number;
363
+ /** Median deposit-to-first-fill seconds, sampled once per deposit for this pair. */
364
+ medianFillSeconds?: number;
365
+ }
366
+ /** Raw demand and speed evidence keyed by `basePlatform:currencyCode`. */
367
+ type CashFillStats = Record<string, CashPairFillStats>;
360
368
  interface CashFillEta {
361
369
  /** Simple headline ETA from recent deposits. Undefined when no recent sample exists. */
362
370
  seconds?: number;
@@ -581,6 +589,13 @@ interface CashClient {
581
589
  capabilities(options: {
582
590
  includeRelaySources: true;
583
591
  }): Promise<CashCapabilities>;
592
+ /**
593
+ * 0c - Raw 30-day demand and first-fill speed evidence keyed by
594
+ * `platform:currency`. A recommended consumer gate is `fills >= 10 &&
595
+ * medianFillSeconds <= 48h`; fail open to the full capability catalog when
596
+ * stats are unavailable or the gate would remove every pair.
597
+ */
598
+ fillStats(): Promise<CashFillStats>;
584
599
  /** Relay-only source discovery helper. */
585
600
  sourceCapabilities(): Promise<CashSourceCapabilities>;
586
601
  /** Quote any Relay-supported EVM source asset into Base USDC. */
@@ -640,4 +655,4 @@ interface CashClient {
640
655
  }
641
656
  declare function createCashClient(options: CashClientOptions): CashClient;
642
657
 
643
- export { type CashoutInput as A, type CashoutOptions as B, type CashPayoutInfo as C, type CuratorPayeeDataInput as D, type EstimateInput as E, RECOMMENDED_MIN_CASHOUT_AMOUNT as F, type RelayOptions as G, type RelayQuoteInput as H, type IntentStatus as I, type RelaySourceInput as J, type RelayTransaction as K, type WatchOptions as L, MIN_CASHOUT_AMOUNT as M, type WithdrawOptions as N, type OrdersOptions as O, type PrepareResult as P, buildCapabilities as Q, type RelayExecutionResult as R, type SignerOptions as S, type TopUpResult as T, createCashClient as U, type WithdrawResult as W, type IntentEntity as a, type CashBuyerProfile as b, type CashDepositInput as c, type CreateDepositParamsArg as d, type CashOrder as e, type CashFill as f, type CashCapabilities as g, type CashoutResult as h, type CashEstimate as i, type CashPreparedStep as j, type RelayQuote as k, type RelayStatus as l, type CashSourceCapabilities as m, CASH_ATTRIBUTION_CODE as n, type CashAsset as o, type CashChain as p, type CashClient as q, type CashClientOptions as r, type CashFillEta as s, type CashLeg as t, type CashNextAction as u, type CashOrderState as v, type CashPayout as w, type CashPayoutPricing as x, type CashPlatformCapability as y, type CashPreparedStepKind as z };
658
+ export { type CashPlatformCapability as A, type CashPreparedStepKind as B, type CashPayoutInfo as C, type CashoutInput as D, type CashoutOptions as E, type CuratorPayeeDataInput as F, type EstimateInput as G, RECOMMENDED_MIN_CASHOUT_AMOUNT as H, type IntentStatus as I, type RelayOptions as J, type RelayQuoteInput as K, type RelaySourceInput as L, MIN_CASHOUT_AMOUNT as M, type RelayTransaction as N, type OrdersOptions as O, type PrepareResult as P, type WatchOptions as Q, type RelayExecutionResult as R, type SignerOptions as S, type TopUpResult as T, type WithdrawOptions as U, buildCapabilities as V, type WithdrawResult as W, createCashClient as X, type IntentEntity as a, type CashBuyerProfile as b, type CashDepositInput as c, type CreateDepositParamsArg as d, type CashOrder as e, type CashFill as f, type CashCapabilities as g, type CashoutResult as h, type CashEstimate as i, type CashFillStats as j, type CashPreparedStep as k, type RelayQuote as l, type RelayStatus as m, type CashSourceCapabilities as n, CASH_ATTRIBUTION_CODE as o, type CashAsset as p, type CashChain as q, type CashClient as r, type CashClientOptions as s, type CashFillEta as t, type CashLeg as u, type CashNextAction as v, type CashOrderState as w, type CashPairFillStats as x, type CashPayout as y, type CashPayoutPricing as z };
@@ -357,6 +357,14 @@ interface CashCapabilities {
357
357
  }
358
358
  declare function buildCapabilities(environment: RuntimeEnv): CashCapabilities;
359
359
 
360
+ interface CashPairFillStats {
361
+ /** Fulfilled intents through this pair inside the rolling 30-day window. */
362
+ fills: number;
363
+ /** Median deposit-to-first-fill seconds, sampled once per deposit for this pair. */
364
+ medianFillSeconds?: number;
365
+ }
366
+ /** Raw demand and speed evidence keyed by `basePlatform:currencyCode`. */
367
+ type CashFillStats = Record<string, CashPairFillStats>;
360
368
  interface CashFillEta {
361
369
  /** Simple headline ETA from recent deposits. Undefined when no recent sample exists. */
362
370
  seconds?: number;
@@ -581,6 +589,13 @@ interface CashClient {
581
589
  capabilities(options: {
582
590
  includeRelaySources: true;
583
591
  }): Promise<CashCapabilities>;
592
+ /**
593
+ * 0c - Raw 30-day demand and first-fill speed evidence keyed by
594
+ * `platform:currency`. A recommended consumer gate is `fills >= 10 &&
595
+ * medianFillSeconds <= 48h`; fail open to the full capability catalog when
596
+ * stats are unavailable or the gate would remove every pair.
597
+ */
598
+ fillStats(): Promise<CashFillStats>;
584
599
  /** Relay-only source discovery helper. */
585
600
  sourceCapabilities(): Promise<CashSourceCapabilities>;
586
601
  /** Quote any Relay-supported EVM source asset into Base USDC. */
@@ -640,4 +655,4 @@ interface CashClient {
640
655
  }
641
656
  declare function createCashClient(options: CashClientOptions): CashClient;
642
657
 
643
- export { type CashoutInput as A, type CashoutOptions as B, type CashPayoutInfo as C, type CuratorPayeeDataInput as D, type EstimateInput as E, RECOMMENDED_MIN_CASHOUT_AMOUNT as F, type RelayOptions as G, type RelayQuoteInput as H, type IntentStatus as I, type RelaySourceInput as J, type RelayTransaction as K, type WatchOptions as L, MIN_CASHOUT_AMOUNT as M, type WithdrawOptions as N, type OrdersOptions as O, type PrepareResult as P, buildCapabilities as Q, type RelayExecutionResult as R, type SignerOptions as S, type TopUpResult as T, createCashClient as U, type WithdrawResult as W, type IntentEntity as a, type CashBuyerProfile as b, type CashDepositInput as c, type CreateDepositParamsArg as d, type CashOrder as e, type CashFill as f, type CashCapabilities as g, type CashoutResult as h, type CashEstimate as i, type CashPreparedStep as j, type RelayQuote as k, type RelayStatus as l, type CashSourceCapabilities as m, CASH_ATTRIBUTION_CODE as n, type CashAsset as o, type CashChain as p, type CashClient as q, type CashClientOptions as r, type CashFillEta as s, type CashLeg as t, type CashNextAction as u, type CashOrderState as v, type CashPayout as w, type CashPayoutPricing as x, type CashPlatformCapability as y, type CashPreparedStepKind as z };
658
+ export { type CashPlatformCapability as A, type CashPreparedStepKind as B, type CashPayoutInfo as C, type CashoutInput as D, type CashoutOptions as E, type CuratorPayeeDataInput as F, type EstimateInput as G, RECOMMENDED_MIN_CASHOUT_AMOUNT as H, type IntentStatus as I, type RelayOptions as J, type RelayQuoteInput as K, type RelaySourceInput as L, MIN_CASHOUT_AMOUNT as M, type RelayTransaction as N, type OrdersOptions as O, type PrepareResult as P, type WatchOptions as Q, type RelayExecutionResult as R, type SignerOptions as S, type TopUpResult as T, type WithdrawOptions as U, buildCapabilities as V, type WithdrawResult as W, createCashClient as X, type IntentEntity as a, type CashBuyerProfile as b, type CashDepositInput as c, type CreateDepositParamsArg as d, type CashOrder as e, type CashFill as f, type CashCapabilities as g, type CashoutResult as h, type CashEstimate as i, type CashFillStats as j, type CashPreparedStep as k, type RelayQuote as l, type RelayStatus as m, type CashSourceCapabilities as n, CASH_ATTRIBUTION_CODE as o, type CashAsset as p, type CashChain as q, type CashClient as r, type CashClientOptions as s, type CashFillEta as t, type CashLeg as u, type CashNextAction as v, type CashOrderState as w, type CashPairFillStats as x, type CashPayout as y, type CashPayoutPricing as z };
package/dist/index.cjs CHANGED
@@ -403,6 +403,26 @@ function parseCompositeDepositId(compositeId) {
403
403
  const onchainDepositId = BigInt(rawDepositId);
404
404
  return { escrowAddress: canonicalEscrowAddress, onchainDepositId };
405
405
  }
406
+
407
+ // src/client/platformGroups.ts
408
+ var PLATFORM_METHOD_GROUPS = {
409
+ zelle: ["zelle", "zelle-chase", "zelle-bofa", "zelle-citi"]
410
+ };
411
+ var METHOD_TO_BASE_PLATFORM = new Map(
412
+ Object.entries(PLATFORM_METHOD_GROUPS).flatMap(
413
+ ([platform, methods]) => methods.map((method) => [method, platform])
414
+ )
415
+ );
416
+ function basePlatformForMethod(method) {
417
+ return METHOD_TO_BASE_PLATFORM.get(method) ?? method;
418
+ }
419
+ function paymentMethodsForPlatform(platform, catalog) {
420
+ const configured = PLATFORM_METHOD_GROUPS[platform];
421
+ const methods = configured ?? [platform];
422
+ return methods.filter((method) => catalog[method] !== void 0);
423
+ }
424
+
425
+ // src/client/capabilities.ts
406
426
  var MIN_CASHOUT_AMOUNT = 10000n;
407
427
  var RECOMMENDED_MIN_CASHOUT_AMOUNT = 1000000n;
408
428
  var PAYEE_HINTS = {
@@ -421,13 +441,20 @@ var PAYEE_HINTS = {
421
441
  var IDENTITY_ATTESTATION_PLATFORMS = /* @__PURE__ */ new Set(["wise", "paypal"]);
422
442
  function buildCapabilities(environment) {
423
443
  const catalog = sdk.getPaymentMethodsCatalog(BASE_CHAIN_ID, environment);
424
- const platforms = Object.entries(catalog).map(([platform, entry]) => {
444
+ const currenciesByPlatform = /* @__PURE__ */ new Map();
445
+ for (const [method, entry] of Object.entries(catalog)) {
446
+ const platform = basePlatformForMethod(method);
425
447
  const currencies2 = (entry.currencies ?? []).map((hash) => sdk.getCurrencyCodeFromHash(hash)).filter(
426
448
  (code) => code != null && isMarketRateSupported(code)
427
449
  );
450
+ const aggregate = currenciesByPlatform.get(platform) ?? /* @__PURE__ */ new Set();
451
+ for (const currency of currencies2) aggregate.add(currency);
452
+ currenciesByPlatform.set(platform, aggregate);
453
+ }
454
+ const platforms = [...currenciesByPlatform.entries()].map(([platform, currencies2]) => {
428
455
  return {
429
456
  platform,
430
- currencies: [...new Set(currencies2)],
457
+ currencies: [...currencies2].sort(),
431
458
  payeeHint: PAYEE_HINTS[platform] ?? "Your payment handle for this platform",
432
459
  requiresIdentityAttestation: IDENTITY_ATTESTATION_PLATFORMS.has(platform)
433
460
  };
@@ -623,6 +650,12 @@ var errors = {
623
650
  },
624
651
  { cause }
625
652
  ),
653
+ sourceNonceManagerRequired: (transactionCount) => new CashError({
654
+ code: "SOURCE_NONCE_MANAGER_REQUIRED",
655
+ message: `This Relay route submits ${transactionCount} source-chain transactions, but the local signer has no nonce manager; the route transaction would reuse the approval nonce and revert.`,
656
+ retryable: false,
657
+ remediation: `Create the source signer with viem's nonce manager - privateKeyToAccount(pk, { nonceManager }) - then request a fresh quote and retry. No transaction was submitted.`
658
+ }),
626
659
  sourceExecutionFailed: (cause, evidence) => new CashError(
627
660
  {
628
661
  code: "SOURCE_EXECUTION_FAILED",
@@ -803,11 +836,8 @@ function mapChainError(verb, err, context = {}) {
803
836
  }
804
837
  return errors.chainCallFailed(verb, err);
805
838
  }
806
- var ETA_WINDOW_DAYS = 30;
807
- var ETA_WINDOW_SECONDS = ETA_WINDOW_DAYS * 24 * 60 * 60;
808
- var ETA_PAGE_LIMIT = 250;
809
- var ETA_MAX_DEPOSIT_SCAN = 2e3;
810
- var FULFILLED = /* @__PURE__ */ new Set(["FULFILLED", "MANUALLY_RELEASED"]);
839
+ var FILL_STATS_WINDOW_SECONDS = 30 * 24 * 60 * 60;
840
+ var FILL_STATS_PAGE_LIMIT = 250;
811
841
  function toUnixSeconds2(value) {
812
842
  if (value === null || value === void 0 || value === "") return void 0;
813
843
  if (value instanceof Date) {
@@ -818,12 +848,22 @@ function toUnixSeconds2(value) {
818
848
  const parsed = Date.parse(value);
819
849
  if (Number.isFinite(parsed)) return Math.floor(parsed / 1e3);
820
850
  }
821
- const n = Number(value);
822
- return Number.isFinite(n) && n > 0 ? n : void 0;
851
+ const numeric = Number(value);
852
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : void 0;
853
+ }
854
+ function normalizeCurrencyCode(value) {
855
+ const raw = value?.trim();
856
+ if (!raw) return void 0;
857
+ if (!raw.toLowerCase().startsWith("0x")) return raw.toUpperCase();
858
+ try {
859
+ return sdk.getCurrencyCodeFromHash(raw)?.toUpperCase();
860
+ } catch {
861
+ return void 0;
862
+ }
823
863
  }
824
864
  function median(values) {
825
865
  if (values.length === 0) return void 0;
826
- const sorted = [...values].sort((a, b) => a - b);
866
+ const sorted = [...values].sort((left, right) => left - right);
827
867
  const mid = Math.floor(sorted.length / 2);
828
868
  return sorted.length % 2 === 1 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
829
869
  }
@@ -835,47 +875,97 @@ function etaLabel(seconds) {
835
875
  const hours = Math.max(1, Math.round(minutes / 60));
836
876
  return `Usually starts in about ${hours} hr`;
837
877
  }
838
- function matchesPayout(deposit, environment, platform, currency) {
839
- const payouts = derivePayouts(
840
- deposit.paymentMethods ?? [],
841
- deposit.currencies ?? [],
842
- sdk.getPaymentMethodsCatalog(BASE_CHAIN_ID, environment)
843
- );
844
- return payouts.some(
845
- (payout) => payout.pricing.marketRate && payout.pricing.spreadBps === 0 && (platform === void 0 || payout.platform === platform) && (currency === void 0 || payout.currency === currency)
846
- );
878
+ function computeFillStatsSample(deposits, nowSeconds, environment) {
879
+ const catalog = sdk.getPaymentMethodsCatalog(BASE_CHAIN_ID, environment);
880
+ const windowStart = nowSeconds - FILL_STATS_WINDOW_SECONDS;
881
+ const fillCounts = /* @__PURE__ */ new Map();
882
+ const latenciesByPair = /* @__PURE__ */ new Map();
883
+ const latenciesByCurrency = /* @__PURE__ */ new Map();
884
+ for (const deposit of deposits) {
885
+ const createdAt = toUnixSeconds2(deposit.createdAt ?? deposit.timestamp);
886
+ const firstFillByPair = /* @__PURE__ */ new Map();
887
+ const firstFillByCurrency = /* @__PURE__ */ new Map();
888
+ for (const intent of deposit.intents ?? []) {
889
+ const fulfilledAt = toUnixSeconds2(intent.fulfillTimestamp);
890
+ if (fulfilledAt === void 0 || fulfilledAt < windowStart) continue;
891
+ let method;
892
+ try {
893
+ method = intent.paymentMethodHash ? sdk.resolvePaymentMethodNameFromHash(intent.paymentMethodHash, catalog) : void 0;
894
+ } catch {
895
+ method = void 0;
896
+ }
897
+ const currency = normalizeCurrencyCode(intent.fiatCurrency);
898
+ if (!method || !currency) continue;
899
+ const pair = `${basePlatformForMethod(method)}:${currency}`;
900
+ fillCounts.set(pair, (fillCounts.get(pair) ?? 0) + 1);
901
+ if (createdAt === void 0 || createdAt < windowStart || fulfilledAt < createdAt) continue;
902
+ const previousPairFill = firstFillByPair.get(pair);
903
+ if (previousPairFill === void 0 || fulfilledAt < previousPairFill) {
904
+ firstFillByPair.set(pair, fulfilledAt);
905
+ }
906
+ const previousCurrencyFill = firstFillByCurrency.get(currency);
907
+ if (previousCurrencyFill === void 0 || fulfilledAt < previousCurrencyFill) {
908
+ firstFillByCurrency.set(currency, fulfilledAt);
909
+ }
910
+ }
911
+ if (createdAt === void 0) continue;
912
+ for (const [pair, firstFill] of firstFillByPair) {
913
+ const latencies = latenciesByPair.get(pair) ?? [];
914
+ latencies.push(firstFill - createdAt);
915
+ latenciesByPair.set(pair, latencies);
916
+ }
917
+ for (const [currency, firstFill] of firstFillByCurrency) {
918
+ const latencies = latenciesByCurrency.get(currency) ?? [];
919
+ latencies.push(firstFill - createdAt);
920
+ latenciesByCurrency.set(currency, latencies);
921
+ }
922
+ }
923
+ const stats = {};
924
+ for (const [pair, fills] of fillCounts) {
925
+ const medianFillSeconds = median(latenciesByPair.get(pair) ?? []);
926
+ stats[pair] = {
927
+ fills,
928
+ ...medianFillSeconds !== void 0 ? { medianFillSeconds } : {}
929
+ };
930
+ }
931
+ const medianFillSecondsByCurrency = /* @__PURE__ */ new Map();
932
+ for (const [currency, latencies] of latenciesByCurrency) {
933
+ const value = median(latencies);
934
+ if (value !== void 0) medianFillSecondsByCurrency.set(currency, value);
935
+ }
936
+ return { stats, medianFillSecondsByCurrency };
847
937
  }
848
- async function readFillEta(client, input) {
938
+ async function readFillStatsSample(client, environment) {
849
939
  const now = Math.floor(Date.now() / 1e3);
850
- const windowStart = now - ETA_WINDOW_SECONDS;
940
+ const windowStart = now - FILL_STATS_WINDOW_SECONDS;
851
941
  const deposits = [];
852
- for (let offset = 0; offset < ETA_MAX_DEPOSIT_SCAN; offset += ETA_PAGE_LIMIT) {
942
+ for (let offset = 0; ; offset += FILL_STATS_PAGE_LIMIT) {
853
943
  const page = await client.indexer.getDepositsWithRelations(
854
944
  { chainId: BASE_CHAIN_ID },
855
- { limit: ETA_PAGE_LIMIT, offset, orderBy: "timestamp", orderDirection: "desc" },
945
+ {
946
+ limit: FILL_STATS_PAGE_LIMIT,
947
+ offset,
948
+ orderBy: "updatedAt",
949
+ orderDirection: "desc"
950
+ },
856
951
  { includeIntents: true, intentStatuses: ["FULFILLED", "MANUALLY_RELEASED"] }
857
952
  );
858
953
  deposits.push(...page);
859
- if (page.length < ETA_PAGE_LIMIT) break;
860
- const oldestCreatedAt = Math.min(
861
- ...page.map((deposit) => toUnixSeconds2(deposit.createdAt ?? deposit.timestamp) ?? Infinity)
954
+ if (page.length < FILL_STATS_PAGE_LIMIT) break;
955
+ const oldestUpdatedAt = Math.min(
956
+ ...page.map((deposit) => toUnixSeconds2(deposit.updatedAt) ?? Infinity)
862
957
  );
863
- if (oldestCreatedAt < windowStart) break;
958
+ if (oldestUpdatedAt < windowStart) break;
864
959
  }
865
- const firstFillLatencies = [];
866
- for (const deposit of deposits) {
867
- const createdAt = toUnixSeconds2(deposit.createdAt ?? deposit.timestamp);
868
- if (createdAt === void 0 || createdAt < windowStart) continue;
869
- if (!matchesPayout(deposit, input.environment, input.platform, input.currency)) continue;
870
- const fulfilled = (deposit.intents ?? []).filter((intent) => intent.status != null && FULFILLED.has(intent.status)).map((intent) => ({
871
- fulfilledAt: toUnixSeconds2(intent.fulfillTimestamp)
872
- })).filter(
873
- (intent) => intent.fulfilledAt !== void 0 && intent.fulfilledAt >= createdAt
874
- ).sort((a, b) => a.fulfilledAt - b.fulfilledAt);
875
- if (fulfilled.length === 0) continue;
876
- firstFillLatencies.push(fulfilled[0].fulfilledAt - createdAt);
877
- }
878
- const seconds = median(firstFillLatencies);
960
+ return computeFillStatsSample(deposits, now, environment);
961
+ }
962
+ async function readFillStats(client, environment) {
963
+ return (await readFillStatsSample(client, environment)).stats;
964
+ }
965
+ async function readFillEta(client, input) {
966
+ const sample = await readFillStatsSample(client, input.environment);
967
+ const currency = input.currency.toUpperCase();
968
+ const seconds = input.platform ? sample.stats[`${basePlatformForMethod(input.platform)}:${currency}`]?.medianFillSeconds : sample.medianFillSecondsByCurrency.get(currency);
879
969
  return {
880
970
  ...seconds !== void 0 ? { seconds } : {},
881
971
  label: etaLabel(seconds)
@@ -1048,6 +1138,14 @@ async function assertRelayExecutionIdentity(quote, wallet, expectedRecipient) {
1048
1138
  throw new Error("Relay quote recipient does not match the expected Base recipient");
1049
1139
  }
1050
1140
  }
1141
+ function assertRelayNonceManagement(quote, wallet) {
1142
+ const account = wallet.account;
1143
+ if (!account || account.type !== "local" || account.nonceManager !== void 0) return;
1144
+ const transactionCount = quote.steps.flatMap(
1145
+ (step) => step.items.filter((item) => asString(asRecord(item.data).to) !== void 0)
1146
+ ).length;
1147
+ if (transactionCount > 1) throw errors.sourceNonceManagerRequired(transactionCount);
1148
+ }
1051
1149
  function isRelaySecretKey(key) {
1052
1150
  const normalized = key.toLowerCase();
1053
1151
  return normalized === "headers" || normalized === "apikey";
@@ -1249,6 +1347,7 @@ async function executeRelayQuote(quote, wallet, options = {}) {
1249
1347
  observedRequestId = quoteRequestId(rawQuote);
1250
1348
  assertCanonicalRelayDestination(rawQuote);
1251
1349
  await assertRelayExecutionIdentity(rawQuote, wallet, options.recipient);
1350
+ assertRelayNonceManagement(rawQuote, wallet);
1252
1351
  const client = relayClient(options.relay);
1253
1352
  const sourceChainId = quoteSourceChainId(rawQuote);
1254
1353
  if (sourceChainId !== void 0 && !(client.chains ?? []).some((chain) => chain.id === sourceChainId)) {
@@ -1534,6 +1633,7 @@ function createCashClient(options) {
1534
1633
  }
1535
1634
  function validatePayout(input) {
1536
1635
  const { receive } = input;
1636
+ const catalog = sdk.getPaymentMethodsCatalog(BASE_CHAIN_ID, environment);
1537
1637
  const platform = buildCapabilities(environment).platforms.find(
1538
1638
  (capability) => capability.platform === receive.platform
1539
1639
  );
@@ -1544,14 +1644,13 @@ function createCashClient(options) {
1544
1644
  if (!platform.currencies.includes(receive.currency)) {
1545
1645
  throw errors.unsupportedPlatformCurrency(receive.platform, receive.currency);
1546
1646
  }
1647
+ const paymentMethods = paymentMethodsForPlatform(receive.platform, catalog);
1547
1648
  return {
1548
- payouts: [
1549
- {
1550
- processorName: receive.platform,
1551
- currency: receive.currency,
1552
- payeeData: receive.payee
1553
- }
1554
- ]
1649
+ payouts: paymentMethods.map((processorName) => ({
1650
+ processorName,
1651
+ currency: receive.currency,
1652
+ payeeData: receive.payee
1653
+ }))
1555
1654
  };
1556
1655
  }
1557
1656
  function validateDepositInput(amount, input, payoutInput = validatePayout(input)) {
@@ -1829,6 +1928,13 @@ function createCashClient(options) {
1829
1928
  ...options.relay ? { relay: options.relay } : {}
1830
1929
  });
1831
1930
  },
1931
+ async fillStats() {
1932
+ try {
1933
+ return await readFillStats(readClient, environment);
1934
+ } catch (err) {
1935
+ throw errors.indexerUnavailable("fill stats", err);
1936
+ }
1937
+ },
1832
1938
  async cashout(input, opts) {
1833
1939
  const client = await signingClient("cashout", opts);
1834
1940
  const owner = opts.signer.account.address;
@@ -2428,6 +2534,11 @@ var cashEstimateJsonSchema = zod.z.object({
2428
2534
  label: zod.z.string()
2429
2535
  }).optional()
2430
2536
  });
2537
+ var cashPairFillStatsJsonSchema = zod.z.object({
2538
+ fills: zod.z.number().int().nonnegative(),
2539
+ medianFillSeconds: zod.z.number().int().nonnegative().optional()
2540
+ }).strict();
2541
+ var cashFillStatsJsonSchema = zod.z.record(zod.z.string(), cashPairFillStatsJsonSchema);
2431
2542
  var preparedTransactionJsonSchema = zod.z.object({
2432
2543
  to: zod.z.string(),
2433
2544
  data: zod.z.string(),
@@ -2559,6 +2670,7 @@ var CASH_ERROR_CODES = defineCashErrorCodes([
2559
2670
  "SOURCE_RECIPIENT_MISMATCH",
2560
2671
  "SOURCE_CAPABILITIES_FAILED",
2561
2672
  "SOURCE_QUOTE_FAILED",
2673
+ "SOURCE_NONCE_MANAGER_REQUIRED",
2562
2674
  "SOURCE_EXECUTION_FAILED",
2563
2675
  "SOURCE_STATUS_FAILED",
2564
2676
  "SOURCE_ROUTE_COMPLETED_CASHOUT_FAILED",
@@ -2731,6 +2843,21 @@ function estimateFromJson(json) {
2731
2843
  } : void 0
2732
2844
  });
2733
2845
  }
2846
+ function fillStatsToJson(stats) {
2847
+ return cashFillStatsJsonSchema.parse(stats);
2848
+ }
2849
+ function fillStatsFromJson(json) {
2850
+ const parsed = cashFillStatsJsonSchema.parse(json);
2851
+ return Object.fromEntries(
2852
+ Object.entries(parsed).map(([pair, stats]) => [
2853
+ pair,
2854
+ {
2855
+ fills: stats.fills,
2856
+ ...stats.medianFillSeconds !== void 0 ? { medianFillSeconds: stats.medianFillSeconds } : {}
2857
+ }
2858
+ ])
2859
+ );
2860
+ }
2734
2861
  function cashAssetFromJson(asset) {
2735
2862
  return {
2736
2863
  chainId: asset.chainId,
@@ -3039,9 +3166,11 @@ exports.cashErrorRecoveryJsonSchema = cashErrorRecoveryJsonSchema;
3039
3166
  exports.cashErrorToJson = cashErrorToJson;
3040
3167
  exports.cashEstimateJsonSchema = cashEstimateJsonSchema;
3041
3168
  exports.cashFillJsonSchema = cashFillJsonSchema;
3169
+ exports.cashFillStatsJsonSchema = cashFillStatsJsonSchema;
3042
3170
  exports.cashNextActionSchema = cashNextActionSchema;
3043
3171
  exports.cashOrderJsonSchema = cashOrderJsonSchema;
3044
3172
  exports.cashOrderStateSchema = cashOrderStateSchema;
3173
+ exports.cashPairFillStatsJsonSchema = cashPairFillStatsJsonSchema;
3045
3174
  exports.cashPayoutInfoJsonSchema = cashPayoutInfoJsonSchema;
3046
3175
  exports.cashPayoutPricingJsonSchema = cashPayoutPricingJsonSchema;
3047
3176
  exports.cashPreparedStepJsonSchema = cashPreparedStepJsonSchema;
@@ -3061,6 +3190,8 @@ exports.explainOrder = explainOrder;
3061
3190
  exports.fiatFromUsdc = fiatFromUsdc;
3062
3191
  exports.fiatToNumber = fiatToNumber;
3063
3192
  exports.fillFromJson = fillFromJson;
3193
+ exports.fillStatsFromJson = fillStatsFromJson;
3194
+ exports.fillStatsToJson = fillStatsToJson;
3064
3195
  exports.fillToJson = fillToJson;
3065
3196
  exports.formatUsdc = formatUsdc;
3066
3197
  exports.intentStatusSchema = intentStatusSchema;