@zkp2p/cash 0.1.2 → 0.1.4

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
@@ -19,8 +19,10 @@ can withdraw an unmatched deposit.
19
19
  the plan, submit the transactions in order, and wait for each receipt.
20
20
  3. **You are a tool-use host** (MCP server, CLI) → import the manifest from
21
21
  `@zkp2p/cash/tools` and map the tool names to the verbs above. Base-USDC
22
- mutating tools return unsigned transactions; source routing should use
23
- `cash_source_quote` / `cash_source_status`, then a Base-USDC cashout.
22
+ mutating tools return unsigned transactions. `cash_source_quote` is a quote,
23
+ not an execution tool; the host must execute and confirm Relay through its
24
+ signer/runtime, use `cash_source_status` to monitor it, then call the
25
+ Base-USDC `cash_cashout` tool. Never pass `source` into `prepare()`.
24
26
 
25
27
  Every transaction (including approves) carries ERC-8021 attribution:
26
28
  `peer-cash`, then any `referrer` codes from `createCashClient` options, then
@@ -28,10 +30,11 @@ the Base builder code.
28
30
 
29
31
  **Two platform caveats, both surfaced in `capabilities()`:**
30
32
 
31
- - **Wise and PayPal** carry `requiresIdentityAttestation: true`. Their curator
33
+ - **Wise and PayPal** carry `requiresIdentityAttestation: true`. A new curator
32
34
  registration needs a signed maker identity attestation this SDK cannot mint
33
- (it comes from the Peer app/extension). A bare-handle `cashout()` to these
34
- fails fast with `PAYEE_VERIFICATION_REQUIRED` before any transaction.
35
+ (it comes from the Peer app/extension). An already-registered handle can be
36
+ reused with bare payee data. A new handle without its attestation fails with
37
+ `PAYEE_VERIFICATION_REQUIRED` before funds move on-chain.
35
38
  - **Venmo, Revolut, Cash App, Monzo** validate the handle against the live
36
39
  platform at registration - the account must exist. The rest (Zelle, Chime,
37
40
  etc.) are format-checked only. Match handles to the `payeeHint`.
@@ -71,17 +74,56 @@ if (order.nextActions.includes('withdraw') && shouldUnwind) {
71
74
  }
72
75
  ```
73
76
 
77
+ Signed source route (exact-input cash-out):
78
+
79
+ ```ts
80
+ const routed = await cash.cashout(
81
+ {
82
+ amount: sourceAmount, // source-token base units
83
+ source: {
84
+ chainId: sourceChainId,
85
+ currency: sourceToken,
86
+ tradeType: 'EXACT_INPUT',
87
+ },
88
+ receive,
89
+ },
90
+ { signer, sourceSigner },
91
+ );
92
+
93
+ // Guaranteed minimum Base USDC and exact order deposit amount, not actual route output.
94
+ console.log(routed.source?.amount);
95
+ console.log(routed.source?.transactions?.origin, routed.source?.transactions?.destination);
96
+ ```
97
+
74
98
  ## Rules that prevent wrong behavior
75
99
 
76
100
  - **Never promise a rate.** `estimate()` is `kind: 'oracle-estimate'`; the
77
101
  binding rate resolves at the oracle when a buyer fills. Do not display or
78
102
  log it as a locked price.
79
103
  - **Do not invent an ETA.** Use `estimate().eta`: `{ seconds, label }` backed
80
- by rolling 7-day indexer data from deposit creation to first fill. Use
81
- `order.explain()` for live order state.
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.
82
107
  - **Do not hardcode Relay source assets.** Use Relay SDK-backed EVM
83
108
  `capabilities({ includeRelaySources: true })` and `cashout({ source, ... })`.
84
- Destination is always Base USDC. Non-Base source chains require `sourceSigner`.
109
+ Destination is always Base USDC. Non-Base source chains require
110
+ `sourceSigner`. Use `EXACT_INPUT` in high-level cash-out flows so `amount`
111
+ remains source-token base units. `source.amount` is Relay's guaranteed
112
+ minimum output and the exact Base USDC deposit amount.
113
+ - **Persist source evidence.** A routed result carries `requestId`, flat
114
+ `txHashes`, and chain-aware `transactions.origin` / `.destination` arrays.
115
+ - **Never repeat a completed or uncertain route.** On `SOURCE_EXECUTION_FAILED`,
116
+ inspect its Relay request and transaction recovery evidence. On
117
+ `SOURCE_ROUTE_COMPLETED_CASHOUT_FAILED`, retry without `source` using
118
+ `BigInt(err.recovery.amount)`. On `SOURCE_CASHOUT_SUBMISSION_UNKNOWN`, inspect
119
+ Base wallet activity and `orders(err.recovery.depositor)`. On
120
+ `SOURCE_CASHOUT_STATUS_UNKNOWN`, inspect
121
+ `err.recovery.depositTxHash`; do not submit again until its receipt is known.
122
+ - **Never resubmit an unknown Base transaction.**
123
+ `TRANSACTION_SUBMISSION_UNKNOWN` means a call returned no hash but may have
124
+ broadcast; follow its recovery action and inspect wallet/protocol state.
125
+ `TRANSACTION_STATUS_UNKNOWN` means the returned hash may already have
126
+ succeeded. Inspect `err.recovery.transactionHash` first.
85
127
  - **`ORDER_NOT_FOUND` seconds after `cashout()` is indexer lag**, not a lost
86
128
  deposit. The tx receipt you hold is the truth. Retry; `watch()` absorbs
87
129
  this automatically.
@@ -96,31 +138,53 @@ if (order.nextActions.includes('withdraw') && shouldUnwind) {
96
138
  - **Serialize with the codecs.** `orderToJson`/`orderFromJson` etc. round-trip
97
139
  bigints losslessly and re-attach `explain()`. Plain `JSON.stringify` on a
98
140
  live object throws on bigints.
141
+ - **Environment selects the curator.** Preproduction defaults to
142
+ `https://api-preprod.zkp2p.xyz`; staging defaults to
143
+ `https://api-staging.zkp2p.xyz`. Use `curatorUrl` only for an explicit
144
+ override.
99
145
 
