@paydlux/agent 0.1.20 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/agent.d.ts +14 -0
- package/dist/agent.js +52 -1
- package/dist/cli.js +162 -8
- package/dist/desk.d.ts +59 -0
- package/dist/desk.js +192 -1
- package/dist/escrow.d.ts +154 -0
- package/dist/escrow.js +325 -0
- package/dist/gateway.d.ts +101 -3
- package/dist/gateway.js +170 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/package.json +3 -2
- package/phone-app/app.js +26 -6
- package/phone-app/index.html +1 -1
- package/phone-app/xdr.js +23 -4
package/dist/escrow.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// Paydlux escrow — a payment the buyer locks, not a payment the seller holds.
|
|
2
|
+
//
|
|
3
|
+
// The claim on the website is: "the escrow that holds a payment until delivery
|
|
4
|
+
// is proven". This file is what makes that claim true, and it deliberately does
|
|
5
|
+
// NOT use a smart contract. Stellar has a native primitive for exactly this
|
|
6
|
+
// shape of deal — a claimable balance with two claimants and a time predicate —
|
|
7
|
+
// and it is strictly better than a contract for a payment rail:
|
|
8
|
+
//
|
|
9
|
+
// * no contract to audit, and no contract address that can be wrong
|
|
10
|
+
// * the money never sits in an account the seller controls
|
|
11
|
+
// * the reserve is paid by whoever creates the balance (the buyer), and the
|
|
12
|
+
// ledger gives it back when the balance is claimed
|
|
13
|
+
// * the rules are enforced by the network, not by our server being up
|
|
14
|
+
//
|
|
15
|
+
// The shape, verified against testnet (see stellar/escrow-check.ts):
|
|
16
|
+
//
|
|
17
|
+
// claimant 1 — the seller vault, predicate NOT(abs_before = releaseAt)
|
|
18
|
+
// so the vault can only claim AFTER the release time
|
|
19
|
+
// claimant 2 — the buyer, predicate abs_before = releaseAt
|
|
20
|
+
// so the buyer can claw the money back ONLY before it
|
|
21
|
+
//
|
|
22
|
+
// Three properties follow, and all three were observed on the ledger:
|
|
23
|
+
// 1. before releaseAt the vault cannot take the money, even with both of its
|
|
24
|
+
// keys (tx_failed on the claim)
|
|
25
|
+
// 2. after releaseAt the vault can take it, but only with BOTH keys — one
|
|
26
|
+
// signature is tx_bad_auth, so a stolen agent key cannot empty an escrow
|
|
27
|
+
// 3. before releaseAt the buyer can always take it back, no seller consent
|
|
28
|
+
//
|
|
29
|
+
// So the seller's incentive is to prove delivery before the timer: releasing
|
|
30
|
+
// early needs the human approval key anyway, and until then the money is the
|
|
31
|
+
// buyer's to reclaim. That is the whole product in two claimants.
|
|
32
|
+
import { Asset, Claimant, Horizon, Memo, Operation, TransactionBuilder, getClaimableBalanceIdFromResult, xdr, } from "@stellar/stellar-sdk";
|
|
33
|
+
import { passphrase } from "./vault.js";
|
|
34
|
+
const HORIZON_URL = {
|
|
35
|
+
testnet: "https://horizon-testnet.stellar.org",
|
|
36
|
+
public: "https://horizon.stellar.org",
|
|
37
|
+
};
|
|
38
|
+
export const horizonUrl = (network) => HORIZON_URL[network];
|
|
39
|
+
/** Horizon writes an asset as "native" or "USDC:Gissuer…". */
|
|
40
|
+
export function assetToHorizonString(asset) {
|
|
41
|
+
return asset.getAssetType() === "native"
|
|
42
|
+
? "native"
|
|
43
|
+
: `${asset.getCode()}:${asset.getIssuer()}`;
|
|
44
|
+
}
|
|
45
|
+
export function assetFromHorizonString(s) {
|
|
46
|
+
if (s === "native")
|
|
47
|
+
return Asset.native();
|
|
48
|
+
const [code, issuer] = String(s).split(":");
|
|
49
|
+
if (!code || !issuer)
|
|
50
|
+
throw new Error(`unrecognised asset string "${s}"`);
|
|
51
|
+
return new Asset(code, issuer);
|
|
52
|
+
}
|
|
53
|
+
async function getJson(url) {
|
|
54
|
+
const r = await fetch(url);
|
|
55
|
+
if (!r.ok) {
|
|
56
|
+
const e = new Error(`horizon ${r.status} on ${url.split("?")[0]}`);
|
|
57
|
+
e.status = r.status;
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
return r.json();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read a claimable balance back from the ledger and turn it into an Escrow.
|
|
64
|
+
*
|
|
65
|
+
* Horizon does NOT put the memo on the balance record — the memo lives on the
|
|
66
|
+
* transaction that created it. So the join between "this pile of money" and
|
|
67
|
+
* "that order" is one extra request. It is not optional: without it, two orders
|
|
68
|
+
* of the same price to the same vault are indistinguishable, and a seller could
|
|
69
|
+
* deliver order A and claim the escrow that was locked for order B.
|
|
70
|
+
*/
|
|
71
|
+
export async function readEscrow(network, balanceId) {
|
|
72
|
+
const h = horizonUrl(network);
|
|
73
|
+
let cb;
|
|
74
|
+
try {
|
|
75
|
+
cb = await getJson(`${h}/claimable_balances/${balanceId}`);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
if (e.status === 404 || e.status === 400)
|
|
79
|
+
return null;
|
|
80
|
+
throw e;
|
|
81
|
+
}
|
|
82
|
+
if (!cb?.claimants)
|
|
83
|
+
return null;
|
|
84
|
+
// The two claimants are distinguished by their predicate, not by position:
|
|
85
|
+
// the seller's is wrapped in `not`, the buyer's is the bare abs_before.
|
|
86
|
+
const seller = cb.claimants.find((c) => c.predicate?.not);
|
|
87
|
+
const buyer = cb.claimants.find((c) => !c.predicate?.not && c.predicate?.abs_before_epoch);
|
|
88
|
+
if (!seller || !buyer)
|
|
89
|
+
return null;
|
|
90
|
+
const txs = await getJson(`${h}/claimable_balances/${balanceId}/transactions?order=asc&limit=1`);
|
|
91
|
+
const tx = txs?._embedded?.records?.[0];
|
|
92
|
+
return {
|
|
93
|
+
balanceId,
|
|
94
|
+
amount: cb.amount,
|
|
95
|
+
asset: cb.asset,
|
|
96
|
+
vault: seller.destination,
|
|
97
|
+
buyer: buyer.destination,
|
|
98
|
+
releaseAt: Number(seller.predicate.not.abs_before_epoch),
|
|
99
|
+
txHash: tx?.hash ?? "",
|
|
100
|
+
memo: tx?.memo ?? "",
|
|
101
|
+
memoType: tx?.memo_type ?? "none",
|
|
102
|
+
claimed: false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Does this on-chain escrow actually pay THIS deal?
|
|
107
|
+
*
|
|
108
|
+
* Every field is checked against the terms the gateway issued, because the
|
|
109
|
+
* buyer is untrusted: a buyer could lock 0.0000001 XLM, or lock money to a
|
|
110
|
+
* different vault, or set the timer to next year, or reuse the escrow from a
|
|
111
|
+
* previous order. Anything short of an exact match is a refusal.
|
|
112
|
+
*/
|
|
113
|
+
export async function verifyEscrow(network, balanceId, want, opts = {}) {
|
|
114
|
+
const e = await readEscrow(network, balanceId);
|
|
115
|
+
if (!e)
|
|
116
|
+
return { ok: false, reason: "no such claimable balance (already claimed, or never created)" };
|
|
117
|
+
if (e.asset !== assetToHorizonString(want.asset))
|
|
118
|
+
return { ok: false, reason: `escrow holds ${e.asset}, this route is priced in ${assetToHorizonString(want.asset)}` };
|
|
119
|
+
if (Number(e.amount) !== Number(want.amount))
|
|
120
|
+
return { ok: false, reason: `escrow holds ${e.amount}, this route costs ${want.amount}` };
|
|
121
|
+
if (e.vault !== want.vault)
|
|
122
|
+
return { ok: false, reason: "escrow names a different seller vault" };
|
|
123
|
+
if (want.buyer && e.buyer !== want.buyer)
|
|
124
|
+
return { ok: false, reason: "escrow was locked by a different account" };
|
|
125
|
+
// The memo is the only link between money and order. Exact match, or no deal.
|
|
126
|
+
if (e.memoType !== "text" || e.memo !== want.orderId)
|
|
127
|
+
return { ok: false, reason: `escrow memo "${e.memo}" does not match order ${want.orderId}` };
|
|
128
|
+
const now = Math.floor(Date.now() / 1000);
|
|
129
|
+
if (e.releaseAt <= now && !opts.allowClosedWindow) {
|
|
130
|
+
return { ok: false, reason: "the refund window has already closed — the seller could already have claimed it" };
|
|
131
|
+
}
|
|
132
|
+
if (opts.minReleaseSeconds != null && e.releaseAt - now < opts.minReleaseSeconds)
|
|
133
|
+
return { ok: false, reason: "release timer is too soon to prove delivery" };
|
|
134
|
+
if (opts.maxReleaseSeconds != null && e.releaseAt - now > opts.maxReleaseSeconds)
|
|
135
|
+
return { ok: false, reason: "release timer is further out than this route allows" };
|
|
136
|
+
return { ok: true, escrow: e };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The balance id of the escrow a submitted transaction created.
|
|
140
|
+
*
|
|
141
|
+
* Horizon does not return this to the submitter, and the id is not derivable
|
|
142
|
+
* from the account alone — it comes out of the transaction RESULT. A buyer who
|
|
143
|
+
* locks money with their own code needs this to know what to put in the
|
|
144
|
+
* `x-payment` header, so it is exported rather than kept private.
|
|
145
|
+
*/
|
|
146
|
+
export function escrowBalanceId(resultXdr, index = 0) {
|
|
147
|
+
return getClaimableBalanceIdFromResult(xdr.TransactionResult.fromXdr(resultXdr, "base64"), index);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Lock an escrow. Needs the BUYER's key: the money leaves the buyer's account,
|
|
151
|
+
* so only the buyer can put it in escrow. The seller never gets a signature on
|
|
152
|
+
* this transaction — that is the point.
|
|
153
|
+
*/
|
|
154
|
+
export async function createEscrow(network, terms, buyerKey) {
|
|
155
|
+
if (!terms.buyer)
|
|
156
|
+
throw new Error("creating an escrow needs terms.buyer — the money leaves that account");
|
|
157
|
+
if (buyerKey.publicKey() !== terms.buyer)
|
|
158
|
+
throw new Error("the signing key is not the escrow buyer");
|
|
159
|
+
const h = new Horizon.Server(horizonUrl(network));
|
|
160
|
+
const acc = await h.loadAccount(terms.buyer);
|
|
161
|
+
const releaseAt = Math.floor(terms.releaseAt);
|
|
162
|
+
const tx = new TransactionBuilder(acc, { fee: "100", networkPassphrase: passphrase(network) })
|
|
163
|
+
.addOperation(Operation.createClaimableBalance({
|
|
164
|
+
asset: terms.asset,
|
|
165
|
+
amount: terms.amount,
|
|
166
|
+
// Order matters for readability only; the predicates are what enforce the deal.
|
|
167
|
+
claimants: [
|
|
168
|
+
new Claimant(terms.vault, Claimant.predicateNot(Claimant.predicateBeforeAbsoluteTime(String(releaseAt)))),
|
|
169
|
+
new Claimant(terms.buyer, Claimant.predicateBeforeAbsoluteTime(String(releaseAt))),
|
|
170
|
+
],
|
|
171
|
+
}))
|
|
172
|
+
// The memo is the order id. Without it the escrow cannot be matched to an
|
|
173
|
+
// order, and verifyEscrow() will refuse it.
|
|
174
|
+
.addMemo(Memo.text(terms.orderId))
|
|
175
|
+
.setTimeout(300)
|
|
176
|
+
.build();
|
|
177
|
+
tx.sign(buyerKey);
|
|
178
|
+
const res = await h.submitTransaction(tx);
|
|
179
|
+
const balanceId = getClaimableBalanceIdFromResult(xdr.TransactionResult.fromXdr(res.result_xdr, "base64"), 0);
|
|
180
|
+
const escrow = await readEscrow(network, balanceId);
|
|
181
|
+
if (!escrow)
|
|
182
|
+
throw new Error("escrow was created but Horizon will not show it yet");
|
|
183
|
+
return { escrow, txHash: res.hash };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The seller takes the escrow. Two keys, always: the vault's threshold is 2 and
|
|
187
|
+
* the claim is a medium-threshold operation, so the agent key alone gets
|
|
188
|
+
* tx_bad_auth. `approverKey` lives on the phone in a real deployment.
|
|
189
|
+
*
|
|
190
|
+
* Refuses before the release time rather than letting Horizon reject it, so the
|
|
191
|
+
* caller gets a sentence instead of an error code — and so a buggy scheduler
|
|
192
|
+
* cannot accidentally try to release early in a loop.
|
|
193
|
+
*/
|
|
194
|
+
export async function claimEscrow(network, balanceId, vaultAddress, keys) {
|
|
195
|
+
const e = await readEscrow(network, balanceId);
|
|
196
|
+
if (!e)
|
|
197
|
+
throw new Error("no such escrow — it has already been claimed or refunded");
|
|
198
|
+
if (e.vault !== vaultAddress)
|
|
199
|
+
throw new Error(`escrow ${balanceId.slice(0, 12)}… pays a different vault`);
|
|
200
|
+
const now = Math.floor(Date.now() / 1000);
|
|
201
|
+
if (now < e.releaseAt)
|
|
202
|
+
throw new Error(`release timer has not passed (${e.releaseAt - now}s to go)`);
|
|
203
|
+
const h = new Horizon.Server(horizonUrl(network));
|
|
204
|
+
const acc = await h.loadAccount(vaultAddress);
|
|
205
|
+
const tx = new TransactionBuilder(acc, { fee: "100", networkPassphrase: passphrase(network) })
|
|
206
|
+
.addOperation(Operation.claimClaimableBalance({ balanceId }))
|
|
207
|
+
.setTimeout(120)
|
|
208
|
+
.build();
|
|
209
|
+
tx.sign(keys.agent);
|
|
210
|
+
tx.sign(keys.approver);
|
|
211
|
+
const res = await h.submitTransaction(tx);
|
|
212
|
+
return { txHash: res.hash, claimedAt: now };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Release an escrow with the approver's key on a PHONE.
|
|
216
|
+
*
|
|
217
|
+
* `claimEscrow` needs both keys in one process. In a real deployment the human
|
|
218
|
+
* half never lives on the seller's machine — it lives on their phone. So build
|
|
219
|
+
* the claim here, sign it with the agent key, and hand it out as the same
|
|
220
|
+
* challenge the payment desk already uses: the phone reads a sentence
|
|
221
|
+
* ("claim 3.5 XLM from escrow ord_…"), signs, and sends back XDR.
|
|
222
|
+
*
|
|
223
|
+
* The phone is the only place the second signature can be added. This process
|
|
224
|
+
* cannot sign for it, cannot submit a completed claim, and cannot do anything
|
|
225
|
+
* with the half-signed transaction except wait.
|
|
226
|
+
*/
|
|
227
|
+
export async function prepareEscrowRelease(network, balanceId, vaultAddress, agent) {
|
|
228
|
+
const e = await readEscrow(network, balanceId);
|
|
229
|
+
if (!e)
|
|
230
|
+
throw new Error("no such escrow — it has already been claimed or refunded");
|
|
231
|
+
if (e.vault !== vaultAddress)
|
|
232
|
+
throw new Error(`escrow ${balanceId.slice(0, 12)}… pays a different vault`);
|
|
233
|
+
const now = Math.floor(Date.now() / 1000);
|
|
234
|
+
if (now < e.releaseAt)
|
|
235
|
+
throw new Error(`release timer has not passed (${e.releaseAt - now}s to go)`);
|
|
236
|
+
const h = new Horizon.Server(horizonUrl(network));
|
|
237
|
+
const acc = await h.loadAccount(vaultAddress);
|
|
238
|
+
const tx = new TransactionBuilder(acc, { fee: "100", networkPassphrase: passphrase(network) })
|
|
239
|
+
.addOperation(Operation.claimClaimableBalance({ balanceId }))
|
|
240
|
+
.addMemo(Memo.text(`release ${e.memo}`))
|
|
241
|
+
.setTimeout(1800)
|
|
242
|
+
.build();
|
|
243
|
+
tx.sign(agent);
|
|
244
|
+
return {
|
|
245
|
+
id: `escrow:${balanceId}`,
|
|
246
|
+
network,
|
|
247
|
+
xdr: tx.toXDR(),
|
|
248
|
+
summary: {
|
|
249
|
+
payee: vaultAddress,
|
|
250
|
+
amount: e.amount,
|
|
251
|
+
token: e.asset === "native" ? "XLM" : e.asset.split(":")[0],
|
|
252
|
+
tokenIssuer: e.asset === "native" ? "" : e.asset.split(":")[1] ?? "",
|
|
253
|
+
from: `escrow ${balanceId.slice(0, 16)}…`,
|
|
254
|
+
memo: `order ${e.memo} — release timer passed`,
|
|
255
|
+
expiresAt: Number(tx.timeBounds?.maxTime) || undefined,
|
|
256
|
+
},
|
|
257
|
+
hash: Buffer.from(tx.hash()).toString("hex"),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The buyer takes their money back. No seller signature, no permission, and it
|
|
262
|
+
* works right up to the release second — which is why the seller has to earn
|
|
263
|
+
* the release by proving delivery instead of waiting for the timer.
|
|
264
|
+
*/
|
|
265
|
+
export async function refundEscrow(network, balanceId, buyerKey) {
|
|
266
|
+
const e = await readEscrow(network, balanceId);
|
|
267
|
+
if (!e)
|
|
268
|
+
throw new Error("no such escrow — it has already been claimed or refunded");
|
|
269
|
+
if (e.buyer !== buyerKey.publicKey())
|
|
270
|
+
throw new Error("only the account that locked this escrow can refund it");
|
|
271
|
+
const now = Math.floor(Date.now() / 1000);
|
|
272
|
+
if (now >= e.releaseAt)
|
|
273
|
+
throw new Error("the refund window has closed");
|
|
274
|
+
const h = new Horizon.Server(horizonUrl(network));
|
|
275
|
+
const acc = await h.loadAccount(buyerKey.publicKey());
|
|
276
|
+
const tx = new TransactionBuilder(acc, { fee: "100", networkPassphrase: passphrase(network) })
|
|
277
|
+
.addOperation(Operation.claimClaimableBalance({ balanceId }))
|
|
278
|
+
.setTimeout(120)
|
|
279
|
+
.build();
|
|
280
|
+
tx.sign(buyerKey);
|
|
281
|
+
const res = await h.submitTransaction(tx);
|
|
282
|
+
return { txHash: res.hash };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Every escrow currently waiting on this vault, newest first.
|
|
286
|
+
*
|
|
287
|
+
* Horizon filters claimable balances by claimant, which is exactly the seller's
|
|
288
|
+
* question: "what money is sitting in escrow for me, and when does each one
|
|
289
|
+
* become mine?" A balance that has already been claimed disappears from this
|
|
290
|
+
* list, so it doubles as the pending-release queue.
|
|
291
|
+
*/
|
|
292
|
+
export async function pendingEscrows(network, vaultAddress, limit = 200) {
|
|
293
|
+
const h = horizonUrl(network);
|
|
294
|
+
const list = await getJson(`${h}/claimable_balances?claimant=${vaultAddress}&order=desc&limit=${Math.min(limit, 200)}`);
|
|
295
|
+
const out = [];
|
|
296
|
+
for (const rec of list?._embedded?.records ?? []) {
|
|
297
|
+
const e = await readEscrow(network, rec.id);
|
|
298
|
+
if (e)
|
|
299
|
+
out.push(e);
|
|
300
|
+
}
|
|
301
|
+
return out.sort((a, b) => a.releaseAt - b.releaseAt);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Has this escrow been settled, and by whom?
|
|
305
|
+
*
|
|
306
|
+
* A claimed balance vanishes from Horizon, so the answer is in the operations
|
|
307
|
+
* of the transaction that claimed it. This is the proof-of-delivery receipt:
|
|
308
|
+
* the buyer can look up the same number on any explorer.
|
|
309
|
+
*/
|
|
310
|
+
export async function escrowOutcome(network, balanceId) {
|
|
311
|
+
const h = horizonUrl(network);
|
|
312
|
+
let ops;
|
|
313
|
+
try {
|
|
314
|
+
ops = await getJson(`${h}/claimable_balances/${balanceId}/operations?order=desc&limit=5`);
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
if (e.status === 404 || e.status === 400)
|
|
318
|
+
return { settled: false };
|
|
319
|
+
throw e;
|
|
320
|
+
}
|
|
321
|
+
const claim = (ops?._embedded?.records ?? []).find((o) => o.type === "claim_claimable_balance");
|
|
322
|
+
if (!claim)
|
|
323
|
+
return { settled: false };
|
|
324
|
+
return { settled: true, by: claim.account ?? claim.source_account, txHash: claim.transaction_hash, at: claim.created_at };
|
|
325
|
+
}
|
package/dist/gateway.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
2
|
-
import { Transaction } from "@stellar/stellar-sdk";
|
|
2
|
+
import { Keypair, Transaction } from "@stellar/stellar-sdk";
|
|
3
3
|
import { StellarVault, StellarTxError, type Amount, type Mandate, type Network } from "./vault.ts";
|
|
4
4
|
import { ApprovalDesk, type DeskStore } from "./desk.ts";
|
|
5
5
|
import type { PolicyEngine } from "./policy.ts";
|
|
6
|
+
import { claimEscrow } from "./escrow.ts";
|
|
6
7
|
export declare const PAY_HEADER = "x-payment";
|
|
7
8
|
export type Route = {
|
|
8
9
|
uri: string;
|
|
9
10
|
price: Amount;
|
|
10
|
-
mode: "preauth" | "approval";
|
|
11
|
+
mode: "preauth" | "approval" | "escrow";
|
|
11
12
|
/** seconds the seller's content stays reserved while a human decides */
|
|
12
13
|
holdSeconds?: number;
|
|
13
14
|
/**
|
|
@@ -15,6 +16,12 @@ export type Route = {
|
|
|
15
16
|
* holdSeconds. Set it to 0 to make every single call cost its own approval.
|
|
16
17
|
*/
|
|
17
18
|
collectSeconds?: number;
|
|
19
|
+
/**
|
|
20
|
+
* escrow mode: how long the buyer keeps the right to take their money back.
|
|
21
|
+
* The seller cannot touch the money before this passes, so it is also the
|
|
22
|
+
* deadline for proving delivery. Default 3600.
|
|
23
|
+
*/
|
|
24
|
+
releaseSeconds?: number;
|
|
18
25
|
};
|
|
19
26
|
export type GatewayConfig = {
|
|
20
27
|
vault: StellarVault;
|
|
@@ -33,6 +40,12 @@ export type GatewayConfig = {
|
|
|
33
40
|
* phone sends back.
|
|
34
41
|
*/
|
|
35
42
|
split?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The agent seed, passed to the desk so it can build an ESCROW RELEASE
|
|
45
|
+
* challenge (the claim is signed agent + phone approver). Same trust level
|
|
46
|
+
* as `principal`: it is the machine-side half, never the approving half.
|
|
47
|
+
*/
|
|
48
|
+
agentSeed?: string;
|
|
36
49
|
deskPort?: number;
|
|
37
50
|
/** gate the approval portal behind a single unguessable link */
|
|
38
51
|
portalKey?: string;
|
|
@@ -69,6 +82,12 @@ export type PaymentProof = {
|
|
|
69
82
|
payload: string;
|
|
70
83
|
signature: string;
|
|
71
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* escrow mode: the id of the claimable balance the BUYER locked for this
|
|
87
|
+
* order. The money is not the seller's yet — it becomes claimable after the
|
|
88
|
+
* release timer, and only with both human keys.
|
|
89
|
+
*/
|
|
90
|
+
escrow?: string;
|
|
72
91
|
};
|
|
73
92
|
export declare class Gateway {
|
|
74
93
|
private routes;
|
|
@@ -80,6 +99,66 @@ export declare class Gateway {
|
|
|
80
99
|
constructor(cfg: GatewayConfig);
|
|
81
100
|
/** Mandates the vault currently holds, so the gateway can hand them out. */
|
|
82
101
|
syncMandates(): Promise<any>;
|
|
102
|
+
/**
|
|
103
|
+
* Escrow orders this gateway has issued, keyed by order id.
|
|
104
|
+
*
|
|
105
|
+
* The order id is the ONLY link between a pile of money on the ledger and a
|
|
106
|
+
* piece of content behind a route: Horizon does not put a memo on the balance
|
|
107
|
+
* record, so matching money to a deal means reading the memo off the creating
|
|
108
|
+
* transaction and comparing it to an order we recognise. An escrow whose memo
|
|
109
|
+
* matches nothing here is money locked for a deal this seller never quoted, so
|
|
110
|
+
* it buys nothing.
|
|
111
|
+
*/
|
|
112
|
+
private orders;
|
|
113
|
+
/**
|
|
114
|
+
* A stable order id for one request. Deterministic on purpose: a buyer who
|
|
115
|
+
* retries while the escrow settles must land on the same order, and a seller
|
|
116
|
+
* who restarts must be able to recognise the memo on money already locked.
|
|
117
|
+
* It is not a secret and it is not a capability — holding an id buys nothing
|
|
118
|
+
* without the matching escrow on the ledger.
|
|
119
|
+
*/
|
|
120
|
+
orderId(uri: string, price: Amount, body: string): string;
|
|
121
|
+
/** Escrow orders, oldest first, with whatever the ledger says about each. */
|
|
122
|
+
escrowOrders(opts?: {
|
|
123
|
+
includeSettled?: boolean;
|
|
124
|
+
}): Promise<{
|
|
125
|
+
id: string;
|
|
126
|
+
uri: string;
|
|
127
|
+
price: Amount;
|
|
128
|
+
at: number;
|
|
129
|
+
escrowId?: string;
|
|
130
|
+
releaseAt?: number;
|
|
131
|
+
served: number;
|
|
132
|
+
state: string;
|
|
133
|
+
escrow: import("./escrow.ts").Escrow | null;
|
|
134
|
+
outcome: {
|
|
135
|
+
settled: boolean;
|
|
136
|
+
by?: string;
|
|
137
|
+
txHash?: string;
|
|
138
|
+
at?: string;
|
|
139
|
+
};
|
|
140
|
+
}[]>;
|
|
141
|
+
/**
|
|
142
|
+
* Take the money the buyer locked. Two keys, always — the vault's threshold
|
|
143
|
+
* is 2, so a stolen agent key cannot empty an escrow on its own.
|
|
144
|
+
*/
|
|
145
|
+
releaseEscrow(orderId: string, keys: {
|
|
146
|
+
agent: Parameters<typeof claimEscrow>[3]["agent"];
|
|
147
|
+
approver: Parameters<typeof claimEscrow>[3]["approver"];
|
|
148
|
+
}): Promise<{
|
|
149
|
+
txHash: string;
|
|
150
|
+
claimedAt: number;
|
|
151
|
+
}>;
|
|
152
|
+
/**
|
|
153
|
+
* The split-key release: build the claim, sign it with the agent key, and
|
|
154
|
+
* hand it to the phone. This process cannot finish it — that is the point.
|
|
155
|
+
*/
|
|
156
|
+
escrowReleaseChallenge(orderId: string, agent: Keypair): Promise<import("./phone.ts").ApprovalChallenge>;
|
|
157
|
+
/** Take the phone's signature back and settle the escrow on the ledger. */
|
|
158
|
+
submitEscrowRelease(signedXdr: string, orderId: string): Promise<{
|
|
159
|
+
hash: string;
|
|
160
|
+
ok: true;
|
|
161
|
+
}>;
|
|
83
162
|
/** Register a pre-authorised mandate for a route (needs both human keys). */
|
|
84
163
|
authorise(uri: string, spec: Parameters<StellarVault["createMandate"]>[0], principal: Parameters<StellarVault["createMandate"]>[1], approver: Parameters<StellarVault["createMandate"]>[2]): Promise<Mandate>;
|
|
85
164
|
/**
|
|
@@ -104,9 +183,28 @@ export declare class Gateway {
|
|
|
104
183
|
issuer: string | undefined;
|
|
105
184
|
};
|
|
106
185
|
vault: string;
|
|
107
|
-
mode: "approval" | "preauth";
|
|
186
|
+
mode: "approval" | "escrow" | "preauth";
|
|
108
187
|
maxTimeoutSeconds: number;
|
|
109
188
|
version: string;
|
|
189
|
+
} | {
|
|
190
|
+
scheme: string;
|
|
191
|
+
network: string;
|
|
192
|
+
maxAmountRequired: string;
|
|
193
|
+
resource: string;
|
|
194
|
+
payTo: string;
|
|
195
|
+
token: {
|
|
196
|
+
code: string;
|
|
197
|
+
issuer: string | undefined;
|
|
198
|
+
};
|
|
199
|
+
vault: string;
|
|
200
|
+
mode: "approval" | "escrow" | "preauth";
|
|
201
|
+
maxTimeoutSeconds: number;
|
|
202
|
+
version: string;
|
|
203
|
+
escrow: {
|
|
204
|
+
vault: string;
|
|
205
|
+
releaseSeconds: number;
|
|
206
|
+
memoMustBe: string;
|
|
207
|
+
};
|
|
110
208
|
};
|
|
111
209
|
/**
|
|
112
210
|
* Check a proof against the live request before spending anything.
|