@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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { MARKET_SPREAD_BPS, ORACLE_MIN_CONVERSION_RATE_SENTINEL, CASH_RETAIN_ON_EMPTY, BASE_USDC_ADDRESS, USDC_DECIMALS, BASE_CHAIN_ID, errors, isCashError, CASH_ORDER_STATUSES, mapChainError, CashError } from './chunk-P3KYZ2FX.js';
2
- export { BASE_CHAIN_ID, BASE_USDC_ADDRESS, CASH_ORDER_POLL_INTERVAL_MS, CASH_ORDER_STATUSES, CASH_RETAIN_ON_EMPTY, CashError, MARKET_SPREAD_BPS, ORACLE_MIN_CONVERSION_RATE_SENTINEL, USDC_DECIMALS, errors, isCashError } from './chunk-P3KYZ2FX.js';
1
+ import { MARKET_SPREAD_BPS, ORACLE_MIN_CONVERSION_RATE_SENTINEL, CASH_RETAIN_ON_EMPTY, BASE_USDC_ADDRESS, USDC_DECIMALS, BASE_CHAIN_ID, errors, isCashError, CASH_ORDER_STATUSES, mapChainError, CashError } from './chunk-TR6JVYYF.js';
2
+ export { BASE_CHAIN_ID, BASE_USDC_ADDRESS, CASH_ORDER_POLL_INTERVAL_MS, CASH_ORDER_STATUSES, CASH_RETAIN_ON_EMPTY, CashError, MARKET_SPREAD_BPS, ORACLE_MIN_CONVERSION_RATE_SENTINEL, USDC_DECIMALS, errors, isCashError } from './chunk-TR6JVYYF.js';
3
3
  import { parseAbi, parseEventLogs, isAddress, http, createWalletClient, encodeFunctionData } from 'viem';
4
4
  import { base } from 'viem/chains';
5
5
  import { getSpreadOracleConfig, currencyInfo, getPaymentMethodsCatalog, getGatingServiceAddress, resolvePaymentMethodHashFromCatalog, resolvePaymentMethodNameFromHash, getCurrencyCodeFromHash, createCompositeDepositId, appendAttributionToCalldata, Zkp2pClient, CHAINLINK_ORACLE_FEEDS } from '@zkp2p/sdk';
@@ -387,6 +387,26 @@ function parseCompositeDepositId(compositeId) {
387
387
  const onchainDepositId = BigInt(rawDepositId);
388
388
  return { escrowAddress: canonicalEscrowAddress, onchainDepositId };
389
389
  }
390
+
391
+ // src/client/platformGroups.ts
392
+ var PLATFORM_METHOD_GROUPS = {
393
+ zelle: ["zelle", "zelle-chase", "zelle-bofa", "zelle-citi"]
394
+ };
395
+ var METHOD_TO_BASE_PLATFORM = new Map(
396
+ Object.entries(PLATFORM_METHOD_GROUPS).flatMap(
397
+ ([platform, methods]) => methods.map((method) => [method, platform])
398
+ )
399
+ );
400
+ function basePlatformForMethod(method) {
401
+ return METHOD_TO_BASE_PLATFORM.get(method) ?? method;
402
+ }
403
+ function paymentMethodsForPlatform(platform, catalog) {
404
+ const configured = PLATFORM_METHOD_GROUPS[platform];
405
+ const methods = configured ?? [platform];
406
+ return methods.filter((method) => catalog[method] !== void 0);
407
+ }
408
+
409
+ // src/client/capabilities.ts
390
410
  var MIN_CASHOUT_AMOUNT = 10000n;
391
411
  var RECOMMENDED_MIN_CASHOUT_AMOUNT = 1000000n;
