@zkp2p/cash 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -5,12 +5,25 @@ on Venmo, Revolut, Wise, Zelle, and more at the live Chainlink market rate,
5
5
  with zero spread and no centralized off-ramp provider.
6
6
 
7
7
  Peer Cash is an **offramp-only** SDK for the [ZKP2P](https://peer.xyz)
8
- protocol. The cashing-out user is the maker: their USDC becomes a
9
- protocol-held deposit, Peer handles the buyer side, and the SDK gives the
10
- integrator a small set of typed verbs plus readable order state. No hosted
11
- widget, no provider custody, no quote engine to maintain.
8
+ protocol. The cashing-out user is the maker: their USDC becomes a deposit in
9
+ the protocol contracts, a buyer pays them fiat and proves the payment, and the
10
+ SDK gives the integrator a small set of typed verbs plus readable order state.
11
+ No hosted widget, no provider custody, no quote engine to maintain.
12
12
 
13
- **[Live demo](https://react-cashout-demo.vercel.app)** · **[Product page](https://peer.xyz/cash)**
13
+ **[npm](https://www.npmjs.com/package/@zkp2p/cash)** · **[Lifecycle and recovery](docs/lifecycle-and-recovery.md)** · **[Agent integration manual](AGENTS.md)**
14
+
15
+ ## Pick the right SDK
16
+
17
+ Peer Cash and the general ZKP2P SDK serve different integration depths:
18
+
19
+ | Package | Use it when | Boundary |
20
+ | ------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | `@zkp2p/cash` | Cash-out is the product | Offramp only. The user is always the maker, the destination is Base USDC, pricing is the live Chainlink rate at fill with zero spread, and the SDK owns the resumable order lifecycle. |
22
+ | `@zkp2p/sdk` | You are composing directly with the Peer protocol | General maker and taker operations, deposits, intents, proofs, quotes, vaults, rate managers, referrals, hooks, and API helpers. Your application owns the workflow and protocol choices. |
23
+
24
+ Peer Cash is a narrow facade over `@zkp2p/sdk`, not a replacement for it. It
25
+ cannot express custom spreads, buyer-side proof flows, vaults, disputes, or
26
+ arbitrary protocol operations.
14
27
 
15
28
  ```ts
16
29
  import { createCashClient, usdc } from '@zkp2p/cash';
@@ -272,6 +272,8 @@ interface RelayExecutionResult {
272
272
  interface RelayTransaction {
273
273
  hash: string;
274
274
  chainId: number;
275
+ /** Relay batch-call identifiers are not transaction hashes. */
276
+ isBatchTx?: boolean | undefined;
275
277
  }
276
278
  interface RelayStatus {
277
279
  requestId: string;
@@ -272,6 +272,8 @@ interface RelayExecutionResult {
272
272
  interface RelayTransaction {
273
273
  hash: string;
274
274
  chainId: number;
275
+ /** Relay batch-call identifiers are not transaction hashes. */
276
+ isBatchTx?: boolean | undefined;
275
277
  }
276
278
  interface RelayStatus {
277
279
  requestId: string;
package/dist/index.cjs CHANGED
@@ -980,10 +980,18 @@ function collectRelayTransactions(steps, sourceChainId) {
980
980
  for (const step of steps) {
981
981
  for (const item of step.items) {
982
982
  for (const tx of item.internalTxHashes ?? []) {
983
- record({ hash: tx.txHash, chainId: tx.chainId });
983
+ record({
984
+ hash: tx.txHash,
985
+ chainId: tx.chainId,
986
+ ...tx.isBatchTx ? { isBatchTx: true } : {}
987
+ });
984
988
  }
985
989
  for (const tx of item.txHashes ?? []) {
986
- record({ hash: tx.txHash, chainId: tx.chainId });
990
+ record({
991
+ hash: tx.txHash,
992
+ chainId: tx.chainId,
993
+ ...tx.isBatchTx ? { isBatchTx: true } : {}
994
+ });
987
995
  }
988
996
  }
989
997
  }
@@ -1703,6 +1711,97 @@ function createCashClient(options) {
1703
1711
  }
1704
1712
  throw errors.allowanceNotVisible(amount, lastReadError);
1705
1713
  }
1714
+ async function waitForBaseSignerAfterRelay(client, cashoutSigner, sourceSigner, owner, sourceChainId, executed) {
1715
+ if (sourceChainId !== BASE_CHAIN_ID) return;
1716
+ const baseTransactions = (executed.transactions?.origin ?? []).filter(
1717
+ (transaction) => transaction.chainId === BASE_CHAIN_ID
1718
+ );
1719
+ const batchIds = baseTransactions.filter((transaction) => transaction.isBatchTx === true).map((transaction) => transaction.hash);
1720
+ const batchExecutionFailed = (cause) => errors.sourceExecutionFailed(cause, {
1721
+ ...executed.requestId ? { requestId: executed.requestId } : {},
1722
+ txHashes: executed.txHashes,
1723
+ ...executed.transactions ? { transactions: executed.transactions } : {}
1724
+ });
1725
+ let batchTransactionHashes = [];
1726
+ if (batchIds.length > 0) {
1727
+ let batchError;
1728
+ let batchesComplete = false;
1729
+ let batchesSucceededWithoutReceipts = false;
1730
+ for (let attempt = 0; attempt < 20; attempt++) {
1731
+ try {
1732
+ const statuses = await Promise.all(
1733
+ batchIds.map((id) => sourceSigner.getCallsStatus({ id }))
1734
+ );
1735
+ if (statuses.some((status) => status.status === "failure")) {
1736
+ throw batchExecutionFailed(new Error("Relay wallet call bundle failed"));
1737
+ }
1738
+ if (statuses.every((status) => status.status === "success")) {
1739
+ const receiptGroups = statuses.map((status) => status.receipts ?? []);
1740
+ if (receiptGroups.some((receipts) => receipts.length === 0)) {
1741
+ batchError = new Error(
1742
+ "Relay wallet call bundle did not include transaction receipts"
1743
+ );
1744
+ batchesSucceededWithoutReceipts = true;
1745
+ break;
1746
+ } else {
1747
+ batchTransactionHashes = receiptGroups.flatMap(
1748
+ (receipts) => receipts.map((receipt) => receipt.transactionHash)
1749
+ );
1750
+ batchesComplete = true;
1751
+ break;
1752
+ }
1753
+ }
1754
+ } catch (err) {
1755
+ if (isCashError(err)) throw err;
1756
+ batchError = err;
1757
+ }
1758
+ await sleep(250);
1759
+ }
1760
+ if (!batchesComplete) {
1761
+ if (batchesSucceededWithoutReceipts) throw batchError;
1762
+ throw batchExecutionFailed(
1763
+ batchError ?? new Error("Relay wallet call bundle did not complete")
1764
+ );
1765
+ }
1766
+ }
1767
+ const hashes = [
1768
+ ...baseTransactions.filter((transaction) => transaction.isBatchTx !== true).map((transaction) => transaction.hash),
1769
+ ...batchTransactionHashes
1770
+ ];
1771
+ if (hashes.length === 0) return;
1772
+ let transactions;
1773
+ let lastLookupError;
1774
+ for (let attempt = 0; attempt < 20; attempt++) {
1775
+ try {
1776
+ transactions = await Promise.all(
1777
+ hashes.map((hash) => client.publicClient.getTransaction({ hash }))
1778
+ );
1779
+ break;
1780
+ } catch (err) {
1781
+ lastLookupError = err;
1782
+ await sleep(250);
1783
+ }
1784
+ }
1785
+ if (!transactions) throw lastLookupError;
1786
+ const ownerNonces = transactions.filter((transaction) => transaction.from.toLowerCase() === owner.toLowerCase()).map((transaction) => transaction.nonce);
1787
+ if (ownerNonces.length === 0) return;
1788
+ const afterRelay = Math.max(...ownerNonces) + 1;
1789
+ let lastNonceError;
1790
+ for (let attempt = 0; attempt < 20; attempt++) {
1791
+ try {
1792
+ const pendingHex = await cashoutSigner.transport.request({
1793
+ method: "eth_getTransactionCount",
1794
+ params: [owner, "pending"]
1795
+ });
1796
+ if (Number(BigInt(pendingHex)) >= afterRelay) return;
1797
+ } catch (err) {
1798
+ lastNonceError = err;
1799
+ }
1800
+ await sleep(250);
1801
+ }
1802
+ if (lastNonceError) throw lastNonceError;
1803
+ throw new Error(`Signer provider did not observe Relay nonce ${afterRelay - 1}`);
1804
+ }
1706
1805
  return {
1707
1806
  capabilities,
1708
1807
  async sourceCapabilities() {
@@ -1774,6 +1873,22 @@ function createCashClient(options) {
1774
1873
  ...executed.transactions ? { transactions: executed.transactions } : {}
1775
1874
  };
1776
1875
  sourceResult = routedSource;
1876
+ try {
1877
+ await waitForBaseSignerAfterRelay(
1878
+ client,
1879
+ opts.signer,
1880
+ sourceSigner,
1881
+ owner,
1882
+ input.source.chainId,
1883
+ executed
1884
+ );
1885
+ } catch (err) {
1886
+ if (isCashError(err)) throw err;
1887
+ throw errors.sourceRouteCompletedCashoutFailed(
1888
+ routedSource,
1889
+ mapChainError("resolve same-chain Relay nonce", err)
1890
+ );
1891
+ }
1777
1892
  const attributedParams2 = { ...params2, txOverrides: attribution };
1778
1893
  const send2 = async () => {
1779
1894
  try {
@@ -2146,7 +2261,8 @@ var bigintString = zod.z.string().regex(/^-?\d+$/, "expected a decimal bigint st
2146
2261
  var nonNegativeBigintString = zod.z.string().regex(/^\d+$/, "expected a non-negative decimal bigint string");
2147
2262
  var relayTransactionJsonSchema = zod.z.object({
2148
2263
  hash: zod.z.string(),
2149
- chainId: zod.z.number()
2264
+ chainId: zod.z.number(),
2265
+ isBatchTx: zod.z.boolean().optional()
2150
2266
  });
2151
2267
  var relayTransactionsJsonSchema = zod.z.object({
2152
2268
  origin: zod.z.array(relayTransactionJsonSchema),