settld 0.2.1 → 0.2.2
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 +21 -0
- package/bin/settld.js +13 -0
- package/docs/QUICKSTART_MCP_HOSTS.md +61 -3
- package/docs/gitbook/quickstart.md +47 -4
- package/package.json +2 -1
- package/scripts/ci/run-mcp-host-smoke.mjs +6 -0
- package/scripts/ci/run-production-cutover-gate.mjs +6 -0
- package/scripts/demo/mcp-paid-exa.mjs +18 -1
- package/scripts/setup/onboard.mjs +14 -0
- package/scripts/vercel/build-mkdocs.sh +3 -3
- package/scripts/vercel/ignore-dashboard.sh +3 -0
- package/scripts/vercel/install-mkdocs.sh +2 -3
- package/scripts/wallet/cli.mjs +871 -0
- package/src/core/wallet-funding-coinbase.js +197 -0
- package/src/core/wallet-funding-hosted.js +155 -0
- package/src/core/wallet-provider-bootstrap.js +95 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
import { defaultSessionPath, readSavedSession } from "../setup/session-store.mjs";
|
|
11
|
+
|
|
12
|
+
const COMMANDS = new Set(["status", "fund", "balance"]);
|
|
13
|
+
const FUND_METHODS = new Set(["card", "bank", "transfer", "faucet"]);
|
|
14
|
+
const FORMAT_OPTIONS = new Set(["text", "json"]);
|
|
15
|
+
|
|
16
|
+
function usage() {
|
|
17
|
+
const text = [
|
|
18
|
+
"usage:",
|
|
19
|
+
" settld wallet status [--base-url <url>] [--tenant-id <id>] [--session-file <path>] [--cookie <cookie>] [--magic-link-api-key <key>] [--format text|json] [--json-out <path>]",
|
|
20
|
+
" settld wallet fund [--method card|bank|transfer|faucet] [--open] [--hosted-url <url>] [--non-interactive] [--base-url <url>] [--tenant-id <id>] [--session-file <path>] [--cookie <cookie>] [--magic-link-api-key <key>] [--format text|json] [--json-out <path>]",
|
|
21
|
+
" settld wallet balance [--watch] [--min-usdc <amount>] [--interval-seconds <n>] [--timeout-seconds <n>] [--base-url <url>] [--tenant-id <id>] [--session-file <path>] [--cookie <cookie>] [--magic-link-api-key <key>] [--format text|json] [--json-out <path>]",
|
|
22
|
+
"",
|
|
23
|
+
"flags:",
|
|
24
|
+
" --method <name> Funding method for `fund`",
|
|
25
|
+
" --open Open hosted link in browser (card/bank)",
|
|
26
|
+
" --hosted-url <url> Override hosted funding URL (card/bank)",
|
|
27
|
+
" --non-interactive Disable prompts for method selection",
|
|
28
|
+
" --watch Poll balance until funded or timeout",
|
|
29
|
+
" --min-usdc <amount> Target spend wallet USDC (default watch target: >0)",
|
|
30
|
+
" --interval-seconds <n> Poll interval for --watch (default: 5)",
|
|
31
|
+
" --timeout-seconds <n> Watch timeout (default: 180)",
|
|
32
|
+
" --base-url <url> Settld onboarding base URL",
|
|
33
|
+
" --tenant-id <id> Tenant ID",
|
|
34
|
+
" --session-file <path> Saved session path (default: ~/.settld/session.json)",
|
|
35
|
+
" --cookie <cookie> Buyer session cookie override",
|
|
36
|
+
" --magic-link-api-key <key> Control-plane API key (admin mode fallback)",
|
|
37
|
+
" --bootstrap-api-key <key> Alias for --magic-link-api-key",
|
|
38
|
+
" --x-api-key <key> Alias for --magic-link-api-key",
|
|
39
|
+
" --provider <name> Wallet provider (default: circle)",
|
|
40
|
+
" --circle-mode <mode> Circle mode hint: auto|sandbox|production (default: auto)",
|
|
41
|
+
" --circle-base-url <url> Circle base URL override",
|
|
42
|
+
" --circle-blockchain <name> Circle blockchain override",
|
|
43
|
+
" --spend-wallet-id <id> Circle spend wallet ID hint",
|
|
44
|
+
" --escrow-wallet-id <id> Circle escrow wallet ID hint",
|
|
45
|
+
" --token-id-usdc <id> Circle USDC token ID hint",
|
|
46
|
+
" --format <text|json> Output format (default: text)",
|
|
47
|
+
" --json-out <path> Write JSON payload to file",
|
|
48
|
+
" --help Show this help"
|
|
49
|
+
].join("\n");
|
|
50
|
+
process.stderr.write(`${text}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fail(message) {
|
|
54
|
+
throw new Error(String(message ?? "wallet command failed"));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readArgValue(argv, index, rawArg) {
|
|
58
|
+
const arg = String(rawArg ?? "");
|
|
59
|
+
const eq = arg.indexOf("=");
|
|
60
|
+
if (eq >= 0) return { value: arg.slice(eq + 1), nextIndex: index };
|
|
61
|
+
return { value: String(argv[index + 1] ?? ""), nextIndex: index + 1 };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeHttpUrl(value) {
|
|
65
|
+
const raw = String(value ?? "").trim();
|
|
66
|
+
if (!raw) return null;
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = new URL(raw);
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
74
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isPlainObject(value) {
|
|
78
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
79
|
+
const proto = Object.getPrototypeOf(value);
|
|
80
|
+
return proto === Object.prototype || proto === null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function safeTrim(value) {
|
|
84
|
+
return typeof value === "string" ? value.trim() : "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseNonNegativeNumber(value, { field }) {
|
|
88
|
+
const num = Number(value);
|
|
89
|
+
if (!Number.isFinite(num) || num < 0) fail(`${field} must be a non-negative number`);
|
|
90
|
+
return num;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parsePositiveNumber(value, { field }) {
|
|
94
|
+
const num = Number(value);
|
|
95
|
+
if (!Number.isFinite(num) || num <= 0) fail(`${field} must be a positive number`);
|
|
96
|
+
return num;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function usdcAmountNumber(value) {
|
|
100
|
+
const num = Number(value);
|
|
101
|
+
return Number.isFinite(num) && num >= 0 ? num : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function parseArgs(argv) {
|
|
105
|
+
const out = {
|
|
106
|
+
command: String(argv[0] ?? "").trim() || null,
|
|
107
|
+
method: null,
|
|
108
|
+
open: false,
|
|
109
|
+
hostedUrl: null,
|
|
110
|
+
nonInteractive: false,
|
|
111
|
+
watch: false,
|
|
112
|
+
minUsdc: null,
|
|
113
|
+
intervalSeconds: 5,
|
|
114
|
+
timeoutSeconds: 180,
|
|
115
|
+
baseUrl: null,
|
|
116
|
+
tenantId: null,
|
|
117
|
+
sessionFile: defaultSessionPath(),
|
|
118
|
+
cookie: null,
|
|
119
|
+
magicLinkApiKey: null,
|
|
120
|
+
provider: "circle",
|
|
121
|
+
circleMode: "auto",
|
|
122
|
+
circleBaseUrl: null,
|
|
123
|
+
circleBlockchain: null,
|
|
124
|
+
spendWalletId: null,
|
|
125
|
+
escrowWalletId: null,
|
|
126
|
+
tokenIdUsdc: null,
|
|
127
|
+
format: "text",
|
|
128
|
+
jsonOut: null,
|
|
129
|
+
help: false
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
for (let i = 1; i < argv.length; i += 1) {
|
|
133
|
+
const arg = String(argv[i] ?? "");
|
|
134
|
+
if (!arg) continue;
|
|
135
|
+
|
|
136
|
+
if (arg === "--help" || arg === "-h") {
|
|
137
|
+
out.help = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (arg === "--open") {
|
|
141
|
+
out.open = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (arg === "--non-interactive" || arg === "--yes") {
|
|
145
|
+
out.nonInteractive = true;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (arg === "--watch") {
|
|
149
|
+
out.watch = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (arg === "--method" || arg.startsWith("--method=")) {
|
|
153
|
+
const parsed = readArgValue(argv, i, arg);
|
|
154
|
+
out.method = String(parsed.value ?? "").trim().toLowerCase();
|
|
155
|
+
i = parsed.nextIndex;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (arg === "--hosted-url" || arg.startsWith("--hosted-url=")) {
|
|
159
|
+
const parsed = readArgValue(argv, i, arg);
|
|
160
|
+
out.hostedUrl = parsed.value;
|
|
161
|
+
i = parsed.nextIndex;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (arg === "--min-usdc" || arg.startsWith("--min-usdc=")) {
|
|
165
|
+
const parsed = readArgValue(argv, i, arg);
|
|
166
|
+
out.minUsdc = parsed.value;
|
|
167
|
+
i = parsed.nextIndex;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (arg === "--interval-seconds" || arg.startsWith("--interval-seconds=")) {
|
|
171
|
+
const parsed = readArgValue(argv, i, arg);
|
|
172
|
+
out.intervalSeconds = parsed.value;
|
|
173
|
+
i = parsed.nextIndex;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (arg === "--timeout-seconds" || arg.startsWith("--timeout-seconds=")) {
|
|
177
|
+
const parsed = readArgValue(argv, i, arg);
|
|
178
|
+
out.timeoutSeconds = parsed.value;
|
|
179
|
+
i = parsed.nextIndex;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (arg === "--base-url" || arg.startsWith("--base-url=")) {
|
|
183
|
+
const parsed = readArgValue(argv, i, arg);
|
|
184
|
+
out.baseUrl = parsed.value;
|
|
185
|
+
i = parsed.nextIndex;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (arg === "--tenant-id" || arg.startsWith("--tenant-id=")) {
|
|
189
|
+
const parsed = readArgValue(argv, i, arg);
|
|
190
|
+
out.tenantId = parsed.value;
|
|
191
|
+
i = parsed.nextIndex;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (arg === "--session-file" || arg.startsWith("--session-file=")) {
|
|
195
|
+
const parsed = readArgValue(argv, i, arg);
|
|
196
|
+
out.sessionFile = parsed.value;
|
|
197
|
+
i = parsed.nextIndex;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (arg === "--cookie" || arg.startsWith("--cookie=")) {
|
|
201
|
+
const parsed = readArgValue(argv, i, arg);
|
|
202
|
+
out.cookie = parsed.value;
|
|
203
|
+
i = parsed.nextIndex;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
arg === "--magic-link-api-key" ||
|
|
208
|
+
arg === "--bootstrap-api-key" ||
|
|
209
|
+
arg === "--x-api-key" ||
|
|
210
|
+
arg.startsWith("--magic-link-api-key=") ||
|
|
211
|
+
arg.startsWith("--bootstrap-api-key=") ||
|
|
212
|
+
arg.startsWith("--x-api-key=")
|
|
213
|
+
) {
|
|
214
|
+
const parsed = readArgValue(argv, i, arg);
|
|
215
|
+
out.magicLinkApiKey = parsed.value;
|
|
216
|
+
i = parsed.nextIndex;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (arg === "--provider" || arg.startsWith("--provider=")) {
|
|
220
|
+
const parsed = readArgValue(argv, i, arg);
|
|
221
|
+
out.provider = String(parsed.value ?? "").trim().toLowerCase();
|
|
222
|
+
i = parsed.nextIndex;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (arg === "--circle-mode" || arg.startsWith("--circle-mode=")) {
|
|
226
|
+
const parsed = readArgValue(argv, i, arg);
|
|
227
|
+
out.circleMode = String(parsed.value ?? "").trim().toLowerCase();
|
|
228
|
+
i = parsed.nextIndex;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (arg === "--circle-base-url" || arg.startsWith("--circle-base-url=")) {
|
|
232
|
+
const parsed = readArgValue(argv, i, arg);
|
|
233
|
+
out.circleBaseUrl = parsed.value;
|
|
234
|
+
i = parsed.nextIndex;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (arg === "--circle-blockchain" || arg.startsWith("--circle-blockchain=")) {
|
|
238
|
+
const parsed = readArgValue(argv, i, arg);
|
|
239
|
+
out.circleBlockchain = parsed.value;
|
|
240
|
+
i = parsed.nextIndex;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (arg === "--spend-wallet-id" || arg.startsWith("--spend-wallet-id=")) {
|
|
244
|
+
const parsed = readArgValue(argv, i, arg);
|
|
245
|
+
out.spendWalletId = parsed.value;
|
|
246
|
+
i = parsed.nextIndex;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (arg === "--escrow-wallet-id" || arg.startsWith("--escrow-wallet-id=")) {
|
|
250
|
+
const parsed = readArgValue(argv, i, arg);
|
|
251
|
+
out.escrowWalletId = parsed.value;
|
|
252
|
+
i = parsed.nextIndex;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (arg === "--token-id-usdc" || arg.startsWith("--token-id-usdc=")) {
|
|
256
|
+
const parsed = readArgValue(argv, i, arg);
|
|
257
|
+
out.tokenIdUsdc = parsed.value;
|
|
258
|
+
i = parsed.nextIndex;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (arg === "--format" || arg.startsWith("--format=")) {
|
|
262
|
+
const parsed = readArgValue(argv, i, arg);
|
|
263
|
+
out.format = String(parsed.value ?? "").trim().toLowerCase();
|
|
264
|
+
i = parsed.nextIndex;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (arg === "--json-out" || arg.startsWith("--json-out=")) {
|
|
268
|
+
const parsed = readArgValue(argv, i, arg);
|
|
269
|
+
out.jsonOut = parsed.value;
|
|
270
|
+
i = parsed.nextIndex;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
fail(`unknown argument: ${arg}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!out.command || out.command === "--help" || out.command === "-h") {
|
|
277
|
+
out.help = true;
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
if (!COMMANDS.has(out.command)) fail(`unsupported wallet command: ${out.command}`);
|
|
281
|
+
if (!FORMAT_OPTIONS.has(out.format)) fail("--format must be text|json");
|
|
282
|
+
if (out.command !== "fund" && out.method !== null) fail("--method only applies to `settld wallet fund`");
|
|
283
|
+
if (out.command === "fund" && out.method !== null && !FUND_METHODS.has(out.method)) {
|
|
284
|
+
fail("--method must be one of: card|bank|transfer|faucet");
|
|
285
|
+
}
|
|
286
|
+
if (out.command !== "fund" && out.open) fail("--open only applies to `settld wallet fund`");
|
|
287
|
+
if (out.command !== "fund" && out.hostedUrl) fail("--hosted-url only applies to `settld wallet fund`");
|
|
288
|
+
if (out.command !== "fund" && out.nonInteractive) fail("--non-interactive only applies to `settld wallet fund`");
|
|
289
|
+
if (out.command !== "balance" && out.watch) fail("--watch only applies to `settld wallet balance`");
|
|
290
|
+
if (out.command !== "balance" && out.minUsdc !== null) fail("--min-usdc only applies to `settld wallet balance`");
|
|
291
|
+
if (out.command !== "balance" && out.intervalSeconds !== 5) fail("--interval-seconds only applies to `settld wallet balance`");
|
|
292
|
+
if (out.command !== "balance" && out.timeoutSeconds !== 180) fail("--timeout-seconds only applies to `settld wallet balance`");
|
|
293
|
+
if (out.command === "balance") {
|
|
294
|
+
out.intervalSeconds = parseNonNegativeNumber(out.intervalSeconds, { field: "--interval-seconds" });
|
|
295
|
+
out.timeoutSeconds = parsePositiveNumber(out.timeoutSeconds, { field: "--timeout-seconds" });
|
|
296
|
+
if (out.minUsdc !== null && String(out.minUsdc).trim() !== "") {
|
|
297
|
+
out.minUsdc = parseNonNegativeNumber(out.minUsdc, { field: "--min-usdc" });
|
|
298
|
+
} else {
|
|
299
|
+
out.minUsdc = null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
out.sessionFile = path.resolve(process.cwd(), String(out.sessionFile ?? "").trim() || defaultSessionPath());
|
|
303
|
+
if (out.jsonOut) out.jsonOut = path.resolve(process.cwd(), String(out.jsonOut));
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function resolveRuntimeConfig({
|
|
308
|
+
args,
|
|
309
|
+
env = process.env,
|
|
310
|
+
readSavedSessionImpl = readSavedSession
|
|
311
|
+
} = {}) {
|
|
312
|
+
const saved = await readSavedSessionImpl({ sessionPath: args.sessionFile });
|
|
313
|
+
const baseUrl = normalizeHttpUrl(args.baseUrl ?? env.SETTLD_BASE_URL ?? saved?.baseUrl ?? "https://api.settld.work");
|
|
314
|
+
if (!baseUrl) fail("base URL must be a valid http(s) URL");
|
|
315
|
+
|
|
316
|
+
const tenantId = safeTrim(args.tenantId ?? env.SETTLD_TENANT_ID ?? saved?.tenantId ?? "");
|
|
317
|
+
if (!tenantId) fail("tenant ID is required (pass --tenant-id or run `settld login` first)");
|
|
318
|
+
|
|
319
|
+
const cookie = safeTrim(args.cookie ?? env.SETTLD_SESSION_COOKIE ?? saved?.cookie ?? "");
|
|
320
|
+
const magicLinkApiKey = safeTrim(
|
|
321
|
+
args.magicLinkApiKey ??
|
|
322
|
+
env.SETTLD_MAGIC_LINK_API_KEY ??
|
|
323
|
+
env.SETTLD_BOOTSTRAP_API_KEY ??
|
|
324
|
+
env.SETTLD_SETUP_API_KEY ??
|
|
325
|
+
""
|
|
326
|
+
);
|
|
327
|
+
if (!cookie && !magicLinkApiKey) {
|
|
328
|
+
fail("auth required: pass --cookie/--magic-link-api-key or run `settld login` first");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
baseUrl,
|
|
333
|
+
tenantId,
|
|
334
|
+
cookie: cookie || null,
|
|
335
|
+
magicLinkApiKey: magicLinkApiKey || null
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function buildCirclePayload(args, { faucet = false, includeBalances = false } = {}) {
|
|
340
|
+
return {
|
|
341
|
+
mode: String(args.circleMode ?? "auto").trim() || "auto",
|
|
342
|
+
faucet: Boolean(faucet),
|
|
343
|
+
includeBalances: Boolean(includeBalances),
|
|
344
|
+
...(safeTrim(args.circleBaseUrl) ? { baseUrl: safeTrim(args.circleBaseUrl) } : {}),
|
|
345
|
+
...(safeTrim(args.circleBlockchain) ? { blockchain: safeTrim(args.circleBlockchain) } : {}),
|
|
346
|
+
...(safeTrim(args.spendWalletId) ? { spendWalletId: safeTrim(args.spendWalletId) } : {}),
|
|
347
|
+
...(safeTrim(args.escrowWalletId) ? { escrowWalletId: safeTrim(args.escrowWalletId) } : {}),
|
|
348
|
+
...(safeTrim(args.tokenIdUsdc) ? { tokenIdUsdc: safeTrim(args.tokenIdUsdc) } : {})
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function buildWalletBootstrapBody({ args, faucet = false, includeBalances = false } = {}) {
|
|
353
|
+
const provider = String(args.provider ?? "circle").trim() || "circle";
|
|
354
|
+
return {
|
|
355
|
+
provider,
|
|
356
|
+
...(provider === "circle" ? { circle: buildCirclePayload(args, { faucet, includeBalances }) } : {})
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function requestJson({ url, method = "GET", body = undefined, cookie = null, magicLinkApiKey = null, fetchImpl = fetch } = {}) {
|
|
361
|
+
const headers = {};
|
|
362
|
+
if (body !== undefined) headers["content-type"] = "application/json";
|
|
363
|
+
if (cookie) headers.cookie = cookie;
|
|
364
|
+
if (magicLinkApiKey) headers["x-api-key"] = magicLinkApiKey;
|
|
365
|
+
const res = await fetchImpl(url, {
|
|
366
|
+
method,
|
|
367
|
+
headers,
|
|
368
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
369
|
+
});
|
|
370
|
+
const text = await res.text();
|
|
371
|
+
let json = null;
|
|
372
|
+
try {
|
|
373
|
+
json = text ? JSON.parse(text) : null;
|
|
374
|
+
} catch {
|
|
375
|
+
json = null;
|
|
376
|
+
}
|
|
377
|
+
return { res, json, text };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function requestWalletBootstrap({
|
|
381
|
+
baseUrl,
|
|
382
|
+
tenantId,
|
|
383
|
+
cookie = null,
|
|
384
|
+
magicLinkApiKey = null,
|
|
385
|
+
body,
|
|
386
|
+
fetchImpl = fetch
|
|
387
|
+
} = {}) {
|
|
388
|
+
const url = new URL(`/v1/tenants/${encodeURIComponent(tenantId)}/onboarding/wallet-bootstrap`, `${baseUrl}/`).toString();
|
|
389
|
+
const { res, json, text } = await requestJson({
|
|
390
|
+
url,
|
|
391
|
+
method: "POST",
|
|
392
|
+
body,
|
|
393
|
+
cookie,
|
|
394
|
+
magicLinkApiKey,
|
|
395
|
+
fetchImpl
|
|
396
|
+
});
|
|
397
|
+
if (!res.ok) {
|
|
398
|
+
const message =
|
|
399
|
+
json && typeof json === "object"
|
|
400
|
+
? json?.message ?? json?.error ?? `HTTP ${res.status}`
|
|
401
|
+
: text || `HTTP ${res.status}`;
|
|
402
|
+
fail(`wallet bootstrap failed (${res.status}): ${String(message)}`);
|
|
403
|
+
}
|
|
404
|
+
const bootstrap = json?.walletBootstrap;
|
|
405
|
+
if (!isPlainObject(bootstrap)) fail("wallet bootstrap response missing walletBootstrap object");
|
|
406
|
+
return bootstrap;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function requestWalletFundingPlan({
|
|
410
|
+
baseUrl,
|
|
411
|
+
tenantId,
|
|
412
|
+
cookie = null,
|
|
413
|
+
magicLinkApiKey = null,
|
|
414
|
+
body,
|
|
415
|
+
fetchImpl = fetch
|
|
416
|
+
} = {}) {
|
|
417
|
+
const url = new URL(`/v1/tenants/${encodeURIComponent(tenantId)}/onboarding/wallet-funding`, `${baseUrl}/`).toString();
|
|
418
|
+
const { res, json, text } = await requestJson({
|
|
419
|
+
url,
|
|
420
|
+
method: "POST",
|
|
421
|
+
body,
|
|
422
|
+
cookie,
|
|
423
|
+
magicLinkApiKey,
|
|
424
|
+
fetchImpl
|
|
425
|
+
});
|
|
426
|
+
if (res.status === 404) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
if (!res.ok) {
|
|
430
|
+
const message =
|
|
431
|
+
json && typeof json === "object"
|
|
432
|
+
? json?.message ?? json?.error ?? `HTTP ${res.status}`
|
|
433
|
+
: text || `HTTP ${res.status}`;
|
|
434
|
+
fail(`wallet funding request failed (${res.status}): ${String(message)}`);
|
|
435
|
+
}
|
|
436
|
+
if (!isPlainObject(json)) fail("wallet funding response must be an object");
|
|
437
|
+
return json;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function extractWalletSnapshot(walletBootstrap) {
|
|
441
|
+
const root = isPlainObject(walletBootstrap) ? walletBootstrap : {};
|
|
442
|
+
const spend = isPlainObject(root.wallets?.spend) ? root.wallets.spend : {};
|
|
443
|
+
const escrow = isPlainObject(root.wallets?.escrow) ? root.wallets.escrow : {};
|
|
444
|
+
const balances = isPlainObject(root.balances) ? root.balances : {};
|
|
445
|
+
const spendBalance = isPlainObject(balances.spend) ? balances.spend : {};
|
|
446
|
+
const escrowBalance = isPlainObject(balances.escrow) ? balances.escrow : {};
|
|
447
|
+
return {
|
|
448
|
+
provider: String(root.provider ?? "circle"),
|
|
449
|
+
mode: safeTrim(root.mode) || null,
|
|
450
|
+
baseUrl: safeTrim(root.baseUrl) || null,
|
|
451
|
+
blockchain: safeTrim(root.blockchain) || null,
|
|
452
|
+
tokenIdUsdc: safeTrim(root.tokenIdUsdc) || null,
|
|
453
|
+
spendWallet: {
|
|
454
|
+
walletId: safeTrim(spend.walletId) || null,
|
|
455
|
+
address: safeTrim(spend.address) || null,
|
|
456
|
+
usdcAmount: usdcAmountNumber(spendBalance.usdcAmount),
|
|
457
|
+
usdcAmountText: safeTrim(spendBalance.usdcAmountText) || null
|
|
458
|
+
},
|
|
459
|
+
escrowWallet: {
|
|
460
|
+
walletId: safeTrim(escrow.walletId) || null,
|
|
461
|
+
address: safeTrim(escrow.address) || null,
|
|
462
|
+
usdcAmount: usdcAmountNumber(escrowBalance.usdcAmount),
|
|
463
|
+
usdcAmountText: safeTrim(escrowBalance.usdcAmountText) || null
|
|
464
|
+
},
|
|
465
|
+
balances: {
|
|
466
|
+
asOf: safeTrim(balances.asOf) || null,
|
|
467
|
+
error: safeTrim(balances.error) || null
|
|
468
|
+
},
|
|
469
|
+
faucetEnabled: Boolean(root.faucetEnabled),
|
|
470
|
+
faucetResults: Array.isArray(root.faucetResults) ? root.faucetResults : []
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function openInBrowser(url) {
|
|
475
|
+
const target = String(url ?? "").trim();
|
|
476
|
+
if (!target) return { ok: false, message: "missing URL" };
|
|
477
|
+
const platform = process.platform;
|
|
478
|
+
let result;
|
|
479
|
+
if (platform === "darwin") {
|
|
480
|
+
result = spawnSync("open", [target], { stdio: "ignore" });
|
|
481
|
+
} else if (platform === "win32") {
|
|
482
|
+
result = spawnSync("cmd", ["/c", "start", "", target], { stdio: "ignore" });
|
|
483
|
+
} else {
|
|
484
|
+
result = spawnSync("xdg-open", [target], { stdio: "ignore" });
|
|
485
|
+
}
|
|
486
|
+
if (result.error) {
|
|
487
|
+
return { ok: false, message: result.error.message || "failed to open browser" };
|
|
488
|
+
}
|
|
489
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
490
|
+
return { ok: false, message: `open command exited with ${result.status}` };
|
|
491
|
+
}
|
|
492
|
+
return { ok: true };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function sleep(ms) {
|
|
496
|
+
await new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function deriveFundingChoiceFromPlan(plan) {
|
|
500
|
+
const optionRows = Array.isArray(plan?.options) ? plan.options : [];
|
|
501
|
+
const cardBank = optionRows.find((row) => String(row?.optionId ?? "") === "card_bank");
|
|
502
|
+
const transfer = optionRows.find((row) => String(row?.optionId ?? "") === "transfer");
|
|
503
|
+
return {
|
|
504
|
+
cardBankAvailable: Boolean(cardBank?.available),
|
|
505
|
+
transferAvailable: Boolean(transfer?.available ?? true),
|
|
506
|
+
recommendedOptionId: safeTrim(plan?.recommendedOptionId) || (cardBank?.available ? "card_bank" : "transfer"),
|
|
507
|
+
preferredHostedMethod: safeTrim(cardBank?.preferredMethod) || "card"
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function promptFundMethod({
|
|
512
|
+
plan,
|
|
513
|
+
stdin = process.stdin,
|
|
514
|
+
stdout = process.stdout
|
|
515
|
+
} = {}) {
|
|
516
|
+
const derived = deriveFundingChoiceFromPlan(plan);
|
|
517
|
+
if (!stdin?.isTTY || !stdout?.isTTY) {
|
|
518
|
+
return derived.recommendedOptionId === "card_bank" ? derived.preferredHostedMethod : "transfer";
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const rows = [];
|
|
522
|
+
if (derived.cardBankAvailable) rows.push({ id: "card_bank", label: "Card/Bank top-up (Recommended)" });
|
|
523
|
+
if (derived.transferAvailable) rows.push({ id: "transfer", label: "USDC transfer" });
|
|
524
|
+
if (rows.length === 0) fail("no available funding options");
|
|
525
|
+
if (rows.length === 1) return rows[0].id === "card_bank" ? derived.preferredHostedMethod : "transfer";
|
|
526
|
+
|
|
527
|
+
stdout.write("Select funding method\n");
|
|
528
|
+
stdout.write("=====================\n");
|
|
529
|
+
rows.forEach((row, index) => {
|
|
530
|
+
stdout.write(`${index + 1}) ${row.label}\n`);
|
|
531
|
+
});
|
|
532
|
+
const defaultChoice = derived.recommendedOptionId === "card_bank" ? "1" : "2";
|
|
533
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
534
|
+
try {
|
|
535
|
+
const answerRaw = String(await rl.question(`Choose [${defaultChoice}]: `) ?? "").trim();
|
|
536
|
+
const answer = answerRaw || defaultChoice;
|
|
537
|
+
const selected = rows[Number(answer) - 1] ?? rows.find((row) => row.id === answer) ?? null;
|
|
538
|
+
if (!selected) return derived.recommendedOptionId === "card_bank" ? derived.preferredHostedMethod : "transfer";
|
|
539
|
+
return selected.id === "card_bank" ? derived.preferredHostedMethod : "transfer";
|
|
540
|
+
} finally {
|
|
541
|
+
rl.close();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function readWalletSnapshot({ args, runtime, fetchImpl, includeBalances = false, faucet = false } = {}) {
|
|
546
|
+
const walletBootstrap = await requestWalletBootstrap({
|
|
547
|
+
baseUrl: runtime.baseUrl,
|
|
548
|
+
tenantId: runtime.tenantId,
|
|
549
|
+
cookie: runtime.cookie,
|
|
550
|
+
magicLinkApiKey: runtime.magicLinkApiKey,
|
|
551
|
+
body: buildWalletBootstrapBody({ args, faucet, includeBalances }),
|
|
552
|
+
fetchImpl
|
|
553
|
+
});
|
|
554
|
+
return extractWalletSnapshot(walletBootstrap);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function runBalance({
|
|
558
|
+
args,
|
|
559
|
+
runtime,
|
|
560
|
+
fetchImpl
|
|
561
|
+
} = {}) {
|
|
562
|
+
if (!args.watch) {
|
|
563
|
+
const wallet = await readWalletSnapshot({ args, runtime, fetchImpl, includeBalances: true, faucet: false });
|
|
564
|
+
return {
|
|
565
|
+
ok: true,
|
|
566
|
+
schemaVersion: "SettldWalletBalance.v1",
|
|
567
|
+
baseUrl: runtime.baseUrl,
|
|
568
|
+
tenantId: runtime.tenantId,
|
|
569
|
+
watch: null,
|
|
570
|
+
wallet
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const target = args.minUsdc === null ? 0.000001 : Number(args.minUsdc);
|
|
575
|
+
const deadline = Date.now() + Math.round(args.timeoutSeconds * 1000);
|
|
576
|
+
const intervalMs = Math.round(Number(args.intervalSeconds) * 1000);
|
|
577
|
+
const samples = [];
|
|
578
|
+
let latest = null;
|
|
579
|
+
let satisfied = false;
|
|
580
|
+
|
|
581
|
+
while (Date.now() <= deadline) {
|
|
582
|
+
latest = await readWalletSnapshot({ args, runtime, fetchImpl, includeBalances: true, faucet: false });
|
|
583
|
+
const amount = usdcAmountNumber(latest?.spendWallet?.usdcAmount);
|
|
584
|
+
samples.push({
|
|
585
|
+
at: new Date().toISOString(),
|
|
586
|
+
spendUsdc: amount
|
|
587
|
+
});
|
|
588
|
+
if (amount !== null && amount >= target) {
|
|
589
|
+
satisfied = true;
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
if (Date.now() >= deadline) break;
|
|
593
|
+
if (intervalMs > 0) await sleep(intervalMs);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
ok: satisfied,
|
|
598
|
+
schemaVersion: "SettldWalletBalance.v1",
|
|
599
|
+
baseUrl: runtime.baseUrl,
|
|
600
|
+
tenantId: runtime.tenantId,
|
|
601
|
+
watch: {
|
|
602
|
+
enabled: true,
|
|
603
|
+
targetSpendUsdc: target,
|
|
604
|
+
intervalSeconds: Number(args.intervalSeconds),
|
|
605
|
+
timeoutSeconds: Number(args.timeoutSeconds),
|
|
606
|
+
attempts: samples.length,
|
|
607
|
+
satisfied
|
|
608
|
+
},
|
|
609
|
+
wallet: latest,
|
|
610
|
+
samples
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function renderText({ payload, args }) {
|
|
615
|
+
if (args.command === "status") {
|
|
616
|
+
const wallet = payload.wallet ?? {};
|
|
617
|
+
const spend = wallet.spendWallet ?? {};
|
|
618
|
+
const escrow = wallet.escrowWallet ?? {};
|
|
619
|
+
const lines = [
|
|
620
|
+
"Wallet status",
|
|
621
|
+
"=============",
|
|
622
|
+
`Tenant: ${payload.tenantId}`,
|
|
623
|
+
`Provider: ${wallet.provider ?? "unknown"} (${wallet.mode ?? "unknown"})`,
|
|
624
|
+
`Network: ${wallet.blockchain ?? "unknown"}`,
|
|
625
|
+
`USDC token: ${wallet.tokenIdUsdc ?? "n/a"}`,
|
|
626
|
+
`Spend wallet: ${spend.walletId ?? "n/a"} (${spend.address ?? "n/a"})`,
|
|
627
|
+
`Escrow wallet: ${escrow.walletId ?? "n/a"} (${escrow.address ?? "n/a"})`
|
|
628
|
+
];
|
|
629
|
+
if (spend.usdcAmount !== null) lines.push(`Spend USDC balance: ${spend.usdcAmount}`);
|
|
630
|
+
if (escrow.usdcAmount !== null) lines.push(`Escrow USDC balance: ${escrow.usdcAmount}`);
|
|
631
|
+
if (wallet.balances?.error) lines.push(`Balance warning: ${wallet.balances.error}`);
|
|
632
|
+
return `${lines.join("\n")}\n`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (args.command === "balance") {
|
|
636
|
+
const wallet = payload.wallet ?? {};
|
|
637
|
+
const spend = wallet.spendWallet ?? {};
|
|
638
|
+
const escrow = wallet.escrowWallet ?? {};
|
|
639
|
+
const lines = [
|
|
640
|
+
"Wallet balance",
|
|
641
|
+
"==============",
|
|
642
|
+
`Tenant: ${payload.tenantId}`,
|
|
643
|
+
`Spend wallet: ${spend.walletId ?? "n/a"} (${spend.address ?? "n/a"})`,
|
|
644
|
+
`Spend USDC: ${spend.usdcAmount ?? "n/a"}`,
|
|
645
|
+
`Escrow wallet: ${escrow.walletId ?? "n/a"} (${escrow.address ?? "n/a"})`,
|
|
646
|
+
`Escrow USDC: ${escrow.usdcAmount ?? "n/a"}`
|
|
647
|
+
];
|
|
648
|
+
if (payload.watch?.enabled) {
|
|
649
|
+
lines.push(
|
|
650
|
+
`Watch: attempts=${payload.watch.attempts} target=${payload.watch.targetSpendUsdc} satisfied=${payload.watch.satisfied}`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
if (wallet.balances?.error) lines.push(`Balance warning: ${wallet.balances.error}`);
|
|
654
|
+
return `${lines.join("\n")}\n`;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const lines = ["Wallet funding", "=============="];
|
|
658
|
+
lines.push(`Tenant: ${payload.tenantId}`);
|
|
659
|
+
lines.push(`Method: ${payload.method}`);
|
|
660
|
+
if (payload.method === "transfer") {
|
|
661
|
+
lines.push(`Send: USDC on ${payload.transfer?.blockchain ?? "unknown"}`);
|
|
662
|
+
lines.push(`To: ${payload.transfer?.address ?? "n/a"}`);
|
|
663
|
+
if (payload.transfer?.walletId) lines.push(`Spend wallet: ${payload.transfer.walletId}`);
|
|
664
|
+
lines.push("Then run: settld wallet balance --watch --min-usdc 1");
|
|
665
|
+
} else if (payload.method === "faucet") {
|
|
666
|
+
const statuses = Array.isArray(payload.faucet?.results)
|
|
667
|
+
? payload.faucet.results.map((row) => `${row.wallet}:HTTP${row.status}`).join(", ")
|
|
668
|
+
: "none";
|
|
669
|
+
lines.push(`Faucet status: ${statuses}`);
|
|
670
|
+
} else {
|
|
671
|
+
lines.push(`Hosted funding URL: ${payload.hosted?.url ?? "n/a"}`);
|
|
672
|
+
if (payload.hosted?.opened === true) {
|
|
673
|
+
lines.push("Opened in browser.");
|
|
674
|
+
} else {
|
|
675
|
+
lines.push("Pass --open to launch this URL in your browser.");
|
|
676
|
+
}
|
|
677
|
+
if (payload.hosted?.openError) lines.push(`Open warning: ${payload.hosted.openError}`);
|
|
678
|
+
}
|
|
679
|
+
return `${lines.join("\n")}\n`;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function writeJsonOut(jsonOutPath, payload) {
|
|
683
|
+
if (!jsonOutPath) return;
|
|
684
|
+
await fs.mkdir(path.dirname(jsonOutPath), { recursive: true });
|
|
685
|
+
await fs.writeFile(jsonOutPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function normalizeHostedSession(session, { hostedOverride = null } = {}) {
|
|
689
|
+
const result = isPlainObject(session) ? { ...session } : {};
|
|
690
|
+
if (hostedOverride) result.url = hostedOverride;
|
|
691
|
+
const url = normalizeHttpUrl(result.url ?? null);
|
|
692
|
+
if (!url) fail("hosted funding URL is missing or invalid");
|
|
693
|
+
return {
|
|
694
|
+
type: "hosted",
|
|
695
|
+
method: safeTrim(result.method) || "card",
|
|
696
|
+
url
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export async function runWalletCli({
|
|
701
|
+
argv = process.argv.slice(2),
|
|
702
|
+
env = process.env,
|
|
703
|
+
stdin = process.stdin,
|
|
704
|
+
stdout = process.stdout,
|
|
705
|
+
fetchImpl = fetch,
|
|
706
|
+
readSavedSessionImpl = readSavedSession,
|
|
707
|
+
openInBrowserImpl = openInBrowser
|
|
708
|
+
} = {}) {
|
|
709
|
+
const args = parseArgs(argv);
|
|
710
|
+
if (args.help) {
|
|
711
|
+
usage();
|
|
712
|
+
return { ok: true, code: 0 };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const runtime = await resolveRuntimeConfig({
|
|
716
|
+
args,
|
|
717
|
+
env,
|
|
718
|
+
readSavedSessionImpl
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
let payload;
|
|
722
|
+
if (args.command === "status") {
|
|
723
|
+
const wallet = await readWalletSnapshot({ args, runtime, fetchImpl, includeBalances: true, faucet: false });
|
|
724
|
+
payload = {
|
|
725
|
+
ok: true,
|
|
726
|
+
schemaVersion: "SettldWalletStatus.v1",
|
|
727
|
+
baseUrl: runtime.baseUrl,
|
|
728
|
+
tenantId: runtime.tenantId,
|
|
729
|
+
wallet
|
|
730
|
+
};
|
|
731
|
+
} else if (args.command === "balance") {
|
|
732
|
+
payload = await runBalance({ args, runtime, fetchImpl });
|
|
733
|
+
if (args.watch && !payload.ok) {
|
|
734
|
+
fail(
|
|
735
|
+
`wallet balance watch timed out after ${args.timeoutSeconds}s without reaching spend USDC >= ${payload.watch?.targetSpendUsdc}`
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
} else if (args.command === "fund") {
|
|
739
|
+
if (args.method === "faucet") {
|
|
740
|
+
const wallet = await readWalletSnapshot({
|
|
741
|
+
args,
|
|
742
|
+
runtime,
|
|
743
|
+
fetchImpl,
|
|
744
|
+
includeBalances: true,
|
|
745
|
+
faucet: true
|
|
746
|
+
});
|
|
747
|
+
payload = {
|
|
748
|
+
ok: true,
|
|
749
|
+
schemaVersion: "SettldWalletFundResult.v1",
|
|
750
|
+
baseUrl: runtime.baseUrl,
|
|
751
|
+
tenantId: runtime.tenantId,
|
|
752
|
+
method: "faucet",
|
|
753
|
+
faucet: {
|
|
754
|
+
blockchain: wallet.blockchain,
|
|
755
|
+
results: Array.isArray(wallet.faucetResults) ? wallet.faucetResults : []
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
} else {
|
|
759
|
+
let selectedMethod = args.method;
|
|
760
|
+
const requestBase = {
|
|
761
|
+
provider: String(args.provider ?? "circle").trim() || "circle",
|
|
762
|
+
...(String(args.provider ?? "circle").trim().toLowerCase() === "circle"
|
|
763
|
+
? { circle: buildCirclePayload(args, { faucet: false, includeBalances: true }) }
|
|
764
|
+
: {})
|
|
765
|
+
};
|
|
766
|
+
if (safeTrim(args.hostedUrl)) requestBase.hostedUrl = safeTrim(args.hostedUrl);
|
|
767
|
+
|
|
768
|
+
if (!selectedMethod) {
|
|
769
|
+
const plan = await requestWalletFundingPlan({
|
|
770
|
+
baseUrl: runtime.baseUrl,
|
|
771
|
+
tenantId: runtime.tenantId,
|
|
772
|
+
cookie: runtime.cookie,
|
|
773
|
+
magicLinkApiKey: runtime.magicLinkApiKey,
|
|
774
|
+
body: requestBase,
|
|
775
|
+
fetchImpl
|
|
776
|
+
});
|
|
777
|
+
if (!plan || !isPlainObject(plan)) fail("wallet funding plan is unavailable");
|
|
778
|
+
if (args.nonInteractive) {
|
|
779
|
+
const derived = deriveFundingChoiceFromPlan(plan);
|
|
780
|
+
selectedMethod = derived.recommendedOptionId === "card_bank" ? derived.preferredHostedMethod : "transfer";
|
|
781
|
+
} else {
|
|
782
|
+
selectedMethod = await promptFundMethod({ plan, stdin, stdout });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (selectedMethod === "transfer") {
|
|
787
|
+
const funding = await requestWalletFundingPlan({
|
|
788
|
+
baseUrl: runtime.baseUrl,
|
|
789
|
+
tenantId: runtime.tenantId,
|
|
790
|
+
cookie: runtime.cookie,
|
|
791
|
+
magicLinkApiKey: runtime.magicLinkApiKey,
|
|
792
|
+
body: {
|
|
793
|
+
...requestBase,
|
|
794
|
+
method: "transfer"
|
|
795
|
+
},
|
|
796
|
+
fetchImpl
|
|
797
|
+
});
|
|
798
|
+
const session = funding?.session;
|
|
799
|
+
if (!isPlainObject(session) || String(session.type) !== "transfer") {
|
|
800
|
+
fail("wallet funding response missing transfer session");
|
|
801
|
+
}
|
|
802
|
+
payload = {
|
|
803
|
+
ok: true,
|
|
804
|
+
schemaVersion: "SettldWalletFundResult.v1",
|
|
805
|
+
baseUrl: runtime.baseUrl,
|
|
806
|
+
tenantId: runtime.tenantId,
|
|
807
|
+
method: "transfer",
|
|
808
|
+
transfer: {
|
|
809
|
+
blockchain: safeTrim(session.blockchain) || null,
|
|
810
|
+
token: safeTrim(session.token) || "USDC",
|
|
811
|
+
tokenIdUsdc: safeTrim(session.tokenIdUsdc) || null,
|
|
812
|
+
walletId: safeTrim(session.walletId) || null,
|
|
813
|
+
address: safeTrim(session.address) || null
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
if (!payload.transfer.address) fail("spend wallet address is missing; cannot produce transfer destination");
|
|
817
|
+
} else if (selectedMethod === "card" || selectedMethod === "bank") {
|
|
818
|
+
const funding = await requestWalletFundingPlan({
|
|
819
|
+
baseUrl: runtime.baseUrl,
|
|
820
|
+
tenantId: runtime.tenantId,
|
|
821
|
+
cookie: runtime.cookie,
|
|
822
|
+
magicLinkApiKey: runtime.magicLinkApiKey,
|
|
823
|
+
body: {
|
|
824
|
+
...requestBase,
|
|
825
|
+
method: selectedMethod
|
|
826
|
+
},
|
|
827
|
+
fetchImpl
|
|
828
|
+
});
|
|
829
|
+
const session = normalizeHostedSession(funding?.session, { hostedOverride: safeTrim(args.hostedUrl) || null });
|
|
830
|
+
const openResult = args.open ? openInBrowserImpl(session.url) : { ok: false, message: null };
|
|
831
|
+
payload = {
|
|
832
|
+
ok: true,
|
|
833
|
+
schemaVersion: "SettldWalletFundResult.v1",
|
|
834
|
+
baseUrl: runtime.baseUrl,
|
|
835
|
+
tenantId: runtime.tenantId,
|
|
836
|
+
method: selectedMethod,
|
|
837
|
+
hosted: {
|
|
838
|
+
url: session.url,
|
|
839
|
+
opened: Boolean(openResult.ok),
|
|
840
|
+
openError: openResult.ok ? null : (openResult.message ?? null)
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
} else {
|
|
844
|
+
fail(`unsupported fund method: ${selectedMethod}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
} else {
|
|
848
|
+
fail(`unsupported wallet command: ${args.command}`);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
await writeJsonOut(args.jsonOut, payload);
|
|
852
|
+
if (args.format === "json") {
|
|
853
|
+
stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
854
|
+
} else {
|
|
855
|
+
stdout.write(renderText({ payload, args }));
|
|
856
|
+
}
|
|
857
|
+
return payload;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async function main(argv = process.argv.slice(2)) {
|
|
861
|
+
try {
|
|
862
|
+
await runWalletCli({ argv });
|
|
863
|
+
} catch (err) {
|
|
864
|
+
process.stderr.write(`${err?.message ?? String(err)}\n`);
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
870
|
+
main();
|
|
871
|
+
}
|