392
412
  var PAYEE_HINTS = {
@@ -405,13 +425,20 @@ var PAYEE_HINTS = {
405
425
  var IDENTITY_ATTESTATION_PLATFORMS = /* @__PURE__ */ new Set(["wise", "paypal"]);
406
426
  function buildCapabilities(environment) {
407
427
  const catalog = getPaymentMethodsCatalog(BASE_CHAIN_ID, environment);
408
- const platforms = Object.entries(catalog).map(([platform, entry]) => {
428
+ const currenciesByPlatform = /* @__PURE__ */ new Map();
429
+ for (const [method, entry] of Object.entries(catalog)) {
430
+ const platform = basePlatformForMethod(method);
409
431
  const currencies2 = (entry.currencies ?? []).map((hash) => getCurrencyCodeFromHash(hash)).filter(
410
432
  (code) => code != null && isMarketRateSupported(code)
411
433
  );
434
+ const aggregate = currenciesByPlatform.get(platform) ?? /* @__PURE__ */ new Set();
435
+ for (const currency of currencies2) aggregate.add(currency);
436
+ currenciesByPlatform.set(platform, aggregate);
437
+ }
438
+ const platforms = [...currenciesByPlatform.entries()].map(([platform, currencies2]) => {
412
439
  return {
413
440
  platform,
414
- currencies: [...new Set(currencies2)],
441
+ currencies: [...currencies2].sort(),
415
442
  payeeHint: PAYEE_HINTS[platform] ?? "Your payment handle for this platform",
416
443
  requiresIdentityAttestation: IDENTITY_ATTESTATION_PLATFORMS.has(platform)
417
444
  };
@@ -430,11 +457,8 @@ function buildCapabilities(environment) {
430
457
  pricing: { kind: "oracle-market-rate", spreadBps: 0 }
431
458
  };
432
459
  }
433
- var ETA_WINDOW_DAYS = 30;
434
- var ETA_WINDOW_SECONDS = ETA_WINDOW_DAYS * 24 * 60 * 60;
435
- var ETA_PAGE_LIMIT = 250;
436
- var ETA_MAX_DEPOSIT_SCAN = 2e3;
437
- var FULFILLED = /* @__PURE__ */ new Set(["FULFILLED", "MANUALLY_RELEASED"]);
460
+ var FILL_STATS_WINDOW_SECONDS = 30 * 24 * 60 * 60;
461
+ var FILL_STATS_PAGE_LIMIT = 250;
438
462
  function toUnixSeconds2(value) {
439
463
  if (value === null || value === void 0 || value === "") return void 0;
440
464
  if (value instanceof Date) {
@@ -445,12 +469,22 @@ function toUnixSeconds2(value) {
445
469
  const parsed = Date.parse(value);
446
470
  if (Number.isFinite(parsed)) return Math.floor(parsed / 1e3);
447
471
  }
448
- const n = Number(value);
449
- return Number.isFinite(n) && n > 0 ? n : void 0;
472
+ const numeric = Number(value);
473
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : void 0;
474
+ }
475
+ function normalizeCurrencyCode(value) {
476
+ const raw = value?.trim();
477
+ if (!raw) return void 0;
478
+ if (!raw.toLowerCase().startsWith("0x")) return raw.toUpperCase();
479
+ try {
480
+ return getCurrencyCodeFromHash(raw)?.toUpperCase();
481
+ } catch {
482
+ return void 0;
483
+ }
450
484
  }
451
485
  function median(values) {
452
486
  if (values.length === 0) return void 0;
453
- const sorted = [...values].sort((a, b) => a - b);
487
+ const sorted = [...values].sort((left, right) => left - right);
454
488
  const mid = Math.floor(sorted.length / 2);
455
489
  return sorted.length % 2 === 1 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
456
490
  }
@@ -462,47 +496,97 @@ function etaLabel(seconds) {
462
496
  const hours = Math.max(1, Math.round(minutes / 60));
463
497
  return `Usually starts in about ${hours} hr`;
464
498
  }
465
- function matchesPayout(deposit, environment, platform, currency) {
466
- const payouts = derivePayouts(
467
- deposit.paymentMethods ?? [],
468
- deposit.currencies ?? [],
469
- getPaymentMethodsCatalog(BASE_CHAIN_ID, environment)
470
- );
471
- return payouts.some(
472
- (payout) => payout.pricing.marketRate && payout.pricing.spreadBps === 0 && (platform === void 0 || payout.platform === platform) && (currency === void 0 || payout.currency === currency)
473
- );
499
+ function computeFillStatsSample(deposits, nowSeconds, environment) {
500
+ const catalog = getPaymentMethodsCatalog(BASE_CHAIN_ID, environment);
501
+ const windowStart = nowSeconds - FILL_STATS_WINDOW_SECONDS;
502
+ const fillCounts = /* @__PURE__ */ new Map();
503
+ const latenciesByPair = /* @__PURE__ */ new Map();
504
+ const latenciesByCurrency = /* @__PURE__ */ new Map();
505
+ for (const deposit of deposits) {
506
+ const createdAt = toUnixSeconds2(deposit.createdAt ?? deposit.timestamp);
507
+ const firstFillByPair = /* @__PURE__ */ new Map();
508
+ const firstFillByCurrency = /* @__PURE__ */ new Map();
509
+ for (const intent of deposit.intents ?? []) {
510
+ const fulfilledAt = toUnixSeconds2(intent.fulfillTimestamp);
511
+ if (fulfilledAt === void 0 || fulfilledAt < windowStart) continue;
512
+ let method;
513
+ try {
514
+ method = intent.paymentMethodHash ? resolvePaymentMethodNameFromHash(intent.paymentMethodHash, catalog) : void 0;
515
+ } catch {
516
+ method = void 0;
517
+ }
518
+ const currency = normalizeCurrencyCode(intent.fiatCurrency);
519
+ if (!method || !currency) continue;
520
+ const pair = `${basePlatformForMethod(method)}:${currency}`;
521
+ fillCounts.set(pair, (fillCounts.get(pair) ?? 0) + 1);
522
+ if (createdAt === void 0 || createdAt < windowStart || fulfilledAt < createdAt) continue;
523
+ const previousPairFill = firstFillByPair.get(pair);
524
+ if (previousPairFill === void 0 || fulfilledAt < previousPairFill) {
525
+ firstFillByPair.set(pair, fulfilledAt);
526
+ }
527
+ const previousCurrencyFill = firstFillByCurrency.get(currency);
528
+ if (previousCurrencyFill === void 0 || fulfilledAt < previousCurrencyFill) {
529
+ firstFillByCurrency.set(currency, fulfilledAt);
530
+ }
531
+ }
532
+ if (createdAt === void 0) continue;
533
+ for (const [pair, firstFill] of firstFillByPair) {
534
+ const latencies = latenciesByPair.get(pair) ?? [];
535
+ latencies.push(firstFill - createdAt);
536
+ latenciesByPair.set(pair, latencies);
537
+ }
538
+ for (const [currency, firstFill] of firstFillByCurrency) {
539
+ const latencies = latenciesByCurrency.get(currency) ?? [];
540
+ latencies.push(firstFill - createdAt);
541
+ latenciesByCurrency.set(currency, latencies);
542
+ }
543
+ }
544
+ const stats = {};
545
+ for (const [pair, fills] of fillCounts) {
546
+ const medianFillSeconds = median(latenciesByPair.get(pair) ?? []);
547
+ stats[pair] = {
548
+ fills,
549
+ ...medianFillSeconds !== void 0 ? { medianFillSeconds } : {}
550
+ };
551
+ }
552
+ const medianFillSecondsByCurrency = /* @__PURE__ */ new Map();
553
+ for (const [currency, latencies] of latenciesByCurrency) {
554
+ const value = median(latencies);
555
+ if (value !== void 0) medianFillSecondsByCurrency.set(currency, value);
556
+ }
557
+ return { stats, medianFillSecondsByCurrency };
474
558
  }
475
- async function readFillEta(client, input) {
559
+ async function readFillStatsSample(client, environment) {
476
560
  const now = Math.floor(Date.now() / 1e3);
477
- const windowStart = now - ETA_WINDOW_SECONDS;
561
+ const windowStart = now - FILL_STATS_WINDOW_SECONDS;
478
562
  const deposits = [];
479
- for (let offset = 0; offset < ETA_MAX_DEPOSIT_SCAN; offset += ETA_PAGE_LIMIT) {
563
+ for (let offset = 0; ; offset += FILL_STATS_PAGE_LIMIT) {
480
564
  const page = await client.indexer.getDepositsWithRelations(
481
565
  { chainId: BASE_CHAIN_ID },
482
- { limit: ETA_PAGE_LIMIT, offset, orderBy: "timestamp", orderDirection: "desc" },
566
+ {
567
+ limit: FILL_STATS_PAGE_LIMIT,
568
+ offset,
569
+ orderBy: "updatedAt",
570
+ orderDirection: "desc"
571
+ },
483
572
  { includeIntents: true, intentStatuses: ["FULFILLED", "MANUALLY_RELEASED"] }
484
573
  );
485
574
  deposits.push(...page);
486
- if (page.length < ETA_PAGE_LIMIT) break;
487
- const oldestCreatedAt = Math.min(
488
- ...page.map((deposit) => toUnixSeconds2(deposit.createdAt ?? deposit.timestamp) ?? Infinity)
575
+ if (page.length < FILL_STATS_PAGE_LIMIT) break;
576
+ const oldestUpdatedAt = Math.min(
577
+ ...page.map((deposit) => toUnixSeconds2(deposit.updatedAt) ?? Infinity)
489
578
  );
490
- if (oldestCreatedAt < windowStart) break;
579
+ if (oldestUpdatedAt < windowStart) break;
491
580
  }
492
- const firstFillLatencies = [];
493
- for (const deposit of deposits) {
494
- const createdAt = toUnixSeconds2(deposit.createdAt ?? deposit.timestamp);
495
- if (createdAt === void 0 || createdAt < windowStart) continue;
496
- if (!matchesPayout(deposit, input.environment, input.platform, input.currency)) continue;
497
- const fulfilled = (deposit.intents ?? []).filter((intent) => intent.status != null && FULFILLED.has(intent.status)).map((intent) => ({
498
- fulfilledAt: toUnixSeconds2(intent.fulfillTimestamp)
499
- })).filter(
500
- (intent) => intent.fulfilledAt !== void 0 && intent.fulfilledAt >= createdAt
501
- ).sort((a, b) => a.fulfilledAt - b.fulfilledAt);
502
- if (fulfilled.length === 0) continue;
503
- firstFillLatencies.push(fulfilled[0].fulfilledAt - createdAt);
504
- }
505
- const seconds = median(firstFillLatencies);
581
+ return computeFillStatsSample(deposits, now, environment);
582
+ }
583
+ async function readFillStats(client, environment) {
584
+ return (await readFillStatsSample(client, environment)).stats;
585
+ }
586
+ async function readFillEta(client, input) {
587
+ const sample = await readFillStatsSample(client, input.environment);
588
+ const currency = input.currency.toUpperCase();
589
+ const seconds = input.platform ? sample.stats[`${basePlatformForMethod(input.platform)}:${currency}`]?.medianFillSeconds : sample.medianFillSecondsByCurrency.get(currency);
506
590
  return {
507
591
  ...seconds !== void 0 ? { seconds } : {},
508
592
  label: etaLabel(seconds)
@@ -675,6 +759,14 @@ async function assertRelayExecutionIdentity(quote, wallet, expectedRecipient) {
675
759
  throw new Error("Relay quote recipient does not match the expected Base recipient");
676
760
  }
677
761
  }
762
+ function assertRelayNonceManagement(quote, wallet) {
763
+ const account = wallet.account;
764
+ if (!account || account.type !== "local" || account.nonceManager !== void 0) return;
765
+ const transactionCount = quote.steps.flatMap(
766
+ (step) => step.items.filter((item) => asString(asRecord(item.data).to) !== void 0)
767
+ ).length;
768
+ if (transactionCount > 1) throw errors.sourceNonceManagerRequired(transactionCount);
769
+ }
678
770
  function isRelaySecretKey(key) {
679
771
  const normalized = key.toLowerCase();
680
772
  return normalized === "headers" || normalized === "apikey";
@@ -876,6 +968,7 @@ async function executeRelayQuote(quote, wallet, options = {}) {
876
968
  observedRequestId = quoteRequestId(rawQuote);
877
969
  assertCanonicalRelayDestination(rawQuote);
878
970
  await assertRelayExecutionIdentity(rawQuote, wallet, options.recipient);
971
+ assertRelayNonceManagement(rawQuote, wallet);
879
972
  const client = relayClient(options.relay);
880
973
  const sourceChainId = quoteSourceChainId(rawQuote);
881
974
  if (sourceChainId !== void 0 && !(client.chains ?? []).some((chain) => chain.id === sourceChainId)) {
@@ -1161,6 +1254,7 @@ function createCashClient(options) {
1161
1254
  }
1162
1255
  function validatePayout(input) {
1163
1256
  const { receive } = input;
1257
+ const catalog = getPaymentMethodsCatalog(BASE_CHAIN_ID, environment);
1164
1258
  const platform = buildCapabilities(environment).platforms.find(
1165
1259
  (capability) => capability.platform === receive.platform
1166
1260
  );
@@ -1171,14 +1265,13 @@ function createCashClient(options) {
1171
1265
  if (!platform.currencies.includes(receive.currency)) {
1172
1266
  throw errors.unsupportedPlatformCurrency(receive.platform, receive.currency);
1173
1267
  }
1268
+ const paymentMethods = paymentMethodsForPlatform(receive.platform, catalog);
1174
1269
  return {
1175
- payouts: [
1176
- {
1177
- processorName: receive.platform,
1178
- currency: receive.currency,
1179
- payeeData: receive.payee
1180
- }
1181
- ]
1270
+ payouts: paymentMethods.map((processorName) => ({
1271
+ processorName,
1272
+ currency: receive.currency,
1273
+ payeeData: receive.payee
1274
+ }))
1182
1275
  };
1183
1276
  }
1184
1277
  function validateDepositInput(amount, input, payoutInput = validatePayout(input)) {
@@ -1456,6 +1549,13 @@ function createCashClient(options) {
1456
1549
  ...options.relay ? { relay: options.relay } : {}
1457
1550
  });
1458
1551
  },
1552
+ async fillStats() {
1553
+ try {
1554
+ return await readFillStats(readClient, environment);
1555
+ } catch (err) {
1556
+ throw errors.indexerUnavailable("fill stats", err);
1557
+ }
1558
+ },
1459
1559
  async cashout(input, opts) {
1460
1560
  const client = await signingClient("cashout", opts);
1461
1561
  const owner = opts.signer.account.address;
@@ -2055,6 +2155,11 @@ var cashEstimateJsonSchema = z.object({
2055
2155
  label: z.string()
2056
2156
  }).optional()
2057
2157
  });
2158
+ var cashPairFillStatsJsonSchema = z.object({
2159
+ fills: z.number().int().nonnegative(),
2160
+ medianFillSeconds: z.number().int().nonnegative().optional()
2161
+ }).strict();
2162
+ var cashFillStatsJsonSchema = z.record(z.string(), cashPairFillStatsJsonSchema);
2058
2163
  var preparedTransactionJsonSchema = z.object({
2059
2164
  to: z.string(),
2060
2165
  data: z.string(),
@@ -2186,6 +2291,7 @@ var CASH_ERROR_CODES = defineCashErrorCodes([
2186
2291
  "SOURCE_RECIPIENT_MISMATCH",
2187
2292
  "SOURCE_CAPABILITIES_FAILED",
2188
2293
  "SOURCE_QUOTE_FAILED",
2294
+ "SOURCE_NONCE_MANAGER_REQUIRED",
2189
2295
  "SOURCE_EXECUTION_FAILED",
2190
2296
  "SOURCE_STATUS_FAILED",
2191
2297
  "SOURCE_ROUTE_COMPLETED_CASHOUT_FAILED",
@@ -2358,6 +2464,21 @@ function estimateFromJson(json) {
2358
2464
  } : void 0
2359
2465
  });
2360
2466
  }
2467
+ function fillStatsToJson(stats) {
2468
+ return cashFillStatsJsonSchema.parse(stats);
2469
+ }
2470
+ function fillStatsFromJson(json) {
2471
+ const parsed = cashFillStatsJsonSchema.parse(json);
2472
+ return Object.fromEntries(
2473
+ Object.entries(parsed).map(([pair, stats]) => [
2474
+ pair,
2475
+ {
2476
+ fills: stats.fills,
2477
+ ...stats.medianFillSeconds !== void 0 ? { medianFillSeconds: stats.medianFillSeconds } : {}
2478
+ }
2479
+ ])
2480
+ );
2481
+ }
2361
2482
  function cashAssetFromJson(asset) {
2362
2483
  return {
2363
2484
  chainId: asset.chainId,
@@ -2635,4 +2756,4 @@ function cashErrorFromJson(json) {
2635
2756
  });
2636
2757
  }
2637
2758
 
2638
- export { CASH_ATTRIBUTION_CODE, MIN_CASHOUT_AMOUNT, RATE_PRECISION, RECOMMENDED_MIN_CASHOUT_AMOUNT, bigintString, buildCapabilities, buildIntentAmountRange, buildMarketRateCurrencyOverride, buyerProfileFromJson, buyerProfileToJson, capabilitiesFromJson, capabilitiesToJson, cashAssetJsonSchema, cashBuyerProfileJsonSchema, cashCapabilitiesJsonSchema, cashChainJsonSchema, cashErrorFromJson, cashErrorJsonSchema, cashErrorRecoveryJsonSchema, cashErrorToJson, cashEstimateJsonSchema, cashFillJsonSchema, cashNextActionSchema, cashOrderJsonSchema, cashOrderStateSchema, cashPayoutInfoJsonSchema, cashPayoutPricingJsonSchema, cashPreparedStepJsonSchema, cashSourceCapabilitiesJsonSchema, cashoutResultFromJson, cashoutResultJsonSchema, cashoutResultToJson, centsToNumber, createCashClient, deriveBuyerProfile, deriveCashOrder, derivePayouts, estimateFromJson, estimateToJson, explainOrder, fiatFromUsdc, fiatToNumber, fillFromJson, fillToJson, formatUsdc, intentStatusSchema, isFillLive, isMarketRateSupported, nonNegativeBigintString, orderFromJson, orderToJson, parseCompositeDepositId, prepareCashDepositParams, prepareResultFromJson, prepareResultJsonSchema, prepareResultToJson, preparedStepFromJson, preparedStepToJson, preparedTransactionJsonSchema, preparedTxFromJson, preparedTxToJson, rateToNumber, relayExecutionResultFromJson, relayExecutionResultJsonSchema, relayExecutionResultToJson, relayQuoteFromJson, relayQuoteJsonSchema, relayQuoteToJson, relayStatusFromJson, relayStatusJsonSchema, relayStatusToJson, relayTransactionJsonSchema, relayTransactionsJsonSchema, resolveCashDepositId, sourceCapabilitiesFromJson, sourceCapabilitiesToJson, topUpResultFromJson, topUpResultJsonSchema, topUpResultToJson, usdc, withExplain, withdrawResultFromJson, withdrawResultJsonSchema, withdrawResultToJson };
2759
+ export { CASH_ATTRIBUTION_CODE, MIN_CASHOUT_AMOUNT, RATE_PRECISION, RECOMMENDED_MIN_CASHOUT_AMOUNT, bigintString, buildCapabilities, buildIntentAmountRange, buildMarketRateCurrencyOverride, buyerProfileFromJson, buyerProfileToJson, capabilitiesFromJson, capabilitiesToJson, cashAssetJsonSchema, cashBuyerProfileJsonSchema, cashCapabilitiesJsonSchema, cashChainJsonSchema, cashErrorFromJson, cashErrorJsonSchema, cashErrorRecoveryJsonSchema, cashErrorToJson, cashEstimateJsonSchema, cashFillJsonSchema, cashFillStatsJsonSchema, cashNextActionSchema, cashOrderJsonSchema, cashOrderStateSchema, cashPairFillStatsJsonSchema, cashPayoutInfoJsonSchema, cashPayoutPricingJsonSchema, cashPreparedStepJsonSchema, cashSourceCapabilitiesJsonSchema, cashoutResultFromJson, cashoutResultJsonSchema, cashoutResultToJson, centsToNumber, createCashClient, deriveBuyerProfile, deriveCashOrder, derivePayouts, estimateFromJson, estimateToJson, explainOrder, fiatFromUsdc, fiatToNumber, fillFromJson, fillStatsFromJson, fillStatsToJson, fillToJson, formatUsdc, intentStatusSchema, isFillLive, isMarketRateSupported, nonNegativeBigintString, orderFromJson, orderToJson, parseCompositeDepositId, prepareCashDepositParams, prepareResultFromJson, prepareResultJsonSchema, prepareResultToJson, preparedStepFromJson, preparedStepToJson, preparedTransactionJsonSchema, preparedTxFromJson, preparedTxToJson, rateToNumber, relayExecutionResultFromJson, relayExecutionResultJsonSchema, relayExecutionResultToJson, relayQuoteFromJson, relayQuoteJsonSchema, relayQuoteToJson, relayStatusFromJson, relayStatusJsonSchema, relayStatusToJson, relayTransactionJsonSchema, relayTransactionsJsonSchema, resolveCashDepositId, sourceCapabilitiesFromJson, sourceCapabilitiesToJson, topUpResultFromJson, topUpResultJsonSchema, topUpResultToJson, usdc, withExplain, withdrawResultFromJson, withdrawResultJsonSchema, withdrawResultToJson };
package/dist/react.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CurrencyType } from '@zkp2p/sdk';
2
- import { q as CashClient, E as EstimateInput, i as CashEstimate, B as CashoutOptions, h as CashoutResult, A as CashoutInput, T as TopUpResult, W as WithdrawResult, e as CashOrder } from './createCashClient-jUA_GNdh.cjs';
2
+ import { r as CashClient, G as EstimateInput, i as CashEstimate, E as CashoutOptions, h as CashoutResult, D as CashoutInput, T as TopUpResult, W as WithdrawResult, e as CashOrder } from './createCashClient-BIzOLHjF.cjs';
3
3
  import { WalletClient } from 'viem';
4
4
  import '@relayprotocol/relay-sdk';
5
5
 
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CurrencyType } from '@zkp2p/sdk';
2
- import { q as CashClient, E as EstimateInput, i as CashEstimate, B as CashoutOptions, h as CashoutResult, A as CashoutInput, T as TopUpResult, W as WithdrawResult, e as CashOrder } from './createCashClient-jUA_GNdh.js';
2
+ import { r as CashClient, G as EstimateInput, i as CashEstimate, E as CashoutOptions, h as CashoutResult, D as CashoutInput, T as TopUpResult, W as WithdrawResult, e as CashOrder } from './createCashClient-BIzOLHjF.js';
3
3
  import { WalletClient } from 'viem';
4
4
  import '@relayprotocol/relay-sdk';
5
5
 
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isCashError, CASH_ORDER_POLL_INTERVAL_MS } from './chunk-P3KYZ2FX.js';
1
+ import { isCashError, CASH_ORDER_POLL_INTERVAL_MS } from './chunk-TR6JVYYF.js';
2
2
  import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
3
3
 
4
4
  function useEstimate({
package/dist/tools.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // package.json
4
4
  var package_default = {
5
- version: "0.1.6"};
5
+ version: "0.1.8"};
6
6
 
7
7
  // src/tools/index.ts
8
8
  var bigintString = {
@@ -112,6 +112,15 @@ var builtInCashTools = [
112
112
  additionalProperties: false
113
113
  }
114
114
  },
115
+ {
116
+ name: "cash_fill_stats",
117
+ description: "Read raw 30-day demand and first-fill speed evidence for every observed platform:currency pair. Consumers should apply their own threshold and fail open to cash_capabilities when stats are unavailable or filtering would empty the catalog.",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: {},
121
+ additionalProperties: false
122
+ }
123
+ },
115
124
  {
116
125
  name: "cash_cashout",
117
126
  description: "Start a Base-USDC cash-out using the custody-separated prepare path. Returns UNSIGNED transactions plus same-index steps [approve, createDeposit]; signing and ordered submission stay host-side. For another source asset, complete cash_source_quote and cash_source_status first, then pass the guaranteed Base USDC output amount here.",
package/dist/tools.d.cts CHANGED
@@ -135,6 +135,14 @@ declare const builtInCashTools: readonly [{
135
135
  readonly required: readonly ["amount", "currency"];
136
136
  readonly additionalProperties: false;
137
137
  };
138
+ }, {
139
+ readonly name: "cash_fill_stats";
140
+ readonly description: "Read raw 30-day demand and first-fill speed evidence for every observed platform:currency pair. Consumers should apply their own threshold and fail open to cash_capabilities when stats are unavailable or filtering would empty the catalog.";
141
+ readonly inputSchema: {
142
+ readonly type: "object";
143
+ readonly properties: {};
144
+ readonly additionalProperties: false;
145
+ };
138
146
  }, {
139
147
  readonly name: "cash_cashout";
140
148
  readonly description: "Start a Base-USDC cash-out using the custody-separated prepare path. Returns UNSIGNED transactions plus same-index steps [approve, createDeposit]; signing and ordered submission stay host-side. For another source asset, complete cash_source_quote and cash_source_status first, then pass the guaranteed Base USDC output amount here.";
package/dist/tools.d.ts CHANGED
@@ -135,6 +135,14 @@ declare const builtInCashTools: readonly [{
135
135
  readonly required: readonly ["amount", "currency"];
136
136
  readonly additionalProperties: false;
137
137
  };
138
+ }, {
139
+ readonly name: "cash_fill_stats";
140
+ readonly description: "Read raw 30-day demand and first-fill speed evidence for every observed platform:currency pair. Consumers should apply their own threshold and fail open to cash_capabilities when stats are unavailable or filtering would empty the catalog.";
141
+ readonly inputSchema: {
142
+ readonly type: "object";
143
+ readonly properties: {};
144
+ readonly additionalProperties: false;
145
+ };
138
146
  }, {
139
147
  readonly name: "cash_cashout";
140
148
  readonly description: "Start a Base-USDC cash-out using the custody-separated prepare path. Returns UNSIGNED transactions plus same-index steps [approve, createDeposit]; signing and ordered submission stay host-side. For another source asset, complete cash_source_quote and cash_source_status first, then pass the guaranteed Base USDC output amount here.";
package/dist/tools.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // package.json
2
2
  var package_default = {
3
- version: "0.1.6"};
3
+ version: "0.1.8"};
4
4
 
5
5
  // src/tools/index.ts
6
6
  var bigintString = {
@@ -110,6 +110,15 @@ var builtInCashTools = [
110
110
  additionalProperties: false
111
111
  }
112
112
  },
113
+ {
114
+ name: "cash_fill_stats",
115
+ description: "Read raw 30-day demand and first-fill speed evidence for every observed platform:currency pair. Consumers should apply their own threshold and fail open to cash_capabilities when stats are unavailable or filtering would empty the catalog.",
116
+ inputSchema: {
117
+ type: "object",
118
+ properties: {},
119
+ additionalProperties: false
120
+ }
121
+ },
113
122
  {
114
123
  name: "cash_cashout",
115
124
  description: "Start a Base-USDC cash-out using the custody-separated prepare path. Returns UNSIGNED transactions plus same-index steps [approve, createDeposit]; signing and ordered submission stay host-side. For another source asset, complete cash_source_quote and cash_source_status first, then pass the guaranteed Base USDC output amount here.",
@@ -36,6 +36,16 @@ For any other EVM source asset, Peer Cash uses `@relayprotocol/relay-sdk`:
36
36
  `executeSourceQuote()` remains available for apps that want a separate
37
37
  bridge step.
38
38
 
39
+ Routes that need more than one source-chain transaction (an ERC-20 `approve`,
40
+ then the route transaction) are submitted back-to-back by the Relay SDK. On a
41
+ plain local account the route transaction reuses the approval's nonce and
42
+ reverts mid-route, so `executeSourceQuote()` and `cashout({ source })` refuse
43
+ multi-transaction routes up front with `SOURCE_NONCE_MANAGER_REQUIRED` unless
44
+ the source signer carries a viem nonce manager:
45
+ `privateKeyToAccount(pk, { nonceManager })`. The check fires before anything
46
+ is submitted. Browser (`json-rpc`) wallets are unaffected - the node
47
+ allocates their nonces.
48
+
39
49
  Cash-out interfaces should use `tradeType: 'EXACT_INPUT'` (also the default),
40
50
  so `amount` always means source-token base units. A `RelayQuote.outputAmount`
41
51
  is the guaranteed minimum Base USDC output. The same value becomes
@@ -62,10 +72,16 @@ destination constant.
62
72
 
63
73
  Do not retry a source route merely because the Base cashout did not finish:
64
74
 
75
+ - `SOURCE_NONCE_MANAGER_REQUIRED` is preflight: nothing was submitted.
76
+ Recreate the source signer with viem's nonce manager and execute a fresh
77
+ quote.
65
78
  - `SOURCE_EXECUTION_FAILED` means Relay execution did not report success. Check
66
79
  its `inspect-relay-route` recovery evidence, submitted wallet transactions,
67
80
  and `relayStatus(requestId)` before taking another action; a blind retry can
68
- route twice.
81
+ route twice. A failed route whose only landed transaction is the approval
82
+ can sit in `relayStatus` `waiting` indefinitely - it never becomes terminal,
83
+ so decide from the persisted recovery payload and the origin transactions,
84
+ not from Relay reaching a final status.
69
85
  - `SOURCE_ROUTE_COMPLETED_CASHOUT_FAILED` means Relay completed, but the Base
70
86
  cashout was not created. Its recovery payload has
71
87
  `kind: 'retry-base-usdc-cashout'`, the guaranteed Base USDC `amount`, Relay
@@ -108,10 +124,17 @@ passes through `delivering` until the last one completes. `filledAmount`,
108
124
  ## The ETA principle
109
125
 
110
126
  `estimate().eta` is historical, not a promise. It uses rolling 30-day indexer
111
- data from deposit/order creation to the first fulfilled fill. It deliberately
112
- does **not** measure buyer signal to fulfillment; that would miss the
113
- buyer-arrival wait that users actually care about. The public shape is small:
114
- `{ seconds, label }`.
127
+ data from deposit/order creation to the first fulfilled fill through the
128
+ intent's actual platform and currency pair. It deliberately does **not**
129
+ measure buyer signal to fulfillment; that would miss the buyer-arrival wait
130
+ that users actually care about. The public shape is small: `{ seconds, label }`.
131
+
132
+ `fillStats()` exposes the sampler's raw evidence for catalog filtering as
133
+ `Record<"platform:currency", { fills, medianFillSeconds? }>`. Bank-scoped Zelle
134
+ methods aggregate to `zelle:USD`. Consumers own thresholding; the recommended
135
+ gate is `fills >= 10 && medianFillSeconds <= 48h`, with a fail-open fallback to
136
+ the full capability catalog when the read fails or filtering would empty it.
137
+ Medians are per-deposit first-fill latencies, never means or censored cohorts.
115
138
 
116
139
  - **Buyer arrival time is market-driven.** A deposit at market rate should
117
140
  fill fast, but the ETA is only a recent historical sample.
@@ -132,7 +155,9 @@ UI built on this SDK is a bug in that UI.
132
155
  - **Partial withdrawal** - `withdraw(depositId, { amount })` pulls part of
133
156
  the _unlocked_ balance back out. A live buyer intent does not block it
134
157
  (their locked portion is untouched); asking for more than the unlocked
135
- balance fails with `INSUFFICIENT_AVAILABLE_FUNDS`.
158
+ balance fails with `INSUFFICIENT_AVAILABLE_FUNDS`. Accounting note: a
159
+ partial withdrawal increments `returnedAmount`; `totalAmount` records
160
+ everything the order has ever held and does not shrink.
136
161
  - There is no retain-on-empty or rate knob to manage - a cash order cleans
137
162
  itself up when fully filled, and the market rate is not configurable.
138
163
 
@@ -252,6 +277,7 @@ explicit override.
252
277
  | `SOURCE_RECIPIENT_MISMATCH` | no | Relay output recipient differs from the cashout depositor. Use the depositor address. |
253
278
  | `SOURCE_CAPABILITIES_FAILED` | yes | Relay source discovery failed. Retry or use Base USDC. |
254
279
  | `SOURCE_QUOTE_FAILED` | yes | Relay returned no valid canonical Base-USDC route. Refresh capabilities and quote again. |
280
+ | `SOURCE_NONCE_MANAGER_REQUIRED` | no | Preflight; nothing was submitted. Recreate the source signer with viem's `nonceManager` and execute a fresh quote. |
255
281
  | `SOURCE_EXECUTION_FAILED` | no | Route execution did not report success. Inspect source transactions and Relay status before retrying. |
256
282
  | `SOURCE_STATUS_FAILED` | yes | Relay status is temporarily unavailable. Retry the status read without resubmitting. |
257
283
  | `SOURCE_ROUTE_COMPLETED_CASHOUT_FAILED` | no | Relay completed but no Base cashout was created. Use the recovery amount for a Base-USDC-only retry; never repeat Relay. |
package/llms.txt CHANGED
@@ -19,9 +19,13 @@ Key facts:
19
19
  source.amount is Relay's guaranteed minimum Base USDC output and the exact
20
20
  order deposit amount, not the route's actual output.
21
21
  - There is NO locked fiat quote. estimate() reads the oracle; the binding rate
22
- resolves at fill time. ETA is `{ seconds, label }` from rolling 30-day,
23
- zero-spread market-rate deposits in the same payout corridor, not a
24
- guarantee.
22
+ resolves at fill time. ETA is `{ seconds, label }` from the same rolling
23
+ 30-day, intent-attributed pair sampler as fillStats(), not a guarantee.
24
+ - fillStats() returns raw `{ fills, medianFillSeconds? }` evidence keyed by
25
+ `platform:currency`. Recommended consumer gate: fills >= 10 and median <=
26
+ 48h; fail open to capabilities() if unavailable or filtering empties it.
27
+ - capabilities() exposes one Zelle platform. A zelle cashout internally attaches
28
+ the generic method plus Chase, Bank of America, and Citi buyer routes.
25
29
  - Resume any order from its depositId alone (composite escrow_onchainId).
26
30
  - One unwind verb: withdraw(depositId) - prunes expired intents automatically;
27
31
  pass amount for a partial withdrawal of the unlocked balance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zkp2p/cash",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Peer Cash - offramp-only SDK for routing crypto to Base USDC, then cashing out to fiat at the live oracle market rate.",
5
5
  "license": "MIT",
6
6
  "author": "Peer (https://peer.xyz)",
@@ -92,13 +92,15 @@
92
92
  "scripts": {
93
93
  "build": "tsup",
94
94
  "typecheck": "tsc --noEmit",
95
- "lint": "eslint src test examples",
95
+ "lint": "eslint src test examples scripts",
96
96
  "format": "prettier --write .",
97
97
  "format:check": "prettier --check .",
98
98
  "test": "vitest run",
99
99
  "test:watch": "vitest",
100
100
  "audit": "bun audit --production",
101
101
  "pack:check": "bun scripts/check-packed-package.ts",
102
+ "verify:production-crosschain-relay": "bun scripts/verify-production-crosschain-relay.ts",
103
+ "verify:production-maker": "bun scripts/verify-production-maker.ts",
102
104
  "prepack": "bun run build",
103
105
  "ci": "bun run typecheck && bun run lint && bun run format:check && bun run test && bun run audit && bun run build && bun run pack:check"
104
106
  },