mppx 0.3.13 → 0.3.15
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/dist/Challenge.d.ts +1 -1
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +107 -15
- package/dist/Challenge.js.map +1 -1
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +26 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1478 -915
- package/dist/cli.js.map +1 -1
- package/dist/client/Mppx.d.ts +2 -0
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +2 -0
- package/dist/client/Mppx.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +2 -1
- package/dist/server/Mppx.js.map +1 -1
- package/dist/stripe/Methods.d.ts +0 -3
- package/dist/stripe/Methods.d.ts.map +1 -1
- package/dist/stripe/Methods.js +0 -2
- package/dist/stripe/Methods.js.map +1 -1
- package/dist/stripe/client/Charge.d.ts +0 -3
- package/dist/stripe/client/Charge.d.ts.map +1 -1
- package/dist/stripe/client/Charge.js +2 -2
- package/dist/stripe/client/Charge.js.map +1 -1
- package/dist/stripe/client/Methods.d.ts +0 -3
- package/dist/stripe/client/Methods.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts +0 -3
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +2 -2
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/Methods.d.ts +0 -3
- package/dist/stripe/server/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.d.ts +0 -3
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +3 -3
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts +13 -3
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +18 -1
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +4 -3
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Charge.d.ts +0 -3
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +2 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +0 -3
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/Challenge.test.ts +94 -18
- package/src/Challenge.ts +118 -15
- package/src/PaymentRequest.test.ts +0 -5
- package/src/bin.ts +4 -0
- package/src/cli.test.ts +180 -252
- package/src/cli.ts +1085 -485
- package/src/client/Mppx.test-d.ts +9 -0
- package/src/client/Mppx.test.ts +83 -5
- package/src/client/Mppx.ts +5 -0
- package/src/client/Transport.test.ts +5 -8
- package/src/client/internal/Fetch.browser.test.ts +135 -0
- package/src/client/internal/Fetch.test.ts +0 -88
- package/src/mcp-sdk/client/McpClient.test.ts +1 -1
- package/src/server/Mppx.ts +3 -1
- package/src/server/Transport.test.ts +6 -9
- package/src/stripe/Methods.ts +0 -2
- package/src/stripe/client/Charge.ts +2 -2
- package/src/stripe/server/Charge.ts +2 -2
- package/src/tempo/Methods.test.ts +22 -0
- package/src/tempo/Methods.ts +3 -3
- package/src/tempo/client/Charge.ts +29 -1
- package/src/tempo/server/Charge.test.ts +34 -72
- package/src/tempo/server/Charge.ts +2 -1
package/dist/cli.js
CHANGED
|
@@ -1,1035 +1,1485 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
import * as child from 'node:child_process';
|
|
3
2
|
import * as fs from 'node:fs';
|
|
4
3
|
import { createRequire } from 'node:module';
|
|
5
4
|
import * as os from 'node:os';
|
|
6
5
|
import * as path from 'node:path';
|
|
7
6
|
import * as readline from 'node:readline';
|
|
8
|
-
import {
|
|
7
|
+
import { Cli, z } from 'incur';
|
|
9
8
|
import { Base64 } from 'ox';
|
|
10
9
|
import { createClient, http } from 'viem';
|
|
11
10
|
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
12
11
|
import { tempo as tempoMainnet, tempoModerato } from 'viem/chains';
|
|
13
|
-
import { z } from 'zod/mini';
|
|
14
12
|
import * as Challenge from './Challenge.js';
|
|
15
13
|
import * as Credential from './Credential.js';
|
|
16
14
|
import * as Mppx from './client/Mppx.js';
|
|
17
15
|
import { stripe } from './stripe/client/index.js';
|
|
18
16
|
import { tempo } from './tempo/client/index.js';
|
|
19
17
|
import { signVoucher } from './tempo/session/Voucher.js';
|
|
18
|
+
function readStdin() {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let data = '';
|
|
21
|
+
process.stdin.setEncoding('utf-8');
|
|
22
|
+
process.stdin.on('data', (chunk) => {
|
|
23
|
+
data += chunk;
|
|
24
|
+
});
|
|
25
|
+
process.stdin.on('end', () => resolve(data.trim()));
|
|
26
|
+
process.stdin.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
20
29
|
const require = createRequire(import.meta.url);
|
|
21
30
|
const { name, version } = require('../package.json');
|
|
22
|
-
const cli =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
31
|
+
const cli = Cli.create('mppx', {
|
|
32
|
+
version,
|
|
33
|
+
description: 'Make HTTP requests with automatic payment',
|
|
34
|
+
usage: [{ suffix: '<url> [options]' }],
|
|
35
|
+
args: z.object({
|
|
36
|
+
url: z.string().describe('URL to make payment request to'),
|
|
37
|
+
}),
|
|
38
|
+
options: z.object({
|
|
39
|
+
account: z.string().optional().describe('Account name (env: MPPX_ACCOUNT)'),
|
|
40
|
+
confirm: z.boolean().optional().describe('Show confirmation prompts'),
|
|
41
|
+
data: z.string().optional().describe('Send request body (implies POST unless -X is set)'),
|
|
42
|
+
fail: z.boolean().optional().describe('Fail silently on HTTP errors (exit 22)'),
|
|
43
|
+
header: z.array(z.string()).optional().describe('Add header (repeatable)'),
|
|
44
|
+
include: z.boolean().optional().describe('Include response headers in output'),
|
|
45
|
+
insecure: z
|
|
46
|
+
.boolean()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Skip TLS certificate verification (true for localhost/.local)'),
|
|
49
|
+
jsonBody: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Send JSON body (sets Content-Type and Accept, implies POST)'),
|
|
53
|
+
location: z.boolean().optional().describe('Follow redirects'),
|
|
54
|
+
method: z.string().optional().describe('HTTP method'),
|
|
55
|
+
methodOpt: z
|
|
56
|
+
.array(z.string())
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('Method-specific option (key=value, repeatable)'),
|
|
59
|
+
rpcUrl: z
|
|
60
|
+
.string()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('RPC endpoint, defaults to public RPC for chain (env: MPPX_RPC_URL)'),
|
|
63
|
+
silent: z.boolean().optional().describe('Silent mode (suppress progress and info)'),
|
|
64
|
+
userAgent: z.string().optional().describe('Set User-Agent header'),
|
|
65
|
+
verbose: z
|
|
66
|
+
.number()
|
|
67
|
+
.default(0)
|
|
68
|
+
.meta({ count: true })
|
|
69
|
+
.describe('Verbosity (-v details, -vv headers)'),
|
|
70
|
+
}),
|
|
71
|
+
alias: {
|
|
72
|
+
account: 'a',
|
|
73
|
+
data: 'd',
|
|
74
|
+
fail: 'f',
|
|
75
|
+
header: 'H',
|
|
76
|
+
include: 'i',
|
|
77
|
+
insecure: 'k',
|
|
78
|
+
jsonBody: 'J',
|
|
79
|
+
location: 'L',
|
|
80
|
+
method: 'X',
|
|
81
|
+
methodOpt: 'M',
|
|
82
|
+
rpcUrl: 'r',
|
|
83
|
+
silent: 's',
|
|
84
|
+
userAgent: 'A',
|
|
85
|
+
verbose: 'v',
|
|
86
|
+
},
|
|
87
|
+
examples: [
|
|
88
|
+
{ args: { url: 'example.com/content' }, description: 'Make a payment request' },
|
|
89
|
+
{
|
|
90
|
+
args: { url: 'example.com/api' },
|
|
91
|
+
options: { jsonBody: '{"key":"value"}' },
|
|
92
|
+
description: 'POST JSON with payment',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
async run({ args, options, error }) {
|
|
96
|
+
const methodOpts = parseMethodOpts(options.methodOpt);
|
|
97
|
+
const silent = options.silent ?? false;
|
|
98
|
+
const info = silent ? (_msg) => { } : (msg) => process.stderr.write(msg);
|
|
99
|
+
let confirmEnabled = options.confirm ?? false;
|
|
100
|
+
if (silent)
|
|
101
|
+
confirmEnabled = false;
|
|
102
|
+
const accountName = resolveAccountName(options.account);
|
|
103
|
+
const headers = {};
|
|
104
|
+
if (options.header) {
|
|
105
|
+
for (const header of options.header) {
|
|
106
|
+
const index = header.indexOf(':');
|
|
107
|
+
if (index === -1) {
|
|
108
|
+
return error({
|
|
109
|
+
code: 'INVALID_HEADER',
|
|
110
|
+
message: `Invalid header format: ${header}`,
|
|
111
|
+
exitCode: 2,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
headers[header.slice(0, index).trim()] = header.slice(index + 1).trim();
|
|
78
115
|
}
|
|
79
|
-
headers[header.slice(0, index).trim()] = header.slice(index + 1).trim();
|
|
80
116
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (options.json) {
|
|
96
|
-
fetchInit.body = options.json;
|
|
97
|
-
headers['Content-Type'] ??= 'application/json';
|
|
98
|
-
headers.Accept ??= 'application/json';
|
|
117
|
+
headers['User-Agent'] = options.userAgent ?? `${name}/${version}`;
|
|
118
|
+
const rawUrl = args.url;
|
|
119
|
+
const url = (() => {
|
|
120
|
+
const hasProtocol = /^https?:\/\//.test(rawUrl);
|
|
121
|
+
const isLocal = /^(localhost|.*\.localhost|127\.0\.0\.1|\[::1\])(:\d+)?/.test(rawUrl);
|
|
122
|
+
return hasProtocol ? rawUrl : `${isLocal ? 'http' : 'https'}://${rawUrl}`;
|
|
123
|
+
})();
|
|
124
|
+
const { hostname } = new URL(url);
|
|
125
|
+
if (options.insecure ||
|
|
126
|
+
hostname === 'localhost' ||
|
|
127
|
+
hostname.endsWith('.localhost') ||
|
|
128
|
+
hostname.endsWith('.local')) {
|
|
129
|
+
process.removeAllListeners('warning');
|
|
130
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
99
131
|
}
|
|
100
|
-
|
|
101
|
-
|
|
132
|
+
// Node.js doesn't resolve *.localhost subdomains to loopback (unlike
|
|
133
|
+
// browsers per RFC 6761). Rewrite the URL to 127.0.0.1 and set the
|
|
134
|
+
// Host header so reverse proxies can route correctly.
|
|
135
|
+
const isSubLocalhost = hostname.endsWith('.localhost') && hostname !== 'localhost';
|
|
136
|
+
const fetchUrl = isSubLocalhost ? url.replace(hostname, '127.0.0.1') : url;
|
|
137
|
+
if (isSubLocalhost) {
|
|
138
|
+
const { host } = new URL(url);
|
|
139
|
+
headers.Host = host;
|
|
102
140
|
}
|
|
103
|
-
|
|
104
|
-
fetchInit
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
141
|
+
try {
|
|
142
|
+
const fetchInit = { redirect: options.location ? 'follow' : 'manual' };
|
|
143
|
+
if (options.jsonBody) {
|
|
144
|
+
fetchInit.body = options.jsonBody;
|
|
145
|
+
headers['Content-Type'] ??= 'application/json';
|
|
146
|
+
headers.Accept ??= 'application/json';
|
|
147
|
+
}
|
|
148
|
+
else if (options.data) {
|
|
149
|
+
fetchInit.body = options.data;
|
|
150
|
+
}
|
|
151
|
+
if (options.method)
|
|
152
|
+
fetchInit.method = options.method.toUpperCase();
|
|
153
|
+
else if (fetchInit.body)
|
|
154
|
+
fetchInit.method = 'POST';
|
|
155
|
+
if (Object.keys(headers).length > 0)
|
|
156
|
+
fetchInit.headers = headers;
|
|
157
|
+
const verbose = options.verbose;
|
|
158
|
+
const printRequestHeaders = (reqUrl, init) => {
|
|
159
|
+
if (verbose < 2)
|
|
160
|
+
return;
|
|
161
|
+
const { pathname, host } = new URL(reqUrl);
|
|
162
|
+
const method = (init.method ?? 'GET').toUpperCase();
|
|
163
|
+
info(`> ${method} ${pathname} HTTP/1.1\n`);
|
|
164
|
+
info(`> Host: ${host}\n`);
|
|
165
|
+
for (const [k, v] of Object.entries((init.headers ?? {})))
|
|
166
|
+
info(`> ${k}: ${v}\n`);
|
|
167
|
+
info('>\n');
|
|
168
|
+
};
|
|
169
|
+
const printResponseHeaders = (res) => {
|
|
170
|
+
if (!options.include && verbose < 2)
|
|
171
|
+
return;
|
|
172
|
+
if (silent)
|
|
173
|
+
return;
|
|
174
|
+
const status = `HTTP/1.1 ${res.status} ${res.statusText}`;
|
|
175
|
+
const out = verbose >= 2 ? process.stderr : process.stdout;
|
|
176
|
+
const prefix = verbose >= 2 ? '< ' : '';
|
|
177
|
+
out.write(`${prefix}${status}\n`);
|
|
178
|
+
for (const [k, v] of res.headers)
|
|
179
|
+
out.write(`${prefix}${k}: ${v}\n`);
|
|
180
|
+
out.write(verbose >= 2 ? '<\n' : '\n');
|
|
181
|
+
};
|
|
182
|
+
printRequestHeaders(url, fetchInit);
|
|
183
|
+
const challengeResponse = await globalThis.fetch(fetchUrl, fetchInit);
|
|
184
|
+
if (challengeResponse.status !== 402) {
|
|
185
|
+
if (options.fail && challengeResponse.status >= 400)
|
|
186
|
+
return error({
|
|
187
|
+
code: 'HTTP_ERROR',
|
|
188
|
+
message: `HTTP error ${challengeResponse.status}`,
|
|
189
|
+
exitCode: 22,
|
|
190
|
+
});
|
|
191
|
+
printResponseHeaders(challengeResponse);
|
|
192
|
+
console.log((await challengeResponse.text()).replace(/\n+$/, ''));
|
|
125
193
|
return;
|
|
126
|
-
const status = `HTTP/1.1 ${res.status} ${res.statusText}`;
|
|
127
|
-
const out = verbose ? process.stderr : process.stdout;
|
|
128
|
-
const prefix = verbose ? '< ' : '';
|
|
129
|
-
out.write(`${prefix}${status}\n`);
|
|
130
|
-
for (const [k, v] of res.headers)
|
|
131
|
-
out.write(`${prefix}${k}: ${v}\n`);
|
|
132
|
-
out.write(verbose ? '<\n' : '\n');
|
|
133
|
-
};
|
|
134
|
-
printRequestHeaders(url, fetchInit);
|
|
135
|
-
const challengeResponse = await globalThis.fetch(url, fetchInit);
|
|
136
|
-
if (challengeResponse.status !== 402) {
|
|
137
|
-
if (options.fail && challengeResponse.status >= 400)
|
|
138
|
-
process.exit(22);
|
|
139
|
-
printResponseHeaders(challengeResponse);
|
|
140
|
-
console.log((await challengeResponse.text()).replace(/\n+$/, ''));
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
const challenge = Challenge.fromResponse(challengeResponse);
|
|
144
|
-
const challengeRequest = challenge.request;
|
|
145
|
-
const currency = challengeRequest.currency;
|
|
146
|
-
const shownKeys = new Set();
|
|
147
|
-
let tokenSymbol = challenge.method === 'stripe' ? (currency?.toUpperCase() ?? '') : (currency ?? '');
|
|
148
|
-
let tokenDecimals = challengeRequest.decimals ?? (challenge.method === 'stripe' ? 2 : 6);
|
|
149
|
-
let explorerUrl;
|
|
150
|
-
// Tempo-specific setup (private key, viem account/client, token info)
|
|
151
|
-
let account;
|
|
152
|
-
let client;
|
|
153
|
-
if (challenge.method === 'tempo') {
|
|
154
|
-
const privateKey = process.env.MPPX_PRIVATE_KEY?.trim() || (await createKeychain(accountName).get());
|
|
155
|
-
if (!privateKey) {
|
|
156
|
-
if (options.account)
|
|
157
|
-
console.error(`Account "${accountName}" not found.`);
|
|
158
|
-
else
|
|
159
|
-
console.error(`No account found.`);
|
|
160
|
-
process.exit(1);
|
|
161
194
|
}
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
explorerUrl
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
195
|
+
const challenge = Challenge.fromResponse(challengeResponse);
|
|
196
|
+
const challengeRequest = challenge.request;
|
|
197
|
+
const currency = challengeRequest.currency;
|
|
198
|
+
const shownKeys = new Set();
|
|
199
|
+
let tokenSymbol = challenge.method === 'stripe' ? (currency?.toUpperCase() ?? '') : (currency ?? '');
|
|
200
|
+
let tokenDecimals = challengeRequest.decimals ?? (challenge.method === 'stripe' ? 2 : 6);
|
|
201
|
+
let explorerUrl;
|
|
202
|
+
// Tempo-specific setup (private key, viem account/client, token info)
|
|
203
|
+
let account;
|
|
204
|
+
let client;
|
|
205
|
+
let useTempoCliSign = false;
|
|
206
|
+
if (challenge.method === 'tempo') {
|
|
207
|
+
const privateKey = process.env.MPPX_PRIVATE_KEY?.trim() ||
|
|
208
|
+
(isTempoAccount(accountName) ? undefined : await createKeychain(accountName).get());
|
|
209
|
+
if (!privateKey && isTempoAccount(accountName) && hasTempoCliSync()) {
|
|
210
|
+
useTempoCliSign = true;
|
|
211
|
+
// Resolve wallet address from keys.toml for display/balance
|
|
212
|
+
const tempoEntry = resolveTempoAccount(accountName);
|
|
213
|
+
if (tempoEntry) {
|
|
214
|
+
const rpcUrl = options.rpcUrl ?? process.env.RPC_URL;
|
|
215
|
+
client = createClient({
|
|
216
|
+
chain: await resolveChain({ ...options, rpcUrl }),
|
|
217
|
+
transport: http(rpcUrl),
|
|
218
|
+
});
|
|
219
|
+
explorerUrl = client.chain?.blockExplorers?.default?.url;
|
|
220
|
+
const tokenInfo = currency
|
|
221
|
+
? await fetchTokenInfo(client, currency, tempoEntry.wallet_address).catch(() => undefined)
|
|
222
|
+
: undefined;
|
|
223
|
+
tokenSymbol = tokenInfo?.symbol ?? currency ?? '';
|
|
224
|
+
tokenDecimals =
|
|
225
|
+
tokenInfo?.decimals ?? challengeRequest.decimals ?? 6;
|
|
226
|
+
}
|
|
184
227
|
}
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
228
|
+
else if (!privateKey) {
|
|
229
|
+
// tempo CLI not available — try silent fallback
|
|
230
|
+
const fallback = fallbackFromTempo();
|
|
231
|
+
if (fallback) {
|
|
232
|
+
const fallbackKey = await createKeychain(fallback).get();
|
|
233
|
+
if (fallbackKey) {
|
|
234
|
+
account = privateKeyToAccount(fallbackKey);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!account) {
|
|
238
|
+
if (options.account)
|
|
239
|
+
return error({
|
|
240
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
241
|
+
message: `Account "${accountName}" not found.`,
|
|
242
|
+
exitCode: 69,
|
|
243
|
+
});
|
|
244
|
+
else
|
|
245
|
+
return error({
|
|
246
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
247
|
+
message: 'No account found.',
|
|
248
|
+
exitCode: 69,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
188
251
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (typeof value === 'string' && /^https?:\/\//.test(value))
|
|
192
|
-
return pc.link(value, value);
|
|
193
|
-
return String(value);
|
|
194
|
-
};
|
|
195
|
-
const decodeMemo = (hex) => {
|
|
196
|
-
try {
|
|
197
|
-
const stripped = hex.replace(/^0x0*/, '');
|
|
198
|
-
if (!stripped)
|
|
199
|
-
return undefined;
|
|
200
|
-
const bytes = Uint8Array.from(stripped.match(/.{1,2}/g).map((b) => Number.parseInt(b, 16)));
|
|
201
|
-
const decoded = new TextDecoder().decode(bytes);
|
|
202
|
-
return /^[\x20-\x7e]+$/.test(decoded) ? decoded : undefined;
|
|
252
|
+
else {
|
|
253
|
+
account = privateKeyToAccount(privateKey);
|
|
203
254
|
}
|
|
204
|
-
|
|
205
|
-
|
|
255
|
+
if (!useTempoCliSign && account) {
|
|
256
|
+
const rpcUrl = options.rpcUrl ?? process.env.RPC_URL;
|
|
257
|
+
client = createClient({
|
|
258
|
+
chain: await resolveChain({ ...options, rpcUrl }),
|
|
259
|
+
transport: http(rpcUrl),
|
|
260
|
+
});
|
|
261
|
+
explorerUrl = client.chain?.blockExplorers?.default?.url;
|
|
262
|
+
const tokenInfo = currency
|
|
263
|
+
? await fetchTokenInfo(client, currency, account.address).catch(() => undefined)
|
|
264
|
+
: undefined;
|
|
265
|
+
tokenSymbol = tokenInfo?.symbol ?? currency ?? '';
|
|
266
|
+
tokenDecimals =
|
|
267
|
+
tokenInfo?.decimals ?? challengeRequest.decimals ?? 6;
|
|
206
268
|
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
269
|
+
}
|
|
270
|
+
{
|
|
271
|
+
printResponseHeaders(challengeResponse);
|
|
272
|
+
const request = challengeRequest;
|
|
273
|
+
const balanceKeys = new Set(['amount', 'suggestedDeposit', 'minVoucherDelta']);
|
|
274
|
+
const skipKeys = new Set(['decimals', 'currency', 'methodDetails']);
|
|
275
|
+
const fmtRequestValue = (key, value) => {
|
|
276
|
+
if (balanceKeys.has(key) && typeof value === 'string') {
|
|
277
|
+
return `${value} ${pc.dim(`(${fmtBalance(BigInt(value), tokenSymbol, tokenDecimals)})`)}`;
|
|
278
|
+
}
|
|
279
|
+
if (key === 'chainId' && typeof value === 'number') {
|
|
280
|
+
const name = chainName({ id: value, name: '' });
|
|
281
|
+
return name ? `${value} ${pc.dim(`(${name})`)}` : String(value);
|
|
282
|
+
}
|
|
283
|
+
if (typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value))
|
|
284
|
+
return explorerUrl ? pc.link(`${explorerUrl}/address/${value}`, value) : value;
|
|
285
|
+
if (typeof value === 'string' && /^https?:\/\//.test(value))
|
|
286
|
+
return pc.link(value, value);
|
|
287
|
+
return String(value);
|
|
288
|
+
};
|
|
289
|
+
const decodeMemo = (hex) => {
|
|
211
290
|
try {
|
|
212
|
-
const
|
|
213
|
-
|
|
291
|
+
const stripped = hex.replace(/^0x0*/, '');
|
|
292
|
+
if (!stripped)
|
|
293
|
+
return undefined;
|
|
294
|
+
const bytes = Uint8Array.from(stripped.match(/.{1,2}/g).map((b) => Number.parseInt(b, 16)));
|
|
295
|
+
const decoded = new TextDecoder().decode(bytes);
|
|
296
|
+
return /^[\x20-\x7e]+$/.test(decoded) ? decoded : undefined;
|
|
214
297
|
}
|
|
215
|
-
catch {
|
|
298
|
+
catch {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const skipChallengeKeys = new Set(['id', 'request']);
|
|
303
|
+
const fmtChallengeValue = (key, value) => {
|
|
304
|
+
if (key === 'realm' && typeof value === 'string') {
|
|
305
|
+
try {
|
|
306
|
+
const realmUrl = new URL(value.includes('://') ? value : `https://${value}`);
|
|
307
|
+
return pc.link(realmUrl.href, value);
|
|
308
|
+
}
|
|
309
|
+
catch { }
|
|
310
|
+
}
|
|
311
|
+
return String(value);
|
|
312
|
+
};
|
|
313
|
+
const challengeRows = [];
|
|
314
|
+
for (const [key, value] of Object.entries(challenge)) {
|
|
315
|
+
if (skipChallengeKeys.has(key) || value === undefined)
|
|
316
|
+
continue;
|
|
317
|
+
challengeRows.push([key, fmtChallengeValue(key, value)]);
|
|
216
318
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (skipChallengeKeys.has(key) || value === undefined)
|
|
222
|
-
continue;
|
|
223
|
-
challengeRows.push([key, fmtChallengeValue(key, value)]);
|
|
224
|
-
}
|
|
225
|
-
challengeRows.sort(([a], [b]) => a.localeCompare(b));
|
|
226
|
-
const requestRows = [];
|
|
227
|
-
for (const [key, value] of Object.entries(request)) {
|
|
228
|
-
if (skipKeys.has(key) || value === undefined)
|
|
229
|
-
continue;
|
|
230
|
-
requestRows.push([key, fmtRequestValue(key, value)]);
|
|
231
|
-
}
|
|
232
|
-
requestRows.sort(([a], [b]) => a.localeCompare(b));
|
|
233
|
-
const detailRows = [];
|
|
234
|
-
const methodDetails = request.methodDetails;
|
|
235
|
-
if (methodDetails) {
|
|
236
|
-
for (const [key, value] of Object.entries(methodDetails)) {
|
|
237
|
-
if (value === undefined)
|
|
319
|
+
challengeRows.sort(([a], [b]) => a.localeCompare(b));
|
|
320
|
+
const requestRows = [];
|
|
321
|
+
for (const [key, value] of Object.entries(request)) {
|
|
322
|
+
if (skipKeys.has(key) || value === undefined)
|
|
238
323
|
continue;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
324
|
+
requestRows.push([key, fmtRequestValue(key, value)]);
|
|
325
|
+
}
|
|
326
|
+
requestRows.sort(([a], [b]) => a.localeCompare(b));
|
|
327
|
+
const detailRows = [];
|
|
328
|
+
const methodDetails = request.methodDetails;
|
|
329
|
+
if (methodDetails) {
|
|
330
|
+
for (const [key, value] of Object.entries(methodDetails)) {
|
|
331
|
+
if (value === undefined)
|
|
332
|
+
continue;
|
|
333
|
+
if (key === 'memo' && typeof value === 'string') {
|
|
334
|
+
const decoded = decodeMemo(value);
|
|
335
|
+
detailRows.push([key, decoded ? `${decoded}\n${pc.dim(value)}` : value]);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
detailRows.push([key, fmtRequestValue(key, value)]);
|
|
339
|
+
}
|
|
242
340
|
}
|
|
243
|
-
|
|
244
|
-
|
|
341
|
+
detailRows.sort(([a], [b]) => a.localeCompare(b));
|
|
342
|
+
}
|
|
343
|
+
const sections = [
|
|
344
|
+
['Challenge', challengeRows],
|
|
345
|
+
['Request', requestRows],
|
|
346
|
+
...(detailRows.length ? [['Details', detailRows]] : []),
|
|
347
|
+
];
|
|
348
|
+
for (const [, rows] of sections)
|
|
349
|
+
for (const [key] of rows)
|
|
350
|
+
shownKeys.add(key);
|
|
351
|
+
const pad = Math.max(...sections.flatMap(([, rows]) => rows.map(([k]) => k.length)));
|
|
352
|
+
const indent = ` ${''.padEnd(pad)} `;
|
|
353
|
+
if (verbose >= 1 || confirmEnabled) {
|
|
354
|
+
info(`${pc.bold(pc.yellow('Payment Required'))}\n`);
|
|
355
|
+
for (const [title, rows] of sections) {
|
|
356
|
+
info(`${pc.bold(title)}\n`);
|
|
357
|
+
for (const [label, value] of rows) {
|
|
358
|
+
const [first, ...rest] = value.split('\n');
|
|
359
|
+
info(` ${pc.dim(label.padEnd(pad))} ${first}\n`);
|
|
360
|
+
for (const line of rest)
|
|
361
|
+
info(`${indent}${line}\n`);
|
|
362
|
+
}
|
|
245
363
|
}
|
|
246
364
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
for (const [, rows] of sections)
|
|
255
|
-
for (const [key] of rows)
|
|
256
|
-
shownKeys.add(key);
|
|
257
|
-
const pad = Math.max(...sections.flatMap(([, rows]) => rows.map(([k]) => k.length)));
|
|
258
|
-
const indent = ` ${''.padEnd(pad)} `;
|
|
259
|
-
info(`${pc.bold(pc.yellow('Payment Required'))}\n`);
|
|
260
|
-
for (const [title, rows] of sections) {
|
|
261
|
-
info(`${pc.bold(title)}\n`);
|
|
262
|
-
for (const [label, value] of rows) {
|
|
263
|
-
const [first, ...rest] = value.split('\n');
|
|
264
|
-
info(` ${pc.dim(label.padEnd(pad))} ${first}\n`);
|
|
265
|
-
for (const line of rest)
|
|
266
|
-
info(`${indent}${line}\n`);
|
|
365
|
+
if (confirmEnabled) {
|
|
366
|
+
info('\n');
|
|
367
|
+
const ok = await confirm(`Proceed with ${challenge.intent}?`, true);
|
|
368
|
+
if (!ok) {
|
|
369
|
+
info('Aborted.\n');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
267
372
|
}
|
|
268
373
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
if (!
|
|
273
|
-
|
|
274
|
-
|
|
374
|
+
let credential;
|
|
375
|
+
if (challenge.method === 'tempo' && useTempoCliSign) {
|
|
376
|
+
const wwwAuth = challengeResponse.headers.get('www-authenticate');
|
|
377
|
+
if (!wwwAuth) {
|
|
378
|
+
return error({
|
|
379
|
+
code: 'MISSING_CHALLENGE',
|
|
380
|
+
message: 'No WWW-Authenticate header in 402 response.',
|
|
381
|
+
exitCode: 2,
|
|
382
|
+
});
|
|
275
383
|
}
|
|
384
|
+
credential = await tempoCliSign(wwwAuth);
|
|
276
385
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
polyfill: false,
|
|
307
|
-
});
|
|
308
|
-
credential = await mppx.createCredential(challengeResponse, (() => {
|
|
309
|
-
if (!tempoOpts.channel)
|
|
310
|
-
return undefined;
|
|
311
|
-
const channelId = tempoOpts.channel;
|
|
312
|
-
const saved = readChannelCumulative(channelId);
|
|
313
|
-
return {
|
|
314
|
-
channelId,
|
|
315
|
-
...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
|
|
316
|
-
};
|
|
317
|
-
})());
|
|
318
|
-
}
|
|
319
|
-
else if (challenge.method === 'stripe') {
|
|
320
|
-
const stripeOpts = parseOptions(z.object({
|
|
321
|
-
paymentMethod: z.string(),
|
|
322
|
-
}), methodOpts);
|
|
323
|
-
const stripeSecretKey = process.env.MPPX_STRIPE_SECRET_KEY;
|
|
324
|
-
if (!stripeSecretKey) {
|
|
325
|
-
console.error('\nMPPX_STRIPE_SECRET_KEY environment variable is required for Stripe payments.');
|
|
326
|
-
process.exit(1);
|
|
327
|
-
}
|
|
328
|
-
if (!stripeSecretKey.startsWith('sk_test_')) {
|
|
329
|
-
console.error('\nStripe CLI payments are currently only supported in test mode (sk_test_... keys).');
|
|
330
|
-
process.exit(1);
|
|
331
|
-
}
|
|
332
|
-
const mppx = Mppx.create({
|
|
333
|
-
methods: [
|
|
334
|
-
stripe.charge({
|
|
335
|
-
paymentMethod: stripeOpts.paymentMethod,
|
|
336
|
-
createToken: async ({ paymentMethod, amount, currency, networkId, expiresAt, metadata, }) => {
|
|
337
|
-
const body = new URLSearchParams({
|
|
338
|
-
payment_method: paymentMethod,
|
|
339
|
-
'usage_limits[currency]': currency,
|
|
340
|
-
'usage_limits[max_amount]': amount,
|
|
341
|
-
'usage_limits[expires_at]': expiresAt.toString(),
|
|
342
|
-
});
|
|
343
|
-
if (networkId)
|
|
344
|
-
body.set('seller_details[network_id]', networkId);
|
|
345
|
-
if (metadata) {
|
|
346
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
347
|
-
body.set(`metadata[${key}]`, value);
|
|
348
|
-
}
|
|
386
|
+
else if (challenge.method === 'tempo') {
|
|
387
|
+
if (!account || !client) {
|
|
388
|
+
return error({
|
|
389
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
390
|
+
message: 'Tempo requires a configured account.',
|
|
391
|
+
exitCode: 69,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const tempoOpts = parseOptions(z.object({
|
|
395
|
+
channel: z.optional(z.coerce.string()),
|
|
396
|
+
deposit: z.optional(z.union([z.string(), z.number()])),
|
|
397
|
+
}), methodOpts);
|
|
398
|
+
const mppx = Mppx.create({
|
|
399
|
+
methods: tempo({
|
|
400
|
+
account,
|
|
401
|
+
getClient: () => client,
|
|
402
|
+
deposit: (() => {
|
|
403
|
+
if (challenge.intent !== 'session')
|
|
404
|
+
return undefined;
|
|
405
|
+
const suggestedDeposit = challenge.request
|
|
406
|
+
.suggestedDeposit;
|
|
407
|
+
const cliDeposit = tempoOpts.deposit !== undefined ? String(tempoOpts.deposit) : undefined;
|
|
408
|
+
const resolved = suggestedDeposit ?? cliDeposit ?? (isTestnet(client.chain) ? '10' : undefined);
|
|
409
|
+
if (!resolved) {
|
|
410
|
+
return error({
|
|
411
|
+
code: 'MISSING_DEPOSIT',
|
|
412
|
+
message: 'Session payment requires a deposit. Use -M deposit=<amount> or connect to testnet.',
|
|
413
|
+
exitCode: 2,
|
|
414
|
+
});
|
|
349
415
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
416
|
+
return resolved;
|
|
417
|
+
})(),
|
|
418
|
+
}),
|
|
419
|
+
polyfill: false,
|
|
420
|
+
});
|
|
421
|
+
credential = await mppx.createCredential(challengeResponse, (() => {
|
|
422
|
+
if (!tempoOpts.channel)
|
|
423
|
+
return undefined;
|
|
424
|
+
const channelId = tempoOpts.channel;
|
|
425
|
+
const saved = readChannelCumulative(channelId);
|
|
426
|
+
return {
|
|
427
|
+
channelId,
|
|
428
|
+
...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
|
|
429
|
+
};
|
|
430
|
+
})());
|
|
431
|
+
}
|
|
432
|
+
else if (challenge.method === 'stripe') {
|
|
433
|
+
const stripeOpts = parseOptions(z.object({
|
|
434
|
+
paymentMethod: z.string(),
|
|
435
|
+
}), methodOpts);
|
|
436
|
+
const stripeSecretKey = process.env.MPPX_STRIPE_SECRET_KEY;
|
|
437
|
+
if (!stripeSecretKey) {
|
|
438
|
+
return error({
|
|
439
|
+
code: 'MISSING_ENV',
|
|
440
|
+
message: 'MPPX_STRIPE_SECRET_KEY environment variable is required for Stripe payments.',
|
|
441
|
+
exitCode: 2,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
if (!stripeSecretKey.startsWith('sk_test_')) {
|
|
445
|
+
return error({
|
|
446
|
+
code: 'UNSUPPORTED_MODE',
|
|
447
|
+
message: 'Stripe CLI payments are currently only supported in test mode (sk_test_... keys).',
|
|
448
|
+
exitCode: 2,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const mppx = Mppx.create({
|
|
452
|
+
methods: [
|
|
453
|
+
stripe.charge({
|
|
454
|
+
paymentMethod: stripeOpts.paymentMethod,
|
|
455
|
+
createToken: async ({ paymentMethod, amount, currency, networkId, expiresAt, metadata, }) => {
|
|
456
|
+
const body = new URLSearchParams({
|
|
457
|
+
payment_method: paymentMethod,
|
|
458
|
+
'usage_limits[currency]': currency,
|
|
459
|
+
'usage_limits[max_amount]': amount,
|
|
460
|
+
'usage_limits[expires_at]': expiresAt.toString(),
|
|
461
|
+
});
|
|
462
|
+
if (networkId)
|
|
463
|
+
body.set('seller_details[network_id]', networkId);
|
|
464
|
+
if (metadata) {
|
|
465
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
466
|
+
body.set(`metadata[${key}]`, value);
|
|
379
467
|
}
|
|
380
468
|
}
|
|
381
|
-
|
|
382
|
-
|
|
469
|
+
const sptUrl = process.env.MPPX_STRIPE_SPT_URL ??
|
|
470
|
+
'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens';
|
|
471
|
+
const sptHeaders = {
|
|
472
|
+
Authorization: `Basic ${btoa(`${stripeSecretKey}:`)}`,
|
|
473
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
474
|
+
};
|
|
475
|
+
let response = await globalThis.fetch(sptUrl, {
|
|
476
|
+
method: 'POST',
|
|
477
|
+
headers: sptHeaders,
|
|
478
|
+
body,
|
|
479
|
+
});
|
|
480
|
+
if (!response.ok) {
|
|
481
|
+
const errorBody = (await response.json());
|
|
482
|
+
if ((metadata || networkId) &&
|
|
483
|
+
errorBody.error.message.includes('Received unknown parameter')) {
|
|
484
|
+
const fallbackBody = new URLSearchParams({
|
|
485
|
+
payment_method: paymentMethod,
|
|
486
|
+
'usage_limits[currency]': currency,
|
|
487
|
+
'usage_limits[max_amount]': amount,
|
|
488
|
+
'usage_limits[expires_at]': expiresAt.toString(),
|
|
489
|
+
});
|
|
490
|
+
response = await globalThis.fetch(sptUrl, {
|
|
491
|
+
method: 'POST',
|
|
492
|
+
headers: sptHeaders,
|
|
493
|
+
body: fallbackBody,
|
|
494
|
+
});
|
|
495
|
+
if (!response.ok) {
|
|
496
|
+
const fallbackError = (await response.json());
|
|
497
|
+
return error({
|
|
498
|
+
code: 'STRIPE_ERROR',
|
|
499
|
+
message: `Failed to create SPT: ${fallbackError.error.message}`,
|
|
500
|
+
exitCode: 77,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
return error({
|
|
506
|
+
code: 'STRIPE_ERROR',
|
|
507
|
+
message: `Failed to create SPT: ${errorBody.error.message}`,
|
|
508
|
+
exitCode: 77,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
383
511
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
},
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
credential = await mppx.createCredential(challengeResponse);
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
console.error(`Unsupported payment method: ${challenge.method}`);
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
const sessionMd = challenge.request.methodDetails;
|
|
399
|
-
let sessionChannelId;
|
|
400
|
-
let sessionEscrowContract;
|
|
401
|
-
let sessionChainId = 0;
|
|
402
|
-
let sessionCumulativeAmount = 0n;
|
|
403
|
-
if (challenge.intent === 'session') {
|
|
404
|
-
const parsed = Credential.deserialize(credential);
|
|
405
|
-
sessionChannelId = parsed.payload.channelId;
|
|
406
|
-
sessionChainId = sessionMd?.chainId ?? client?.chain?.id ?? 0;
|
|
407
|
-
sessionEscrowContract = sessionMd?.escrowContract;
|
|
408
|
-
if ('cumulativeAmount' in parsed.payload && parsed.payload.cumulativeAmount)
|
|
409
|
-
sessionCumulativeAmount = BigInt(parsed.payload.cumulativeAmount);
|
|
410
|
-
if (parsed.payload.action === 'open') {
|
|
411
|
-
const depositRaw = challengeRequest.suggestedDeposit;
|
|
412
|
-
const depositDisplay = depositRaw
|
|
413
|
-
? ` ${pc.dim(`(deposit ${depositRaw} ${tokenSymbol})`)}`
|
|
414
|
-
: '';
|
|
415
|
-
const prefix = options.confirm ? '' : '\n';
|
|
416
|
-
info(`${prefix}${pc.dim(`Channel opened ${parsed.payload.channelId}`)}${depositDisplay}\n`);
|
|
512
|
+
const { id } = (await response.json());
|
|
513
|
+
return id;
|
|
514
|
+
},
|
|
515
|
+
}),
|
|
516
|
+
],
|
|
517
|
+
polyfill: false,
|
|
518
|
+
});
|
|
519
|
+
credential = await mppx.createCredential(challengeResponse);
|
|
417
520
|
}
|
|
418
521
|
else {
|
|
419
|
-
|
|
420
|
-
|
|
522
|
+
return error({
|
|
523
|
+
code: 'UNSUPPORTED_METHOD',
|
|
524
|
+
message: `Unsupported payment method: ${challenge.method}`,
|
|
525
|
+
exitCode: 2,
|
|
526
|
+
});
|
|
421
527
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
528
|
+
const sessionMd = challenge.request.methodDetails;
|
|
529
|
+
let sessionChannelId;
|
|
530
|
+
let sessionEscrowContract;
|
|
531
|
+
let sessionChainId = 0;
|
|
532
|
+
let sessionCumulativeAmount = 0n;
|
|
533
|
+
if (challenge.intent === 'session') {
|
|
534
|
+
const parsed = Credential.deserialize(credential);
|
|
535
|
+
sessionChannelId = parsed.payload.channelId;
|
|
536
|
+
sessionChainId = sessionMd?.chainId ?? client?.chain?.id ?? 0;
|
|
537
|
+
sessionEscrowContract = sessionMd?.escrowContract;
|
|
538
|
+
if ('cumulativeAmount' in parsed.payload && parsed.payload.cumulativeAmount)
|
|
539
|
+
sessionCumulativeAmount = BigInt(parsed.payload.cumulativeAmount);
|
|
540
|
+
if (verbose >= 1) {
|
|
541
|
+
if (parsed.payload.action === 'open') {
|
|
542
|
+
const depositRaw = challengeRequest.suggestedDeposit;
|
|
543
|
+
const depositDisplay = depositRaw
|
|
544
|
+
? ` ${pc.dim(`(deposit ${depositRaw} ${tokenSymbol})`)}`
|
|
545
|
+
: '';
|
|
546
|
+
const prefix = confirmEnabled ? '' : '\n';
|
|
547
|
+
info(`${prefix}${pc.dim(`Channel opened ${parsed.payload.channelId}`)}${depositDisplay}\n`);
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
const prefix = confirmEnabled ? '' : '\n';
|
|
551
|
+
info(`${prefix}${pc.dim(`Channel reused ${parsed.payload.channelId}`)}\n`);
|
|
552
|
+
}
|
|
441
553
|
}
|
|
442
|
-
rows.sort(([a], [b]) => a.localeCompare(b));
|
|
443
|
-
const pad = Math.max(...rows.map(([k]) => k.length));
|
|
444
|
-
for (const [label, value] of rows)
|
|
445
|
-
info(` ${pc.dim(label.padEnd(pad))} ${value}\n`);
|
|
446
554
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
555
|
+
const credentialFetchInit = {
|
|
556
|
+
...fetchInit,
|
|
557
|
+
headers: {
|
|
558
|
+
...fetchInit.headers,
|
|
559
|
+
...(challenge.intent === 'session' ? { Accept: 'text/event-stream' } : {}),
|
|
560
|
+
Authorization: credential,
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
printRequestHeaders(url, credentialFetchInit);
|
|
564
|
+
let credentialResponse = await globalThis.fetch(fetchUrl, credentialFetchInit);
|
|
565
|
+
if (challenge.intent === 'session' &&
|
|
566
|
+
credentialResponse.ok &&
|
|
567
|
+
!credentialResponse.headers.get('Content-Type')?.includes('text/event-stream')) {
|
|
568
|
+
const parsed = Credential.deserialize(credential);
|
|
569
|
+
if (parsed.payload.action === 'open' && 'cumulativeAmount' in parsed.payload) {
|
|
570
|
+
const tickAmount = BigInt(challenge.request.amount);
|
|
571
|
+
sessionCumulativeAmount = BigInt(parsed.payload.cumulativeAmount) + tickAmount;
|
|
572
|
+
if (sessionEscrowContract && account && client) {
|
|
573
|
+
const signature = await signVoucher(client, account, { channelId: sessionChannelId, cumulativeAmount: sessionCumulativeAmount }, sessionEscrowContract, sessionChainId);
|
|
574
|
+
const voucherPayload = {
|
|
575
|
+
action: 'voucher',
|
|
576
|
+
channelId: sessionChannelId,
|
|
577
|
+
cumulativeAmount: sessionCumulativeAmount.toString(),
|
|
578
|
+
signature,
|
|
579
|
+
};
|
|
580
|
+
const voucherCred = Credential.serialize({
|
|
581
|
+
challenge,
|
|
582
|
+
payload: voucherPayload,
|
|
583
|
+
source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
|
|
584
|
+
});
|
|
585
|
+
credentialResponse = await globalThis.fetch(fetchUrl, {
|
|
586
|
+
...fetchInit,
|
|
587
|
+
headers: {
|
|
588
|
+
...fetchInit.headers,
|
|
589
|
+
Accept: 'text/event-stream',
|
|
590
|
+
Authorization: voucherCred,
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
450
595
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
596
|
+
if (options.fail && credentialResponse.status >= 400)
|
|
597
|
+
return error({
|
|
598
|
+
code: 'HTTP_ERROR',
|
|
599
|
+
message: `HTTP error ${credentialResponse.status}`,
|
|
600
|
+
exitCode: 22,
|
|
601
|
+
});
|
|
602
|
+
if (credentialResponse.status === 402) {
|
|
603
|
+
const body = await credentialResponse.text();
|
|
604
|
+
info(`${pc.bold(pc.red('Payment Rejected'))}\n`);
|
|
457
605
|
try {
|
|
458
|
-
const
|
|
459
|
-
if (typeof receiptJson.acceptedCumulative === 'string' &&
|
|
460
|
-
receiptJson.acceptedCumulative) {
|
|
461
|
-
sessionCumulativeAmount = BigInt(receiptJson.acceptedCumulative);
|
|
462
|
-
if (sessionChannelId)
|
|
463
|
-
writeChannelCumulative(sessionChannelId, sessionCumulativeAmount);
|
|
464
|
-
}
|
|
465
|
-
info(`\n${pc.bold(pc.green('Payment Receipt'))}\n`);
|
|
606
|
+
const problem = JSON.parse(body);
|
|
466
607
|
const rows = [];
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
const skipReference = channelId && reference && channelId === reference;
|
|
470
|
-
const receiptBalanceKeys = new Set(['acceptedCumulative', 'spent']);
|
|
471
|
-
for (const [key, value] of Object.entries(receiptJson)) {
|
|
472
|
-
if (value === undefined || shownKeys.has(key))
|
|
608
|
+
for (const [key, value] of Object.entries(problem)) {
|
|
609
|
+
if (value === undefined)
|
|
473
610
|
continue;
|
|
474
|
-
|
|
475
|
-
continue;
|
|
476
|
-
if (receiptBalanceKeys.has(key) && typeof value === 'string') {
|
|
477
|
-
rows.push([
|
|
478
|
-
key,
|
|
479
|
-
`${value} ${pc.dim(`(${fmtBalance(BigInt(value), tokenSymbol, tokenDecimals)})`)}`,
|
|
480
|
-
]);
|
|
481
|
-
}
|
|
482
|
-
else if ((key === 'reference' || key === 'txHash') &&
|
|
483
|
-
typeof value === 'string' &&
|
|
484
|
-
explorerUrl) {
|
|
485
|
-
rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)]);
|
|
486
|
-
}
|
|
487
|
-
else if (key === 'reference' &&
|
|
488
|
-
typeof value === 'string' &&
|
|
489
|
-
challenge.method === 'stripe' &&
|
|
490
|
-
value.startsWith('pi_')) {
|
|
491
|
-
const isTest = process.env.MPPX_STRIPE_SECRET_KEY?.startsWith('sk_test_');
|
|
492
|
-
const dashboardUrl = `https://dashboard.stripe.com${isTest ? '/test' : ''}/payments/${value}`;
|
|
493
|
-
rows.push([key, pc.link(dashboardUrl, value)]);
|
|
494
|
-
}
|
|
495
|
-
else
|
|
496
|
-
rows.push([key, String(value)]);
|
|
611
|
+
rows.push([key, String(value)]);
|
|
497
612
|
}
|
|
498
613
|
rows.sort(([a], [b]) => a.localeCompare(b));
|
|
499
614
|
const pad = Math.max(...rows.map(([k]) => k.length));
|
|
500
615
|
for (const [label, value] of rows)
|
|
501
616
|
info(` ${pc.dim(label.padEnd(pad))} ${value}\n`);
|
|
502
|
-
info('\n');
|
|
503
617
|
}
|
|
504
|
-
catch {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (contentType.includes('text/event-stream')) {
|
|
508
|
-
const reader = credentialResponse.body?.getReader();
|
|
509
|
-
if (!reader) {
|
|
510
|
-
console.error('No response body');
|
|
511
|
-
process.exit(1);
|
|
618
|
+
catch {
|
|
619
|
+
if (body)
|
|
620
|
+
info(` ${body}\n`);
|
|
512
621
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
622
|
+
return error({ code: 'PAYMENT_REJECTED', message: 'Payment rejected', exitCode: 75 });
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
printResponseHeaders(credentialResponse);
|
|
626
|
+
const receiptHeader = credentialResponse.headers.get('Payment-Receipt');
|
|
627
|
+
if (receiptHeader) {
|
|
628
|
+
try {
|
|
629
|
+
const receiptJson = JSON.parse(Base64.toString(receiptHeader));
|
|
630
|
+
if (typeof receiptJson.acceptedCumulative === 'string' &&
|
|
631
|
+
receiptJson.acceptedCumulative) {
|
|
632
|
+
sessionCumulativeAmount = BigInt(receiptJson.acceptedCumulative);
|
|
633
|
+
if (sessionChannelId)
|
|
634
|
+
writeChannelCumulative(sessionChannelId, sessionCumulativeAmount);
|
|
635
|
+
}
|
|
636
|
+
if (verbose >= 1) {
|
|
637
|
+
info(`\n${pc.bold(pc.green('Payment Receipt'))}\n`);
|
|
638
|
+
const rows = [];
|
|
639
|
+
const channelId = receiptJson.channelId;
|
|
640
|
+
const reference = receiptJson.reference;
|
|
641
|
+
const skipReference = channelId && reference && channelId === reference;
|
|
642
|
+
const receiptBalanceKeys = new Set(['acceptedCumulative', 'spent']);
|
|
643
|
+
for (const [key, value] of Object.entries(receiptJson)) {
|
|
644
|
+
if (value === undefined || shownKeys.has(key))
|
|
645
|
+
continue;
|
|
646
|
+
if (key === 'reference' && skipReference)
|
|
647
|
+
continue;
|
|
648
|
+
if (receiptBalanceKeys.has(key) && typeof value === 'string') {
|
|
649
|
+
rows.push([
|
|
650
|
+
key,
|
|
651
|
+
`${value} ${pc.dim(`(${fmtBalance(BigInt(value), tokenSymbol, tokenDecimals)})`)}`,
|
|
652
|
+
]);
|
|
653
|
+
}
|
|
654
|
+
else if ((key === 'reference' || key === 'txHash') &&
|
|
655
|
+
typeof value === 'string' &&
|
|
656
|
+
explorerUrl) {
|
|
657
|
+
rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)]);
|
|
658
|
+
}
|
|
659
|
+
else if (key === 'reference' &&
|
|
660
|
+
typeof value === 'string' &&
|
|
661
|
+
challenge.method === 'stripe' &&
|
|
662
|
+
value.startsWith('pi_')) {
|
|
663
|
+
const isTest = process.env.MPPX_STRIPE_SECRET_KEY?.startsWith('sk_test_');
|
|
664
|
+
const dashboardUrl = `https://dashboard.stripe.com${isTest ? '/test' : ''}/payments/${value}`;
|
|
665
|
+
rows.push([key, pc.link(dashboardUrl, value)]);
|
|
666
|
+
}
|
|
667
|
+
else
|
|
668
|
+
rows.push([key, String(value)]);
|
|
669
|
+
}
|
|
670
|
+
rows.sort(([a], [b]) => a.localeCompare(b));
|
|
671
|
+
const pad = Math.max(...rows.map(([k]) => k.length));
|
|
672
|
+
for (const [label, value] of rows)
|
|
673
|
+
info(` ${pc.dim(label.padEnd(pad))} ${value}\n`);
|
|
674
|
+
info('\n');
|
|
675
|
+
}
|
|
550
676
|
}
|
|
551
|
-
|
|
552
|
-
|
|
677
|
+
catch { }
|
|
678
|
+
}
|
|
679
|
+
const contentType = credentialResponse.headers.get('Content-Type') ?? '';
|
|
680
|
+
if (contentType.includes('text/event-stream')) {
|
|
681
|
+
const reader = credentialResponse.body?.getReader();
|
|
682
|
+
if (!reader) {
|
|
683
|
+
return error({ code: 'NO_RESPONSE_BODY', message: 'No response body' });
|
|
553
684
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
685
|
+
const decoder = new TextDecoder();
|
|
686
|
+
let buffer = '';
|
|
687
|
+
let currentEvent = '';
|
|
688
|
+
const sessionCred = challenge.intent === 'session'
|
|
689
|
+
? Credential.deserialize(credential)
|
|
690
|
+
: undefined;
|
|
691
|
+
const channelId = sessionCred?.payload.channelId;
|
|
692
|
+
const md = challenge.request.methodDetails;
|
|
693
|
+
const sessionChainId = md?.chainId ?? client?.chain?.id ?? 0;
|
|
694
|
+
const escrowContract = md?.escrowContract;
|
|
695
|
+
let cumulativeAmount = sessionCred?.payload &&
|
|
696
|
+
'cumulativeAmount' in sessionCred.payload &&
|
|
697
|
+
sessionCred.payload.cumulativeAmount
|
|
698
|
+
? BigInt(sessionCred.payload.cumulativeAmount)
|
|
699
|
+
: 0n;
|
|
700
|
+
let _voucherSeq = 0;
|
|
701
|
+
const termBg = verbose ? await detectTerminalBg() : undefined;
|
|
702
|
+
const chunkBgs = (() => {
|
|
703
|
+
if (!termBg || !pc.isColorSupported)
|
|
704
|
+
return undefined;
|
|
705
|
+
const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
706
|
+
const isDark = 0.299 * termBg.r + 0.587 * termBg.g + 0.114 * termBg.b < 128;
|
|
707
|
+
const offset = isDark ? 1 : -1;
|
|
708
|
+
const bgRgb = (d) => (s) => {
|
|
709
|
+
const r = clamp(termBg.r + d * offset);
|
|
710
|
+
const g = clamp(termBg.g + d * offset);
|
|
711
|
+
const b = clamp(termBg.b + d * offset);
|
|
712
|
+
return `\x1b[48;2;${r};${g};${b}m${s}\x1b[49m`;
|
|
713
|
+
};
|
|
714
|
+
return [bgRgb(12), bgRgb(24)];
|
|
715
|
+
})();
|
|
716
|
+
let chunkIdx = 0;
|
|
717
|
+
const writeContent = (chunk) => {
|
|
718
|
+
if (chunkBgs) {
|
|
719
|
+
const bgFn = chunkBgs[chunkIdx % chunkBgs.length];
|
|
720
|
+
process.stdout.write(chunk.replace(/[^\n]+/g, (m) => bgFn(m)));
|
|
721
|
+
chunkIdx++;
|
|
560
722
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
currentEvent = '';
|
|
564
|
-
continue;
|
|
723
|
+
else {
|
|
724
|
+
process.stdout.write(chunk);
|
|
565
725
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
sessionChainId) {
|
|
573
|
-
try {
|
|
574
|
-
const event = JSON.parse(data);
|
|
575
|
-
const required = BigInt(event.requiredCumulative);
|
|
576
|
-
cumulativeAmount = cumulativeAmount > required ? cumulativeAmount : required;
|
|
577
|
-
const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract, sessionChainId);
|
|
578
|
-
const voucherCred = Credential.serialize({
|
|
579
|
-
challenge,
|
|
580
|
-
payload: {
|
|
581
|
-
action: 'voucher',
|
|
582
|
-
channelId,
|
|
583
|
-
cumulativeAmount: cumulativeAmount.toString(),
|
|
584
|
-
signature,
|
|
585
|
-
},
|
|
586
|
-
source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
|
|
587
|
-
});
|
|
588
|
-
await globalThis.fetch(url, {
|
|
589
|
-
method: 'POST',
|
|
590
|
-
headers: { Authorization: voucherCred },
|
|
591
|
-
});
|
|
592
|
-
_voucherSeq++;
|
|
726
|
+
};
|
|
727
|
+
const processLines = async (lines) => {
|
|
728
|
+
for (const line of lines) {
|
|
729
|
+
if (line.startsWith('event: ')) {
|
|
730
|
+
currentEvent = line.slice(7).trim();
|
|
731
|
+
continue;
|
|
593
732
|
}
|
|
594
|
-
|
|
595
|
-
|
|
733
|
+
if (!line.startsWith('data: ')) {
|
|
734
|
+
if (line === '')
|
|
735
|
+
currentEvent = '';
|
|
736
|
+
continue;
|
|
596
737
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
738
|
+
const data = line.slice(6);
|
|
739
|
+
if (data.trim() === '[DONE]')
|
|
740
|
+
continue;
|
|
741
|
+
if (currentEvent === 'payment-need-voucher' &&
|
|
742
|
+
channelId &&
|
|
743
|
+
escrowContract &&
|
|
744
|
+
sessionChainId) {
|
|
745
|
+
try {
|
|
746
|
+
const event = JSON.parse(data);
|
|
747
|
+
const required = BigInt(event.requiredCumulative);
|
|
748
|
+
cumulativeAmount = cumulativeAmount > required ? cumulativeAmount : required;
|
|
749
|
+
const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract, sessionChainId);
|
|
750
|
+
const voucherCred = Credential.serialize({
|
|
751
|
+
challenge,
|
|
752
|
+
payload: {
|
|
753
|
+
action: 'voucher',
|
|
754
|
+
channelId,
|
|
755
|
+
cumulativeAmount: cumulativeAmount.toString(),
|
|
756
|
+
signature,
|
|
757
|
+
},
|
|
758
|
+
source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
|
|
759
|
+
});
|
|
760
|
+
await globalThis.fetch(fetchUrl, {
|
|
761
|
+
method: 'POST',
|
|
762
|
+
headers: { Authorization: voucherCred },
|
|
763
|
+
});
|
|
764
|
+
_voucherSeq++;
|
|
765
|
+
}
|
|
766
|
+
catch (e) {
|
|
767
|
+
info(pc.dim(pc.yellow(` [voucher failed: ${e instanceof Error ? e.message : e}]`)));
|
|
768
|
+
}
|
|
769
|
+
currentEvent = '';
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (currentEvent === 'payment-receipt') {
|
|
773
|
+
if (verbose >= 1) {
|
|
774
|
+
try {
|
|
775
|
+
const receipt = JSON.parse(data);
|
|
776
|
+
info(`\n\n${pc.bold(pc.green('Payment Receipt'))}\n`);
|
|
777
|
+
const rows = [];
|
|
778
|
+
const skipRef = receipt.channelId &&
|
|
779
|
+
receipt.reference &&
|
|
780
|
+
receipt.channelId === receipt.reference;
|
|
781
|
+
for (const [key, value] of Object.entries(receipt)) {
|
|
782
|
+
if (value === undefined || shownKeys.has(key))
|
|
783
|
+
continue;
|
|
784
|
+
if (key === 'reference' && skipRef)
|
|
785
|
+
continue;
|
|
786
|
+
const receiptBalanceKeys = ['acceptedCumulative', 'spent'];
|
|
787
|
+
if (receiptBalanceKeys.includes(key) && typeof value === 'string') {
|
|
788
|
+
rows.push([
|
|
789
|
+
key,
|
|
790
|
+
`${value} ${pc.dim(`(${fmtBalance(BigInt(value), tokenSymbol, tokenDecimals)})`)}`,
|
|
791
|
+
]);
|
|
792
|
+
}
|
|
793
|
+
else if ((key === 'reference' || key === 'txHash') &&
|
|
794
|
+
typeof value === 'string' &&
|
|
795
|
+
explorerUrl) {
|
|
796
|
+
rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)]);
|
|
797
|
+
}
|
|
798
|
+
else
|
|
799
|
+
rows.push([key, String(value)]);
|
|
800
|
+
}
|
|
801
|
+
rows.sort(([a], [b]) => a.localeCompare(b));
|
|
802
|
+
const rpad = Math.max(...rows.map(([k]) => k.length));
|
|
803
|
+
for (const [label, value] of rows)
|
|
804
|
+
info(` ${pc.dim(label.padEnd(rpad))} ${value}\n`);
|
|
624
805
|
}
|
|
625
|
-
|
|
626
|
-
|
|
806
|
+
catch { }
|
|
807
|
+
}
|
|
808
|
+
currentEvent = '';
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (data.length === 0) {
|
|
812
|
+
writeContent('\n');
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
try {
|
|
816
|
+
const parsed = JSON.parse(data);
|
|
817
|
+
writeContent(parsed.token ?? parsed.choices?.[0]?.delta?.content ?? data);
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
writeContent(data);
|
|
627
821
|
}
|
|
628
|
-
rows.sort(([a], [b]) => a.localeCompare(b));
|
|
629
|
-
const rpad = Math.max(...rows.map(([k]) => k.length));
|
|
630
|
-
for (const [label, value] of rows)
|
|
631
|
-
info(` ${pc.dim(label.padEnd(rpad))} ${value}\n`);
|
|
632
822
|
}
|
|
633
|
-
catch { }
|
|
634
823
|
currentEvent = '';
|
|
635
|
-
continue;
|
|
636
824
|
}
|
|
637
|
-
|
|
638
|
-
|
|
825
|
+
};
|
|
826
|
+
while (true) {
|
|
827
|
+
const { done, value } = await reader.read();
|
|
828
|
+
if (done)
|
|
829
|
+
break;
|
|
830
|
+
buffer += decoder.decode(value, { stream: true });
|
|
831
|
+
const lines = buffer.split('\n');
|
|
832
|
+
buffer = lines.pop();
|
|
833
|
+
await processLines(lines);
|
|
834
|
+
}
|
|
835
|
+
if (buffer.trim())
|
|
836
|
+
await processLines([buffer]);
|
|
837
|
+
if (channelId && escrowContract && sessionChainId) {
|
|
838
|
+
const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract, sessionChainId);
|
|
839
|
+
const closePayload = {
|
|
840
|
+
action: 'close',
|
|
841
|
+
channelId,
|
|
842
|
+
cumulativeAmount: cumulativeAmount.toString(),
|
|
843
|
+
signature,
|
|
844
|
+
};
|
|
845
|
+
const closeCred = Credential.serialize({
|
|
846
|
+
challenge,
|
|
847
|
+
payload: closePayload,
|
|
848
|
+
source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
|
|
849
|
+
});
|
|
850
|
+
const closeRes = await globalThis.fetch(fetchUrl, {
|
|
851
|
+
method: 'POST',
|
|
852
|
+
headers: { Authorization: closeCred },
|
|
853
|
+
});
|
|
854
|
+
if (closeRes.ok) {
|
|
855
|
+
if (verbose >= 1) {
|
|
856
|
+
const closeReceiptHeader = closeRes.headers.get('Payment-Receipt');
|
|
857
|
+
let closeTxHash;
|
|
858
|
+
if (closeReceiptHeader) {
|
|
859
|
+
try {
|
|
860
|
+
const r = JSON.parse(Base64.toString(closeReceiptHeader));
|
|
861
|
+
if (typeof r.txHash === 'string')
|
|
862
|
+
closeTxHash = r.txHash;
|
|
863
|
+
}
|
|
864
|
+
catch { }
|
|
865
|
+
}
|
|
866
|
+
const txInfo = closeTxHash && explorerUrl
|
|
867
|
+
? ` ${pc.dim(pc.link(`${explorerUrl}/tx/${closeTxHash}`, closeTxHash))}`
|
|
868
|
+
: '';
|
|
869
|
+
info(`\n${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(cumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`);
|
|
870
|
+
}
|
|
639
871
|
}
|
|
640
872
|
else {
|
|
641
|
-
|
|
642
|
-
const parsed = JSON.parse(data);
|
|
643
|
-
writeContent(parsed.token ?? parsed.choices?.[0]?.delta?.content ?? data);
|
|
644
|
-
}
|
|
645
|
-
catch {
|
|
646
|
-
writeContent(data);
|
|
647
|
-
}
|
|
873
|
+
info(`\n${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`);
|
|
648
874
|
}
|
|
649
|
-
currentEvent = '';
|
|
650
875
|
}
|
|
651
|
-
};
|
|
652
|
-
while (true) {
|
|
653
|
-
const { done, value } = await reader.read();
|
|
654
|
-
if (done)
|
|
655
|
-
break;
|
|
656
|
-
buffer += decoder.decode(value, { stream: true });
|
|
657
|
-
const lines = buffer.split('\n');
|
|
658
|
-
buffer = lines.pop();
|
|
659
|
-
await processLines(lines);
|
|
660
876
|
}
|
|
661
|
-
|
|
662
|
-
await
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const closeCred = Credential.serialize({
|
|
672
|
-
challenge,
|
|
673
|
-
payload: closePayload,
|
|
674
|
-
source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
|
|
675
|
-
});
|
|
676
|
-
const closeRes = await globalThis.fetch(url, {
|
|
677
|
-
method: 'POST',
|
|
678
|
-
headers: { Authorization: closeCred },
|
|
679
|
-
});
|
|
680
|
-
if (closeRes.ok) {
|
|
681
|
-
const closeReceiptHeader = closeRes.headers.get('Payment-Receipt');
|
|
682
|
-
let closeTxHash;
|
|
683
|
-
if (closeReceiptHeader) {
|
|
684
|
-
try {
|
|
685
|
-
const r = JSON.parse(Base64.toString(closeReceiptHeader));
|
|
686
|
-
if (typeof r.txHash === 'string')
|
|
687
|
-
closeTxHash = r.txHash;
|
|
688
|
-
}
|
|
689
|
-
catch { }
|
|
690
|
-
}
|
|
691
|
-
const txInfo = closeTxHash && explorerUrl
|
|
692
|
-
? ` ${pc.dim(pc.link(`${explorerUrl}/tx/${closeTxHash}`, closeTxHash))}`
|
|
693
|
-
: '';
|
|
694
|
-
info(`\n${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(cumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`);
|
|
877
|
+
else {
|
|
878
|
+
const body = (await credentialResponse.text()).replace(/\n+$/, '');
|
|
879
|
+
console.log(body);
|
|
880
|
+
const shouldClose = challenge.intent === 'session' &&
|
|
881
|
+
credentialResponse.ok &&
|
|
882
|
+
sessionChannelId &&
|
|
883
|
+
sessionEscrowContract &&
|
|
884
|
+
sessionChainId;
|
|
885
|
+
if (shouldClose && confirmEnabled) {
|
|
886
|
+
info('\n');
|
|
695
887
|
}
|
|
696
|
-
|
|
697
|
-
|
|
888
|
+
if (shouldClose && confirmEnabled && !(await confirm('Close channel?', true))) {
|
|
889
|
+
if (verbose >= 1)
|
|
890
|
+
info(`${pc.dim('Kept channel open.')}\n`);
|
|
698
891
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const closeReceiptHeader = closeRes.headers.get('Payment-Receipt');
|
|
738
|
-
let closeTxHash;
|
|
739
|
-
if (closeReceiptHeader) {
|
|
740
|
-
try {
|
|
741
|
-
const r = JSON.parse(Base64.toString(closeReceiptHeader));
|
|
742
|
-
if (typeof r.txHash === 'string')
|
|
743
|
-
closeTxHash = r.txHash;
|
|
892
|
+
else if (shouldClose) {
|
|
893
|
+
const signature = await signVoucher(client, account, { channelId: sessionChannelId, cumulativeAmount: sessionCumulativeAmount }, sessionEscrowContract, sessionChainId);
|
|
894
|
+
const closePayload = {
|
|
895
|
+
action: 'close',
|
|
896
|
+
channelId: sessionChannelId,
|
|
897
|
+
cumulativeAmount: sessionCumulativeAmount.toString(),
|
|
898
|
+
signature,
|
|
899
|
+
};
|
|
900
|
+
const closeCred = Credential.serialize({
|
|
901
|
+
challenge,
|
|
902
|
+
payload: closePayload,
|
|
903
|
+
source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
|
|
904
|
+
});
|
|
905
|
+
const closeRes = await globalThis.fetch(fetchUrl, {
|
|
906
|
+
...fetchInit,
|
|
907
|
+
headers: {
|
|
908
|
+
...fetchInit.headers,
|
|
909
|
+
Authorization: closeCred,
|
|
910
|
+
},
|
|
911
|
+
});
|
|
912
|
+
if (closeRes.ok) {
|
|
913
|
+
deleteChannelState(sessionChannelId);
|
|
914
|
+
if (verbose >= 1) {
|
|
915
|
+
const closeReceiptHeader = closeRes.headers.get('Payment-Receipt');
|
|
916
|
+
let closeTxHash;
|
|
917
|
+
if (closeReceiptHeader) {
|
|
918
|
+
try {
|
|
919
|
+
const r = JSON.parse(Base64.toString(closeReceiptHeader));
|
|
920
|
+
if (typeof r.txHash === 'string')
|
|
921
|
+
closeTxHash = r.txHash;
|
|
922
|
+
}
|
|
923
|
+
catch { }
|
|
924
|
+
}
|
|
925
|
+
const txInfo = closeTxHash && explorerUrl
|
|
926
|
+
? ` ${pc.dim(pc.link(`${explorerUrl}/tx/${closeTxHash}`, closeTxHash))}`
|
|
927
|
+
: '';
|
|
928
|
+
const closePrefix = confirmEnabled ? '' : '\n';
|
|
929
|
+
info(`${closePrefix}${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(sessionCumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`);
|
|
744
930
|
}
|
|
745
|
-
catch { }
|
|
746
931
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
`${pc.dim(` cumulativeAmount: ${sessionCumulativeAmount}`)}\n` +
|
|
758
|
-
`${pc.dim(` escrowContract: ${sessionEscrowContract}`)}\n` +
|
|
759
|
-
`${pc.dim(` chainId: ${sessionChainId}`)}\n` +
|
|
760
|
-
`${pc.dim(` account: ${account?.address}`)}\n` +
|
|
761
|
-
`${pc.dim(` response: ${closeBody || '(empty)'}`)}\n`);
|
|
932
|
+
else {
|
|
933
|
+
const closeBody = await closeRes.text().catch(() => '');
|
|
934
|
+
info(`\n${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`);
|
|
935
|
+
info(`${pc.dim(` channelId: ${sessionChannelId}`)}\n` +
|
|
936
|
+
`${pc.dim(` cumulativeAmount: ${sessionCumulativeAmount}`)}\n` +
|
|
937
|
+
`${pc.dim(` escrowContract: ${sessionEscrowContract}`)}\n` +
|
|
938
|
+
`${pc.dim(` chainId: ${sessionChainId}`)}\n` +
|
|
939
|
+
`${pc.dim(` account: ${account?.address}`)}\n` +
|
|
940
|
+
`${pc.dim(` response: ${closeBody || '(empty)'}`)}\n`);
|
|
941
|
+
}
|
|
762
942
|
}
|
|
763
943
|
}
|
|
764
944
|
}
|
|
765
945
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
946
|
+
catch (err) {
|
|
947
|
+
// TODO: revert cast when https://github.com/wevm/zile/pull/26 is merged
|
|
948
|
+
const errCause = err instanceof Error ? err.cause : undefined;
|
|
949
|
+
const cause = errCause instanceof Error ? errCause : undefined;
|
|
950
|
+
if (cause && 'code' in cause) {
|
|
951
|
+
const code = cause.code;
|
|
952
|
+
if (code === 'ENOTFOUND')
|
|
953
|
+
return error({
|
|
954
|
+
code: 'DNS_ERROR',
|
|
955
|
+
message: `Could not resolve host "${hostname}". Check the URL and try again.`,
|
|
956
|
+
exitCode: 6,
|
|
957
|
+
});
|
|
958
|
+
else if (code === 'ECONNREFUSED')
|
|
959
|
+
return error({
|
|
960
|
+
code: 'CONNECTION_REFUSED',
|
|
961
|
+
message: `Connection refused by "${hostname}". Is the server running?`,
|
|
962
|
+
retryable: true,
|
|
963
|
+
exitCode: 7,
|
|
964
|
+
});
|
|
965
|
+
else if (code === 'ECONNRESET')
|
|
966
|
+
return error({
|
|
967
|
+
code: 'CONNECTION_RESET',
|
|
968
|
+
message: `Connection to "${hostname}" was reset.`,
|
|
969
|
+
retryable: true,
|
|
970
|
+
exitCode: 56,
|
|
971
|
+
});
|
|
972
|
+
else if (code === 'ETIMEDOUT')
|
|
973
|
+
return error({
|
|
974
|
+
code: 'CONNECTION_TIMEOUT',
|
|
975
|
+
message: `Connection to "${hostname}" timed out.`,
|
|
976
|
+
retryable: true,
|
|
977
|
+
exitCode: 28,
|
|
978
|
+
});
|
|
979
|
+
else if (code === 'CERT_HAS_EXPIRED' || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE')
|
|
980
|
+
return error({
|
|
981
|
+
code: 'TLS_ERROR',
|
|
982
|
+
message: `TLS certificate error for "${hostname}". Use --insecure to skip verification.`,
|
|
983
|
+
exitCode: 60,
|
|
984
|
+
});
|
|
985
|
+
else
|
|
986
|
+
return error({
|
|
987
|
+
code: 'REQUEST_FAILED',
|
|
988
|
+
message: `Request to "${hostname}" failed: ${cause.message}`,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
783
991
|
else {
|
|
784
|
-
|
|
992
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
993
|
+
return error({
|
|
994
|
+
code: 'REQUEST_FAILED',
|
|
995
|
+
message: cause
|
|
996
|
+
? `Request failed: ${msg} (Cause: ${cause.message})`
|
|
997
|
+
: `Request failed: ${msg}`,
|
|
998
|
+
});
|
|
785
999
|
}
|
|
786
1000
|
}
|
|
787
|
-
|
|
788
|
-
console.error('Request failed:', err instanceof Error ? err.message : err);
|
|
789
|
-
if (cause)
|
|
790
|
-
console.error('Cause:', cause.message);
|
|
791
|
-
}
|
|
792
|
-
process.exit(1);
|
|
793
|
-
}
|
|
1001
|
+
},
|
|
794
1002
|
});
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
rpcUrl: z.optional(z.string()),
|
|
798
|
-
yes: z.optional(z.boolean()),
|
|
1003
|
+
const account = Cli.create('account', {
|
|
1004
|
+
description: 'Manage accounts (create, default, delete, fund, list, view)',
|
|
799
1005
|
});
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
.
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const existing = await createKeychain().list();
|
|
816
|
-
if (existing.length === 0)
|
|
817
|
-
resolvedName = 'main';
|
|
818
|
-
else {
|
|
819
|
-
const input = await prompt('Account name');
|
|
820
|
-
if (!input)
|
|
821
|
-
return;
|
|
822
|
-
resolvedName = input;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
let keychain = createKeychain(resolvedName);
|
|
826
|
-
while (await keychain.get()) {
|
|
827
|
-
process.stderr.write(`${pc.dim(`Account "${resolvedName}" already exists.`)}\n\n`);
|
|
828
|
-
const input = await prompt('Enter different name');
|
|
1006
|
+
account.command('create', {
|
|
1007
|
+
description: 'Create new account',
|
|
1008
|
+
options: z.object({
|
|
1009
|
+
account: z.string().optional().describe('Account name (env: MPPX_ACCOUNT)'),
|
|
1010
|
+
rpcUrl: z.string().optional().describe('RPC endpoint (env: MPPX_RPC_URL)'),
|
|
1011
|
+
}),
|
|
1012
|
+
alias: { account: 'a', rpcUrl: 'r' },
|
|
1013
|
+
async run({ options }) {
|
|
1014
|
+
let resolvedName = options.account;
|
|
1015
|
+
if (!resolvedName) {
|
|
1016
|
+
const existing = await createKeychain().list();
|
|
1017
|
+
if (existing.length === 0)
|
|
1018
|
+
resolvedName = 'main';
|
|
1019
|
+
else {
|
|
1020
|
+
const input = await prompt('Account name');
|
|
829
1021
|
if (!input)
|
|
830
1022
|
return;
|
|
831
1023
|
resolvedName = input;
|
|
832
|
-
keychain = createKeychain(resolvedName);
|
|
833
1024
|
}
|
|
834
|
-
const privateKey = generatePrivateKey();
|
|
835
|
-
const account = privateKeyToAccount(privateKey);
|
|
836
|
-
await keychain.set(privateKey);
|
|
837
|
-
const accounts = await createKeychain().list();
|
|
838
|
-
if (accounts.length === 1)
|
|
839
|
-
createDefaultStore().set(resolvedName);
|
|
840
|
-
console.log(`Account "${resolvedName}" saved to keychain.`);
|
|
841
|
-
const explorerUrl = tempoMainnet.blockExplorers?.default?.url;
|
|
842
|
-
const addrDisplay = explorerUrl
|
|
843
|
-
? pc.link(`${explorerUrl}/address/${account.address}`, account.address)
|
|
844
|
-
: account.address;
|
|
845
|
-
console.log(pc.dim(`Address ${addrDisplay}`));
|
|
846
|
-
resolveChain(options)
|
|
847
|
-
.then((chain) => createClient({ chain, transport: http(options.rpcUrl) }))
|
|
848
|
-
.then((client) => import('viem/tempo').then(({ Actions }) => Actions.faucet.fund(client, { account }).catch(() => { })));
|
|
849
|
-
return;
|
|
850
1025
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1026
|
+
let keychain = createKeychain(resolvedName);
|
|
1027
|
+
while (await keychain.get()) {
|
|
1028
|
+
process.stderr.write(`${pc.dim(`Account "${resolvedName}" already exists.`)}\n\n`);
|
|
1029
|
+
const input = await prompt('Enter different name');
|
|
1030
|
+
if (!input)
|
|
1031
|
+
return;
|
|
1032
|
+
resolvedName = input;
|
|
1033
|
+
keychain = createKeychain(resolvedName);
|
|
1034
|
+
}
|
|
1035
|
+
const privateKey = generatePrivateKey();
|
|
1036
|
+
const acct = privateKeyToAccount(privateKey);
|
|
1037
|
+
await keychain.set(privateKey);
|
|
1038
|
+
const accounts = await createKeychain().list();
|
|
1039
|
+
if (accounts.length === 1)
|
|
1040
|
+
createDefaultStore().set(resolvedName);
|
|
1041
|
+
console.log(`Account "${resolvedName}" saved to keychain.`);
|
|
1042
|
+
const explorerUrl = tempoMainnet.blockExplorers?.default?.url;
|
|
1043
|
+
const addrDisplay = explorerUrl
|
|
1044
|
+
? pc.link(`${explorerUrl}/address/${acct.address}`, acct.address)
|
|
1045
|
+
: acct.address;
|
|
1046
|
+
console.log(pc.dim(`Address ${addrDisplay}`));
|
|
1047
|
+
resolveChain(options)
|
|
1048
|
+
.then((chain) => createClient({ chain, transport: http(options.rpcUrl) }))
|
|
1049
|
+
.then((client) => import('viem/tempo').then(({ Actions }) => Actions.faucet.fund(client, { account: acct }).catch(() => { })));
|
|
1050
|
+
},
|
|
1051
|
+
});
|
|
1052
|
+
account.command('default', {
|
|
1053
|
+
description: 'Set default account',
|
|
1054
|
+
options: z.object({
|
|
1055
|
+
account: z.string().describe('Account name'),
|
|
1056
|
+
}),
|
|
1057
|
+
alias: { account: 'a' },
|
|
1058
|
+
async run({ options, error }) {
|
|
1059
|
+
const accountName = options.account;
|
|
1060
|
+
if (isTempoAccount(accountName)) {
|
|
1061
|
+
const tempoEntry = resolveTempoAccount(accountName);
|
|
1062
|
+
if (!tempoEntry) {
|
|
1063
|
+
return error({
|
|
1064
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
1065
|
+
message: `Account "${accountName}" not found. Is Tempo wallet configured?`,
|
|
1066
|
+
exitCode: 69,
|
|
1067
|
+
});
|
|
861
1068
|
}
|
|
862
1069
|
createDefaultStore().set(accountName);
|
|
863
1070
|
console.log(`Default account set to "${accountName}"`);
|
|
864
1071
|
return;
|
|
865
1072
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
if (!key) {
|
|
874
|
-
console.log(`Account "${options.account}" not found.`);
|
|
875
|
-
process.exit(1);
|
|
876
|
-
}
|
|
877
|
-
const account = privateKeyToAccount(key);
|
|
878
|
-
const balanceLines = await fetchBalanceLines(account.address, { includeTestnet: false });
|
|
879
|
-
if (!options.yes) {
|
|
880
|
-
const explorerUrl = tempoMainnet.blockExplorers?.default?.url;
|
|
881
|
-
const addrDisplay = explorerUrl
|
|
882
|
-
? pc.link(`${explorerUrl}/address/${account.address}`, account.address)
|
|
883
|
-
: account.address;
|
|
884
|
-
process.stderr.write(pc.dim(`Delete account "${options.account}"\n`));
|
|
885
|
-
process.stderr.write(pc.dim(` Address ${addrDisplay}\n`));
|
|
886
|
-
for (let i = 0; i < balanceLines.length; i++)
|
|
887
|
-
process.stderr.write(pc.dim(` ${i === 0 ? 'Balance' : ' '} ${balanceLines[i]}\n`));
|
|
888
|
-
process.stderr.write(pc.dim('This action cannot be undone\n\n'));
|
|
889
|
-
const confirmed = await confirm('Confirm delete?');
|
|
890
|
-
if (!confirmed) {
|
|
891
|
-
console.log('Canceled');
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
await keychain.delete();
|
|
896
|
-
const currentDefault = createDefaultStore().get();
|
|
897
|
-
if (currentDefault === options.account) {
|
|
898
|
-
const remaining = await createKeychain().list();
|
|
899
|
-
if (remaining.length > 0) {
|
|
900
|
-
createDefaultStore().set(remaining[0]);
|
|
901
|
-
console.log(`Default account set to "${remaining[0]}"`);
|
|
902
|
-
}
|
|
903
|
-
else {
|
|
904
|
-
createDefaultStore().clear();
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
console.log(`Account "${options.account}" deleted`);
|
|
908
|
-
return;
|
|
1073
|
+
const key = await createKeychain(accountName).get();
|
|
1074
|
+
if (!key) {
|
|
1075
|
+
return error({
|
|
1076
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
1077
|
+
message: `Account "${accountName}" not found.`,
|
|
1078
|
+
exitCode: 69,
|
|
1079
|
+
});
|
|
909
1080
|
}
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1081
|
+
createDefaultStore().set(accountName);
|
|
1082
|
+
console.log(`Default account set to "${accountName}"`);
|
|
1083
|
+
},
|
|
1084
|
+
});
|
|
1085
|
+
account.command('delete', {
|
|
1086
|
+
description: 'Delete account',
|
|
1087
|
+
options: z.object({
|
|
1088
|
+
account: z.string().describe('Account name'),
|
|
1089
|
+
yes: z.boolean().optional().describe('DANGER!! Skip confirmation prompts'),
|
|
1090
|
+
}),
|
|
1091
|
+
alias: { account: 'a' },
|
|
1092
|
+
async run({ options, error }) {
|
|
1093
|
+
const keychain = createKeychain(options.account);
|
|
1094
|
+
const key = await keychain.get();
|
|
1095
|
+
if (!key) {
|
|
1096
|
+
return error({
|
|
1097
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
1098
|
+
message: `Account "${options.account}" not found.`,
|
|
1099
|
+
exitCode: 69,
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
const acct = privateKeyToAccount(key);
|
|
1103
|
+
const balanceLines = await fetchBalanceLines(acct.address, { includeTestnet: false });
|
|
1104
|
+
if (!options.yes) {
|
|
1105
|
+
const explorerUrl = tempoMainnet.blockExplorers?.default?.url;
|
|
1106
|
+
const addrDisplay = explorerUrl
|
|
1107
|
+
? pc.link(`${explorerUrl}/address/${acct.address}`, acct.address)
|
|
1108
|
+
: acct.address;
|
|
1109
|
+
process.stderr.write(pc.dim(`Delete account "${options.account}"\n`));
|
|
1110
|
+
process.stderr.write(pc.dim(` Address ${addrDisplay}\n`));
|
|
1111
|
+
for (let i = 0; i < balanceLines.length; i++)
|
|
1112
|
+
process.stderr.write(pc.dim(` ${i === 0 ? 'Balance' : ' '} ${balanceLines[i]}\n`));
|
|
1113
|
+
process.stderr.write(pc.dim('This action cannot be undone\n\n'));
|
|
1114
|
+
const confirmed = await confirm('Confirm delete?');
|
|
1115
|
+
if (!confirmed) {
|
|
1116
|
+
console.log('Canceled');
|
|
1117
|
+
return;
|
|
920
1118
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
for (const hash of hashes) {
|
|
930
|
-
const label = explorerUrl ? pc.link(`${explorerUrl}/tx/${hash}`, pc.gray(hash)) : hash;
|
|
931
|
-
console.log(` ${label}`);
|
|
932
|
-
}
|
|
933
|
-
const { waitForTransactionReceipt } = await import('viem/actions');
|
|
934
|
-
await Promise.all(hashes.map((hash) => waitForTransactionReceipt(client, { hash })));
|
|
935
|
-
console.log('Funded successfully');
|
|
1119
|
+
}
|
|
1120
|
+
await keychain.delete();
|
|
1121
|
+
const currentDefault = createDefaultStore().get();
|
|
1122
|
+
if (currentDefault === options.account) {
|
|
1123
|
+
const remaining = await createKeychain().list();
|
|
1124
|
+
if (remaining.length > 0) {
|
|
1125
|
+
createDefaultStore().set(remaining[0]);
|
|
1126
|
+
console.log(`Default account set to "${remaining[0]}"`);
|
|
936
1127
|
}
|
|
937
|
-
|
|
938
|
-
|
|
1128
|
+
else {
|
|
1129
|
+
createDefaultStore().clear();
|
|
939
1130
|
}
|
|
940
|
-
return;
|
|
941
1131
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1132
|
+
console.log(`Account "${options.account}" deleted`);
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
account.command('fund', {
|
|
1136
|
+
description: 'Fund account with testnet tokens',
|
|
1137
|
+
options: z.object({
|
|
1138
|
+
account: z.string().optional().describe('Account name (env: MPPX_ACCOUNT)'),
|
|
1139
|
+
rpcUrl: z.string().optional().describe('RPC endpoint (env: MPPX_RPC_URL)'),
|
|
1140
|
+
}),
|
|
1141
|
+
alias: { account: 'a', rpcUrl: 'r' },
|
|
1142
|
+
async run({ options, error }) {
|
|
1143
|
+
const accountName = resolveAccountName(options.account);
|
|
1144
|
+
const keychain = createKeychain(accountName);
|
|
1145
|
+
const key = await keychain.get();
|
|
1146
|
+
if (!key) {
|
|
1147
|
+
if (options.account)
|
|
1148
|
+
return error({
|
|
1149
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
1150
|
+
message: `Account "${accountName}" not found.`,
|
|
1151
|
+
exitCode: 69,
|
|
1152
|
+
});
|
|
1153
|
+
else
|
|
1154
|
+
return error({ code: 'ACCOUNT_NOT_FOUND', message: 'No account found.', exitCode: 69 });
|
|
1155
|
+
}
|
|
1156
|
+
const acct = privateKeyToAccount(key);
|
|
1157
|
+
const chain = await resolveChain(options);
|
|
1158
|
+
const client = createClient({ chain, transport: http(options.rpcUrl) });
|
|
1159
|
+
console.log(`Funding "${accountName}" on ${chainName(chain)}`);
|
|
1160
|
+
try {
|
|
1161
|
+
const { Actions } = await import('viem/tempo');
|
|
1162
|
+
const hashes = await Actions.faucet.fund(client, { account: acct });
|
|
1163
|
+
const explorerUrl = chain.blockExplorers?.default?.url;
|
|
1164
|
+
for (const hash of hashes) {
|
|
1165
|
+
const label = explorerUrl ? pc.link(`${explorerUrl}/tx/${hash}`, pc.gray(hash)) : hash;
|
|
1166
|
+
console.log(` ${label}`);
|
|
969
1167
|
}
|
|
1168
|
+
const { waitForTransactionReceipt } = await import('viem/actions');
|
|
1169
|
+
await Promise.all(hashes.map((hash) => waitForTransactionReceipt(client, { hash })));
|
|
1170
|
+
console.log('Funded successfully');
|
|
1171
|
+
}
|
|
1172
|
+
catch (err) {
|
|
1173
|
+
console.error('Funding failed:', err instanceof Error ? err.message : err);
|
|
1174
|
+
}
|
|
1175
|
+
},
|
|
1176
|
+
});
|
|
1177
|
+
account.command('list', {
|
|
1178
|
+
description: 'List all accounts',
|
|
1179
|
+
async run() {
|
|
1180
|
+
const currentDefault = createDefaultStore().get();
|
|
1181
|
+
const accounts = (await createKeychain().list()).sort();
|
|
1182
|
+
const resolved = [];
|
|
1183
|
+
for (const accountName of accounts) {
|
|
1184
|
+
const key = await createKeychain(accountName).get();
|
|
1185
|
+
if (!key)
|
|
1186
|
+
continue;
|
|
1187
|
+
resolved.push({
|
|
1188
|
+
name: accountName,
|
|
1189
|
+
address: privateKeyToAccount(key).address,
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
const tempoEntries = readTempoKeystore();
|
|
1193
|
+
for (let i = 0; i < tempoEntries.length; i++) {
|
|
1194
|
+
const entry = tempoEntries[i];
|
|
1195
|
+
const tempoName = i === 0 ? 'tempo:default' : `tempo:${i}`;
|
|
1196
|
+
if (entry.wallet_address)
|
|
1197
|
+
resolved.push({ name: tempoName, address: entry.wallet_address, source: 'tempo wallet' });
|
|
1198
|
+
}
|
|
1199
|
+
if (resolved.length === 0) {
|
|
1200
|
+
console.log(`No accounts found.`);
|
|
970
1201
|
return;
|
|
971
1202
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1203
|
+
const explorerUrl = tempoMainnet.blockExplorers?.default?.url;
|
|
1204
|
+
const maxWidth = Math.max(...resolved.map((e) => e.name.length + (e.name === currentDefault ? 1 : 0)));
|
|
1205
|
+
for (const entry of resolved) {
|
|
1206
|
+
const isDefault = entry.name === currentDefault;
|
|
1207
|
+
const label = isDefault ? `${entry.name}${pc.dim('*')}` : entry.name;
|
|
1208
|
+
const width = entry.name.length + (isDefault ? 1 : 0);
|
|
1209
|
+
const addrDisplay = explorerUrl
|
|
1210
|
+
? pc.link(`${explorerUrl}/address/${entry.address}`, entry.address)
|
|
1211
|
+
: entry.address;
|
|
1212
|
+
const sourceLabel = entry.source ? ` ${pc.dim(`(${entry.source})`)}` : '';
|
|
1213
|
+
console.log(`${label}${' '.repeat(maxWidth - width + 2)}${pc.dim(addrDisplay)}${sourceLabel}`);
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
});
|
|
1217
|
+
account.command('view', {
|
|
1218
|
+
description: 'View account address',
|
|
1219
|
+
options: z.object({
|
|
1220
|
+
account: z.string().optional().describe('Account name (env: MPPX_ACCOUNT)'),
|
|
1221
|
+
rpcUrl: z.string().optional().describe('RPC endpoint (env: MPPX_RPC_URL)'),
|
|
1222
|
+
}),
|
|
1223
|
+
alias: { account: 'a', rpcUrl: 'r' },
|
|
1224
|
+
async run({ options, error }) {
|
|
1225
|
+
const accountName = resolveAccountName(options.account);
|
|
1226
|
+
if (isTempoAccount(accountName)) {
|
|
1227
|
+
const tempoEntry = resolveTempoAccount(accountName);
|
|
1228
|
+
if (!tempoEntry) {
|
|
1229
|
+
return error({
|
|
1230
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
1231
|
+
message: `Account "${accountName}" not found. Is Tempo wallet configured?`,
|
|
1232
|
+
exitCode: 69,
|
|
1233
|
+
});
|
|
982
1234
|
}
|
|
983
|
-
const
|
|
1235
|
+
const address = tempoEntry.wallet_address;
|
|
984
1236
|
const rpcUrl = options.rpcUrl ?? (process.env.MPPX_RPC_URL || undefined);
|
|
985
1237
|
const chain = rpcUrl ? await resolveChain({ rpcUrl }) : tempoMainnet;
|
|
986
1238
|
const explorerUrl = chain.blockExplorers?.default?.url;
|
|
987
1239
|
const addrDisplay = explorerUrl
|
|
988
|
-
? pc.link(`${explorerUrl}/address/${
|
|
989
|
-
:
|
|
1240
|
+
? pc.link(`${explorerUrl}/address/${address}`, address)
|
|
1241
|
+
: address;
|
|
990
1242
|
console.log(`${pc.dim('Address')} ${addrDisplay}`);
|
|
991
|
-
const balanceLines = await fetchBalanceLines(
|
|
1243
|
+
const balanceLines = await fetchBalanceLines(address, chain && rpcUrl ? { chain, rpcUrl } : undefined);
|
|
992
1244
|
for (let i = 0; i < balanceLines.length; i++)
|
|
993
1245
|
console.log(`${pc.dim(i === 0 ? 'Balance' : ' ')} ${balanceLines[i]}`);
|
|
994
1246
|
console.log(`${pc.dim('Name')} ${accountName}`);
|
|
1247
|
+
console.log(`${pc.dim('Type')} ${tempoEntry.wallet_type} ${pc.dim('(tempo wallet)')}`);
|
|
995
1248
|
return;
|
|
996
1249
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1250
|
+
const keychain = createKeychain(accountName);
|
|
1251
|
+
const key = await keychain.get();
|
|
1252
|
+
if (!key) {
|
|
1253
|
+
if (options.account)
|
|
1254
|
+
return error({
|
|
1255
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
1256
|
+
message: `Account "${accountName}" not found.`,
|
|
1257
|
+
exitCode: 69,
|
|
1258
|
+
});
|
|
1259
|
+
else
|
|
1260
|
+
return error({ code: 'ACCOUNT_NOT_FOUND', message: 'No account found.', exitCode: 69 });
|
|
1261
|
+
}
|
|
1262
|
+
const acct = privateKeyToAccount(key);
|
|
1263
|
+
const rpcUrl = options.rpcUrl ?? (process.env.MPPX_RPC_URL || undefined);
|
|
1264
|
+
const chain = rpcUrl ? await resolveChain({ rpcUrl }) : tempoMainnet;
|
|
1265
|
+
const explorerUrl = chain.blockExplorers?.default?.url;
|
|
1266
|
+
const addrDisplay = explorerUrl
|
|
1267
|
+
? pc.link(`${explorerUrl}/address/${acct.address}`, acct.address)
|
|
1268
|
+
: acct.address;
|
|
1269
|
+
console.log(`${pc.dim('Address')} ${addrDisplay}`);
|
|
1270
|
+
const balanceLines = await fetchBalanceLines(acct.address, chain && rpcUrl ? { chain, rpcUrl } : undefined);
|
|
1271
|
+
for (let i = 0; i < balanceLines.length; i++)
|
|
1272
|
+
console.log(`${pc.dim(i === 0 ? 'Balance' : ' ')} ${balanceLines[i]}`);
|
|
1273
|
+
console.log(`${pc.dim('Name')} ${accountName}`);
|
|
1274
|
+
},
|
|
1002
1275
|
});
|
|
1003
|
-
cli.
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1276
|
+
cli.command(account);
|
|
1277
|
+
const sign = Cli.create('sign', {
|
|
1278
|
+
description: 'Sign a payment challenge and output the Authorization header',
|
|
1279
|
+
usage: [
|
|
1280
|
+
{ suffix: '--challenge <value> [options]' },
|
|
1281
|
+
{ prefix: 'echo <challenge> |', suffix: '[options]' },
|
|
1282
|
+
],
|
|
1283
|
+
options: z.object({
|
|
1284
|
+
account: z.string().optional().describe('Account name (env: MPPX_ACCOUNT)'),
|
|
1285
|
+
challenge: z.string().optional().describe('WWW-Authenticate challenge value'),
|
|
1286
|
+
dryRun: z.boolean().optional().describe('Validate and parse the challenge without signing'),
|
|
1287
|
+
methodOpt: z
|
|
1288
|
+
.array(z.string())
|
|
1289
|
+
.optional()
|
|
1290
|
+
.describe('Method-specific option (key=value, repeatable)'),
|
|
1291
|
+
rpcUrl: z
|
|
1292
|
+
.string()
|
|
1293
|
+
.optional()
|
|
1294
|
+
.describe('RPC endpoint, defaults to public RPC for chain (env: MPPX_RPC_URL)'),
|
|
1295
|
+
}),
|
|
1296
|
+
alias: {
|
|
1297
|
+
account: 'a',
|
|
1298
|
+
challenge: 'c',
|
|
1299
|
+
methodOpt: 'M',
|
|
1300
|
+
rpcUrl: 'r',
|
|
1301
|
+
},
|
|
1302
|
+
async run({ options, format, error }) {
|
|
1303
|
+
const raw = options.challenge || (process.stdin.isTTY === false ? await readStdin() : undefined);
|
|
1304
|
+
if (!raw) {
|
|
1305
|
+
return error({
|
|
1306
|
+
code: 'NO_CHALLENGE',
|
|
1307
|
+
message: 'No challenge provided. Use --challenge or pipe via stdin.',
|
|
1308
|
+
exitCode: 2,
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
let challenge;
|
|
1312
|
+
try {
|
|
1313
|
+
challenge = Challenge.deserialize(raw);
|
|
1314
|
+
}
|
|
1315
|
+
catch (err) {
|
|
1316
|
+
return error({
|
|
1317
|
+
code: 'INVALID_CHALLENGE',
|
|
1318
|
+
message: `Failed to parse challenge: ${err instanceof Error ? err.message : err}`,
|
|
1319
|
+
exitCode: 2,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
if (options.dryRun) {
|
|
1323
|
+
process.stderr.write('Challenge is valid.\n');
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
if (challenge.method === 'tempo') {
|
|
1327
|
+
const accountName = resolveAccountName(options.account);
|
|
1328
|
+
// Delegate to tempo CLI for tempo wallet accounts
|
|
1329
|
+
if (isTempoAccount(accountName) && hasTempoCliSync()) {
|
|
1330
|
+
const wwwAuth = Challenge.serialize(challenge);
|
|
1331
|
+
const result = await tempoCliSign(wwwAuth);
|
|
1332
|
+
if (format === 'json') {
|
|
1333
|
+
const tempoEntry = resolveTempoAccount(accountName);
|
|
1334
|
+
console.log(JSON.stringify({ authorization: result, from: tempoEntry?.wallet_address }));
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
console.log(result);
|
|
1338
|
+
}
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
let privateKey = process.env.MPPX_PRIVATE_KEY?.trim() ||
|
|
1342
|
+
(isTempoAccount(accountName) ? undefined : await createKeychain(accountName).get());
|
|
1343
|
+
if (!privateKey) {
|
|
1344
|
+
const fallback = fallbackFromTempo();
|
|
1345
|
+
if (fallback)
|
|
1346
|
+
privateKey = await createKeychain(fallback).get();
|
|
1347
|
+
}
|
|
1348
|
+
if (!privateKey) {
|
|
1349
|
+
if (options.account)
|
|
1350
|
+
return error({
|
|
1351
|
+
code: 'ACCOUNT_NOT_FOUND',
|
|
1352
|
+
message: `Account "${accountName}" not found.`,
|
|
1353
|
+
exitCode: 69,
|
|
1354
|
+
});
|
|
1355
|
+
else
|
|
1356
|
+
return error({ code: 'ACCOUNT_NOT_FOUND', message: 'No account found.', exitCode: 69 });
|
|
1357
|
+
}
|
|
1358
|
+
const account = privateKeyToAccount(privateKey);
|
|
1359
|
+
const rpcUrl = options.rpcUrl ?? process.env.RPC_URL;
|
|
1360
|
+
const client = createClient({
|
|
1361
|
+
chain: await resolveChain({ rpcUrl }),
|
|
1362
|
+
transport: http(rpcUrl),
|
|
1363
|
+
});
|
|
1364
|
+
const methodOpts = parseMethodOpts(options.methodOpt);
|
|
1365
|
+
const tempoOpts = parseOptions(z.object({
|
|
1366
|
+
channel: z.optional(z.coerce.string()),
|
|
1367
|
+
deposit: z.optional(z.union([z.string(), z.number()])),
|
|
1368
|
+
}), methodOpts);
|
|
1369
|
+
const mppx = Mppx.create({
|
|
1370
|
+
methods: tempo({
|
|
1371
|
+
account,
|
|
1372
|
+
getClient: () => client,
|
|
1373
|
+
deposit: (() => {
|
|
1374
|
+
if (challenge.intent !== 'session')
|
|
1375
|
+
return undefined;
|
|
1376
|
+
const suggestedDeposit = challenge.request
|
|
1377
|
+
.suggestedDeposit;
|
|
1378
|
+
const cliDeposit = tempoOpts.deposit !== undefined ? String(tempoOpts.deposit) : undefined;
|
|
1379
|
+
return suggestedDeposit ?? cliDeposit;
|
|
1380
|
+
})(),
|
|
1381
|
+
}),
|
|
1382
|
+
polyfill: false,
|
|
1383
|
+
});
|
|
1384
|
+
const wwwAuth = Challenge.serialize(challenge);
|
|
1385
|
+
const fakeResponse = new Response(null, {
|
|
1386
|
+
status: 402,
|
|
1387
|
+
headers: { 'WWW-Authenticate': wwwAuth },
|
|
1388
|
+
});
|
|
1389
|
+
const credential = await mppx.createCredential(fakeResponse, (() => {
|
|
1390
|
+
if (!tempoOpts.channel)
|
|
1391
|
+
return undefined;
|
|
1392
|
+
const channelId = tempoOpts.channel;
|
|
1393
|
+
const saved = readChannelCumulative(channelId);
|
|
1394
|
+
return {
|
|
1395
|
+
channelId,
|
|
1396
|
+
...(saved !== undefined && { cumulativeAmountRaw: saved.toString() }),
|
|
1397
|
+
};
|
|
1398
|
+
})());
|
|
1399
|
+
if (format === 'json') {
|
|
1400
|
+
console.log(JSON.stringify({ authorization: credential, from: account.address }));
|
|
1401
|
+
}
|
|
1402
|
+
else {
|
|
1403
|
+
console.log(credential);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
else if (challenge.method === 'stripe') {
|
|
1407
|
+
const stripeSecretKey = process.env.MPPX_STRIPE_SECRET_KEY;
|
|
1408
|
+
if (!stripeSecretKey) {
|
|
1409
|
+
return error({
|
|
1410
|
+
code: 'MISSING_ENV',
|
|
1411
|
+
message: 'MPPX_STRIPE_SECRET_KEY environment variable is required for Stripe payments.',
|
|
1412
|
+
exitCode: 2,
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
const methodOpts = parseMethodOpts(options.methodOpt);
|
|
1416
|
+
const stripeOpts = parseOptions(z.object({ paymentMethod: z.string() }), methodOpts);
|
|
1417
|
+
const mppx = Mppx.create({
|
|
1418
|
+
methods: [
|
|
1419
|
+
stripe.charge({
|
|
1420
|
+
paymentMethod: stripeOpts.paymentMethod,
|
|
1421
|
+
createToken: async ({ paymentMethod, amount, currency, networkId, expiresAt, metadata, }) => {
|
|
1422
|
+
const body = new URLSearchParams({
|
|
1423
|
+
payment_method: paymentMethod,
|
|
1424
|
+
'usage_limits[currency]': currency,
|
|
1425
|
+
'usage_limits[max_amount]': amount,
|
|
1426
|
+
'usage_limits[expires_at]': expiresAt.toString(),
|
|
1427
|
+
});
|
|
1428
|
+
if (networkId)
|
|
1429
|
+
body.set('seller_details[network_id]', networkId);
|
|
1430
|
+
if (metadata) {
|
|
1431
|
+
for (const [key, value] of Object.entries(metadata))
|
|
1432
|
+
body.set(`metadata[${key}]`, value);
|
|
1433
|
+
}
|
|
1434
|
+
const sptUrl = process.env.MPPX_STRIPE_SPT_URL ??
|
|
1435
|
+
'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens';
|
|
1436
|
+
const response = await globalThis.fetch(sptUrl, {
|
|
1437
|
+
method: 'POST',
|
|
1438
|
+
headers: {
|
|
1439
|
+
Authorization: `Basic ${btoa(`${stripeSecretKey}:`)}`,
|
|
1440
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1441
|
+
},
|
|
1442
|
+
body,
|
|
1443
|
+
});
|
|
1444
|
+
if (!response.ok) {
|
|
1445
|
+
const errorBody = (await response.json());
|
|
1446
|
+
return error({
|
|
1447
|
+
code: 'STRIPE_ERROR',
|
|
1448
|
+
message: `Failed to create SPT: ${errorBody.error.message}`,
|
|
1449
|
+
exitCode: 77,
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
const { id } = (await response.json());
|
|
1453
|
+
return id;
|
|
1454
|
+
},
|
|
1455
|
+
}),
|
|
1456
|
+
],
|
|
1457
|
+
polyfill: false,
|
|
1458
|
+
});
|
|
1459
|
+
const wwwAuth = Challenge.serialize(challenge);
|
|
1460
|
+
const fakeResponse = new Response(null, {
|
|
1461
|
+
status: 402,
|
|
1462
|
+
headers: { 'WWW-Authenticate': wwwAuth },
|
|
1463
|
+
});
|
|
1464
|
+
const credential = await mppx.createCredential(fakeResponse);
|
|
1465
|
+
if (format === 'json') {
|
|
1466
|
+
console.log(JSON.stringify({ authorization: credential }));
|
|
1467
|
+
}
|
|
1468
|
+
else {
|
|
1469
|
+
console.log(credential);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
else {
|
|
1473
|
+
return error({
|
|
1474
|
+
code: 'UNSUPPORTED_METHOD',
|
|
1475
|
+
message: `Unsupported payment method: ${challenge.method}`,
|
|
1476
|
+
exitCode: 2,
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
},
|
|
1025
1480
|
});
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
}
|
|
1029
|
-
catch (err) {
|
|
1030
|
-
console.error(err instanceof Error ? err.message : err);
|
|
1031
|
-
process.exit(1);
|
|
1032
|
-
}
|
|
1481
|
+
cli.command(sign);
|
|
1482
|
+
export default cli;
|
|
1033
1483
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
1034
1484
|
function parseMethodOpts(raw) {
|
|
1035
1485
|
if (!raw)
|
|
@@ -1039,8 +1489,7 @@ function parseMethodOpts(raw) {
|
|
|
1039
1489
|
for (const item of list) {
|
|
1040
1490
|
const idx = item.indexOf('=');
|
|
1041
1491
|
if (idx === -1) {
|
|
1042
|
-
|
|
1043
|
-
process.exit(1);
|
|
1492
|
+
throw new Error(`Invalid method option format: ${item} (expected key=value)`);
|
|
1044
1493
|
}
|
|
1045
1494
|
result[item.slice(0, idx)] = item.slice(idx + 1);
|
|
1046
1495
|
}
|
|
@@ -1118,6 +1567,120 @@ function resolveAccountName(explicit) {
|
|
|
1118
1567
|
return process.env.MPPX_ACCOUNT;
|
|
1119
1568
|
return createDefaultStore().get();
|
|
1120
1569
|
}
|
|
1570
|
+
function isTempoAccount(accountName) {
|
|
1571
|
+
return accountName.startsWith('tempo:');
|
|
1572
|
+
}
|
|
1573
|
+
function tempoKeystorePath() {
|
|
1574
|
+
const platform = os.platform();
|
|
1575
|
+
if (platform === 'darwin')
|
|
1576
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'tempo', 'wallet', 'keys.toml');
|
|
1577
|
+
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'tempo', 'wallet', 'keys.toml');
|
|
1578
|
+
}
|
|
1579
|
+
function readTempoKeystore() {
|
|
1580
|
+
try {
|
|
1581
|
+
const raw = fs.readFileSync(tempoKeystorePath(), 'utf-8');
|
|
1582
|
+
const entries = [];
|
|
1583
|
+
let current;
|
|
1584
|
+
for (const line of raw.split('\n')) {
|
|
1585
|
+
const trimmed = line.trim();
|
|
1586
|
+
if (trimmed === '[[keys]]') {
|
|
1587
|
+
if (current?.wallet_address)
|
|
1588
|
+
entries.push(current);
|
|
1589
|
+
current = { wallet_type: 'local', wallet_address: '', chain_id: 0 };
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
if (!current)
|
|
1593
|
+
continue;
|
|
1594
|
+
const m = trimmed.match(/^(\w+)\s*=\s*"?([^"]*)"?$/);
|
|
1595
|
+
if (!m)
|
|
1596
|
+
continue;
|
|
1597
|
+
const [, key, value] = m;
|
|
1598
|
+
if (key === 'wallet_type')
|
|
1599
|
+
current.wallet_type = value;
|
|
1600
|
+
else if (key === 'wallet_address')
|
|
1601
|
+
current.wallet_address = value;
|
|
1602
|
+
else if (key === 'chain_id')
|
|
1603
|
+
current.chain_id = Number.parseInt(value, 10);
|
|
1604
|
+
}
|
|
1605
|
+
if (current?.wallet_address)
|
|
1606
|
+
entries.push(current);
|
|
1607
|
+
return entries;
|
|
1608
|
+
}
|
|
1609
|
+
catch {
|
|
1610
|
+
return [];
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
function resolveTempoAccount(accountName) {
|
|
1614
|
+
const entries = readTempoKeystore();
|
|
1615
|
+
if (entries.length === 0)
|
|
1616
|
+
return undefined;
|
|
1617
|
+
const suffix = accountName.slice('tempo:'.length);
|
|
1618
|
+
if (suffix === 'default' || suffix === '')
|
|
1619
|
+
return entries[0];
|
|
1620
|
+
const idx = Number.parseInt(suffix, 10);
|
|
1621
|
+
if (!Number.isNaN(idx) && idx >= 0 && idx < entries.length)
|
|
1622
|
+
return entries[idx];
|
|
1623
|
+
return undefined;
|
|
1624
|
+
}
|
|
1625
|
+
let _tempoCliAvailable;
|
|
1626
|
+
function hasTempoCliSync() {
|
|
1627
|
+
if (_tempoCliAvailable !== undefined)
|
|
1628
|
+
return _tempoCliAvailable;
|
|
1629
|
+
try {
|
|
1630
|
+
child.execFileSync('which', ['tempo'], { stdio: 'ignore' });
|
|
1631
|
+
_tempoCliAvailable = true;
|
|
1632
|
+
}
|
|
1633
|
+
catch {
|
|
1634
|
+
_tempoCliAvailable = false;
|
|
1635
|
+
}
|
|
1636
|
+
return _tempoCliAvailable;
|
|
1637
|
+
}
|
|
1638
|
+
async function tempoCliSign(wwwAuth) {
|
|
1639
|
+
return new Promise((resolve, reject) => {
|
|
1640
|
+
child.execFile('tempo', ['mpp', 'sign', '--challenge', wwwAuth], (error, stdout, stderr) => {
|
|
1641
|
+
if (error) {
|
|
1642
|
+
const msg = stderr?.trim() || error.message;
|
|
1643
|
+
reject(new Error(`tempo mpp sign failed: ${msg}`));
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
const trimmed = stdout.trim();
|
|
1647
|
+
if (!trimmed) {
|
|
1648
|
+
reject(new Error('tempo mpp sign returned empty output'));
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
resolve(trimmed);
|
|
1652
|
+
});
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
function fallbackFromTempo() {
|
|
1656
|
+
const store = createDefaultStore();
|
|
1657
|
+
const currentDefault = store.get();
|
|
1658
|
+
if (!isTempoAccount(currentDefault))
|
|
1659
|
+
return undefined;
|
|
1660
|
+
if (hasTempoCliSync())
|
|
1661
|
+
return undefined;
|
|
1662
|
+
// tempo CLI not installed, fall back to first mppx account
|
|
1663
|
+
// (sync list via security dump-keychain to avoid async in hot path)
|
|
1664
|
+
const platform = os.platform();
|
|
1665
|
+
if (platform === 'darwin') {
|
|
1666
|
+
try {
|
|
1667
|
+
const stdout = child.execFileSync('security', ['dump-keychain'], { encoding: 'utf-8' });
|
|
1668
|
+
const mppxAccounts = [];
|
|
1669
|
+
for (const block of stdout.split('keychain:')) {
|
|
1670
|
+
const serviceMatch = block.match(/"svce"<blob>="([^"]*)"/);
|
|
1671
|
+
const accountMatch = block.match(/"acct"<blob>="([^"]*)"/);
|
|
1672
|
+
if (serviceMatch?.[1] === name && accountMatch?.[1])
|
|
1673
|
+
mppxAccounts.push(accountMatch[1]);
|
|
1674
|
+
}
|
|
1675
|
+
if (mppxAccounts.length > 0) {
|
|
1676
|
+
store.set(mppxAccounts[0]);
|
|
1677
|
+
return mppxAccounts[0];
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
catch { }
|
|
1681
|
+
}
|
|
1682
|
+
return undefined;
|
|
1683
|
+
}
|
|
1121
1684
|
// biome-ignore format: compact shell commands
|
|
1122
1685
|
function createKeychain(account = 'main') {
|
|
1123
1686
|
const service = name;
|