@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/dist/tools.js CHANGED
@@ -1,14 +1,28 @@
1
+ // package.json
2
+ var package_default = {
3
+ version: "0.1.4"};
4
+
1
5
  // src/tools/index.ts
2
6
  var bigintString = {
3
7
  type: "string",
4
- pattern: "^[0-9]+$",
8
+ pattern: "^0*[1-9][0-9]*$",
5
9
  description: "Base units as a decimal string. For the default path this is USDC 6 decimals; with source it is source-token base units."
6
10
  };
7
11
  var depositId = {
8
12
  type: "string",
13
+ pattern: "^0x[0-9a-fA-F]{40}_[0-9]+$",
9
14
  description: "Composite deposit id (escrow_onchainId) returned by cash_cashout - the resume key"
10
15
  };
11
- var cashTools = [
16
+ var address = {
17
+ type: "string",
18
+ pattern: "^0x[0-9a-fA-F]{40}$"
19
+ };
20
+ var chainId = {
21
+ type: "integer",
22
+ minimum: 1,
23
+ maximum: Number.MAX_SAFE_INTEGER
24
+ };
25
+ var builtInCashTools = [
12
26
  {
13
27
  name: "cash_capabilities",
14
28
  description: "Discover what Peer Cash can do: payout platforms, oracle-priced currencies per platform, Base USDC destination, default Base USDC source, payee handle hints, and amount bounds. Set includeRelaySources=true to fetch live Relay-supported EVM source chains/tokens through the Relay SDK.",
@@ -25,23 +39,23 @@ var cashTools = [
25
39
  },
26
40
  {
27
41
  name: "cash_source_quote",
28
- description: "Quote any Relay-supported EVM source asset into Base USDC through @relayprotocol/relay-sdk. Use this before cash_cashout when the user starts with an asset other than Base USDC.",
42
+ description: "Quote any Relay-supported EVM source asset into Base USDC through @relayprotocol/relay-sdk. A custody-capable host must submit the returned route, poll cash_source_status to success, then call Base-USDC cash_cashout with the guaranteed output amount. Never submit the route twice.",
29
43
  inputSchema: {
30
44
  type: "object",
31
45
  properties: {
32
- user: { type: "string", description: "Source wallet submitting the Relay transaction." },
46
+ user: { ...address, description: "Source wallet submitting the Relay transaction." },
33
47
  amount: bigintString,
34
48
  source: {
35
49
  type: "object",
36
50
  properties: {
37
- chainId: { type: "number", description: "Relay-supported EVM source chain id." },
38
- currency: { type: "string", description: "Source token/native address." }
51
+ chainId: { ...chainId, description: "Relay-supported EVM source chain id." },
52
+ currency: { ...address, description: "Source token/native address." }
39
53
  },
40
54
  required: ["chainId", "currency"],
41
55
  additionalProperties: false
42
56
  },
43
57
  recipient: {
44
- type: "string",
58
+ ...address,
45
59
  description: "Base recipient for Relay-delivered USDC. Defaults to user."
46
60
  },
47
61
  tradeType: {
@@ -73,14 +87,14 @@ var cashTools = [
73
87
  type: "object",
74
88
  description: "Optional Relay EVM source asset. Omit for the Base USDC default path.",
75
89
  properties: {
76
- chainId: { type: "number", description: "Relay-supported EVM source chain id." },
77
- currency: { type: "string", description: "Source token/native address." },
90
+ chainId: { ...chainId, description: "Relay-supported EVM source chain id." },
91
+ currency: { ...address, description: "Source token/native address." },
78
92
  user: {
79
- type: "string",
93
+ ...address,
80
94
  description: "Source wallet submitting the Relay transaction."
81
95
  },
82
96
  recipient: {
83
- type: "string",
97
+ ...address,
84
98
  description: "Base recipient for Relay-delivered USDC. Defaults to user."
85
99
  },
86
100
  tradeType: {
@@ -98,29 +112,11 @@ var cashTools = [
98
112
  },
99
113
  {
100
114
  name: "cash_cashout",
101
- description: "Start a cash-out. Default path: Base USDC amount and tool hosts can return UNSIGNED transactions plus same-index steps [approve, createDeposit]. With source: signer-backed clients first execute a Relay SDK EVM route into Base USDC, then register the payee and create the protocol-held cash-out order. Non-Base source chains need a source-chain signer. Custody-separated hosts should use cash_source_quote/cash_source_status first, then Base USDC cash_cashout.",
115
+ 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.",
102
116
  inputSchema: {
103
117
  type: "object",
104
118
  properties: {
105
119
  amount: bigintString,
106
- source: {
107
- type: "object",
108
- description: "Optional Relay EVM source asset. Omit for the Base USDC default path.",
109
- properties: {
110
- chainId: { type: "number", description: "Relay-supported EVM source chain id." },
111
- currency: { type: "string", description: "Source token/native address." },
112
- recipient: {
113
- type: "string",
114
- description: "Base recipient for Relay-delivered USDC. Defaults to signer."
115
- },
116
- tradeType: {
117
- type: "string",
118
- enum: ["EXACT_INPUT", "EXACT_OUTPUT", "EXPECTED_OUTPUT"]
119
- }
120
- },
121
- required: ["chainId", "currency"],
122
- additionalProperties: false
123
- },
124
120
  receive: {
125
121
  type: "object",
126
122
  description: "Where the fiat should arrive",
@@ -167,12 +163,17 @@ var cashTools = [
167
163
  inputSchema: {
168
164
  type: "object",
169
165
  properties: {
170
- owner: { type: "string", description: "The maker wallet address (0x...)" },
166
+ owner: { ...address, description: "The maker wallet address (0x...)" },
171
167
  inFlight: {
172
168
  type: "boolean",
173
169
  description: "Only awaiting-buyer / matched / delivering orders"
174
170
  },
175
- limit: { type: "number", description: "Max deposits to scan (default 100)" }
171
+ limit: {
172
+ type: "integer",
173
+ minimum: 1,
174
+ maximum: 1e3,
175
+ description: "Max deposits to scan (default 100)"
176
+ }
176
177
  },
177
178
  required: ["owner"],
178
179
  additionalProperties: false
@@ -184,7 +185,7 @@ var cashTools = [
184
185
  inputSchema: {
185
186
  type: "object",
186
187
  properties: {
187
- address: { type: "string", description: "The buyer (taker) wallet address (0x...)" }
188
+ address: { ...address, description: "The buyer (taker) wallet address (0x...)" }
188
189
  },
189
190
  required: ["address"],
190
191
  additionalProperties: false
@@ -229,13 +230,12 @@ var cashTools = [
229
230
  }
230
231
  }
231
232
  ];
233
+ var cashTools = [...builtInCashTools];
232
234
  var cashToolManifest = {
233
235
  name: "@zkp2p/cash",
234
- version: "0.1.2",
236
+ version: package_default.version,
235
237
  description: "Peer Cash - offramp-only: route any Relay-supported EVM source asset to Base USDC, then cash out to fiat at the live oracle market rate (0% spread). Mutating protocol tools return unsigned transactions plus step labels with ERC-8021 peer-cash attribution.",
236
238
  tools: cashTools
237
239
  };
238
240
 
239
241
  export { cashToolManifest, cashTools };
240
- //# sourceMappingURL=tools.js.map
241
- //# sourceMappingURL=tools.js.map
@@ -0,0 +1,278 @@
1
+ # Lifecycle and recovery
2
+
3
+ The one deep guide: what states exist, how source routing works, how partial
4
+ fills and ETA work, how unwinding works, and why every order survives a crash.
5
+
6
+ ## The model: you are the maker
7
+
8
+ A Peer Cash order is a **deposit** in the ZKP2P protocol. When you
9
+ `cashout()`, Base USDC becomes protocol-held funds priced at the live Chainlink
10
+ oracle rate with zero spread. A buyer (a standard protocol taker) _signals an
11
+ intent_ against your deposit, pays you fiat offchain (Venmo, Revolut, Wise,
12
+ ...), and proves the payment via TEE-TLS. The protocol then releases your USDC
13
+ to them.
14
+ The protocol runs in its normal direction - nothing here is inverted or
15
+ special-cased.
16
+
17
+ Because your deposit is priced at market with no spread, it is the best price
18
+ a rational maker can offer. That is the fill incentive.
19
+
20
+ ## Source routing
21
+
22
+ The cashout destination is always canonical Base USDC. The minimal/default path
23
+ is still same-chain Base USDC: pass USDC base units to `estimate()` and
24
+ `cashout()`.
25
+
26
+ For any other EVM source asset, Peer Cash uses `@relayprotocol/relay-sdk`:
27
+
28
+ 1. `capabilities({ includeRelaySources: true })` or `sourceCapabilities()`
29
+ fetches live Relay-supported EVM chains and tokens, excluding disabled,
30
+ deposit-disabled, and block-lagging chains.
31
+ 2. `quoteSource()` calls Relay SDK `actions.getQuote` for source to Base USDC.
32
+ 3. `cashout({ amount, source, receive }, { signer, sourceSigner })` settles
33
+ Base allowance, calls Relay SDK `actions.execute`, then creates the Peer Cash
34
+ order with Relay's guaranteed minimum Base USDC output. Non-Base source
35
+ chains require `sourceSigner`.
36
+ `executeSourceQuote()` remains available for apps that want a separate
37
+ bridge step.
38
+
39
+ Cash-out interfaces should use `tradeType: 'EXACT_INPUT'` (also the default),
40
+ so `amount` always means source-token base units. A `RelayQuote.outputAmount`
41
+ is the guaranteed minimum Base USDC output. The same value becomes
42
+ `CashoutResult.source.amount` and the exact amount deposited into the Peer Cash
43
+ order; it is not a claim about the route's actual output.
44
+
45
+ When a source route succeeds, `CashoutResult.source` also retains the Relay
46
+ `requestId`, a flat `txHashes` list, and chain-aware
47
+ `transactions.origin` / `transactions.destination` entries. Persist them with
48
+ the `depositId` so route and deposit recovery do not depend on browser state.
49
+
50
+ The unsigned `prepare()` path is Base-USDC-only and rejects `source` with
51
+ `SOURCE_ROUTE_UNSUPPORTED_IN_PREPARE`. The tool manifest follows the same
52
+ boundary: `cash_source_quote` and `cash_source_status` quote and observe Relay,
53
+ but the host or a signer-backed client must execute the route before
54
+ `cash_cashout` prepares the Base-USDC order.
55
+
56
+ There is no static chain/token allowlist in Peer Cash. Relay decides source
57
+ support through its metadata and quote execution, filtered to the viem/EVM
58
+ execution surface this SDK can sign. Peer Cash hardcodes only the Base USDC
59
+ destination constant.
60
+
61
+ ### Source-route failure boundaries
62
+
63
+ Do not retry a source route merely because the Base cashout did not finish:
64
+
65
+ - `SOURCE_EXECUTION_FAILED` means Relay execution did not report success. Check
66
+ its `inspect-relay-route` recovery evidence, submitted wallet transactions,
67
+ and `relayStatus(requestId)` before taking another action; a blind retry can
68
+ route twice.
69
+ - `SOURCE_ROUTE_COMPLETED_CASHOUT_FAILED` means Relay completed, but the Base
70
+ cashout was not created. Its recovery payload has
71
+ `kind: 'retry-base-usdc-cashout'`, the guaranteed Base USDC `amount`, Relay
72
+ request and transaction evidence. Do not route again; retry without `source`.
73
+ - `SOURCE_CASHOUT_SUBMISSION_UNKNOWN` means Relay completed, but Base
74
+ submission returned no transaction hash. Inspect recent Base wallet activity
75
+ and `orders(recovery.depositor)` to prove no deposit exists before retrying.
76
+ - `SOURCE_CASHOUT_STATUS_UNKNOWN` means Relay completed and the Base cashout
77
+ transaction was submitted, but its receipt could not be confirmed. Its
78
+ recovery payload has `kind: 'inspect-base-cashout-transaction'` and
79
+ `depositTxHash`. Do not submit anything again until that Base transaction is
80
+ known. If it succeeded, recover `depositId` from `DepositReceived`; if it
81
+ reverted, retry a Base-USDC-only cashout with the recovery amount.
82
+
83
+ ## States
84
+
85
+ Every state derives from on-chain events. There are no synthetic states.
86
+
87
+ | State | On-chain meaning | `nextActions` |
88
+ | ---------------- | --------------------------------------------------- | ------------------------------------------------------------- |
89
+ | `awaiting-buyer` | Deposit live, no active intent | `['wait', 'withdraw']` |
90
+ | `matched` | A buyer signaled; funds locked | `['wait']` (or `['wait','withdraw']` once the intent expires) |
91
+ | `delivering` | Partial fill in progress: some delivered, more live | `['wait']` / `['wait','withdraw']` |
92
+ | `delivered` | Fully paid and proven; protocol released funds | `[]` |
93
+ | `returned` | Funds back in your wallet (withdrawn) | `[]` |
94
+
95
+ Intent statuses underneath: `SIGNALED → FULFILLED` (paid + proven),
96
+ `PRUNED` (expired unpaid), `MANUALLY_RELEASED` (support path, counts as
97
+ fulfilled). Always query the full set - the indexer defaults to `SIGNALED`
98
+ only, which silently hides terminal states.
99
+
100
+ ## Partial fills
101
+
102
+ A 1,000 USDC order does not need one 1,000 USDC buyer. The deposit accepts
103
+ intents between `intentAmountRange.min` (default 1 USDC) and the full amount.
104
+ Three buyers can take 400 + 350 + 250. The order shows each as a `fill` and
105
+ passes through `delivering` until the last one completes. `filledAmount`,
106
+ `pendingAmount`, and `totalAmount` always add up against the chain.
107
+
108
+ ## The ETA principle
109
+
110
+ `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 }`.
115
+
116
+ - **Buyer arrival time is market-driven.** A deposit at market rate should
117
+ fill fast, but the ETA is only a recent historical sample.
118
+ - **The binding rate resolves at fill time.** `estimate()` reads the same
119
+ Chainlink feed the escrow will read, but between estimate and fill the
120
+ market moves. `kind: 'oracle-estimate'` is the API telling you this.
121
+ - **The label is display-ready.** Use `eta.label` in simple UIs; use
122
+ `eta.seconds` only if you need your own formatting.
123
+
124
+ Anything that looks like a committed quote or guaranteed delivery timer in a
125
+ UI built on this SDK is a bug in that UI.
126
+
127
+ ## Managing a live order
128
+
129
+ - **Top up** - `topUp(depositId, amount)` adds USDC to a live order: same
130
+ payee, same market-rate pricing, no new registration. Closed orders reject
131
+ with `ORDER_NOT_ACTIVE`; start a new `cashout()` instead.
132
+ - **Partial withdrawal** - `withdraw(depositId, { amount })` pulls part of
133
+ the _unlocked_ balance back out. A live buyer intent does not block it
134
+ (their locked portion is untouched); asking for more than the unlocked
135
+ balance fails with `INSUFFICIENT_AVAILABLE_FUNDS`.
136
+ - There is no retain-on-empty or rate knob to manage - a cash order cleans
137
+ itself up when fully filled, and the market rate is not configurable.
138
+
139
+ ## Receipts, decoded
140
+
141
+ Everything an order serves is decoded to human units: platform ids and
142
+ currency codes instead of bytes32 hashes, plain-number rates instead of 1e18
143
+ bigints (raw values stay available as `*Hash` / `conversionRate` fields).
144
+
145
+ Each fill is a receipt that sharpens over its life:
146
+
147
+ - **At signal**: `rate` (the oracle rate locked for THIS fill - the moment
148
+ "approximately" becomes exact) and `fiatOwed` (`amount × rate`, rounded up
149
+ to the cent, matching what the buyer's client tells them to pay).
150
+ - **After the proof**: `fiatPaid` (the verified amount actually sent),
151
+ `paidCurrency`, `paymentId` (the platform's own payment reference),
152
+ `paidAt`, `releasedAmount` (USDC actually released), and
153
+ `fillLatencySeconds` (signal → proven delivery).
154
+
155
+ `fiatOwed` and `fiatPaid` are decoded to whole currency units. The verified
156
+ `paymentAmount` arrives from the indexer in cents (2 decimals) - the same
157
+ convention the first-party Peer clients display it with - and the SDK
158
+ divides by 100. The signal-time `fiatOwed` derives from `amount × rate` at
159
+ 1e18 precision; `resolveIntentFiatAmount` in the reference clients uses the
160
+ same ceil-to-cent math, and the decode is verified against live production
161
+ receipts.
162
+
163
+ Orders also carry their `payouts` legs reconstructed from the chain -
164
+ platform, currency, payee hash, and a pricing proof (`spreadBps: 0`,
165
+ `kind: 'oracle_chainlink'`, `marketRate: true`): the zero-spread claim is a
166
+ queryable fact, not marketing copy.
167
+
168
+ ## Who is this buyer?
169
+
170
+ `buyer(address)` aggregates the matched buyer's full intent history into a
171
+ track record: lifetime intents, fulfilled vs pruned counts, a success rate in
172
+ basis points, first/last seen. Use it during `matched` - the moment a
173
+ stranger's address is holding your order is exactly when a 95%-success,
174
+ 200-order counterparty reads very differently from a fresh wallet.
175
+
176
+ ## Unwinding: one verb
177
+
178
+ `withdraw(depositId, { signer })` handles every recovery case:
179
+
180
+ 1. **No buyer yet** (`awaiting-buyer`): withdraws directly. Funds return in
181
+ one transaction.
182
+ 2. **Buyer signaled but never paid**: their intent expires on-chain. The
183
+ escrow still counts it as active, so `withdraw()` first sends
184
+ `pruneExpiredIntents`, then withdraws. Two transactions, one call.
185
+ 3. **Buyer is actively paying** (live intent): withdrawal would strand the
186
+ buyer, so the escrow blocks it and the SDK throws
187
+ `ACTIVE_INTENT_BLOCKS_WITHDRAWAL` (`retryable: true`). Wait for delivery
188
+ or expiry, then call `withdraw()` again.
189
+ 4. **Nothing left** (`delivered`/`returned`): throws `NOTHING_TO_WITHDRAW`.
190
+
191
+ There is deliberately no `cancel` vs `recover` split - the deposit state
192
+ decides, not the caller. Agents needing host-side signing use
193
+ `prepareWithdraw(depositId)` for the same logic as unsigned `txs[]`.
194
+ Prepare results also carry `steps[]` in the same order as `txs[]`, so a host
195
+ can show `pruneExpiredIntents` before `withdrawDeposit` instead of asking a
196
+ user or policy engine to approve opaque calldata.
197
+
198
+ ## Resumability
199
+
200
+ The `depositId` (composite `escrow_onchainId`) is the only key you need:
201
+
202
+ - `cashout()` returns it, parsed from the `DepositReceived` event in the
203
+ transaction receipt - available immediately, no indexer wait.
204
+ - `order(depositId)` reconstructs the full order from the indexer at any
205
+ time, on any machine. There is no session, no cache, no local store.
206
+ - Bind orders to your own users with one column in **your** database:
207
+ `userId → depositId`.
208
+
209
+ ## Indexer lag
210
+
211
+ Right after `cashout()`, the indexer may not have seen the deposit yet
212
+ (typically a few seconds). `order()` throws `ORDER_NOT_FOUND` with
213
+ `retryable: true`; `watch()` and the React hooks absorb this and keep
214
+ polling. Do not treat an immediate `ORDER_NOT_FOUND` as a lost deposit - the
215
+ transaction receipt you already hold is the source of truth.
216
+
217
+ ## Payee registration, over time
218
+
219
+ `cashout()` registers your payee with the curator once, keyed by a hash of the
220
+ handle; the registration has no TTL and stays resolvable for the deposit's
221
+ whole life. Two things to know for long-lived orders:
222
+
223
+ - For the **live-validated platforms** (Venmo, Revolut, Cash App, Monzo), if
224
+ the underlying account is later deleted, the curator can revoke the payee at
225
+ a buyer's intent-signing time - a buyer then cannot take the deposit until
226
+ the payee is re-registered. Format-only platforms (Zelle, Chime, …) are
227
+ never re-checked.
228
+ - **Wise and PayPal** require a signed identity attestation for a new payee
229
+ registration. A previously registered handle can be reused with bare payee
230
+ data. If the handle is new and no attestation is supplied, the SDK surfaces
231
+ `PAYEE_VERIFICATION_REQUIRED`; `capabilities()` flags these platforms with
232
+ `requiresIdentityAttestation: true`.
233
+
234
+ The client selects a curator with its environment. Preproduction defaults to
235
+ `https://api-preprod.zkp2p.xyz`, staging defaults to
236
+ `https://api-staging.zkp2p.xyz`, and `curatorUrl` remains available as an
237
+ explicit override.
238
+
239
+ ## Failure table
240
+
241
+ | Code | Retryable | What happened / what to do |
242
+ | --------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
243
+ | `ORACLE_UNSUPPORTED_CURRENCY` | no | Currency has no Chainlink feed. Pick from `capabilities()`. |
244
+ | `ORACLE_READ_FAILED` | yes | The live Chainlink read failed. Retry through a healthy Base RPC; do not present a cached value as live. |
245
+ | `UNSUPPORTED_PLATFORM` | no | Platform is absent from this environment's catalog. Pick from `capabilities()`. |
246
+ | `UNSUPPORTED_PLATFORM_CURRENCY` | no | The platform does not support that currency. Use its `capabilities()` currencies. |
247
+ | `AMOUNT_BELOW_MINIMUM` | no | Amount is below the $0.01 hard floor. The recommended minimum is 1 USDC. |
248
+ | `INVALID_INTENT_AMOUNT_RANGE` | no | Min/max is non-positive, inverted, or exceeds the deposit. Correct the range. |
249
+ | `PAYEE_VERIFICATION_REQUIRED` | no | A new Wise/PayPal payee needs an attestation. Register it through Peer first; an existing registration can be reused. |
250
+ | `PAYEE_REGISTRATION_FAILED` | yes | Curator rejected the handle or was unavailable. Check `payeeHint` and retry. |
251
+ | `SOURCE_ROUTE_UNSUPPORTED_IN_PREPARE` | no | `prepare()` accepts Base USDC only. Use signed source execution, or complete Relay first and then prepare the Base cashout. |
252
+ | `SOURCE_RECIPIENT_MISMATCH` | no | Relay output recipient differs from the cashout depositor. Use the depositor address. |
253
+ | `SOURCE_CAPABILITIES_FAILED` | yes | Relay source discovery failed. Retry or use Base USDC. |
254
+ | `SOURCE_QUOTE_FAILED` | yes | Relay returned no valid canonical Base-USDC route. Refresh capabilities and quote again. |
255
+ | `SOURCE_EXECUTION_FAILED` | no | Route execution did not report success. Inspect source transactions and Relay status before retrying. |
256
+ | `SOURCE_STATUS_FAILED` | yes | Relay status is temporarily unavailable. Retry the status read without resubmitting. |
257
+ | `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. |
258
+ | `SOURCE_CASHOUT_SUBMISSION_UNKNOWN` | no | Relay completed but Base submission returned no hash. Inspect wallet activity and orders before any retry. |
259
+ | `SOURCE_CASHOUT_STATUS_UNKNOWN` | no | Relay completed and the Base tx was submitted, but its receipt is unknown. Inspect `depositTxHash`; do not resubmit while status is unknown. |
260
+ | `INSUFFICIENT_TOKEN_BALANCE` | no | Wallet lacks the required token amount. Fund it, then retry. |
261
+ | `ALLOWANCE_NOT_VISIBLE` | yes | Approval mined but a stale RPC replica hid it. Retry the same call after the allowance becomes visible. |
262
+ | `TRANSACTION_FAILED` | no | The on-chain call failed or reverted. Inspect the cause and transaction before another action. |
263
+ | `TRANSACTION_SUBMISSION_UNKNOWN` | no | A Base mutation returned no hash but may have broadcast. Inspect wallet/protocol state and its recovery action before any retry. |
264
+ | `TRANSACTION_STATUS_UNKNOWN` | no | A transaction was submitted but its receipt is unknown. Inspect `recovery.transactionHash` before resubmitting. |
265
+ | `DEPOSIT_RESOLUTION_FAILED` | no | Base tx succeeded but no `DepositReceived` was decoded. Inspect its logs and recover the composite id. |
266
+ | `INVALID_DEPOSIT_ID` | no | The id is not `escrowAddress_onchainId`. A bare number cannot cold-hydrate; use the value returned by `cashout()`. |
267
+ | `ORDER_NOT_FOUND` | yes | Unknown id or immediate indexer lag. Verify the id and retry shortly after creation. |
268
+ | `INDEXER_LAG` | yes | Indexer trails the chain. Retry the read shortly. |
269
+ | `INDEXER_UNAVAILABLE` | yes | The indexer read failed. Retry only that read with the same id or owner; never repeat an on-chain transaction. |
270
+ | `ACTIVE_INTENT_BLOCKS_WITHDRAWAL` | yes | A buyer may still deliver. Wait for fill or expiry, or withdraw only the unlocked amount. |
271
+ | `INSUFFICIENT_AVAILABLE_FUNDS` | yes | Partial withdrawal exceeds unlocked funds. Lower the amount. |
272
+ | `NOTHING_TO_WITHDRAW` | no | Order is terminal. Reconcile `order(depositId)`. |
273
+ | `ORDER_NOT_ACTIVE` | no | A closed order cannot be topped up. Start a new cashout. |
274
+ | `SIGNER_REQUIRED` | no | Pass a signer or use a Base-USDC `prepare*` path. |
275
+ | `SIGNER_CHAIN_MISMATCH` | no | Switch the signer to the required chain and refresh any Relay quote before retrying. |
276
+ | `SIGNER_CHAIN_UNAVAILABLE` | yes | The wallet could not report its live chain. Reconnect it and prove the required chain before retrying. |
277
+ | `WATCH_TIMEOUT` | yes | Order remains live. Resume `watch()` or `order()` later. |
278
+ | `ESCROW_PAUSED` | yes | Deposits are paused. Existing funds remain withdrawable. |
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Example: wiring the cash verbs into an agent tool-use loop.
3
+ *
4
+ * The host owns signing: mutating tools return UNSIGNED transactions the host
5
+ * submits with its own key management. Everything crossing the tool boundary
6
+ * is JSON - the codecs make that lossless.
7
+ *
8
+ * Run: bun examples/agent-tool-use.ts (read-only tools run against staging)
9
+ */
10
+ import { createCashClient, usdc, isCashError } from '@zkp2p/cash';
11
+ import {
12
+ buyerProfileToJson,
13
+ capabilitiesToJson,
14
+ estimateToJson,
15
+ orderToJson,
16
+ prepareResultToJson,
17
+ preparedTxToJson,
18
+ relayQuoteToJson,
19
+ relayStatusToJson,
20
+ } from '@zkp2p/cash';
21
+ import { cashToolManifest } from '@zkp2p/cash/tools';
22
+
23
+ const cash = createCashClient({ environment: 'staging' });
24
+
25
+ /** The host's tool executor: tool name + JSON args in, JSON result out. */
26
+ async function executeTool(name: string, args: Record<string, unknown>): Promise<unknown> {
27
+ try {
28
+ switch (name) {
29
+ case 'cash_capabilities':
30
+ return capabilitiesToJson(
31
+ args.includeRelaySources
32
+ ? await cash.capabilities({ includeRelaySources: true })
33
+ : cash.capabilities(),
34
+ );
35
+ case 'cash_source_quote':
36
+ return relayQuoteToJson(
37
+ await cash.quoteSource({
38
+ user: args.user as string,
39
+ amount: BigInt(args.amount as string),
40
+ source: args.source as never,
41
+ ...(args.recipient ? { recipient: args.recipient as string } : {}),
42
+ ...(args.tradeType ? { tradeType: args.tradeType as never } : {}),
43
+ }),
44
+ );
45
+ case 'cash_estimate':
46
+ return estimateToJson(
47
+ await cash.estimate({
48
+ amount: BigInt(args.amount as string),
49
+ currency: args.currency as never,
50
+ ...(args.platform ? { platform: args.platform as string } : {}),
51
+ ...(args.source ? { source: args.source as never } : {}),
52
+ }),
53
+ );
54
+ case 'cash_cashout': {
55
+ // Tool/prepare path: Base USDC only. cash_source_quote is read-only;
56
+ // the host must execute and confirm Relay with its own signer/runtime,
57
+ // then call this tool with the guaranteed Base USDC amount.
58
+ const input = {
59
+ amount: BigInt(args.amount as string),
60
+ receive: args.receive as never,
61
+ };
62
+ return prepareResultToJson(await cash.prepare(input));
63
+ }
64
+ case 'cash_order':
65
+ return orderToJson(await cash.order(args.depositId as string));
66
+ case 'cash_orders': {
67
+ const orders = await cash.orders(args.owner as string, {
68
+ ...(args.inFlight !== undefined ? { inFlight: args.inFlight as boolean } : {}),
69
+ ...(args.limit !== undefined ? { limit: args.limit as number } : {}),
70
+ });
71
+ return orders.map(orderToJson);
72
+ }
73
+ case 'cash_buyer':
74
+ return buyerProfileToJson(await cash.buyer(args.address as string));
75
+ case 'cash_source_status':
76
+ return relayStatusToJson(await cash.relayStatus(args.requestId as string));
77
+ case 'cash_withdraw': {
78
+ const amount = args.amount !== undefined ? BigInt(args.amount as string) : undefined;
79
+ const { txs, steps } = await cash.prepareWithdraw(args.depositId as string, {
80
+ ...(amount !== undefined ? { amount } : {}),
81
+ });
82
+ return { txs: txs.map(preparedTxToJson), steps };
83
+ }
84
+ case 'cash_topup': {
85
+ const { txs, steps } = await cash.prepareTopUp(
86
+ args.depositId as string,
87
+ BigInt(args.amount as string),
88
+ );
89
+ return { txs: txs.map(preparedTxToJson), steps };
90
+ }
91
+ default:
92
+ throw new Error(`Unknown tool: ${name}`);
93
+ }
94
+ } catch (err) {
95
+ // Typed errors serialize cleanly into tool results the model can act on.
96
+ if (isCashError(err)) return { error: err.toJSON() };
97
+ throw err;
98
+ }
99
+ }
100
+
101
+ // --- Demo loop: what an agent host would do with the manifest ---
102
+
103
+ console.log(`manifest: ${cashToolManifest.name}@${cashToolManifest.version}`);
104
+ console.log(`tools: ${cashToolManifest.tools.map((t) => t.name).join(', ')}\n`);
105
+
106
+ const caps = (await executeTool('cash_capabilities', {})) as {
107
+ platforms: { platform: string; currencies: string[]; payeeHint: string }[];
108
+ };
109
+ console.log(`agent sees ${caps.platforms.length} platforms; venmo hint:`);
110
+ console.log(` "${caps.platforms.find((p) => p.platform === 'venmo')?.payeeHint}"\n`);
111
+
112
+ const est = await executeTool('cash_estimate', {
113
+ amount: usdc(250).toString(),
114
+ currency: 'EUR',
115
+ });
116
+ console.log('cash_estimate →', JSON.stringify(est), '\n');
117
+
118
+ // A typed error round-trips as data, not an exception:
119
+ const notFound = await executeTool('cash_order', {
120
+ depositId: '0x1111111111111111111111111111111111111111_999999',
121
+ });
122
+ console.log('cash_order on unknown id →', JSON.stringify(notFound));
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Example: server-side cash-out with a private-key signer, plus order tracking.
3
+ *
4
+ * Run: PRIVATE_KEY=0x... bun examples/node-cashout.ts
5
+ * (Use environment 'staging' and a throwaway dev wallet with a few USDC.)
6
+ *
7
+ * By default this demo withdraws its own deposit at the end so the test wallet
8
+ * is left untouched. A real integration leaves the deposit open for buyers -
9
+ * set CASH_KEEP_OPEN=1 for that behavior.
10
+ *
11
+ * The curator validates supported handles against the live platform, so the
12
+ * payee must be a real account. A new Wise/PayPal registration also needs the
13
+ * identity attestation created by Peer; an existing registered handle can be
14
+ * reused. Override the demo corridor with:
15
+ * CASH_PLATFORM=venmo CASH_CURRENCY=USD CASH_PAYEE=@your-venmo
16
+ */
17
+ import { createWalletClient, http } from 'viem';
18
+ import { privateKeyToAccount } from 'viem/accounts';
19
+ import { base } from 'viem/chains';
20
+ import { createCashClient, usdc, formatUsdc, isCashError } from '@zkp2p/cash';
21
+ import type { CurrencyType } from '@zkp2p/cash';
22
+
23
+ const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
24
+ const signer = createWalletClient({ account, chain: base, transport: http() });
25
+
26
+ const receive = {
27
+ platform: process.env.CASH_PLATFORM ?? 'venmo',
28
+ currency: (process.env.CASH_CURRENCY ?? 'USD') as CurrencyType,
29
+ payee: { offchainId: process.env.CASH_PAYEE ?? '@your-venmo' },
30
+ };
31
+
32
+ const cash = createCashClient({ environment: 'staging' });
33
+
34
+ // 0 - What can we do?
35
+ const caps = cash.capabilities();
36
+ console.log(
37
+ 'platforms:',
38
+ caps.platforms.map((p) => `${p.platform}(${p.currencies.join(',')})`).join(' '),
39
+ );
40
+
41
+ // 1 - What would 1 USDC get us, roughly?
42
+ const est = await cash.estimate({ amount: usdc(1), currency: receive.currency });
43
+ console.log(
44
+ `estimate: ≈ ${est.receiveAmount} ${receive.currency} at rate ${est.rate} (${est.kind})`,
45
+ );
46
+
47
+ // 2 - Cash out.
48
+ const result = await cash.cashout({ amount: usdc(1), receive }, { signer });
49
+ console.log(`deposit created: ${result.depositId} (tx ${result.txHash})`);
50
+ // Persist this in YOUR system: userId → result.depositId
51
+
52
+ // 3/5 - Track it. A real service would watch until terminal; the demo bails
53
+ // out after 30s (an unmatched deposit stays awaiting-buyer until someone bites).
54
+ try {
55
+ for await (const order of cash.watch(result.depositId, { timeoutMs: 30_000 })) {
56
+ console.log(`[${order.state}] ${order.explain()}`);
57
+ if (order.nextActions.length === 0) break;
58
+ }
59
+ } catch (err) {
60
+ if (isCashError(err) && err.code === 'WATCH_TIMEOUT') {
61
+ console.log('still live - resume any time with order(depositId)');
62
+ } else {
63
+ throw err;
64
+ }
65
+ }
66
+
67
+ // 4 - The wallet's full order history, in-flight first.
68
+ const open = await cash.orders(account.address, { inFlight: true });
69
+ console.log(`in-flight orders: ${open.length}`);
70
+ for (const order of open) {
71
+ console.log(` ${order.depositId}: ${order.state}, ${formatUsdc(order.totalAmount)} USDC`);
72
+ }
73
+
74
+ // 6 - Unwind. The demo cleans up after itself; a real integration would leave
75
+ // the deposit open for buyers instead (CASH_KEEP_OPEN=1).
76
+ if (!process.env.CASH_KEEP_OPEN) {
77
+ const withdrawn = await cash.withdraw(result.depositId, { signer });
78
+ console.log(`returned via ${withdrawn.withdrawTxHash}`);
79
+ }