100
146
  ## Error → remediation table
101
147
 
102
148
  Every `CashError` carries `code`, `retryable`, `remediation`. Behavior:
103
149
 
104
- | Code | Retryable | Agent action |
105
- | ------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------- |
106
- | `ORACLE_UNSUPPORTED_CURRENCY` | no | Re-pick currency from `capabilities()` |
107
- | `UNSUPPORTED_PLATFORM` | no | Re-pick platform from `capabilities()` |
108
- | `AMOUNT_BELOW_MINIMUM` | no | Raise amount (hard floor $0.01, recommended ≥ 1 USDC) |
109
- | `PAYEE_VERIFICATION_REQUIRED` | no | Wise/PayPal need a signed identity attestation - register the payee via the Peer app first |
110
- | `SOURCE_ROUTE_UNSUPPORTED_IN_PREPARE` | no | Use signer-backed `cashout({ source })` with a source signer, or bridge with Relay first and then `prepare()` Base USDC |
111
- | `PAYEE_REGISTRATION_FAILED` | yes | Validate handle against `payeeHint`, retry with backoff (curator caps at 20 registrations/min) |
112
- | `ALLOWANCE_NOT_VISIBLE` | yes | Approve mined but a stale RPC replica hid it; retry the same call in a few seconds |
113
- | `TRANSACTION_FAILED` | no | The on-chain call reverted or was mapped from a raw error; surface to operator; funds unchanged |
114
- | `DEPOSIT_RESOLUTION_FAILED` | no | Extract depositId from the `DepositReceived` log in the receipt |
115
- | `ORDER_NOT_FOUND` | yes | Retry (indexer lag) unless the id is provably wrong |
116
- | `INDEXER_LAG` | yes | Retry after a few seconds |
117
- | `ACTIVE_INTENT_BLOCKS_WITHDRAWAL` | yes | Wait; retry full `withdraw()` after intent expiry (or withdraw the unlocked part with `amount`) |
118
- | `INSUFFICIENT_AVAILABLE_FUNDS` | yes | Partial amount exceeds the unlocked balance; lower it or close fully later |
119
- | `NOTHING_TO_WITHDRAW` | no | Order is terminal; reconcile your records |
120
- | `ORDER_NOT_ACTIVE` | no | Top-up target is closed; start a new `cashout()` instead |
121
- | `SIGNER_REQUIRED` | no | Provide `{ signer }` or switch to the prepare path |
122
- | `WATCH_TIMEOUT` | yes | Resume `watch(depositId)` whenever convenient |
123
- | `ESCROW_PAUSED` | yes | Back off; existing funds remain withdrawable |
150
+ | Code | Retryable | Agent action |
151
+ | --------------------------------------- | --------- | ------------------------------------------------------------------------------------------ |
152
+ | `ORACLE_UNSUPPORTED_CURRENCY` | no | Re-pick currency from `capabilities()` |
153
+ | `ORACLE_READ_FAILED` | yes | Retry the read through a healthy Base RPC; do not present a cached value as live |
154
+ | `UNSUPPORTED_PLATFORM` | no | Re-pick platform from `capabilities()` |
155
+ | `UNSUPPORTED_PLATFORM_CURRENCY` | no | Use a currency listed for that platform |
156
+ | `AMOUNT_BELOW_MINIMUM` | no | Raise amount (hard floor $0.01, recommended at least 1 USDC) |
157
+ | `INVALID_INTENT_AMOUNT_RANGE` | no | Use a positive min, max at least min, and max no greater than amount |
158
+ | `PAYEE_VERIFICATION_REQUIRED` | no | Register a new Wise/PayPal payee through Peer; an existing registered handle can be reused |
159
+ | `PAYEE_REGISTRATION_FAILED` | yes | Validate against `payeeHint`, then retry |
160
+ | `SOURCE_ROUTE_UNSUPPORTED_IN_PREPARE` | no | Execute Relay with a signer first, then prepare a Base-USDC cashout |
161
+ | `SOURCE_RECIPIENT_MISMATCH` | no | Route Base USDC to the cashout depositor |
162
+ | `SOURCE_CAPABILITIES_FAILED` | yes | Retry discovery or fall back to Base USDC |
163
+ | `SOURCE_QUOTE_FAILED` | yes | Refresh capabilities and request a new canonical Base-USDC quote |
164
+ | `SOURCE_EXECUTION_FAILED` | no | Inspect source transactions and Relay status before any retry |
165
+ | `SOURCE_STATUS_FAILED` | yes | Retry only the status read |
166
+ | `SOURCE_ROUTE_COMPLETED_CASHOUT_FAILED` | no | Do not route again; retry Base-only with `recovery.amount` |
167
+ | `SOURCE_CASHOUT_SUBMISSION_UNKNOWN` | no | Inspect Base activity and orders; prove no deposit exists before retrying |
168
+ | `SOURCE_CASHOUT_STATUS_UNKNOWN` | no | Inspect `recovery.depositTxHash`; do not resubmit while its receipt is unknown |
169
+ | `INSUFFICIENT_TOKEN_BALANCE` | no | Fund the required token amount, then retry |
170
+ | `ALLOWANCE_NOT_VISIBLE` | yes | Approval mined but a stale RPC hid it; retry after it becomes visible |
171
+ | `TRANSACTION_FAILED` | no | Inspect the failed/reverted call before another action |
172
+ | `TRANSACTION_SUBMISSION_UNKNOWN` | no | Inspect Base wallet/protocol state and the recovery action before any resubmission |
173
+ | `TRANSACTION_STATUS_UNKNOWN` | no | Inspect `recovery.transactionHash` before resubmitting |
174
+ | `DEPOSIT_RESOLUTION_FAILED` | no | Inspect the confirmed Base receipt and recover the id from `DepositReceived` |
175
+ | `INVALID_DEPOSIT_ID` | no | Use the exact id returned by `cashout()` |
176
+ | `ORDER_NOT_FOUND` | yes | Retry through immediate indexer lag; otherwise verify the id |
177
+ | `INDEXER_LAG` | yes | Retry after a few seconds |
178
+ | `INDEXER_UNAVAILABLE` | yes | Retry only the failed read; keep the id/owner and never repeat a transaction |
179
+ | `ACTIVE_INTENT_BLOCKS_WITHDRAWAL` | yes | Wait for fill/expiry, or withdraw only the unlocked amount |
180
+ | `INSUFFICIENT_AVAILABLE_FUNDS` | yes | Lower the partial withdrawal amount |
181
+ | `NOTHING_TO_WITHDRAW` | no | Order is terminal; reconcile records |
182
+ | `ORDER_NOT_ACTIVE` | no | Start a new cashout instead of topping up |
183
+ | `SIGNER_REQUIRED` | no | Provide a signer or use a Base-USDC prepare path |
184
+ | `SIGNER_CHAIN_MISMATCH` | no | Switch to the required chain and refresh any Relay quote before retrying |
185
+ | `SIGNER_CHAIN_UNAVAILABLE` | yes | Reconnect the wallet and prove its chain before retrying |
186
+ | `WATCH_TIMEOUT` | yes | Resume `watch(depositId)` later |
187
+ | `ESCROW_PAUSED` | yes | Back off; existing funds remain withdrawable |
124
188
 
