@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/desk.js
CHANGED
|
@@ -19,6 +19,7 @@ import { TransactionBuilder } from "@stellar/stellar-sdk";
|
|
|
19
19
|
import { passphrase } from "./vault.js";
|
|
20
20
|
import { portalPage } from "./portal.js";
|
|
21
21
|
import { createApprovalChallenge, submitApproved, challengeQrPayload, } from "./phone.js";
|
|
22
|
+
import { claimEscrow, prepareEscrowRelease } from "./escrow.js";
|
|
22
23
|
/**
|
|
23
24
|
* The approver app is plain static files under phone-app/. Read on each request
|
|
24
25
|
* rather than bundled, so the desk and the app can never drift apart.
|
|
@@ -121,6 +122,37 @@ function verifyAgainstChallenge(signedXdr, rec) {
|
|
|
121
122
|
function recNetwork(rec) {
|
|
122
123
|
return rec.challenge.network;
|
|
123
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Same binding check as verifyAgainstChallenge, for an escrow release. The
|
|
127
|
+
* signed transaction must be the claim the desk issued (same hash), carry both
|
|
128
|
+
* signatures, and — the escrow-specific part — claim EXACTLY the balance this
|
|
129
|
+
* record names. Without that last check a signature collected for one escrow
|
|
130
|
+
* could be replayed against another order's queue entry.
|
|
131
|
+
*/
|
|
132
|
+
function verifyEscrowAgainstChallenge(signedXdr, rec) {
|
|
133
|
+
const ch = rec.challenge;
|
|
134
|
+
let signed;
|
|
135
|
+
try {
|
|
136
|
+
signed = TransactionBuilder.fromXDR(signedXdr, passphrase(recNetwork(rec)));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return "the phone returned something that is not a valid transaction for this network";
|
|
140
|
+
}
|
|
141
|
+
const hash = Buffer.from(signed.hash()).toString("hex");
|
|
142
|
+
if (hash !== ch.hash) {
|
|
143
|
+
return `signed a different transaction (${hash.slice(0, 12)}… was not issued for ${rec.id}, which is ${ch.hash.slice(0, 12)}…)`;
|
|
144
|
+
}
|
|
145
|
+
if (signed.signatures.length < 2) {
|
|
146
|
+
return `only ${signed.signatures.length} signature(s); the vault needs 2`;
|
|
147
|
+
}
|
|
148
|
+
const op = signed.operations?.[0];
|
|
149
|
+
if (!op || op.type !== "claimClaimableBalance")
|
|
150
|
+
return "the transaction is not an escrow claim";
|
|
151
|
+
if (op.balanceId !== rec.escrowId) {
|
|
152
|
+
return `the claim targets ${String(op.balanceId).slice(0, 12)}…, not the escrow ${String(rec.escrowId).slice(0, 12)}… this approval is for`;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
124
156
|
function readBody(req) {
|
|
125
157
|
return new Promise((resolve, reject) => {
|
|
126
158
|
let data = "";
|
|
@@ -222,6 +254,46 @@ export class ApprovalDesk {
|
|
|
222
254
|
findByKey(key) {
|
|
223
255
|
return this.allByKey(key)[0] ?? null;
|
|
224
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Queue an escrow release for the human. This is the missing half of
|
|
259
|
+
* `paydlux escrow --release` on a --split vault: the money already sits in a
|
|
260
|
+
* claimable balance the buyer locked, so releasing it is the vault CLAIMING
|
|
261
|
+
* that balance — money coming IN, not going out. The claim still needs the
|
|
262
|
+
* vault's threshold of 2 (agent + approver), and the approver half lives on
|
|
263
|
+
* the phone, so it goes through the same challenge/sign/submit loop as a
|
|
264
|
+
* payment, tagged `kind: "escrow"` so the spending mandate — which caps
|
|
265
|
+
* money OUT — never touches it.
|
|
266
|
+
*
|
|
267
|
+
* The generic fields (uri/price/body) are filled so the portal page renders
|
|
268
|
+
* the row without knowing about escrow; the structured fields carry the
|
|
269
|
+
* truth the verification uses.
|
|
270
|
+
*/
|
|
271
|
+
addEscrow(input) {
|
|
272
|
+
this.sweepExpired();
|
|
273
|
+
// One live record per balance id. A seller who runs --release twice must
|
|
274
|
+
// not get two Approve buttons for the same money: the second signature
|
|
275
|
+
// would fail on the ledger (balance already claimed) and confuse the human.
|
|
276
|
+
for (const r of this.records.values()) {
|
|
277
|
+
if (r.kind === "escrow" && r.escrowId === input.escrowId &&
|
|
278
|
+
r.status !== "denied" && r.status !== "failed" && r.status !== "expired")
|
|
279
|
+
return r;
|
|
280
|
+
}
|
|
281
|
+
const id = `esc_${input.escrowId.slice(-8)}${randomBytes(3).toString("hex")}`;
|
|
282
|
+
const label = input.order || `${input.escrowId.slice(0, 16)}…`;
|
|
283
|
+
const rec = {
|
|
284
|
+
id, at: Date.now(),
|
|
285
|
+
uri: `/escrow/${label}`,
|
|
286
|
+
// amount only, like a payment price; the asset rides in `asset` and
|
|
287
|
+
// reaches the phone as `token`, so the row never prints "3.5 XLM XLM".
|
|
288
|
+
price: input.amount,
|
|
289
|
+
body: `escrow release — buyer ${input.buyer ?? "?"} — balance ${input.escrowId}`,
|
|
290
|
+
status: "pending",
|
|
291
|
+
kind: "escrow", escrowId: input.escrowId, order: input.order, asset: input.asset,
|
|
292
|
+
};
|
|
293
|
+
this.records.set(id, rec);
|
|
294
|
+
console.log(`[desk] ${id} escrow release awaiting a human: ${rec.price} (${label})`);
|
|
295
|
+
return rec;
|
|
296
|
+
}
|
|
225
297
|
get(id) { this.sweepExpired(); return this.records.get(id) ?? null; }
|
|
226
298
|
list() { this.sweepExpired(); return [...this.records.values()]; }
|
|
227
299
|
/**
|
|
@@ -285,6 +357,27 @@ export class ApprovalDesk {
|
|
|
285
357
|
throw new Error("unknown handle " + id);
|
|
286
358
|
if (rec.status !== "pending")
|
|
287
359
|
throw new Error(`already ${rec.status}`);
|
|
360
|
+
// An escrow release is money coming IN. The spending mandate caps money
|
|
361
|
+
// going OUT, so applying it here would refuse a release for a budget that
|
|
362
|
+
// the release does not touch — and would make the seller's own money
|
|
363
|
+
// unclaimable on a day they already spent.
|
|
364
|
+
if (rec.kind === "escrow") {
|
|
365
|
+
// dev mode (both keys on this machine): claim straight away.
|
|
366
|
+
if (this.cfg.approver && this.cfg.agentSeed) {
|
|
367
|
+
try {
|
|
368
|
+
const out = await claimEscrow(this.cfg.vault.network, rec.escrowId, this.cfg.vault.address, { agent: Keypair.fromSecret(this.cfg.agentSeed), approver: Keypair.fromSecret(this.cfg.approver) });
|
|
369
|
+
rec.status = "settled";
|
|
370
|
+
rec.tx = out.txHash;
|
|
371
|
+
console.log(`[desk] ${rec.id} ESCROW RELEASED ${rec.price} — ledger ${rec.tx}`);
|
|
372
|
+
}
|
|
373
|
+
catch (e) {
|
|
374
|
+
rec.status = "failed";
|
|
375
|
+
rec.reason = e.message;
|
|
376
|
+
}
|
|
377
|
+
return rec;
|
|
378
|
+
}
|
|
379
|
+
return this.requestEscrowSignature(rec);
|
|
380
|
+
}
|
|
288
381
|
// Check before asking the phone. A human who approves a payment the mandate
|
|
289
382
|
// will refuse has spent a Face ID scan and a signature for nothing.
|
|
290
383
|
if (this.mandateBlock(rec, "refused before asking"))
|
|
@@ -344,6 +437,64 @@ export class ApprovalDesk {
|
|
|
344
437
|
}
|
|
345
438
|
return rec;
|
|
346
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* Split mode step 1 for an ESCROW release: build the claim, sign it with the
|
|
442
|
+
* agent key, hand it to the phone. `prepareEscrowRelease` reads the ledger
|
|
443
|
+
* and refuses a balance that does not exist or whose timer has not passed,
|
|
444
|
+
* so a queued-but-fake escrow dies here with a real reason instead of
|
|
445
|
+
* reaching the human as a signable challenge.
|
|
446
|
+
*/
|
|
447
|
+
async requestEscrowSignature(rec) {
|
|
448
|
+
if (!this.cfg.agentSeed) {
|
|
449
|
+
rec.status = "failed";
|
|
450
|
+
rec.reason = "an escrow release needs the agent seed on this machine to build the claim";
|
|
451
|
+
return rec;
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const challenge = await prepareEscrowRelease(this.cfg.vault.network, rec.escrowId, this.cfg.vault.address, Keypair.fromSecret(this.cfg.agentSeed));
|
|
455
|
+
// The challenge carries its own id; keep the desk's handle so
|
|
456
|
+
// /challenge/:id and /signed/:id line up with what the queue shows.
|
|
457
|
+
challenge.id = rec.id;
|
|
458
|
+
rec.challenge = challenge;
|
|
459
|
+
rec.qr = challengeQrPayload(challenge);
|
|
460
|
+
rec.status = "awaiting_phone";
|
|
461
|
+
console.log(`[desk] ${rec.id} escrow claim sent to the phone: ${rec.price}`);
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
rec.status = "failed";
|
|
465
|
+
rec.reason = e.message;
|
|
466
|
+
console.log(`[desk] ${rec.id} escrow claim refused before asking: ${rec.reason}`);
|
|
467
|
+
}
|
|
468
|
+
return rec;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Split mode step 2 for an ESCROW release: the phone signed the claim.
|
|
472
|
+
* No mandate check — this is money coming IN — and no recordSpend: the
|
|
473
|
+
* daily budget counts payments OUT, and counting a release against it would
|
|
474
|
+
* make the seller's own earned money unclaimable.
|
|
475
|
+
*/
|
|
476
|
+
async completeEscrow(rec, signedXdr) {
|
|
477
|
+
const problem = verifyEscrowAgainstChallenge(signedXdr, rec);
|
|
478
|
+
if (problem) {
|
|
479
|
+
rec.status = "failed";
|
|
480
|
+
rec.reason = problem;
|
|
481
|
+
console.log(`[desk] ${rec.id} rejected: ${problem}`);
|
|
482
|
+
return rec;
|
|
483
|
+
}
|
|
484
|
+
rec.signedXdr = signedXdr;
|
|
485
|
+
try {
|
|
486
|
+
const res = await submitApproved(signedXdr, this.cfg.vault.network, this.cfg.vault.horizon);
|
|
487
|
+
rec.status = "settled";
|
|
488
|
+
rec.tx = res.hash;
|
|
489
|
+
console.log(`[desk] ${rec.id} ESCROW RELEASED ${rec.price} — ledger ${rec.tx}`);
|
|
490
|
+
}
|
|
491
|
+
catch (e) {
|
|
492
|
+
rec.status = "failed";
|
|
493
|
+
rec.reason = e.message;
|
|
494
|
+
console.log(`[desk] ${rec.id} failed on the ledger: ${rec.reason}`);
|
|
495
|
+
}
|
|
496
|
+
return rec;
|
|
497
|
+
}
|
|
347
498
|
/**
|
|
348
499
|
* Split mode step 2: the phone answered. `signedXdr` is the transaction with
|
|
349
500
|
* the approver signature added, returned by signChallenge() on the phone.
|
|
@@ -359,6 +510,11 @@ export class ApprovalDesk {
|
|
|
359
510
|
throw new Error(`not awaiting a phone signature (it is ${rec.status})`);
|
|
360
511
|
if (!rec.challenge)
|
|
361
512
|
throw new Error("no challenge was issued for " + id);
|
|
513
|
+
// An escrow release verifies against its own shape (a claim of THIS
|
|
514
|
+
// balance id) and bypasses the spending mandate, which only caps payments
|
|
515
|
+
// going out.
|
|
516
|
+
if (rec.kind === "escrow")
|
|
517
|
+
return this.completeEscrow(rec, signedXdr);
|
|
362
518
|
// Bind the answer to the question. Without this check the desk would submit
|
|
363
519
|
// any signed transaction posted to this route — including one the phone
|
|
364
520
|
// signed for a DIFFERENT payment, at a different price. That is how a
|
|
@@ -432,9 +588,14 @@ export class ApprovalDesk {
|
|
|
432
588
|
tx: r.tx,
|
|
433
589
|
/** what the buyer says it wants. Agent-controlled text: render as text. */
|
|
434
590
|
wants: (r.body ?? "").slice(0, 200),
|
|
435
|
-
token: this.cfg.vault.token.getCode(),
|
|
591
|
+
token: r.kind === "escrow" ? r.asset ?? this.cfg.vault.token.getCode() : this.cfg.vault.token.getCode(),
|
|
436
592
|
expiresAt: r.challenge?.summary.expiresAt,
|
|
437
593
|
hash: r.challenge?.hash,
|
|
594
|
+
// The phone renders an escrow row differently: it is money IN, and the
|
|
595
|
+
// human verifies the balance id, not a payee.
|
|
596
|
+
kind: r.kind ?? "payment",
|
|
597
|
+
escrowId: r.escrowId,
|
|
598
|
+
order: r.order,
|
|
438
599
|
}));
|
|
439
600
|
}
|
|
440
601
|
/** Serve the pending list as JSON (the portal polls this). */
|
|
@@ -635,7 +796,37 @@ export class ApprovalDesk {
|
|
|
635
796
|
wants: (rec.body ?? "").slice(0, 200),
|
|
636
797
|
memo: rec.challenge.summary.memo ?? "",
|
|
637
798
|
expiresAt: rec.challenge.summary.expiresAt,
|
|
799
|
+
// An escrow challenge renders differently on the phone: the op
|
|
800
|
+
// carries no amount and no payee, so the app shows the amount from
|
|
801
|
+
// here and asks the human to check the balance id.
|
|
802
|
+
kind: rec.kind ?? "payment",
|
|
803
|
+
escrowId: rec.escrowId,
|
|
804
|
+
// The claim op carries no amount, so the phone shows the amount
|
|
805
|
+
// the desk read from the ledger alongside the balance id it puts
|
|
806
|
+
// in front of the human.
|
|
807
|
+
amount: rec.kind === "escrow" ? rec.price : undefined,
|
|
808
|
+
asset: rec.kind === "escrow" ? rec.asset : undefined,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
// The CLI's hand-off for `paydlux escrow --release` on a --split vault:
|
|
812
|
+
// queue this balance on the running desk so the phone can sign it.
|
|
813
|
+
// Key-gated like every other route, and it only queues — approving
|
|
814
|
+
// still needs the human's hold-and-sign on the phone.
|
|
815
|
+
if (url.pathname === "/escrow" && req.method === "POST") {
|
|
816
|
+
const body = await readBody(req);
|
|
817
|
+
const input = JSON.parse(body || "{}");
|
|
818
|
+
if (!input.escrowId)
|
|
819
|
+
return send(400, { error: "escrowId is required" });
|
|
820
|
+
if (!input.amount || !input.asset)
|
|
821
|
+
return send(400, { error: "amount and asset are required" });
|
|
822
|
+
const rec = this.addEscrow({
|
|
823
|
+
escrowId: String(input.escrowId),
|
|
824
|
+
order: input.order ? String(input.order) : undefined,
|
|
825
|
+
amount: String(input.amount),
|
|
826
|
+
asset: String(input.asset),
|
|
827
|
+
buyer: input.buyer ? String(input.buyer) : undefined,
|
|
638
828
|
});
|
|
829
|
+
return send(200, { id: rec.id, status: rec.status, escrowId: rec.escrowId });
|
|
639
830
|
}
|
|
640
831
|
const m = url.pathname.match(/^\/(approve|deny|signed)\/([\w-]+)$/);
|
|
641
832
|
if (m && req.method === "POST") {
|
package/dist/escrow.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Asset, Keypair } from "@stellar/stellar-sdk";
|
|
2
|
+
import { type Amount, type Network } from "./vault.ts";
|
|
3
|
+
export declare const horizonUrl: (network: Network) => string;
|
|
4
|
+
/** Horizon writes an asset as "native" or "USDC:Gissuer…". */
|
|
5
|
+
export declare function assetToHorizonString(asset: Asset): string;
|
|
6
|
+
export declare function assetFromHorizonString(s: string): Asset;
|
|
7
|
+
/** The deal the buyer must lock into the ledger. */
|
|
8
|
+
export type EscrowTerms = {
|
|
9
|
+
/** the order id the buyer must put in the creating transaction's memo */
|
|
10
|
+
orderId: string;
|
|
11
|
+
asset: Asset;
|
|
12
|
+
amount: Amount;
|
|
13
|
+
/**
|
|
14
|
+
* The account that funds and locks the escrow. Required to create one.
|
|
15
|
+
* Optional when VERIFYING: a seller usually does not care who the buyer is,
|
|
16
|
+
* only that the money is locked to the right order, price and vault. Pin it
|
|
17
|
+
* when the buyer's account was agreed up front, and any other account is
|
|
18
|
+
* refused.
|
|
19
|
+
*/
|
|
20
|
+
buyer?: string;
|
|
21
|
+
/** the seller vault allowed to claim after release */
|
|
22
|
+
vault: string;
|
|
23
|
+
/** unix seconds; the vault cannot claim before this, the buyer can refund until this */
|
|
24
|
+
releaseAt: number;
|
|
25
|
+
};
|
|
26
|
+
export type Escrow = {
|
|
27
|
+
balanceId: string;
|
|
28
|
+
amount: Amount;
|
|
29
|
+
asset: string;
|
|
30
|
+
vault: string;
|
|
31
|
+
buyer: string;
|
|
32
|
+
releaseAt: number;
|
|
33
|
+
/** the transaction that created it, and the memo it carried */
|
|
34
|
+
txHash: string;
|
|
35
|
+
memo: string;
|
|
36
|
+
memoType: string;
|
|
37
|
+
/** true once the ledger says the balance is gone (claimed or swept) */
|
|
38
|
+
claimed: boolean;
|
|
39
|
+
};
|
|
40
|
+
export type EscrowCheck = {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
reason?: string;
|
|
43
|
+
escrow?: Escrow;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Read a claimable balance back from the ledger and turn it into an Escrow.
|
|
47
|
+
*
|
|
48
|
+
* Horizon does NOT put the memo on the balance record — the memo lives on the
|
|
49
|
+
* transaction that created it. So the join between "this pile of money" and
|
|
50
|
+
* "that order" is one extra request. It is not optional: without it, two orders
|
|
51
|
+
* of the same price to the same vault are indistinguishable, and a seller could
|
|
52
|
+
* deliver order A and claim the escrow that was locked for order B.
|
|
53
|
+
*/
|
|
54
|
+
export declare function readEscrow(network: Network, balanceId: string): Promise<Escrow | null>;
|
|
55
|
+
/**
|
|
56
|
+
* Does this on-chain escrow actually pay THIS deal?
|
|
57
|
+
*
|
|
58
|
+
* Every field is checked against the terms the gateway issued, because the
|
|
59
|
+
* buyer is untrusted: a buyer could lock 0.0000001 XLM, or lock money to a
|
|
60
|
+
* different vault, or set the timer to next year, or reuse the escrow from a
|
|
61
|
+
* previous order. Anything short of an exact match is a refusal.
|
|
62
|
+
*/
|
|
63
|
+
export declare function verifyEscrow(network: Network, balanceId: string, want: EscrowTerms, opts?: {
|
|
64
|
+
minReleaseSeconds?: number;
|
|
65
|
+
maxReleaseSeconds?: number;
|
|
66
|
+
/**
|
|
67
|
+
* The refund window being open is a requirement when ACCEPTING a deal —
|
|
68
|
+
* a buyer who locks money with a timer that already expired has locked
|
|
69
|
+
* nothing, because the seller could claim it the instant it lands.
|
|
70
|
+
*
|
|
71
|
+
* It is not a requirement when RELEASING one. An escrow that reached its
|
|
72
|
+
* timer is still a valid escrow; that is the normal, successful case. So
|
|
73
|
+
* the release path passes allowClosedWindow and gets the money it earned.
|
|
74
|
+
*/
|
|
75
|
+
allowClosedWindow?: boolean;
|
|
76
|
+
}): Promise<EscrowCheck>;
|
|
77
|
+
/**
|
|
78
|
+
* The balance id of the escrow a submitted transaction created.
|
|
79
|
+
*
|
|
80
|
+
* Horizon does not return this to the submitter, and the id is not derivable
|
|
81
|
+
* from the account alone — it comes out of the transaction RESULT. A buyer who
|
|
82
|
+
* locks money with their own code needs this to know what to put in the
|
|
83
|
+
* `x-payment` header, so it is exported rather than kept private.
|
|
84
|
+
*/
|
|
85
|
+
export declare function escrowBalanceId(resultXdr: string, index?: number): string;
|
|
86
|
+
/**
|
|
87
|
+
* Lock an escrow. Needs the BUYER's key: the money leaves the buyer's account,
|
|
88
|
+
* so only the buyer can put it in escrow. The seller never gets a signature on
|
|
89
|
+
* this transaction — that is the point.
|
|
90
|
+
*/
|
|
91
|
+
export declare function createEscrow(network: Network, terms: EscrowTerms, buyerKey: Keypair): Promise<{
|
|
92
|
+
escrow: Escrow;
|
|
93
|
+
txHash: string;
|
|
94
|
+
}>;
|
|
95
|
+
/**
|
|
96
|
+
* The seller takes the escrow. Two keys, always: the vault's threshold is 2 and
|
|
97
|
+
* the claim is a medium-threshold operation, so the agent key alone gets
|
|
98
|
+
* tx_bad_auth. `approverKey` lives on the phone in a real deployment.
|
|
99
|
+
*
|
|
100
|
+
* Refuses before the release time rather than letting Horizon reject it, so the
|
|
101
|
+
* caller gets a sentence instead of an error code — and so a buggy scheduler
|
|
102
|
+
* cannot accidentally try to release early in a loop.
|
|
103
|
+
*/
|
|
104
|
+
export declare function claimEscrow(network: Network, balanceId: string, vaultAddress: string, keys: {
|
|
105
|
+
agent: Keypair;
|
|
106
|
+
approver: Keypair;
|
|
107
|
+
}): Promise<{
|
|
108
|
+
txHash: string;
|
|
109
|
+
claimedAt: number;
|
|
110
|
+
}>;
|
|
111
|
+
/**
|
|
112
|
+
* Release an escrow with the approver's key on a PHONE.
|
|
113
|
+
*
|
|
114
|
+
* `claimEscrow` needs both keys in one process. In a real deployment the human
|
|
115
|
+
* half never lives on the seller's machine — it lives on their phone. So build
|
|
116
|
+
* the claim here, sign it with the agent key, and hand it out as the same
|
|
117
|
+
* challenge the payment desk already uses: the phone reads a sentence
|
|
118
|
+
* ("claim 3.5 XLM from escrow ord_…"), signs, and sends back XDR.
|
|
119
|
+
*
|
|
120
|
+
* The phone is the only place the second signature can be added. This process
|
|
121
|
+
* cannot sign for it, cannot submit a completed claim, and cannot do anything
|
|
122
|
+
* with the half-signed transaction except wait.
|
|
123
|
+
*/
|
|
124
|
+
export declare function prepareEscrowRelease(network: Network, balanceId: string, vaultAddress: string, agent: Keypair): Promise<import("./phone.ts").ApprovalChallenge>;
|
|
125
|
+
/**
|
|
126
|
+
* The buyer takes their money back. No seller signature, no permission, and it
|
|
127
|
+
* works right up to the release second — which is why the seller has to earn
|
|
128
|
+
* the release by proving delivery instead of waiting for the timer.
|
|
129
|
+
*/
|
|
130
|
+
export declare function refundEscrow(network: Network, balanceId: string, buyerKey: Keypair): Promise<{
|
|
131
|
+
txHash: string;
|
|
132
|
+
}>;
|
|
133
|
+
/**
|
|
134
|
+
* Every escrow currently waiting on this vault, newest first.
|
|
135
|
+
*
|
|
136
|
+
* Horizon filters claimable balances by claimant, which is exactly the seller's
|
|
137
|
+
* question: "what money is sitting in escrow for me, and when does each one
|
|
138
|
+
* become mine?" A balance that has already been claimed disappears from this
|
|
139
|
+
* list, so it doubles as the pending-release queue.
|
|
140
|
+
*/
|
|
141
|
+
export declare function pendingEscrows(network: Network, vaultAddress: string, limit?: number): Promise<Escrow[]>;
|
|
142
|
+
/**
|
|
143
|
+
* Has this escrow been settled, and by whom?
|
|
144
|
+
*
|
|
145
|
+
* A claimed balance vanishes from Horizon, so the answer is in the operations
|
|
146
|
+
* of the transaction that claimed it. This is the proof-of-delivery receipt:
|
|
147
|
+
* the buyer can look up the same number on any explorer.
|
|
148
|
+
*/
|
|
149
|
+
export declare function escrowOutcome(network: Network, balanceId: string): Promise<{
|
|
150
|
+
settled: boolean;
|
|
151
|
+
by?: string;
|
|
152
|
+
txHash?: string;
|
|
153
|
+
at?: string;
|
|
154
|
+
}>;
|