@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 +89 -26
- package/README.md +66 -24
- package/dist/chunk-P3KYZ2FX.js +373 -0
- package/dist/{createCashClient-iHuGgjH_.d.cts → createCashClient-BbkfxILl.d.cts} +37 -8
- package/dist/{createCashClient-iHuGgjH_.d.ts → createCashClient-BbkfxILl.d.ts} +37 -8
- package/dist/index.cjs +1081 -245
- package/dist/index.d.cts +1554 -74
- package/dist/index.d.ts +1554 -74
- package/dist/index.js +868 -239
- package/dist/react.cjs +239 -59
- package/dist/react.d.cts +6 -4
- package/dist/react.d.ts +6 -4
- package/dist/react.js +236 -59
- package/dist/tools.cjs +36 -36
- package/dist/tools.d.cts +282 -3
- package/dist/tools.d.ts +282 -3
- package/dist/tools.js +36 -36
- package/docs/lifecycle-and-recovery.md +278 -0
- package/examples/agent-tool-use.ts +122 -0
- package/examples/node-cashout.ts +79 -0
- package/llms.txt +23 -5
- package/package.json +51 -21
- package/skills/peer-cash-integration/SKILL.md +79 -17
- package/dist/chunk-FKVPZVFH.js +0 -188
- package/dist/chunk-FKVPZVFH.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/react.cjs.map +0 -1
- package/dist/react.js.map +0 -1
- package/dist/tools.cjs.map +0 -1
- package/dist/tools.js.map +0 -1
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
|
|
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.
|
|
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: {
|
|
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: {
|
|
38
|
-
currency: {
|
|
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
|
-
|
|
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: {
|
|
77
|
-
currency: {
|
|
90
|
+
chainId: { ...chainId, description: "Relay-supported EVM source chain id." },
|
|
91
|
+
currency: { ...address, description: "Source token/native address." },
|
|
78
92
|
user: {
|
|
79
|
-
|
|
93
|
+
...address,
|
|
80
94
|
description: "Source wallet submitting the Relay transaction."
|
|
81
95
|
},
|
|
82
96
|
recipient: {
|
|
83
|
-
|
|
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
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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:
|
|
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
|
+
}
|