125
189
  `isCashError(err)` narrows unknown errors; `err.toJSON()` is safe for logs
126
190
  and tool results.
package/README.md CHANGED
@@ -40,13 +40,21 @@ Source asset path:
40
40
  ```ts
41
41
  const { depositId, source } = await cash.cashout(
42
42
  {
43
- amount: 100000n, // source-token base units
44
- source: { chainId: 1, currency: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' },
43
+ amount: 10_000_000n, // exact input: 10 USDC in source-token base units
44
+ source: {
45
+ chainId: 1,
46
+ currency: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
47
+ tradeType: 'EXACT_INPUT',
48
+ },
45
49
  receive: { platform: 'venmo', currency: 'USD', payee: { offchainId: '@you' } },
46
50
  },
47
51
  { signer, sourceSigner },
48
52
  );
49
- // source.amount is the Base USDC amount Relay delivered before the cash-out order.
53
+
54
+ // source.amount is Relay's guaranteed minimum Base USDC output and the exact
55
+ // amount deposited into the cash-out order. It is not the route's actual output.
56
+ console.log(source?.amount, source?.requestId);
57
+ console.log(source?.transactions?.origin, source?.transactions?.destination);
50
58
  ```
51
59
 
52
60
  ## The core verbs
@@ -69,24 +77,59 @@ Base-USDC cashout, withdraw, and top-up have unsigned counterparts (`prepare`,
69
77
  `prepareWithdraw`, `prepareTopUp`). The unsigned path returns raw `txs[]` plus
