@zkp2p/cash 0.1.3 → 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,6 +74,27 @@ 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
@@ -82,7 +106,24 @@ if (order.nextActions.includes('withdraw') && shouldUnwind) {
82
106
  fill. Use `order.explain()` for live order state.
83
107
  - **Do not hardcode Relay source assets.** Use Relay SDK-backed EVM
84
108
  `capabilities({ includeRelaySources: true })` and `cashout({ source, ... })`.
85
- 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.
86
127
  - **`ORDER_NOT_FOUND` seconds after `cashout()` is indexer lag**, not a lost
87
128
  deposit. The tx receipt you hold is the truth. Retry; `watch()` absorbs
88
129
  this automatically.
@@ -97,31 +138,53 @@ if (order.nextActions.includes('withdraw') && shouldUnwind) {
97
138
  - **Serialize with the codecs.** `orderToJson`/`orderFromJson` etc. round-trip
98
139
  bigints losslessly and re-attach `explain()`. Plain `JSON.stringify` on a
99
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.
100
145
 
101
146
  ## Error → remediation table
102
147
 
103
148
  Every `CashError` carries `code`, `retryable`, `remediation`. Behavior:
104
149
 
105
- | Code | Retryable | Agent action |
106
- | ------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------- |
107
- | `ORACLE_UNSUPPORTED_CURRENCY` | no | Re-pick currency from `capabilities()` |
108
- | `UNSUPPORTED_PLATFORM` | no | Re-pick platform from `capabilities()` |
109
- | `AMOUNT_BELOW_MINIMUM` | no | Raise amount (hard floor $0.01, recommended ≥ 1 USDC) |
110
- | `PAYEE_VERIFICATION_REQUIRED` | no | Wise/PayPal need a signed identity attestation - register the payee via the Peer app first |
111
- | `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 |
112
- | `PAYEE_REGISTRATION_FAILED` | yes | Validate handle against `payeeHint`, retry with backoff (curator caps at 20 registrations/min) |
113
- | `ALLOWANCE_NOT_VISIBLE` | yes | Approve mined but a stale RPC replica hid it; retry the same call in a few seconds |
114
- | `TRANSACTION_FAILED` | no | The on-chain call reverted or was mapped from a raw error; surface to operator; funds unchanged |
115
- | `DEPOSIT_RESOLUTION_FAILED` | no | Extract depositId from the `DepositReceived` log in the receipt |
116
- | `ORDER_NOT_FOUND` | yes | Retry (indexer lag) unless the id is provably wrong |
117
- | `INDEXER_LAG` | yes | Retry after a few seconds |
118
- | `ACTIVE_INTENT_BLOCKS_WITHDRAWAL` | yes | Wait; retry full `withdraw()` after intent expiry (or withdraw the unlocked part with `amount`) |
119
- | `INSUFFICIENT_AVAILABLE_FUNDS` | yes | Partial amount exceeds the unlocked balance; lower it or close fully later |
120
- | `NOTHING_TO_WITHDRAW` | no | Order is terminal; reconcile your records |
121
- | `ORDER_NOT_ACTIVE` | no | Top-up target is closed; start a new `cashout()` instead |
122
- | `SIGNER_REQUIRED` | no | Provide `{ signer }` or switch to the prepare path |
123
- | `WATCH_TIMEOUT` | yes | Resume `watch(depositId)` whenever convenient |
124
- | `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 |
125
188
 
126
189
  `isCashError(err)` narrows unknown errors; `err.toJSON()` is safe for logs
127
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
 
@@ -147,10 +190,11 @@ React is an optional peer dependency - the root entry never imports it.
147
190
 
148
191
  ## Environments
149
192
 
150
- `production` | `preproduction` | `staging` - selects contracts, curator, and
151
- indexer. Indexer, curator, and Relay options are overridable via
152
- `createCashClient` options. Base USDC on Base is the default source and the
153
- 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.
154
198
 
155
199
  ## Install
156
200
 
@@ -167,12 +211,10 @@ Runnable first-party examples in [`examples/`](examples):
167
211
 
168
212
  ## Trust model, honestly
169
213
 
170
- This SDK is open source, so the code that constructs the parameters moving
171
- your USDC into protocol-held funds is auditable. It depends on the published
172
- `@zkp2p/sdk` for protocol internals, which currently ships from private source.
173
- The Peer Cash facade is verifiable here; the dependency is not yet fully open.
174
- Onchain custody is still enforced by the protocol: only the contract holds
175
- 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.
176
218
 
177
219
  ## License
178
220