@zkp2p/cash 0.1.7 → 0.1.9

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,12 @@ 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
+ // Progressive UI: do not let indexer-backed history hold up the oracle rate.
58
+ const rateOnly = await cash.estimate({ amount: usdc(500), currency: 'EUR' }, { includeEta: false });
59
+
60
+ // Optional: raw demand + speed evidence per offered platform:currency pair.
61
+ const stats = await cash.fillStats();
62
+
57
63
  // 3. Execute.
58
64
  const { depositId } = await cash.cashout(
59
65
  {
@@ -101,9 +107,11 @@ console.log(routed.source?.transactions?.origin, routed.source?.transactions?.de
101
107
  binding rate resolves at the oracle when a buyer fills. Do not display or
102
108
  log it as a locked price.
103
109
  - **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.
110
+ by the same rolling 30-day, intent-attributed pair sample as `fillStats()`,
111
+ measured from deposit creation to first fill. Use `order.explain()` for live
112
+ order state. For progressive UIs, call `estimate(..., { includeEta: false })`
113
+ and load `fillStats()["platform:CURRENCY"]` separately. The SDK caches the
114
+ raw snapshot for 15 minutes, but never substitutes another pair's data.
107
115
  - **Do not hardcode Relay source assets.** Use Relay SDK-backed EVM
108
116
  `capabilities({ includeRelaySources: true })` and `cashout({ source, ... })`.
109
117
  Destination is always Base USDC. Non-Base source chains require
package/README.md CHANGED
@@ -34,6 +34,13 @@ const est = await cash.estimate({ amount: usdc(1000), currency: 'USD' });
34
34
  // { rate: 1, receiveAmount: 1000, kind: 'oracle-estimate', eta: { seconds, label } }
35
35
  // "≈", never a locked quote. Base USDC remains the default source.
36
36
 
37
+ // Progressive UI: render rate/receive first, then resolve the exact pair ETA.
38
+ const rateOnly = await cash.estimate(
39
+ { amount: usdc(1000), currency: 'USD' },
40
+ { includeEta: false },
41
+ );
42
+ const pairStats = (await cash.fillStats())['venmo:USD'];
43
+
37
44
  const { depositId } = await cash.cashout(
38
45
  {
39
46
  amount: usdc(1000),
@@ -76,9 +83,10 @@ console.log(source?.transactions?.origin, source?.transactions?.destination);
76
83
  | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
77
84
  | `capabilities()` | Sync discovery: Base USDC destination/default source, platforms × currencies × payee hints × amount bounds |
78
85
  | `capabilities({ includeRelaySources: true })` | Async discovery: adds live Relay SDK EVM source chains/tokens |
86
+ | `fillStats()` | Cached 30-day fill counts and median first-fill time per exact `platform:currency` pair |
79
87
  | `quoteSource(input)` / `executeSourceQuote(quote, { signer })` | Relay SDK EVM source routing into Base USDC before cashout |
80
88
  | `relayStatus(requestId)` | Relay request status from the Relay SDK request path |
81
- | `estimate({ amount, currency })` | Base USDC oracle estimate plus simple recent-fill ETA |
89
+ | `estimate({ amount, currency }, { includeEta? })` | Base USDC oracle estimate; optionally skip the historical ETA for progressive rendering |
82
90
  | `cashout(input, { signer })` | Registers your payee, creates the protocol-held order, returns the `depositId` |
83
91
  | `order(depositId)` / `orders(owner)` | Resume any order from its id alone; list all orders for a wallet |
84
92
  | `watch(depositId)` | Async iterator: yields on every state change until terminal, abort, or timeout |
@@ -97,6 +105,11 @@ must execute and confirm its Relay route before preparing the Base-USDC
97
105
  cashout. Every Peer Cash transaction, including approves, carries ERC-8021
98
106
  attribution: `peer-cash` first, your own `referrer` code(s) after it.
99
107
 
108
+ `capabilities()` presents Zelle as one platform. A cashout with
109
+ `receive.platform: 'zelle'` automatically attaches the generic Zelle method
110
+ and its Chase, Bank of America, and Citi buyer routes to the deposit; the payee
111
+ handle and public API remain bank-agnostic.
112
+
100
113
  The default/minimal flow is unchanged: pass Base USDC base units to
101
114
  `estimate()` and `cashout()`. For any other source asset, pass `source` to
102
115
  `cashout()` with a source-chain signer. The SDK settles the Base allowance,
@@ -172,9 +185,15 @@ awaiting-buyer ──────────► matched ───────
172
185
  fills. `estimate()` says "approximately"; nothing in this API pretends to
173
186
  lock a price.
174
187
  - **ETA is historical.** `estimate().eta` is just `{ seconds, label }`, backed
175
- by rolling 30-day indexer data from zero-spread (`spreadBps: 0`) market-rate
176
- deposits in the same payout corridor, measured from deposit creation to first
177
- fulfilled fill.
188
+ by the same rolling 30-day, intent-attributed pair sampler as `fillStats()`,
189
+ measured from deposit creation to the first fulfilled fill through the pair.
190
+ The raw snapshot is cached for 15 minutes per client and each ETA is still
191
+ resolved from its exact normalized `platform:currency` key. Use
192
+ `{ includeEta: false }` when rate and receive amount should render first.
193
+ - **Availability thresholds belong to the consumer.** `fillStats()` returns raw
194
+ evidence. A recommended gate is `fills >= 10 && medianFillSeconds <= 48h`.
195
+ Fail open to the full `capabilities()` catalog when stats are unavailable or
196
+ the gate would empty the offered catalog.
178
197
  - **Everything is resumable.** An order is reconstructed from the chain by
179
198
  `depositId` alone. Close the tab, switch devices, crash the process - then
180
199
  call `order(depositId)`.
@@ -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;
@@ -393,6 +401,13 @@ interface EstimateInput {
393
401
  tradeType?: 'EXACT_INPUT' | 'EXACT_OUTPUT' | 'EXPECTED_OUTPUT';
394
402
  };
395
403
  }
404
+ interface EstimateOptions {
405
+ /**
406
+ * Include the historical indexer-backed ETA. Disable for progressive UIs
407
+ * that render the oracle rate first and load pair fill stats separately.
408
+ */
409
+ includeEta?: boolean;
410
+ }
396
411
  interface CashEstimate {
397
412
  /** Always `'oracle-estimate'` - there is no committed quote in Peer Cash. */
398
413
  kind: 'oracle-estimate';
@@ -581,6 +596,13 @@ interface CashClient {
581
596
  capabilities(options: {
582
597
  includeRelaySources: true;
583
598
  }): Promise<CashCapabilities>;
599
+ /**
600
+ * 0c - Raw 30-day demand and first-fill speed evidence keyed by
601
+ * `platform:currency`. A recommended consumer gate is `fills >= 10 &&
602
+ * medianFillSeconds <= 48h`; fail open to the full capability catalog when
603
+ * stats are unavailable or the gate would remove every pair.
604
+ */
605
+ fillStats(): Promise<CashFillStats>;
584
606
  /** Relay-only source discovery helper. */
585
607
  sourceCapabilities(): Promise<CashSourceCapabilities>;
586
608
  /** Quote any Relay-supported EVM source asset into Base USDC. */
@@ -597,7 +619,7 @@ interface CashClient {
597
619
  /** Track Relay execution status by quote/request id. */
598
620
  relayStatus(requestId: string): Promise<RelayStatus>;
599
621
  /** 1 - Estimate: currency + amount only. No payee, no side effects, no expiry. */
600
- estimate(input: EstimateInput): Promise<CashEstimate>;
622
+ estimate(input: EstimateInput, options?: EstimateOptions): Promise<CashEstimate>;
601
623
  /** 2 - Cash out: payee registration + deposit params + submission happen here. */
602
624
  cashout(input: CashoutInput, opts: CashoutOptions): Promise<CashoutResult>;
603
625
  /** 2b - Unsigned path: `txs[]` for agent wallets, AA, server keys, policy layers. */
@@ -640,4 +662,4 @@ interface CashClient {
640
662
  }
641
663
  declare function createCashClient(options: CashClientOptions): CashClient;
642
664
 
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 };
665
+ 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, type EstimateOptions as H, type IntentStatus as I, RECOMMENDED_MIN_CASHOUT_AMOUNT as J, type RelayOptions as K, type RelayQuoteInput as L, MIN_CASHOUT_AMOUNT as M, type RelaySourceInput as N, type OrdersOptions as O, type PrepareResult as P, type RelayTransaction as Q, type RelayExecutionResult as R, type SignerOptions as S, type TopUpResult as T, type WatchOptions as U, type WithdrawOptions as V, type WithdrawResult as W, buildCapabilities as X, createCashClient as Y, 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;
@@ -393,6 +401,13 @@ interface EstimateInput {
393
401
  tradeType?: 'EXACT_INPUT' | 'EXACT_OUTPUT' | 'EXPECTED_OUTPUT';
394
402
  };
395
403
  }
404
+ interface EstimateOptions {
405
+ /**
406
+ * Include the historical indexer-backed ETA. Disable for progressive UIs
407
+ * that render the oracle rate first and load pair fill stats separately.
408
+ */
409
+ includeEta?: boolean;
410
+ }
396
411
  interface CashEstimate {
397
412
  /** Always `'oracle-estimate'` - there is no committed quote in Peer Cash. */
398
413
  kind: 'oracle-estimate';
@@ -581,6 +596,13 @@ interface CashClient {
581
596
  capabilities(options: {
582
597
  includeRelaySources: true;
583
598
  }): Promise<CashCapabilities>;
599
+ /**
600
+ * 0c - Raw 30-day demand and first-fill speed evidence keyed by
601
+ * `platform:currency`. A recommended consumer gate is `fills >= 10 &&
602
+ * medianFillSeconds <= 48h`; fail open to the full capability catalog when
603
+ * stats are unavailable or the gate would remove every pair.
604
+ */
605
+ fillStats(): Promise<CashFillStats>;
584
606
  /** Relay-only source discovery helper. */
585
607
  sourceCapabilities(): Promise<CashSourceCapabilities>;
586
608
  /** Quote any Relay-supported EVM source asset into Base USDC. */
@@ -597,7 +619,7 @@ interface CashClient {
597
619
  /** Track Relay execution status by quote/request id. */
598
620
  relayStatus(requestId: string): Promise<RelayStatus>;
599
621
  /** 1 - Estimate: currency + amount only. No payee, no side effects, no expiry. */
600
- estimate(input: EstimateInput): Promise<CashEstimate>;
622
+ estimate(input: EstimateInput, options?: EstimateOptions): Promise<CashEstimate>;
601
623
  /** 2 - Cash out: payee registration + deposit params + submission happen here. */
602
624
  cashout(input: CashoutInput, opts: CashoutOptions): Promise<CashoutResult>;
603
625
  /** 2b - Unsigned path: `txs[]` for agent wallets, AA, server keys, policy layers. */
@@ -640,4 +662,4 @@ interface CashClient {
640
662
  }
641
663
  declare function createCashClient(options: CashClientOptions): CashClient;
642
664
 
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 };
665
+ 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, type EstimateOptions as H, type IntentStatus as I, RECOMMENDED_MIN_CASHOUT_AMOUNT as J, type RelayOptions as K, type RelayQuoteInput as L, MIN_CASHOUT_AMOUNT as M, type RelaySourceInput as N, type OrdersOptions as O, type PrepareResult as P, type RelayTransaction as Q, type RelayExecutionResult as R, type SignerOptions as S, type TopUpResult as T, type WatchOptions as U, type WithdrawOptions as V, type WithdrawResult as W, buildCapabilities as X, createCashClient as Y, 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
  };
@@ -809,11 +836,8 @@ function mapChainError(verb, err, context = {}) {
809
836
  }
810
837
  return errors.chainCallFailed(verb, err);
811
838
  }
812
- var ETA_WINDOW_DAYS = 30;
813
- var ETA_WINDOW_SECONDS = ETA_WINDOW_DAYS * 24 * 60 * 60;
814
- var ETA_PAGE_LIMIT = 250;
815
- var ETA_MAX_DEPOSIT_SCAN = 2e3;
816
- 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;
817
841
  function toUnixSeconds2(value) {
818
842
  if (value === null || value === void 0 || value === "") return void 0;
819
843
  if (value instanceof Date) {
@@ -824,12 +848,22 @@ function toUnixSeconds2(value) {
824
848
  const parsed = Date.parse(value);
825
849
  if (Number.isFinite(parsed)) return Math.floor(parsed / 1e3);
826
850
  }
827
- const n = Number(value);
828
- 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
+ }
829
863
  }
830
864
  function median(values) {
831
865
  if (values.length === 0) return void 0;
832
- const sorted = [...values].sort((a, b) => a - b);
866
+ const sorted = [...values].sort((left, right) => left - right);
833
867
  const mid = Math.floor(sorted.length / 2);
834
868
  return sorted.length % 2 === 1 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
835
869
  }
@@ -841,52 +875,102 @@ function etaLabel(seconds) {
841
875
  const hours = Math.max(1, Math.round(minutes / 60));
842
876
  return `Usually starts in about ${hours} hr`;
843
877
  }
844
- function matchesPayout(deposit, environment, platform, currency) {
845
- const payouts = derivePayouts(
846
- deposit.paymentMethods ?? [],
847
- deposit.currencies ?? [],
848
- sdk.getPaymentMethodsCatalog(BASE_CHAIN_ID, environment)
849
- );
850
- return payouts.some(
851
- (payout) => payout.pricing.marketRate && payout.pricing.spreadBps === 0 && (platform === void 0 || payout.platform === platform) && (currency === void 0 || payout.currency === currency)
852
- );
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 };
853
937
  }
854
- async function readFillEta(client, input) {
938
+ async function readFillStatsSample(client, environment) {
855
939
  const now = Math.floor(Date.now() / 1e3);
856
- const windowStart = now - ETA_WINDOW_SECONDS;
940
+ const windowStart = now - FILL_STATS_WINDOW_SECONDS;
857
941
  const deposits = [];
858
- for (let offset = 0; offset < ETA_MAX_DEPOSIT_SCAN; offset += ETA_PAGE_LIMIT) {
942
+ for (let offset = 0; ; offset += FILL_STATS_PAGE_LIMIT) {
859
943
  const page = await client.indexer.getDepositsWithRelations(
860
944
  { chainId: BASE_CHAIN_ID },
861
- { 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
+ },
862
951
  { includeIntents: true, intentStatuses: ["FULFILLED", "MANUALLY_RELEASED"] }
863
952
  );
864
953
  deposits.push(...page);
865
- if (page.length < ETA_PAGE_LIMIT) break;
866
- const oldestCreatedAt = Math.min(
867
- ...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)
868
957
  );
869
- if (oldestCreatedAt < windowStart) break;
958
+ if (oldestUpdatedAt < windowStart) break;
870
959
  }
871
- const firstFillLatencies = [];
872
- for (const deposit of deposits) {
873
- const createdAt = toUnixSeconds2(deposit.createdAt ?? deposit.timestamp);
874
- if (createdAt === void 0 || createdAt < windowStart) continue;
875
- if (!matchesPayout(deposit, input.environment, input.platform, input.currency)) continue;
876
- const fulfilled = (deposit.intents ?? []).filter((intent) => intent.status != null && FULFILLED.has(intent.status)).map((intent) => ({
877
- fulfilledAt: toUnixSeconds2(intent.fulfillTimestamp)
878
- })).filter(
879
- (intent) => intent.fulfilledAt !== void 0 && intent.fulfilledAt >= createdAt
880
- ).sort((a, b) => a.fulfilledAt - b.fulfilledAt);
881
- if (fulfilled.length === 0) continue;
882
- firstFillLatencies.push(fulfilled[0].fulfilledAt - createdAt);
883
- }
884
- const seconds = median(firstFillLatencies);
960
+ return computeFillStatsSample(deposits, now, environment);
961
+ }
962
+ function fillEtaFromSample(sample, input) {
963
+ const currency = input.currency.toUpperCase();
964
+ const seconds = input.platform ? sample.stats[`${basePlatformForMethod(input.platform)}:${currency}`]?.medianFillSeconds : sample.medianFillSecondsByCurrency.get(currency);
885
965
  return {
886
966
  ...seconds !== void 0 ? { seconds } : {},
887
967
  label: etaLabel(seconds)
888
968
  };
889
969
  }
970
+ async function readFillEta(client, input) {
971
+ const sample = await readFillStatsSample(client, input.environment);
972
+ return fillEtaFromSample(sample, input);
973
+ }
890
974
  var RELAY_API_URL = relaySdk.MAINNET_RELAY_API;
891
975
  var NATIVE_TOKEN_ADDRESS = "0x0000000000000000000000000000000000000000";
892
976
  var BASE_USDC_ASSET = {
@@ -1422,13 +1506,18 @@ async function readEstimate(publicClient, input, context = {}) {
1422
1506
  }
1423
1507
  } : {}
1424
1508
  };
1425
- if (context.indexerClient && context.environment) {
1509
+ if (context.includeEta !== false && context.environment) {
1426
1510
  try {
1427
- estimate.eta = await readFillEta(context.indexerClient, {
1511
+ const etaInput = {
1428
1512
  environment: context.environment,
1429
1513
  currency,
1430
1514
  ...input.platform ? { platform: input.platform } : {}
1431
- });
1515
+ };
1516
+ if (context.etaReader) {
1517
+ estimate.eta = await context.etaReader(etaInput);
1518
+ } else if (context.indexerClient) {
1519
+ estimate.eta = await readFillEta(context.indexerClient, etaInput);
1520
+ }
1432
1521
  } catch {
1433
1522
  }
1434
1523
  }
@@ -1437,6 +1526,7 @@ async function readEstimate(publicClient, input, context = {}) {
1437
1526
 
1438
1527
  // src/client/createCashClient.ts
1439
1528
  var DEFAULT_RPC_URL = "https://mainnet.base.org";
1529
+ var FILL_STATS_CACHE_MS = 15 * 60 * 1e3;
1440
1530
  var CASH_ATTRIBUTION_CODE = "peer-cash";
1441
1531
  var DEFAULT_CURATOR_URLS = {
1442
1532
  preproduction: "https://api-preprod.zkp2p.xyz",
@@ -1535,6 +1625,25 @@ function createCashClient(options) {
1535
1625
  });
1536
1626
  }
1537
1627
  const readClient = buildSdkClient(viem.createWalletClient({ chain: chains.base, transport }));
1628
+ let fillStatsCache = null;
1629
+ let fillStatsRequest = null;
1630
+ async function getFillStatsSample() {
1631
+ if (fillStatsCache && fillStatsCache.expiresAt > Date.now()) {
1632
+ return fillStatsCache.sample;
1633
+ }
1634
+ if (fillStatsRequest) return fillStatsRequest;
1635
+ fillStatsRequest = readFillStatsSample(readClient, environment);
1636
+ try {
1637
+ const sample = await fillStatsRequest;
1638
+ fillStatsCache = {
1639
+ sample,
1640
+ expiresAt: Date.now() + FILL_STATS_CACHE_MS
1641
+ };
1642
+ return sample;
1643
+ } finally {
1644
+ fillStatsRequest = null;
1645
+ }
1646
+ }
1538
1647
  const signingClients = /* @__PURE__ */ new WeakMap();
1539
1648
  async function signingClient(verb, opts) {
1540
1649
  const signer = opts?.signer;
@@ -1549,6 +1658,7 @@ function createCashClient(options) {
1549
1658
  }
1550
1659
  function validatePayout(input) {
1551
1660
  const { receive } = input;
1661
+ const catalog = sdk.getPaymentMethodsCatalog(BASE_CHAIN_ID, environment);
1552
1662
  const platform = buildCapabilities(environment).platforms.find(
1553
1663
  (capability) => capability.platform === receive.platform
1554
1664
  );
@@ -1559,14 +1669,13 @@ function createCashClient(options) {
1559
1669
  if (!platform.currencies.includes(receive.currency)) {
1560
1670
  throw errors.unsupportedPlatformCurrency(receive.platform, receive.currency);
1561
1671
  }
1672
+ const paymentMethods = paymentMethodsForPlatform(receive.platform, catalog);
1562
1673
  return {
1563
- payouts: [
1564
- {
1565
- processorName: receive.platform,
1566
- currency: receive.currency,
1567
- payeeData: receive.payee
1568
- }
1569
- ]
1674
+ payouts: paymentMethods.map((processorName) => ({
1675
+ processorName,
1676
+ currency: receive.currency,
1677
+ payeeData: receive.payee
1678
+ }))
1570
1679
  };
1571
1680
  }
1572
1681
  function validateDepositInput(amount, input, payoutInput = validatePayout(input)) {
@@ -1837,13 +1946,21 @@ function createCashClient(options) {
1837
1946
  async relayStatus(requestId) {
1838
1947
  return readRelayStatus(requestId, options.relay);
1839
1948
  },
1840
- async estimate(input) {
1949
+ async estimate(input, estimateOptions) {
1841
1950
  return readEstimate(readClient.publicClient, input, {
1842
- indexerClient: readClient,
1843
1951
  environment,
1952
+ ...estimateOptions?.includeEta !== void 0 ? { includeEta: estimateOptions.includeEta } : {},
1953
+ etaReader: async (etaInput) => fillEtaFromSample(await getFillStatsSample(), etaInput),
1844
1954
  ...options.relay ? { relay: options.relay } : {}
1845
1955
  });
1846
1956
  },
1957
+ async fillStats() {
1958
+ try {
1959
+ return (await getFillStatsSample()).stats;
1960
+ } catch (err) {
1961
+ throw errors.indexerUnavailable("fill stats", err);
1962
+ }
1963
+ },
1847
1964
  async cashout(input, opts) {
1848
1965
  const client = await signingClient("cashout", opts);
1849
1966
  const owner = opts.signer.account.address;
@@ -2443,6 +2560,11 @@ var cashEstimateJsonSchema = zod.z.object({
2443
2560
  label: zod.z.string()
2444
2561
  }).optional()
2445
2562
  });
2563
+ var cashPairFillStatsJsonSchema = zod.z.object({
2564
+ fills: zod.z.number().int().nonnegative(),
2565
+ medianFillSeconds: zod.z.number().int().nonnegative().optional()
2566
+ }).strict();
2567
+ var cashFillStatsJsonSchema = zod.z.record(zod.z.string(), cashPairFillStatsJsonSchema);
2446
2568
  var preparedTransactionJsonSchema = zod.z.object({
2447
2569
  to: zod.z.string(),
2448
2570
  data: zod.z.string(),
@@ -2747,6 +2869,21 @@ function estimateFromJson(json) {
2747
2869
  } : void 0
2748
2870
  });
2749
2871
  }
2872
+ function fillStatsToJson(stats) {
2873
+ return cashFillStatsJsonSchema.parse(stats);
2874
+ }
2875
+ function fillStatsFromJson(json) {
2876
+ const parsed = cashFillStatsJsonSchema.parse(json);
2877
+ return Object.fromEntries(
2878
+ Object.entries(parsed).map(([pair, stats]) => [
2879
+ pair,
2880
+ {
2881
+ fills: stats.fills,
2882
+ ...stats.medianFillSeconds !== void 0 ? { medianFillSeconds: stats.medianFillSeconds } : {}
2883
+ }
2884
+ ])
2885
+ );
2886
+ }
2750
2887
  function cashAssetFromJson(asset) {
2751
2888
  return {
2752
2889
  chainId: asset.chainId,
@@ -3055,9 +3192,11 @@ exports.cashErrorRecoveryJsonSchema = cashErrorRecoveryJsonSchema;
3055
3192
  exports.cashErrorToJson = cashErrorToJson;
3056
3193
  exports.cashEstimateJsonSchema = cashEstimateJsonSchema;
3057
3194
  exports.cashFillJsonSchema = cashFillJsonSchema;
3195
+ exports.cashFillStatsJsonSchema = cashFillStatsJsonSchema;
3058
3196
  exports.cashNextActionSchema = cashNextActionSchema;
3059
3197
  exports.cashOrderJsonSchema = cashOrderJsonSchema;
3060
3198
  exports.cashOrderStateSchema = cashOrderStateSchema;
3199
+ exports.cashPairFillStatsJsonSchema = cashPairFillStatsJsonSchema;
3061
3200
  exports.cashPayoutInfoJsonSchema = cashPayoutInfoJsonSchema;
3062
3201
  exports.cashPayoutPricingJsonSchema = cashPayoutPricingJsonSchema;
3063
3202
  exports.cashPreparedStepJsonSchema = cashPreparedStepJsonSchema;
@@ -3077,6 +3216,8 @@ exports.explainOrder = explainOrder;
3077
3216
  exports.fiatFromUsdc = fiatFromUsdc;
3078
3217
  exports.fiatToNumber = fiatToNumber;
3079
3218
  exports.fillFromJson = fillFromJson;
3219
+ exports.fillStatsFromJson = fillStatsFromJson;
3220
+ exports.fillStatsToJson = fillStatsToJson;
3080
3221
  exports.fillToJson = fillToJson;
3081
3222
  exports.formatUsdc = formatUsdc;
3082
3223
  exports.intentStatusSchema = intentStatusSchema;