70
78
  a same-index `steps[]` plan such as `approve`, `createDeposit`, or
71
79
  `withdrawDeposit`, so wallets, AA systems, and agents can show what each
72
- transaction does before signing. Source-routed cashout runs Relay first, so it
73
- uses the signed `cashout({ source }, { signer, sourceSigner })` path for
74
- non-Base sources or an explicit `quoteSource()` / `executeSourceQuote()` pre-step. Every Peer Cash transaction
75
- including approves carries ERC-8021 attribution: `peer-cash` first, your own
76
- `referrer` code(s) after it.
80
+ transaction does before signing. `prepare()` is Base-USDC-only and rejects a
81
+ `source` with `SOURCE_ROUTE_UNSUPPORTED_IN_PREPARE`. A signer-backed app can
82
+ use `cashout({ source }, { signer, sourceSigner })`; a custody-separated host
83
+ must execute and confirm its Relay route before preparing the Base-USDC
84
+ cashout. Every Peer Cash transaction, including approves, carries ERC-8021
85
+ attribution: `peer-cash` first, your own `referrer` code(s) after it.
77
86
 
78
87
  The default/minimal flow is unchanged: pass Base USDC base units to
79
88
  `estimate()` and `cashout()`. For any other source asset, pass `source` to
80
- `cashout()` with a source-chain signer and the SDK first executes the Relay route into Base USDC, then
81
- creates the Peer Cash order. The destination is always canonical Base USDC
89
+ `cashout()` with a source-chain signer. The SDK settles the Base allowance,
90
+ executes the Relay route into Base USDC, then creates the Peer Cash order. Use
91
+ `EXACT_INPUT` in cash-out UIs so `amount` always means source-token base units.
92
+ The destination is always canonical Base USDC
82
93
  (`8453:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913`); source support is
83
94
  discovered and quoted by `@relayprotocol/relay-sdk`, not a static token
84
95
  allowlist.
85
96
 
