@paydlux/agent 0.1.21 → 0.1.23
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 +25 -0
- package/dist/cli.js +102 -23
- package/dist/desk.d.ts +101 -1
- package/dist/desk.js +400 -29
- package/dist/gateway.d.ts +12 -1
- package/dist/gateway.js +14 -0
- package/dist/policy.d.ts +51 -1
- package/dist/policy.js +102 -3
- package/dist/portal.d.ts +17 -0
- package/dist/portal.js +97 -1
- package/package.json +4 -2
- package/phone-app/app.js +26 -6
- package/phone-app/index.html +1 -1
- package/phone-app/xdr.js +23 -4
- package/portal-app/portal.css +22 -0
- package/portal-app/rules.js +62 -0
package/README.md
CHANGED
|
@@ -242,6 +242,31 @@ too, because a budget that silently reads as unspent is worse than no budget.
|
|
|
242
242
|
The banner shows what is left: `policy: max 10 per payment, 50 per day (27.50
|
|
243
243
|
left today)`.
|
|
244
244
|
|
|
245
|
+
### Spend it by category, and edit the rules in a browser
|
|
246
|
+
|
|
247
|
+
One vault, several pockets. Open **spending rules** on the approvals page (or
|
|
248
|
+
`/rules` on the desk) and set, per named category, its own per-payment ceiling and
|
|
249
|
+
daily budget — then say which seller wallet belongs to which category:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
data-api max 3 / payment 6 / day ← api.example.com wallet
|
|
253
|
+
coding max 1 / payment 2 / day ← assistant wallet
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
A category can only be **tighter** than the limits above it; it can never raise
|
|
257
|
+
them, because the base ceilings are checked first. That asymmetry is the security
|
|
258
|
+
property, and `npm run test:policy` proves it, along with the queue counting
|
|
259
|
+
inside a category and the fact that a form posted from another site cannot raise
|
|
260
|
+
your budget.
|
|
261
|
+
|
|
262
|
+
Edits are saved to the vault's config file, so they survive a restart. Once you
|
|
263
|
+
have edited them there, the saved numbers win over `--max`/`--daily` on the next
|
|
264
|
+
start, and the seller prints which it is using — a rule that quietly reverts by
|
|
265
|
+
morning is worse than no rule.
|
|
266
|
+
|
|
267
|
+
`stellar/policy.ts` is the engine; the page is a plain HTML form, so it works with
|
|
268
|
+
JavaScript off.
|
|
269
|
+
|
|
245
270
|
### What is enforced where (said honestly)
|
|
246
271
|
|
|
247
272
|
| enforced by Stellar | enforced by the desk (`policy.ts`) |
|
package/dist/cli.js
CHANGED
|
@@ -221,7 +221,26 @@ async function sell() {
|
|
|
221
221
|
// desk is even built.
|
|
222
222
|
const maxPer = Number(arg("max") ?? data.policy?.maxPerPayment ?? 2);
|
|
223
223
|
const daily = Number(arg("daily") ?? data.policy?.dailyBudget ?? 10);
|
|
224
|
-
|
|
224
|
+
// Categories and the wallet map come from the config file only: they are set
|
|
225
|
+
// at the desk (/rules), not from the command line. A flag for them would be a
|
|
226
|
+
// second, easier way to say the same thing, and the page would drift from it.
|
|
227
|
+
//
|
|
228
|
+
// The same is true of the two ceilings once an owner edits them at the desk:
|
|
229
|
+
// the saved file wins over the flags, because the app scripts pass --max and
|
|
230
|
+
// --daily on every start and would otherwise silently undo whatever was set in
|
|
231
|
+
// the browser. The line printed below is what keeps that from being a
|
|
232
|
+
// surprise — a rule that quietly reverts is worse than no rule at all.
|
|
233
|
+
const saved = data.policy?.savedByOwner === true;
|
|
234
|
+
const maxPerFinal = saved ? Number(data.policy.maxPerPayment ?? maxPer) : maxPer;
|
|
235
|
+
const dailyFinal = saved ? Number(data.policy.dailyBudget ?? daily) : daily;
|
|
236
|
+
if (saved && (maxPerFinal !== maxPer || dailyFinal !== daily)) {
|
|
237
|
+
log(`using the rules you saved at the desk (max ${maxPerFinal}, daily ${dailyFinal}) instead of the command line (${maxPer}/${daily}).\n to change them: open the approvals link and press "spending rules".`);
|
|
238
|
+
}
|
|
239
|
+
const policy = new PolicyEngine({
|
|
240
|
+
maxPerPayment: maxPerFinal, dailyBudget: dailyFinal,
|
|
241
|
+
allowlist: data.policy?.allowlist, denylist: data.policy?.denylist,
|
|
242
|
+
categories: data.policy?.categories, payeeCategory: data.policy?.payeeCategory,
|
|
243
|
+
});
|
|
225
244
|
// A ceiling below the price blocks every payment, and the failure looks like
|
|
226
245
|
// a bug in the desk: the buyer waits, the human never sees an approvable
|
|
227
246
|
// item, and the log says "mandate refused". Say it now, before anything is
|
|
@@ -233,16 +252,16 @@ async function sell() {
|
|
|
233
252
|
// takes money IN from the buyer, so the ceiling does not apply to it — and
|
|
234
253
|
// applying it would refuse a perfectly good escrow price for no reason.
|
|
235
254
|
if (mode === "escrow") {
|
|
236
|
-
if (priceNum >
|
|
237
|
-
log(`note: --escrow price ${price} is above --max ${
|
|
255
|
+
if (priceNum > maxPerFinal) {
|
|
256
|
+
log(`note: --escrow price ${price} is above --max ${maxPerFinal}. That is fine here — escrow money comes FROM the buyer, so your spending mandate never touches it.`);
|
|
238
257
|
}
|
|
239
258
|
}
|
|
240
259
|
else {
|
|
241
|
-
if (priceNum >
|
|
242
|
-
bad(`--price ${price} is above --max ${
|
|
260
|
+
if (priceNum > maxPerFinal) {
|
|
261
|
+
bad(`--price ${price} is above --max ${maxPerFinal}, so every payment would be refused by your own mandate.\n raise --max to at least ${price} (for example --max ${price}) or lower --price.`);
|
|
243
262
|
}
|
|
244
|
-
if (priceNum >
|
|
245
|
-
bad(`--price ${price} is above --daily ${
|
|
263
|
+
if (priceNum > dailyFinal) {
|
|
264
|
+
bad(`--price ${price} is above --daily ${dailyFinal}: one payment would spend the whole day's budget and the second would be refused.\n set --daily to at least ${price}.`);
|
|
246
265
|
}
|
|
247
266
|
}
|
|
248
267
|
const vault = await StellarVault.load(data.vault, data.token.code, data.token.issuer, { principal: pub(keys.principal), approver: keys.approver ? pub(keys.approver) : undefined, agent: pub(keys.agent) }, network);
|
|
@@ -267,20 +286,31 @@ async function sell() {
|
|
|
267
286
|
+ `the daily budget of ${daily} cannot be enforced without it, so nothing was started. `
|
|
268
287
|
+ `Check the network and try again.`);
|
|
269
288
|
}
|
|
270
|
-
policy.seedFromLedger(alreadyPaid);
|
|
289
|
+
policy.seedFromLedger(alreadyPaid, policy.categoryOf(payee));
|
|
271
290
|
if (alreadyPaid > 0) {
|
|
272
|
-
log(`ledger says ${alreadyPaid.toFixed(2)} of the ${
|
|
273
|
-
if (alreadyPaid >=
|
|
274
|
-
bad(`today's budget is already gone: ${alreadyPaid.toFixed(2)} paid, ${
|
|
291
|
+
log(`ledger says ${alreadyPaid.toFixed(2)} of the ${dailyFinal} daily budget is already spent today`);
|
|
292
|
+
if (alreadyPaid >= dailyFinal) {
|
|
293
|
+
bad(`today's budget is already gone: ${alreadyPaid.toFixed(2)} paid, ${dailyFinal} allowed.\n `
|
|
275
294
|
+ `the mandate would refuse every payment. Raise --daily or wait for the UTC day to roll over.`);
|
|
276
295
|
}
|
|
277
296
|
}
|
|
297
|
+
// Say out loud what the rules are, including the categories, so the numbers
|
|
298
|
+
// an owner set in a browser are visible to whoever starts the seller. A rule
|
|
299
|
+
// nobody can see is a rule nobody trusts.
|
|
300
|
+
const catNames = Object.keys(policy.policy.categories ?? {});
|
|
301
|
+
if (catNames.length) {
|
|
302
|
+
const mapped = Object.entries(policy.policy.payeeCategory ?? {});
|
|
303
|
+
log(`spending categories: ${catNames.map((c) => {
|
|
304
|
+
const r = policy.policy.categories[c];
|
|
305
|
+
return `${c} (${r.maxPerPayment ?? policy.policy.maxPerPayment}/payment, ${r.dailyBudget ?? dailyFinal}/day)`;
|
|
306
|
+
}).join(", ")}${mapped.length ? ` · ${mapped.length} wallet(s) mapped` : ""}`);
|
|
307
|
+
}
|
|
278
308
|
if (network === "public") {
|
|
279
309
|
console.log(`
|
|
280
310
|
MAINNET — real money.
|
|
281
311
|
token ${data.token.code}
|
|
282
|
-
max per payment ${
|
|
283
|
-
daily budget ${
|
|
312
|
+
max per payment ${maxPerFinal}
|
|
313
|
+
daily budget ${dailyFinal}
|
|
284
314
|
vault ${vault.address}
|
|
285
315
|
balance ${await vault.balance()} ${data.token.code}
|
|
286
316
|
`);
|
|
@@ -322,6 +352,7 @@ async function sell() {
|
|
|
322
352
|
// --split is the honest mode: this process cannot approve anything.
|
|
323
353
|
approver: split ? undefined : keys.approver,
|
|
324
354
|
split,
|
|
355
|
+
agentSeed: keys.agent,
|
|
325
356
|
portalKey,
|
|
326
357
|
publicHost,
|
|
327
358
|
agentKeys: [pub(keys.agent)],
|
|
@@ -336,6 +367,25 @@ async function sell() {
|
|
|
336
367
|
],
|
|
337
368
|
// The banner prints these numbers. This line is what makes them true.
|
|
338
369
|
policy,
|
|
370
|
+
// Rules edited at the desk must survive the next restart, or the mandate in
|
|
371
|
+
// the banner is a promise that lasts until ctrl-c. Written back to the same
|
|
372
|
+
// config file, with the same 0600 as everywhere else in this file, because
|
|
373
|
+
// it also holds key material.
|
|
374
|
+
onPolicyChange: (p) => {
|
|
375
|
+
data.policy = {
|
|
376
|
+
...(data.policy ?? {}),
|
|
377
|
+
// The marker that says "a human set these in a browser". Without it the
|
|
378
|
+
// app scripts' --max/--daily flags win on every restart and the edit is
|
|
379
|
+
// lost by morning.
|
|
380
|
+
savedByOwner: true,
|
|
381
|
+
maxPerPayment: p.maxPerPayment, dailyBudget: p.dailyBudget,
|
|
382
|
+
allowlist: p.allowlist ?? [], denylist: p.denylist ?? [],
|
|
383
|
+
categories: p.categories ?? {}, payeeCategory: p.payeeCategory ?? {},
|
|
384
|
+
};
|
|
385
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
386
|
+
chmodSync(path, 0o600);
|
|
387
|
+
log(`rules saved to ${path}`);
|
|
388
|
+
},
|
|
339
389
|
});
|
|
340
390
|
let tunnel = null;
|
|
341
391
|
if (tls) {
|
|
@@ -846,21 +896,50 @@ async function escrow() {
|
|
|
846
896
|
const keys = data.keys ?? {};
|
|
847
897
|
const agent = Keypair.fromSecret(keys.agent);
|
|
848
898
|
if (!keys.approver) {
|
|
899
|
+
// The recommended setup: the desk is running with the phone approver, so
|
|
900
|
+
// hand the release to the DESK instead of writing a file nothing reads.
|
|
901
|
+
// The desk queues it, the phone signs the claim, the desk submits it.
|
|
902
|
+
const deskPort = Number(arg("desk") ?? 4022);
|
|
903
|
+
const portalKey = data.desk?.portalKey;
|
|
904
|
+
if (portalKey) {
|
|
905
|
+
try {
|
|
906
|
+
const res = await fetch(`http://127.0.0.1:${deskPort}/escrow?k=${encodeURIComponent(portalKey)}`, {
|
|
907
|
+
method: "POST",
|
|
908
|
+
headers: { "content-type": "application/json" },
|
|
909
|
+
body: JSON.stringify({
|
|
910
|
+
escrowId: hit.balanceId, order: hit.memo || undefined,
|
|
911
|
+
amount: hit.amount,
|
|
912
|
+
asset: hit.asset === "native" ? "XLM" : hit.asset.split(":")[0],
|
|
913
|
+
buyer: hit.buyer,
|
|
914
|
+
}),
|
|
915
|
+
});
|
|
916
|
+
if (res.ok) {
|
|
917
|
+
const queued = await res.json();
|
|
918
|
+
console.log(`\nrelease queued on the approval desk as ${queued.id}.`);
|
|
919
|
+
console.log(` open: ${flag("tls") ? `https://${PUBLIC_HOST}` : `http://127.0.0.1:${deskPort}`}/phone/?k=${portalKey}`);
|
|
920
|
+
console.log(` the order is ${hit.memo || hit.balanceId} — approve on the phone only if you delivered it.`);
|
|
921
|
+
console.log(` the phone signs the claim; the desk submits it. vault balance updates after.\n`);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
console.log(`the desk answered ${res.status}; falling back to the manual challenge.\n`);
|
|
925
|
+
}
|
|
926
|
+
catch {
|
|
927
|
+
// No desk listening on that port. Fall through to the file path.
|
|
928
|
+
}
|
|
929
|
+
}
|
|
849
930
|
const challenge = await prepareEscrowRelease(network, hit.balanceId, vault.address, agent);
|
|
850
931
|
console.log(`\nthis laptop holds one of the two keys, so the release needs your phone.\n`);
|
|
851
932
|
console.log(describeChallenge(challenge));
|
|
852
|
-
console.log(`\n
|
|
853
|
-
console.log(`
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
//
|
|
857
|
-
//
|
|
933
|
+
console.log(`\n the order id is ${hit.memo || hit.balanceId} — sign only if you delivered it.\n`);
|
|
934
|
+
console.log(`no approval desk is answering on port ${deskPort}. start one with \`paydlux sell\` (or the`);
|
|
935
|
+
console.log(`app:mainnet:sell script) and re-run this command, and the release goes to your phone.`);
|
|
936
|
+
console.log(`or finish it on a machine that holds the approver key.\n`);
|
|
937
|
+
// Written out so the second signature can still be added by hand from a
|
|
938
|
+
// machine that holds the approver key. Said exactly: nothing reads this
|
|
939
|
+
// file back on its own.
|
|
858
940
|
const file = path.replace(/\.json$/, ".escrow-release.json");
|
|
859
941
|
writeFileSync(file, JSON.stringify({ order: hit.memo || hit.balanceId, challenge }, null, 2), { mode: 0o600 });
|
|
860
|
-
console.log(`challenge written to ${file} (0600)
|
|
861
|
-
console.log(`nothing completes this by itself. finish it from a machine that holds the approver key,`);
|
|
862
|
-
console.log(`or run this command there. the phone approver releases PAYMENTS today; releasing ESCROW`);
|
|
863
|
-
console.log(`from the phone is not wired yet, so until it is, use the second key's machine.\n`);
|
|
942
|
+
console.log(`challenge also written to ${file} (0600) for the manual path.\n`);
|
|
864
943
|
return;
|
|
865
944
|
}
|
|
866
945
|
const out = await claimEscrow(network, hit.balanceId, vault.address, { agent, approver: Keypair.fromSecret(keys.approver) });
|
package/dist/desk.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
2
|
import type { StellarVault } from "./vault.ts";
|
|
3
|
-
import type { PolicyEngine } from "./policy.ts";
|
|
3
|
+
import type { PolicyEngine, Policy } from "./policy.ts";
|
|
4
4
|
import { type ApprovalChallenge } from "./phone.ts";
|
|
5
5
|
/** A portal link is a capability: whoever holds it can approve. So it is long,
|
|
6
6
|
* random, and compared without leaking timing. */
|
|
@@ -32,6 +32,20 @@ export type DeskRecord = {
|
|
|
32
32
|
challenge?: ApprovalChallenge;
|
|
33
33
|
qr?: string;
|
|
34
34
|
signedXdr?: string;
|
|
35
|
+
/**
|
|
36
|
+
* What the human is being asked to authorise. `payment` is the vault paying
|
|
37
|
+
* money OUT. `escrow` is the vault claiming a claimable balance the buyer
|
|
38
|
+
* already locked — money coming IN. The distinction is load-bearing: the
|
|
39
|
+
* spending mandate caps payments out, so it must not touch an escrow
|
|
40
|
+
* release, and the signed transaction is verified against a different
|
|
41
|
+
* operation type.
|
|
42
|
+
*/
|
|
43
|
+
kind?: "payment" | "escrow";
|
|
44
|
+
/** escrow records: the claimable balance to claim, and the order it belongs to */
|
|
45
|
+
escrowId?: string;
|
|
46
|
+
order?: string;
|
|
47
|
+
/** escrow records: the asset the balance pays out ("XLM", "USDC", ...) */
|
|
48
|
+
asset?: string;
|
|
35
49
|
};
|
|
36
50
|
export type DeskStore = DeskRecord;
|
|
37
51
|
export type DeskConfig = {
|
|
@@ -46,6 +60,15 @@ export type DeskConfig = {
|
|
|
46
60
|
* the desk submits it. The approver seed never touches this machine.
|
|
47
61
|
*/
|
|
48
62
|
split?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* The agent seed (the machine-side key). Payments are built with the
|
|
65
|
+
* principal key; an ESCROW CLAIM is built with the agent key — that is the
|
|
66
|
+
* key pair the escrow was designed around (agent + phone approver). The desk
|
|
67
|
+
* needs it to build the release challenge, exactly as it needs the principal
|
|
68
|
+
* seed to build a payment challenge. It is not the approving half: the claim
|
|
69
|
+
* still needs the phone's signature to reach the vault's threshold of 2.
|
|
70
|
+
*/
|
|
71
|
+
agentSeed?: string;
|
|
49
72
|
/** how long a human has to decide before the payment is dead */
|
|
50
73
|
decideWithinSeconds?: number;
|
|
51
74
|
port: number;
|
|
@@ -73,10 +96,22 @@ export type DeskConfig = {
|
|
|
73
96
|
* ceiling can actually hold.
|
|
74
97
|
*/
|
|
75
98
|
policy?: PolicyEngine;
|
|
99
|
+
/**
|
|
100
|
+
* Called when the human edits the spending rules from the portal. The CLI
|
|
101
|
+
* wires this to the config file, so a rule set at the desk survives the next
|
|
102
|
+
* restart instead of resetting to the command-line defaults.
|
|
103
|
+
*
|
|
104
|
+
* A callback rather than a path: the desk must not know where secrets live.
|
|
105
|
+
* The config file also holds key material, so the writer stays with the code
|
|
106
|
+
* that already handles it (0600, same serialisation).
|
|
107
|
+
*/
|
|
108
|
+
onPolicyChange?: (policy: Policy) => void;
|
|
76
109
|
};
|
|
77
110
|
export declare class ApprovalDesk {
|
|
78
111
|
private records;
|
|
79
112
|
private seq;
|
|
113
|
+
/** when /rules last saved, so the page can say "saved" without a query flag */
|
|
114
|
+
private rulesSavedAt;
|
|
80
115
|
private server?;
|
|
81
116
|
cfg: DeskConfig;
|
|
82
117
|
constructor(cfg: DeskConfig);
|
|
@@ -99,6 +134,27 @@ export declare class ApprovalDesk {
|
|
|
99
134
|
/** Every record carrying this key, newest first. */
|
|
100
135
|
allByKey(key: string): DeskRecord[];
|
|
101
136
|
findByKey(key: string): DeskRecord | null;
|
|
137
|
+
/**
|
|
138
|
+
* Queue an escrow release for the human. This is the missing half of
|
|
139
|
+
* `paydlux escrow --release` on a --split vault: the money already sits in a
|
|
140
|
+
* claimable balance the buyer locked, so releasing it is the vault CLAIMING
|
|
141
|
+
* that balance — money coming IN, not going out. The claim still needs the
|
|
142
|
+
* vault's threshold of 2 (agent + approver), and the approver half lives on
|
|
143
|
+
* the phone, so it goes through the same challenge/sign/submit loop as a
|
|
144
|
+
* payment, tagged `kind: "escrow"` so the spending mandate — which caps
|
|
145
|
+
* money OUT — never touches it.
|
|
146
|
+
*
|
|
147
|
+
* The generic fields (uri/price/body) are filled so the portal page renders
|
|
148
|
+
* the row without knowing about escrow; the structured fields carry the
|
|
149
|
+
* truth the verification uses.
|
|
150
|
+
*/
|
|
151
|
+
addEscrow(input: {
|
|
152
|
+
escrowId: string;
|
|
153
|
+
order?: string;
|
|
154
|
+
amount: string;
|
|
155
|
+
asset: string;
|
|
156
|
+
buyer?: string;
|
|
157
|
+
}): DeskRecord;
|
|
102
158
|
get(id: string): DeskRecord | null;
|
|
103
159
|
list(): DeskRecord[];
|
|
104
160
|
/**
|
|
@@ -124,9 +180,53 @@ export declare class ApprovalDesk {
|
|
|
124
180
|
private mandateBlock;
|
|
125
181
|
/** The mandate has been satisfied: count the money as spent. */
|
|
126
182
|
private recordSpend;
|
|
183
|
+
/** The rules as they stand, for the /rules page. */
|
|
184
|
+
policyView(): Policy;
|
|
185
|
+
/** What today's counters read, per category, for the /rules page. */
|
|
186
|
+
private spentView;
|
|
187
|
+
/**
|
|
188
|
+
* The cross-origin gate used by every route that changes money or the rules
|
|
189
|
+
* that gate it. A cross-origin fetch POST is a CORS *simple request*: no
|
|
190
|
+
* preflight, so the attacker's page never sees the reply but the action still
|
|
191
|
+
* happens. Browsers always send Origin on POST and Sec-Fetch-Site on every
|
|
192
|
+
* request, so checking both closes that path while leaving local automation
|
|
193
|
+
* (curl, tests, the phone app on its own origin) alone.
|
|
194
|
+
*
|
|
195
|
+
* Returns a reason string when the request must be refused, null when it may
|
|
196
|
+
* proceed.
|
|
197
|
+
*/
|
|
198
|
+
private crossOriginGate;
|
|
199
|
+
/**
|
|
200
|
+
* Apply an edit from the /rules form.
|
|
201
|
+
*
|
|
202
|
+
* The form carries the whole rule set, so this rebuilds rather than patches:
|
|
203
|
+
* what you see on the page is what gets saved, and a stale field cannot
|
|
204
|
+
* survive an edit. Names and numbers are validated inside PolicyEngine.update
|
|
205
|
+
* — this function only decides which fields the form is allowed to touch.
|
|
206
|
+
*
|
|
207
|
+
* Deliberately absent: maxMandateTtl and maxLiveMandates. Those are ledger
|
|
208
|
+
* constraints (sequence binding), not owner preferences, and a form field for
|
|
209
|
+
* them would be an invitation to configure a mandate the chain will refuse.
|
|
210
|
+
*/
|
|
211
|
+
private applyRules;
|
|
127
212
|
approve(id: string): Promise<DeskRecord>;
|
|
128
213
|
/** Split mode step 1: ask the phone to sign. Nothing is submitted yet. */
|
|
129
214
|
private requestPhoneSignature;
|
|
215
|
+
/**
|
|
216
|
+
* Split mode step 1 for an ESCROW release: build the claim, sign it with the
|
|
217
|
+
* agent key, hand it to the phone. `prepareEscrowRelease` reads the ledger
|
|
218
|
+
* and refuses a balance that does not exist or whose timer has not passed,
|
|
219
|
+
* so a queued-but-fake escrow dies here with a real reason instead of
|
|
220
|
+
* reaching the human as a signable challenge.
|
|
221
|
+
*/
|
|
222
|
+
private requestEscrowSignature;
|
|
223
|
+
/**
|
|
224
|
+
* Split mode step 2 for an ESCROW release: the phone signed the claim.
|
|
225
|
+
* No mandate check — this is money coming IN — and no recordSpend: the
|
|
226
|
+
* daily budget counts payments OUT, and counting a release against it would
|
|
227
|
+
* make the seller's own earned money unclaimable.
|
|
228
|
+
*/
|
|
229
|
+
private completeEscrow;
|
|
130
230
|
/**
|
|
131
231
|
* Split mode step 2: the phone answered. `signedXdr` is the transaction with
|
|
132
232
|
* the approver signature added, returned by signChallenge() on the phone.
|