mppx 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -52
- package/dist/Challenge.d.ts +8 -0
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +20 -4
- package/dist/Challenge.js.map +1 -1
- package/dist/Errors.d.ts +7 -7
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +7 -7
- package/dist/Errors.js.map +1 -1
- package/dist/cli.js +280 -119
- package/dist/cli.js.map +1 -1
- package/dist/internal/env.js +2 -2
- package/dist/internal/env.js.map +1 -1
- package/dist/server/Mppx.d.ts +2 -0
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +4 -3
- package/dist/server/Mppx.js.map +1 -1
- package/dist/tempo/client/ChannelOps.d.ts +5 -5
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/client/ChannelOps.js +3 -3
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/client/Session.d.ts +2 -2
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js +3 -3
- package/dist/tempo/client/Session.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts +4 -4
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +4 -4
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/index.d.ts +1 -1
- package/dist/tempo/index.d.ts.map +1 -1
- package/dist/tempo/index.js +1 -1
- package/dist/tempo/index.js.map +1 -1
- package/dist/tempo/server/Charge.js +1 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +1 -1
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Session.d.ts +8 -8
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +24 -24
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/index.d.ts +2 -2
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.js +2 -2
- package/dist/tempo/server/index.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts +4 -4
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +3 -3
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts.map +1 -0
- package/dist/tempo/session/Chain.js.map +1 -0
- package/dist/tempo/session/Channel.d.ts.map +1 -0
- package/dist/tempo/session/Channel.js.map +1 -0
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -0
- package/dist/tempo/session/ChannelStore.js.map +1 -0
- package/dist/tempo/session/Receipt.d.ts +22 -0
- package/dist/tempo/session/Receipt.d.ts.map +1 -0
- package/dist/tempo/{stream → session}/Receipt.js +6 -6
- package/dist/tempo/session/Receipt.js.map +1 -0
- package/dist/tempo/{stream → session}/Sse.d.ts +7 -7
- package/dist/tempo/session/Sse.d.ts.map +1 -0
- package/dist/tempo/{stream → session}/Sse.js +4 -4
- package/dist/tempo/session/Sse.js.map +1 -0
- package/dist/tempo/{stream → session}/Types.d.ts +4 -4
- package/dist/tempo/session/Types.d.ts.map +1 -0
- package/dist/tempo/{stream → session}/Types.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts.map +1 -0
- package/dist/tempo/session/Voucher.js.map +1 -0
- package/dist/tempo/{stream → session}/escrow.abi.d.ts.map +1 -1
- package/dist/tempo/session/escrow.abi.js.map +1 -0
- package/dist/tempo/session/index.d.ts.map +1 -0
- package/dist/tempo/session/index.js.map +1 -0
- package/package.json +1 -1
- package/src/Challenge.test.ts +201 -11
- package/src/Challenge.ts +34 -4
- package/src/Errors.test.ts +10 -10
- package/src/Errors.ts +7 -7
- package/src/Store.test.ts +93 -0
- package/src/cli.test.ts +234 -38
- package/src/cli.ts +340 -135
- package/src/client/Transport.test.ts +4 -4
- package/src/internal/env.test.ts +42 -0
- package/src/internal/env.ts +2 -2
- package/src/middlewares/express.test.ts +1 -1
- package/src/middlewares/hono.test.ts +1 -1
- package/src/middlewares/nextjs.test.ts +1 -1
- package/src/server/Mppx.test.ts +173 -0
- package/src/server/Mppx.ts +6 -3
- package/src/server/Transport.test.ts +6 -6
- package/src/tempo/client/ChannelOps.test.ts +2 -2
- package/src/tempo/client/ChannelOps.ts +8 -8
- package/src/tempo/client/Session.test.ts +3 -3
- package/src/tempo/client/Session.ts +9 -9
- package/src/tempo/client/SessionManager.test.ts +3 -3
- package/src/tempo/client/SessionManager.ts +9 -9
- package/src/tempo/index.ts +1 -1
- package/src/tempo/server/Charge.ts +1 -1
- package/src/tempo/server/Session.test.ts +61 -9
- package/src/tempo/server/Session.ts +47 -47
- package/src/tempo/server/Sse.test.ts +3 -3
- package/src/tempo/server/index.ts +2 -2
- package/src/tempo/server/internal/transport.test.ts +285 -0
- package/src/tempo/server/internal/transport.ts +6 -6
- package/src/tempo/{stream → session}/Chain.test.ts +1 -1
- package/src/tempo/{stream → session}/Receipt.test.ts +16 -12
- package/src/tempo/{stream → session}/Receipt.ts +9 -9
- package/src/tempo/{stream → session}/Sse.test.ts +5 -5
- package/src/tempo/{stream → session}/Sse.ts +11 -11
- package/src/tempo/{stream → session}/Types.ts +4 -4
- package/dist/tempo/stream/Chain.d.ts.map +0 -1
- package/dist/tempo/stream/Chain.js.map +0 -1
- package/dist/tempo/stream/Channel.d.ts.map +0 -1
- package/dist/tempo/stream/Channel.js.map +0 -1
- package/dist/tempo/stream/ChannelStore.d.ts.map +0 -1
- package/dist/tempo/stream/ChannelStore.js.map +0 -1
- package/dist/tempo/stream/Receipt.d.ts +0 -22
- package/dist/tempo/stream/Receipt.d.ts.map +0 -1
- package/dist/tempo/stream/Receipt.js.map +0 -1
- package/dist/tempo/stream/Sse.d.ts.map +0 -1
- package/dist/tempo/stream/Sse.js.map +0 -1
- package/dist/tempo/stream/Types.d.ts.map +0 -1
- package/dist/tempo/stream/Voucher.d.ts.map +0 -1
- package/dist/tempo/stream/Voucher.js.map +0 -1
- package/dist/tempo/stream/escrow.abi.js.map +0 -1
- package/dist/tempo/stream/index.d.ts.map +0 -1
- package/dist/tempo/stream/index.js.map +0 -1
- /package/dist/tempo/{stream → session}/Chain.d.ts +0 -0
- /package/dist/tempo/{stream → session}/Chain.js +0 -0
- /package/dist/tempo/{stream → session}/Channel.d.ts +0 -0
- /package/dist/tempo/{stream → session}/Channel.js +0 -0
- /package/dist/tempo/{stream → session}/ChannelStore.d.ts +0 -0
- /package/dist/tempo/{stream → session}/ChannelStore.js +0 -0
- /package/dist/tempo/{stream → session}/Types.js +0 -0
- /package/dist/tempo/{stream → session}/Voucher.d.ts +0 -0
- /package/dist/tempo/{stream → session}/Voucher.js +0 -0
- /package/dist/tempo/{stream → session}/escrow.abi.d.ts +0 -0
- /package/dist/tempo/{stream → session}/escrow.abi.js +0 -0
- /package/dist/tempo/{stream → session}/index.d.ts +0 -0
- /package/dist/tempo/{stream → session}/index.js +0 -0
- /package/src/tempo/{stream → session}/Chain.ts +0 -0
- /package/src/tempo/{stream → session}/Channel.test.ts +0 -0
- /package/src/tempo/{stream → session}/Channel.ts +0 -0
- /package/src/tempo/{stream → session}/ChannelStore.test.ts +0 -0
- /package/src/tempo/{stream → session}/ChannelStore.ts +0 -0
- /package/src/tempo/{stream → session}/Voucher.test.ts +0 -0
- /package/src/tempo/{stream → session}/Voucher.ts +0 -0
- /package/src/tempo/{stream → session}/escrow.abi.ts +0 -0
- /package/src/tempo/{stream → session}/index.ts +0 -0
package/dist/cli.js
CHANGED
|
@@ -14,8 +14,9 @@ import { z } from 'zod/mini';
|
|
|
14
14
|
import * as Challenge from './Challenge.js';
|
|
15
15
|
import * as Credential from './Credential.js';
|
|
16
16
|
import * as Mppx from './client/Mppx.js';
|
|
17
|
+
import { stripe } from './stripe/client/index.js';
|
|
17
18
|
import { tempo } from './tempo/client/index.js';
|
|
18
|
-
import { signVoucher } from './tempo/
|
|
19
|
+
import { signVoucher } from './tempo/session/Voucher.js';
|
|
19
20
|
const require = createRequire(import.meta.url);
|
|
20
21
|
const { name, version } = require('../package.json');
|
|
21
22
|
const cli = cac(name);
|
|
@@ -33,19 +34,16 @@ cli
|
|
|
33
34
|
.option('-H, --header <header>', 'Add header (repeatable)')
|
|
34
35
|
.option('-L, --location', 'Follow redirects')
|
|
35
36
|
.option('-X, --method <method>', 'HTTP method')
|
|
36
|
-
.option('--
|
|
37
|
+
.option('-M, --method-opt <opt>', 'Method-specific option (key=value, repeatable)')
|
|
37
38
|
.option('--confirm', 'Show confirmation prompts')
|
|
38
|
-
.option('--deposit <amount>', 'Deposit amount for stream payments (human-readable units)')
|
|
39
39
|
.option('--json <json>', 'Send JSON body (sets Content-Type and Accept, implies POST)')
|
|
40
40
|
.example(`${name} example.com/content`)
|
|
41
41
|
.example(`${name} example.com/api --json '{"key":"value"}'`)
|
|
42
42
|
.action(async (rawUrl, rawOptions) => {
|
|
43
43
|
const options = parseOptions(z.object({
|
|
44
44
|
account: z.optional(z.string()),
|
|
45
|
-
channel: z.optional(z.coerce.string()),
|
|
46
45
|
confirm: z.optional(z.boolean()),
|
|
47
46
|
data: z.optional(z.string()),
|
|
48
|
-
deposit: z.optional(z.union([z.string(), z.number()])),
|
|
49
47
|
fail: z.optional(z.boolean()),
|
|
50
48
|
header: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
51
49
|
include: z.optional(z.boolean()),
|
|
@@ -53,11 +51,13 @@ cli
|
|
|
53
51
|
json: z.optional(z.string()),
|
|
54
52
|
location: z.optional(z.boolean()),
|
|
55
53
|
method: z.optional(z.string()),
|
|
54
|
+
methodOpt: z.optional(z.union([z.string(), z.array(z.string())])),
|
|
56
55
|
rpcUrl: z.optional(z.string()),
|
|
57
56
|
silent: z.optional(z.boolean()),
|
|
58
57
|
userAgent: z.optional(z.string()),
|
|
59
58
|
verbose: z.optional(z.boolean()),
|
|
60
59
|
}), rawOptions);
|
|
60
|
+
const methodOpts = parseMethodOpts(options.methodOpt);
|
|
61
61
|
if (!rawUrl) {
|
|
62
62
|
cli.outputHelp();
|
|
63
63
|
return;
|
|
@@ -67,14 +67,6 @@ cli
|
|
|
67
67
|
if (silent)
|
|
68
68
|
options.confirm = false;
|
|
69
69
|
const accountName = resolveAccountName(options.account);
|
|
70
|
-
const privateKey = process.env.MPPX_PRIVATE_KEY ?? (await createKeychain(accountName).get());
|
|
71
|
-
if (!privateKey) {
|
|
72
|
-
if (options.account)
|
|
73
|
-
console.log(`Account "${accountName}" not found.`);
|
|
74
|
-
else
|
|
75
|
-
console.log(`No account found.`);
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
70
|
const headers = {};
|
|
79
71
|
if (options.header) {
|
|
80
72
|
const headerList = Array.isArray(options.header) ? options.header : [options.header];
|
|
@@ -148,22 +140,39 @@ cli
|
|
|
148
140
|
console.log((await challengeResponse.text()).replace(/\n+$/, ''));
|
|
149
141
|
return;
|
|
150
142
|
}
|
|
151
|
-
const account = privateKeyToAccount(privateKey);
|
|
152
|
-
const rpcUrl = options.rpcUrl ?? process.env.MPPX_RPC_URL;
|
|
153
|
-
const client = createClient({
|
|
154
|
-
chain: await resolveChain({ ...options, rpcUrl }),
|
|
155
|
-
transport: http(rpcUrl),
|
|
156
|
-
});
|
|
157
143
|
const challenge = Challenge.fromResponse(challengeResponse);
|
|
158
|
-
const explorerUrl = client.chain?.blockExplorers?.default?.url;
|
|
159
|
-
const shownKeys = new Set();
|
|
160
144
|
const challengeRequest = challenge.request;
|
|
161
145
|
const currency = challengeRequest.currency;
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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 ?? (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
|
+
}
|
|
162
|
+
account = privateKeyToAccount(privateKey);
|
|
163
|
+
const rpcUrl = options.rpcUrl ?? process.env.RPC_URL;
|
|
164
|
+
client = createClient({
|
|
165
|
+
chain: await resolveChain({ ...options, rpcUrl }),
|
|
166
|
+
transport: http(rpcUrl),
|
|
167
|
+
});
|
|
168
|
+
explorerUrl = client.chain?.blockExplorers?.default?.url;
|
|
169
|
+
const tokenInfo = currency
|
|
170
|
+
? await fetchTokenInfo(client, currency, account.address).catch(() => undefined)
|
|
171
|
+
: undefined;
|
|
172
|
+
tokenSymbol = tokenInfo?.symbol ?? currency ?? '';
|
|
173
|
+
tokenDecimals =
|
|
174
|
+
tokenInfo?.decimals ?? challengeRequest.decimals ?? 6;
|
|
175
|
+
}
|
|
167
176
|
{
|
|
168
177
|
printResponseHeaders(challengeResponse);
|
|
169
178
|
const request = challengeRequest;
|
|
@@ -266,55 +275,149 @@ cli
|
|
|
266
275
|
}
|
|
267
276
|
}
|
|
268
277
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
278
|
+
let credential;
|
|
279
|
+
if (challenge.method === 'tempo') {
|
|
280
|
+
if (!account || !client) {
|
|
281
|
+
console.error('Tempo requires a configured account.');
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
const tempoOpts = parseOptions(z.object({
|
|
285
|
+
channel: z.optional(z.coerce.string()),
|
|
286
|
+
deposit: z.optional(z.union([z.string(), z.number()])),
|
|
287
|
+
}), methodOpts);
|
|
288
|
+
const mppx = Mppx.create({
|
|
289
|
+
methods: tempo({
|
|
290
|
+
account,
|
|
291
|
+
getClient: () => client,
|
|
292
|
+
deposit: (() => {
|
|
293
|
+
if (challenge.intent !== 'session')
|
|
294
|
+
return undefined;
|
|
295
|
+
const suggestedDeposit = challenge.request
|
|
296
|
+
.suggestedDeposit;
|
|
297
|
+
const cliDeposit = tempoOpts.deposit !== undefined ? String(tempoOpts.deposit) : undefined;
|
|
298
|
+
const resolved = suggestedDeposit ?? cliDeposit ?? (isTestnet(client.chain) ? '10' : undefined);
|
|
299
|
+
if (!resolved) {
|
|
300
|
+
console.error('Session payment requires a deposit. Use -M deposit=<amount> or connect to testnet.');
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
return resolved;
|
|
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
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const sptUrl = process.env.MPPX_STRIPE_SPT_URL ??
|
|
351
|
+
'https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens';
|
|
352
|
+
const sptHeaders = {
|
|
353
|
+
Authorization: `Basic ${btoa(`${stripeSecretKey}:`)}`,
|
|
354
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
355
|
+
};
|
|
356
|
+
let response = await globalThis.fetch(sptUrl, {
|
|
357
|
+
method: 'POST',
|
|
358
|
+
headers: sptHeaders,
|
|
359
|
+
body,
|
|
360
|
+
});
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
const errorBody = (await response.json());
|
|
363
|
+
if ((metadata || networkId) &&
|
|
364
|
+
errorBody.error.message.includes('Received unknown parameter')) {
|
|
365
|
+
const fallbackBody = new URLSearchParams({
|
|
366
|
+
payment_method: paymentMethod,
|
|
367
|
+
'usage_limits[currency]': currency,
|
|
368
|
+
'usage_limits[max_amount]': amount,
|
|
369
|
+
'usage_limits[expires_at]': expiresAt.toString(),
|
|
370
|
+
});
|
|
371
|
+
response = await globalThis.fetch(sptUrl, {
|
|
372
|
+
method: 'POST',
|
|
373
|
+
headers: sptHeaders,
|
|
374
|
+
body: fallbackBody,
|
|
375
|
+
});
|
|
376
|
+
if (!response.ok) {
|
|
377
|
+
const fallbackError = (await response.json());
|
|
378
|
+
throw new Error(`Failed to create SPT: ${fallbackError.error.message}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
throw new Error(`Failed to create SPT: ${errorBody.error.message}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const { id } = (await response.json());
|
|
386
|
+
return id;
|
|
387
|
+
},
|
|
388
|
+
}),
|
|
389
|
+
],
|
|
390
|
+
polyfill: false,
|
|
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;
|
|
305
403
|
if (challenge.intent === 'session') {
|
|
306
404
|
const parsed = Credential.deserialize(credential);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
405
|
+
sessionChannelId = parsed.payload.channelId;
|
|
406
|
+
sessionChainId = sessionMd?.chainId ?? client?.chain?.id ?? 0;
|
|
407
|
+
sessionEscrowContract = sessionMd?.escrowContract;
|
|
310
408
|
if ('cumulativeAmount' in parsed.payload && parsed.payload.cumulativeAmount)
|
|
311
|
-
|
|
409
|
+
sessionCumulativeAmount = BigInt(parsed.payload.cumulativeAmount);
|
|
312
410
|
if (parsed.payload.action === 'open') {
|
|
313
|
-
const depositRaw = challengeRequest.suggestedDeposit
|
|
411
|
+
const depositRaw = challengeRequest.suggestedDeposit;
|
|
314
412
|
const depositDisplay = depositRaw
|
|
315
413
|
? ` ${pc.dim(`(deposit ${depositRaw} ${tokenSymbol})`)}`
|
|
316
414
|
: '';
|
|
317
|
-
|
|
415
|
+
const prefix = options.confirm ? '' : '\n';
|
|
416
|
+
info(`${prefix}${pc.dim(`Channel opened ${parsed.payload.channelId}`)}${depositDisplay}\n`);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
const prefix = options.confirm ? '' : '\n';
|
|
420
|
+
info(`${prefix}${pc.dim(`Channel reused ${parsed.payload.channelId}`)}\n`);
|
|
318
421
|
}
|
|
319
422
|
}
|
|
320
423
|
const credentialFetchInit = {
|
|
@@ -355,9 +458,9 @@ cli
|
|
|
355
458
|
const receiptJson = JSON.parse(Base64.toString(receiptHeader));
|
|
356
459
|
if (typeof receiptJson.acceptedCumulative === 'string' &&
|
|
357
460
|
receiptJson.acceptedCumulative) {
|
|
358
|
-
|
|
359
|
-
if (
|
|
360
|
-
writeChannelCumulative(
|
|
461
|
+
sessionCumulativeAmount = BigInt(receiptJson.acceptedCumulative);
|
|
462
|
+
if (sessionChannelId)
|
|
463
|
+
writeChannelCumulative(sessionChannelId, sessionCumulativeAmount);
|
|
361
464
|
}
|
|
362
465
|
info(`\n${pc.bold(pc.green('Payment Receipt'))}\n`);
|
|
363
466
|
const rows = [];
|
|
@@ -381,6 +484,14 @@ cli
|
|
|
381
484
|
explorerUrl) {
|
|
382
485
|
rows.push([key, pc.link(`${explorerUrl}/tx/${value}`, value)]);
|
|
383
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
|
+
}
|
|
384
495
|
else
|
|
385
496
|
rows.push([key, String(value)]);
|
|
386
497
|
}
|
|
@@ -402,17 +513,17 @@ cli
|
|
|
402
513
|
const decoder = new TextDecoder();
|
|
403
514
|
let buffer = '';
|
|
404
515
|
let currentEvent = '';
|
|
405
|
-
const
|
|
516
|
+
const sessionCred = challenge.intent === 'session'
|
|
406
517
|
? Credential.deserialize(credential)
|
|
407
518
|
: undefined;
|
|
408
|
-
const channelId =
|
|
519
|
+
const channelId = sessionCred?.payload.channelId;
|
|
409
520
|
const md = challenge.request.methodDetails;
|
|
410
|
-
const
|
|
521
|
+
const sessionChainId = md?.chainId ?? client?.chain?.id ?? 0;
|
|
411
522
|
const escrowContract = md?.escrowContract;
|
|
412
|
-
let cumulativeAmount =
|
|
413
|
-
'cumulativeAmount' in
|
|
414
|
-
|
|
415
|
-
? BigInt(
|
|
523
|
+
let cumulativeAmount = sessionCred?.payload &&
|
|
524
|
+
'cumulativeAmount' in sessionCred.payload &&
|
|
525
|
+
sessionCred.payload.cumulativeAmount
|
|
526
|
+
? BigInt(sessionCred.payload.cumulativeAmount)
|
|
416
527
|
: 0n;
|
|
417
528
|
let _voucherSeq = 0;
|
|
418
529
|
const termBg = verbose ? await detectTerminalBg() : undefined;
|
|
@@ -458,12 +569,12 @@ cli
|
|
|
458
569
|
if (currentEvent === 'payment-need-voucher' &&
|
|
459
570
|
channelId &&
|
|
460
571
|
escrowContract &&
|
|
461
|
-
|
|
572
|
+
sessionChainId) {
|
|
462
573
|
try {
|
|
463
574
|
const event = JSON.parse(data);
|
|
464
575
|
const required = BigInt(event.requiredCumulative);
|
|
465
576
|
cumulativeAmount = cumulativeAmount > required ? cumulativeAmount : required;
|
|
466
|
-
const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract,
|
|
577
|
+
const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract, sessionChainId);
|
|
467
578
|
const voucherCred = Credential.serialize({
|
|
468
579
|
challenge,
|
|
469
580
|
payload: {
|
|
@@ -472,7 +583,7 @@ cli
|
|
|
472
583
|
cumulativeAmount: cumulativeAmount.toString(),
|
|
473
584
|
signature,
|
|
474
585
|
},
|
|
475
|
-
source: `did:pkh:eip155:${
|
|
586
|
+
source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
|
|
476
587
|
});
|
|
477
588
|
await globalThis.fetch(url, {
|
|
478
589
|
method: 'POST',
|
|
@@ -549,8 +660,8 @@ cli
|
|
|
549
660
|
}
|
|
550
661
|
if (buffer.trim())
|
|
551
662
|
await processLines([buffer]);
|
|
552
|
-
if (channelId && escrowContract &&
|
|
553
|
-
const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract,
|
|
663
|
+
if (channelId && escrowContract && sessionChainId) {
|
|
664
|
+
const signature = await signVoucher(client, account, { channelId, cumulativeAmount }, escrowContract, sessionChainId);
|
|
554
665
|
const closePayload = {
|
|
555
666
|
action: 'close',
|
|
556
667
|
channelId,
|
|
@@ -560,7 +671,7 @@ cli
|
|
|
560
671
|
const closeCred = Credential.serialize({
|
|
561
672
|
challenge,
|
|
562
673
|
payload: closePayload,
|
|
563
|
-
source: `did:pkh:eip155:${
|
|
674
|
+
source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
|
|
564
675
|
});
|
|
565
676
|
const closeRes = await globalThis.fetch(url, {
|
|
566
677
|
method: 'POST',
|
|
@@ -592,9 +703,9 @@ cli
|
|
|
592
703
|
console.log(body);
|
|
593
704
|
const shouldClose = challenge.intent === 'session' &&
|
|
594
705
|
credentialResponse.ok &&
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
706
|
+
sessionChannelId &&
|
|
707
|
+
sessionEscrowContract &&
|
|
708
|
+
sessionChainId;
|
|
598
709
|
if (shouldClose && options.confirm) {
|
|
599
710
|
info('\n');
|
|
600
711
|
}
|
|
@@ -602,17 +713,17 @@ cli
|
|
|
602
713
|
info(`${pc.dim('Kept channel open.')}\n`);
|
|
603
714
|
}
|
|
604
715
|
else if (shouldClose) {
|
|
605
|
-
const signature = await signVoucher(client, account, { channelId:
|
|
716
|
+
const signature = await signVoucher(client, account, { channelId: sessionChannelId, cumulativeAmount: sessionCumulativeAmount }, sessionEscrowContract, sessionChainId);
|
|
606
717
|
const closePayload = {
|
|
607
718
|
action: 'close',
|
|
608
|
-
channelId:
|
|
609
|
-
cumulativeAmount:
|
|
719
|
+
channelId: sessionChannelId,
|
|
720
|
+
cumulativeAmount: sessionCumulativeAmount.toString(),
|
|
610
721
|
signature,
|
|
611
722
|
};
|
|
612
723
|
const closeCred = Credential.serialize({
|
|
613
724
|
challenge,
|
|
614
725
|
payload: closePayload,
|
|
615
|
-
source: `did:pkh:eip155:${
|
|
726
|
+
source: `did:pkh:eip155:${sessionChainId}:${account.address}`,
|
|
616
727
|
});
|
|
617
728
|
const closeRes = await globalThis.fetch(url, {
|
|
618
729
|
...fetchInit,
|
|
@@ -622,7 +733,7 @@ cli
|
|
|
622
733
|
},
|
|
623
734
|
});
|
|
624
735
|
if (closeRes.ok) {
|
|
625
|
-
deleteChannelState(
|
|
736
|
+
deleteChannelState(sessionChannelId);
|
|
626
737
|
const closeReceiptHeader = closeRes.headers.get('Payment-Receipt');
|
|
627
738
|
let closeTxHash;
|
|
628
739
|
if (closeReceiptHeader) {
|
|
@@ -636,16 +747,17 @@ cli
|
|
|
636
747
|
const txInfo = closeTxHash && explorerUrl
|
|
637
748
|
? ` ${pc.dim(pc.link(`${explorerUrl}/tx/${closeTxHash}`, closeTxHash))}`
|
|
638
749
|
: '';
|
|
639
|
-
|
|
750
|
+
const closePrefix = options.confirm ? '' : '\n';
|
|
751
|
+
info(`${closePrefix}${pc.dim('Channel closed.')} ${pc.dim(`Spent ${fmtBalance(sessionCumulativeAmount, tokenSymbol, tokenDecimals)}.`)}${txInfo}\n`);
|
|
640
752
|
}
|
|
641
753
|
else {
|
|
642
754
|
const closeBody = await closeRes.text().catch(() => '');
|
|
643
|
-
info(
|
|
644
|
-
info(`${pc.dim(` channelId: ${
|
|
645
|
-
`${pc.dim(` cumulativeAmount: ${
|
|
646
|
-
`${pc.dim(` escrowContract: ${
|
|
647
|
-
`${pc.dim(` chainId: ${
|
|
648
|
-
`${pc.dim(` account: ${account
|
|
755
|
+
info(`\n${pc.dim(pc.yellow('Channel close failed'))} ${pc.dim(`(${closeRes.status})`)}\n`);
|
|
756
|
+
info(`${pc.dim(` channelId: ${sessionChannelId}`)}\n` +
|
|
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` +
|
|
649
761
|
`${pc.dim(` response: ${closeBody || '(empty)'}`)}\n`);
|
|
650
762
|
}
|
|
651
763
|
}
|
|
@@ -726,7 +838,11 @@ cli
|
|
|
726
838
|
if (accounts.length === 1)
|
|
727
839
|
createDefaultStore().set(resolvedName);
|
|
728
840
|
console.log(`Account "${resolvedName}" saved to keychain.`);
|
|
729
|
-
|
|
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}`));
|
|
730
846
|
resolveChain(options)
|
|
731
847
|
.then((chain) => createClient({ chain, transport: http(options.rpcUrl) }))
|
|
732
848
|
.then((client) => import('viem/tempo').then(({ Actions }) => Actions.faucet.fund(client, { account }).catch(() => { })));
|
|
@@ -761,8 +877,12 @@ cli
|
|
|
761
877
|
const account = privateKeyToAccount(key);
|
|
762
878
|
const balanceLines = await fetchBalanceLines(account.address, { includeTestnet: false });
|
|
763
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;
|
|
764
884
|
process.stderr.write(pc.dim(`Delete account "${options.account}"\n`));
|
|
765
|
-
process.stderr.write(pc.dim(` Address ${
|
|
885
|
+
process.stderr.write(pc.dim(` Address ${addrDisplay}\n`));
|
|
766
886
|
for (let i = 0; i < balanceLines.length; i++)
|
|
767
887
|
process.stderr.write(pc.dim(` ${i === 0 ? 'Balance' : ' '} ${balanceLines[i]}\n`));
|
|
768
888
|
process.stderr.write(pc.dim('This action cannot be undone\n\n'));
|
|
@@ -836,12 +956,16 @@ cli
|
|
|
836
956
|
};
|
|
837
957
|
}));
|
|
838
958
|
const resolved = entries.filter((e) => e !== undefined);
|
|
959
|
+
const explorerUrl = tempoMainnet.blockExplorers?.default?.url;
|
|
839
960
|
const maxWidth = Math.max(...resolved.map((e) => e.name.length + (e.name === currentDefault ? 1 : 0)));
|
|
840
961
|
for (const entry of resolved) {
|
|
841
962
|
const isDefault = entry.name === currentDefault;
|
|
842
963
|
const label = isDefault ? `${entry.name}${pc.dim('*')}` : entry.name;
|
|
843
964
|
const width = entry.name.length + (isDefault ? 1 : 0);
|
|
844
|
-
|
|
965
|
+
const addrDisplay = explorerUrl
|
|
966
|
+
? pc.link(`${explorerUrl}/address/${entry.address}`, entry.address)
|
|
967
|
+
: entry.address;
|
|
968
|
+
console.log(`${label}${' '.repeat(maxWidth - width + 2)}${pc.dim(addrDisplay)}`);
|
|
845
969
|
}
|
|
846
970
|
return;
|
|
847
971
|
}
|
|
@@ -857,9 +981,13 @@ cli
|
|
|
857
981
|
process.exit(1);
|
|
858
982
|
}
|
|
859
983
|
const account = privateKeyToAccount(key);
|
|
860
|
-
|
|
861
|
-
const
|
|
862
|
-
const
|
|
984
|
+
const rpcUrl = options.rpcUrl ?? (process.env.MPPX_RPC_URL || undefined);
|
|
985
|
+
const chain = rpcUrl ? await resolveChain({ rpcUrl }) : tempoMainnet;
|
|
986
|
+
const explorerUrl = chain.blockExplorers?.default?.url;
|
|
987
|
+
const addrDisplay = explorerUrl
|
|
988
|
+
? pc.link(`${explorerUrl}/address/${account.address}`, account.address)
|
|
989
|
+
: account.address;
|
|
990
|
+
console.log(`${pc.dim('Address')} ${addrDisplay}`);
|
|
863
991
|
const balanceLines = await fetchBalanceLines(account.address, chain && rpcUrl ? { chain, rpcUrl } : undefined);
|
|
864
992
|
for (let i = 0; i < balanceLines.length; i++)
|
|
865
993
|
console.log(`${pc.dim(i === 0 ? 'Balance' : ' ')} ${balanceLines[i]}`);
|
|
@@ -903,6 +1031,21 @@ catch (err) {
|
|
|
903
1031
|
process.exit(1);
|
|
904
1032
|
}
|
|
905
1033
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
|
1034
|
+
function parseMethodOpts(raw) {
|
|
1035
|
+
if (!raw)
|
|
1036
|
+
return {};
|
|
1037
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
1038
|
+
const result = {};
|
|
1039
|
+
for (const item of list) {
|
|
1040
|
+
const idx = item.indexOf('=');
|
|
1041
|
+
if (idx === -1) {
|
|
1042
|
+
console.error(`Invalid method option format: ${item} (expected key=value)`);
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
}
|
|
1045
|
+
result[item.slice(0, idx)] = item.slice(idx + 1);
|
|
1046
|
+
}
|
|
1047
|
+
return result;
|
|
1048
|
+
}
|
|
906
1049
|
function parseOptions(schema, rawOptions) {
|
|
907
1050
|
const result = schema.safeParse(rawOptions ?? {});
|
|
908
1051
|
if (result.success)
|
|
@@ -971,7 +1114,7 @@ function createDefaultStore() {
|
|
|
971
1114
|
function resolveAccountName(explicit) {
|
|
972
1115
|
if (explicit)
|
|
973
1116
|
return explicit;
|
|
974
|
-
if (process.env.MPPX_ACCOUNT)
|
|
1117
|
+
if (process.env.MPPX_ACCOUNT?.trim())
|
|
975
1118
|
return process.env.MPPX_ACCOUNT;
|
|
976
1119
|
return createDefaultStore().get();
|
|
977
1120
|
}
|
|
@@ -1154,10 +1297,10 @@ const pc = (() => {
|
|
|
1154
1297
|
bgMagentaBright: f('\x1b[105m', '\x1b[49m'),
|
|
1155
1298
|
bgCyanBright: f('\x1b[106m', '\x1b[49m'),
|
|
1156
1299
|
bgWhiteBright: f('\x1b[107m', '\x1b[49m'),
|
|
1157
|
-
link(url, text) {
|
|
1300
|
+
link(url, text, noUnderline) {
|
|
1158
1301
|
if (!isColorSupported)
|
|
1159
1302
|
return text;
|
|
1160
|
-
return `\x1b]8;;${url}\x07${pc.underline(text)}\x1b]8;;\x07`;
|
|
1303
|
+
return `\x1b]8;;${url}\x07${noUnderline ? text : pc.underline(text)}\x1b]8;;\x07`;
|
|
1161
1304
|
},
|
|
1162
1305
|
};
|
|
1163
1306
|
})();
|
|
@@ -1181,17 +1324,22 @@ function chainName(chain) {
|
|
|
1181
1324
|
return chainNames[chain.id] ?? chain.name;
|
|
1182
1325
|
}
|
|
1183
1326
|
const pathUsd = '0x20c0000000000000000000000000000000000000';
|
|
1327
|
+
const usdc = '0x20C000000000000000000000b9537d11c60E8b50';
|
|
1328
|
+
const mainnetTokens = [pathUsd, usdc];
|
|
1184
1329
|
const testnetTokens = [
|
|
1185
1330
|
'0x20c0000000000000000000000000000000000000',
|
|
1186
1331
|
'0x20c0000000000000000000000000000000000001',
|
|
1187
1332
|
'0x20c0000000000000000000000000000000000002',
|
|
1188
1333
|
'0x20c0000000000000000000000000000000000003',
|
|
1189
1334
|
];
|
|
1190
|
-
function fmtBalance(b, symbol, decimals = 6) {
|
|
1335
|
+
function fmtBalance(b, symbol, decimals = 6, opts) {
|
|
1191
1336
|
const value = Number(b) / 10 ** decimals;
|
|
1192
1337
|
const [int, dec] = value.toString().split('.');
|
|
1193
1338
|
const formatted = int.replace(/\B(?=(\d{3})+(?!\d))/g, '_');
|
|
1194
|
-
|
|
1339
|
+
const sym = opts?.explorerUrl && opts.token
|
|
1340
|
+
? pc.dim(pc.link(`${opts.explorerUrl}/token/${opts.token}`, symbol, true))
|
|
1341
|
+
: pc.dim(symbol);
|
|
1342
|
+
return `${dec ? `${formatted}.${dec}` : formatted} ${sym}`;
|
|
1195
1343
|
}
|
|
1196
1344
|
function isTestnet(chain) {
|
|
1197
1345
|
return chain.id !== tempoMainnet.id;
|
|
@@ -1202,9 +1350,13 @@ async function fetchTokenInfo(client, token, account) {
|
|
|
1202
1350
|
Actions.token.getBalance(client, { account, token }).catch(() => 0n),
|
|
1203
1351
|
Actions.token.getMetadata(client, { token }).catch(() => ({ symbol: token })),
|
|
1204
1352
|
]);
|
|
1205
|
-
const
|
|
1353
|
+
const knownSymbols = {
|
|
1354
|
+
[pathUsd]: 'PathUSD',
|
|
1355
|
+
[usdc]: 'USDC',
|
|
1356
|
+
};
|
|
1357
|
+
const symbol = knownSymbols[token] ?? metadata.symbol;
|
|
1206
1358
|
const decimals = 'decimals' in metadata ? metadata.decimals : 6;
|
|
1207
|
-
return { balance, symbol, decimals };
|
|
1359
|
+
return { balance, symbol, decimals, token };
|
|
1208
1360
|
}
|
|
1209
1361
|
function detectTerminalBg(timeoutMs = 100) {
|
|
1210
1362
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
@@ -1242,25 +1394,34 @@ function detectTerminalBg(timeoutMs = 100) {
|
|
|
1242
1394
|
async function fetchBalanceLines(address, opts) {
|
|
1243
1395
|
if (opts?.chain) {
|
|
1244
1396
|
const client = createClient({ chain: opts.chain, transport: http(opts.rpcUrl) });
|
|
1397
|
+
const explorerUrl = opts.chain.blockExplorers?.default?.url;
|
|
1245
1398
|
const label = pc.dim(`(${chainName(opts.chain)})`);
|
|
1246
1399
|
if (isTestnet(opts.chain)) {
|
|
1247
1400
|
const results = await Promise.all(testnetTokens.map((token) => fetchTokenInfo(client, token, address)));
|
|
1248
1401
|
return results
|
|
1249
1402
|
.filter((t) => t.balance > 0n)
|
|
1250
|
-
.map((t) => `${fmtBalance(t.balance, t.symbol, t.decimals)} ${label}`);
|
|
1403
|
+
.map((t) => `${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl, token: t.token })} ${label}`);
|
|
1251
1404
|
}
|
|
1252
|
-
const
|
|
1253
|
-
return
|
|
1405
|
+
const results = await Promise.all(mainnetTokens.map((token) => fetchTokenInfo(client, token, address)));
|
|
1406
|
+
return results.map((t) => `${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl, token: t.token })} ${label}`);
|
|
1254
1407
|
}
|
|
1255
|
-
const mainnetClient = createClient({
|
|
1256
|
-
|
|
1257
|
-
|
|
1408
|
+
const mainnetClient = createClient({
|
|
1409
|
+
chain: tempoMainnet,
|
|
1410
|
+
transport: http(process.env.MPPX_RPC_URL || undefined),
|
|
1411
|
+
});
|
|
1412
|
+
const mainnetExplorerUrl = tempoMainnet.blockExplorers?.default?.url;
|
|
1413
|
+
const mainnetResults = await Promise.all(mainnetTokens.map((token) => fetchTokenInfo(mainnetClient, token, address)));
|
|
1414
|
+
const lines = mainnetResults.map((t) => fmtBalance(t.balance, t.symbol, t.decimals, {
|
|
1415
|
+
explorerUrl: mainnetExplorerUrl,
|
|
1416
|
+
token: t.token,
|
|
1417
|
+
}));
|
|
1258
1418
|
if (opts?.includeTestnet !== false) {
|
|
1259
1419
|
const testnetClient = createClient({ chain: tempoModerato, transport: http() });
|
|
1420
|
+
const testnetExplorerUrl = tempoModerato.blockExplorers?.default?.url;
|
|
1260
1421
|
const testnetResults = await Promise.all(testnetTokens.map((token) => fetchTokenInfo(testnetClient, token, address)));
|
|
1261
1422
|
for (const t of testnetResults) {
|
|
1262
1423
|
if (t.balance > 0n)
|
|
1263
|
-
lines.push(`${fmtBalance(t.balance, t.symbol, t.decimals)} ${pc.dim('(testnet)')}`);
|
|
1424
|
+
lines.push(`${fmtBalance(t.balance, t.symbol, t.decimals, { explorerUrl: testnetExplorerUrl, token: t.token })} ${pc.dim('(testnet)')}`);
|
|
1264
1425
|
}
|
|
1265
1426
|
}
|
|
1266
1427
|
return lines;
|