86
- `capabilities()` tells you which platforms need a verified identity to
87
- register a payee (`requiresIdentityAttestation` - Wise and PayPal today); a
88
- bare-handle `cashout()` to those fails fast with `PAYEE_VERIFICATION_REQUIRED`
89
- rather than reverting on-chain.
97
+ `capabilities()` tells you which platforms need a verified identity for a new
98
+ payee registration (`requiresIdentityAttestation` - Wise and PayPal today).
99
+ An already-registered Wise or PayPal handle can be reused with bare payee data.
100
+ A new handle without its signed attestation fails during curator registration
101
+ with `PAYEE_VERIFICATION_REQUIRED`, before funds move on-chain.
102
+
103
+ ## Source-route recovery
104
+
105
+ Persist `depositId`, transaction hashes, and the Relay `requestId` as soon as
106
+ they are available. A source-routed result includes both a flat
107
+ `source.txHashes` list and chain-aware `source.transactions.origin` /
108
+ `.destination` entries.
109
+
110
+ - `SOURCE_ROUTE_COMPLETED_CASHOUT_FAILED`: Relay completed, but the Base
111
+ cashout was not created. Do not route again. Retry a Base-USDC-only
112
+ `cashout()` with `BigInt(error.recovery.amount)`.
113
+ - `SOURCE_CASHOUT_SUBMISSION_UNKNOWN`: Relay completed, but Base submission
114
+ returned no transaction hash. Inspect recent Base wallet activity and
115
+ `orders(error.recovery.depositor)` to prove no deposit exists before retrying.
116
+ - `SOURCE_CASHOUT_STATUS_UNKNOWN`: the Base cashout transaction was submitted,
117
+ but its receipt is unknown. Do not route or submit again. Inspect
118
+ `error.recovery.depositTxHash`; recover the `depositId` from its
119
+ `DepositReceived` log if it succeeded, or use the recovery amount for a
120
+ Base-USDC-only retry only after confirming it reverted.
121
+ - `TRANSACTION_SUBMISSION_UNKNOWN`: a Base-only cashout or another mutation
122
+ returned no hash. Treat it as potentially broadcast. Inspect recent Base
123
+ wallet activity and the supplied recovery action before any retry.
124
+
125
+ Wallet clients pinned to the wrong chain fail with `SIGNER_CHAIN_MISMATCH`
126
+ before a quote or transaction is submitted. Chainless wallets are checked
127
+ through `getChainId()`; a disconnected wallet returns retryable
128
+ `SIGNER_CHAIN_UNAVAILABLE`. Indexer and oracle transport outages are typed as
129
+ retryable `INDEXER_UNAVAILABLE` and `ORACLE_READ_FAILED` reads; retry the read
130
+ without repeating any transaction. `TRANSACTION_STATUS_UNKNOWN` carries the
131
+ submitted hash in `error.recovery.transactionHash` so recovery never depends
132
+ on parsing an error message.
90
133
 
91
134
  ## Lifecycle
92
135
 
@@ -105,7 +148,9 @@ awaiting-buyer ──────────► matched ───────
105
148
  fills. `estimate()` says "approximately"; nothing in this API pretends to
106
149
  lock a price.
107
150
  - **ETA is historical.** `estimate().eta` is just `{ seconds, label }`, backed
108
- by rolling 7-day indexer data from deposit creation to first fulfilled fill.
151
+ by rolling 30-day indexer data from zero-spread (`spreadBps: 0`) market-rate
152
+ deposits in the same payout corridor, measured from deposit creation to first
153
+ fulfilled fill.
109
154
  - **Everything is resumable.** An order is reconstructed from the chain by
110
155
  `depositId` alone. Close the tab, switch devices, crash the process - then
111
156
  call `order(depositId)`.
@@ -145,10 +190,11 @@ React is an optional peer dependency - the root entry never imports it.
145
190
 
146
191
  ## Environments
147
192
 
148
- `production` | `preproduction` | `staging` - selects contracts, curator, and
149
- indexer. Indexer, curator, and Relay options are overridable via
150
- `createCashClient` options. Base USDC on Base is the default source and the
151
- only destination asset for cashout orders.
193
+ `production` | `preproduction` | `staging` selects contracts, curator, and
194
+ indexer. Preproduction defaults to `https://api-preprod.zkp2p.xyz`; staging
195
+ defaults to `https://api-staging.zkp2p.xyz`. Indexer, curator, and Relay
196
+ options remain overridable via `createCashClient` options. Base USDC on Base
197
+ is the default source and the only destination asset for cashout orders.
152
198
 
153
199
  ## Install
154
200
 
@@ -165,12 +211,10 @@ Runnable first-party examples in [`examples/`](examples):
165
211
 
166
212
  ## Trust model, honestly
167
213
 
168
- This SDK is open source, so the code that constructs the parameters moving
169
- your USDC into protocol-held funds is auditable. It depends on the published
170
- `@zkp2p/sdk` for protocol internals, which currently ships from private source.
171
- The Peer Cash facade is verifiable here; the dependency is not yet fully open.
172
- Onchain custody is still enforced by the protocol: only the contract holds
173
- funds, and only you can withdraw an unmatched deposit.
214
+ The published package depends on `@zkp2p/sdk` for protocol internals; that
215
+ dependency currently ships from private source. Onchain custody is enforced by
216
+ the protocol: only the contract holds funds, and only the maker can withdraw
217
+ an unmatched deposit.
174
218
 
175
219
  ## License
176
220