mppx 0.3.4 → 0.3.6
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/cli.js +193 -66
- package/dist/cli.js.map +1 -1
- package/dist/internal/types.d.ts +10 -0
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/proxy/internal/Headers.d.ts +2 -0
- package/dist/proxy/internal/Headers.d.ts.map +1 -1
- package/dist/proxy/internal/Headers.js +2 -0
- package/dist/proxy/internal/Headers.js.map +1 -1
- package/dist/proxy/internal/Route.d.ts +4 -0
- package/dist/proxy/internal/Route.d.ts.map +1 -1
- package/dist/proxy/internal/Route.js +4 -0
- package/dist/proxy/internal/Route.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/server/NodeListener.d.ts +6 -0
- package/dist/server/NodeListener.d.ts.map +1 -1
- package/dist/server/NodeListener.js +6 -0
- package/dist/server/NodeListener.js.map +1 -1
- package/dist/server/Response.d.ts +17 -0
- package/dist/server/Response.d.ts.map +1 -1
- package/dist/server/Response.js +17 -0
- package/dist/server/Response.js.map +1 -1
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/internal/defaults.d.ts +34 -8
- package/dist/tempo/internal/defaults.d.ts.map +1 -1
- package/dist/tempo/internal/defaults.js +30 -8
- package/dist/tempo/internal/defaults.js.map +1 -1
- package/dist/tempo/server/Charge.js +2 -2
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +8 -3
- package/dist/tempo/server/Session.js.map +1 -1
- package/package.json +1 -1
- package/src/Challenge.test.ts +201 -11
- package/src/Challenge.ts +34 -4
- package/src/Store.test.ts +93 -0
- package/src/cli.test.ts +233 -37
- package/src/cli.ts +229 -79
- package/src/client/Transport.test.ts +4 -4
- package/src/internal/env.test.ts +42 -0
- package/src/internal/types.ts +11 -0
- package/src/proxy/internal/Headers.ts +2 -0
- package/src/proxy/internal/Route.ts +4 -0
- package/src/server/Mppx.test.ts +173 -0
- package/src/server/Mppx.ts +6 -3
- package/src/server/NodeListener.ts +6 -0
- package/src/server/Response.ts +17 -0
- package/src/server/Transport.test.ts +5 -5
- package/src/tempo/client/ChannelOps.ts +1 -1
- package/src/tempo/internal/defaults.test.ts +94 -0
- package/src/tempo/internal/defaults.ts +41 -8
- package/src/tempo/server/Charge.test.ts +150 -0
- package/src/tempo/server/Charge.ts +2 -2
- package/src/tempo/server/Session.test.ts +241 -1
- package/src/tempo/server/Session.ts +8 -3
- package/src/tempo/server/internal/transport.test.ts +285 -0
- package/src/tempo/session/Voucher.test.ts +46 -0
package/src/server/Mppx.test.ts
CHANGED
|
@@ -344,3 +344,176 @@ describe('receipt handling', () => {
|
|
|
344
344
|
expect(result.status).toBe(200)
|
|
345
345
|
})
|
|
346
346
|
})
|
|
347
|
+
|
|
348
|
+
describe('withReceipt', () => {
|
|
349
|
+
const mockCharge = Method.from({
|
|
350
|
+
name: 'mock',
|
|
351
|
+
intent: 'charge',
|
|
352
|
+
schema: {
|
|
353
|
+
credential: {
|
|
354
|
+
payload: z.object({ token: z.string() }),
|
|
355
|
+
},
|
|
356
|
+
request: z.object({
|
|
357
|
+
amount: z.string(),
|
|
358
|
+
currency: z.string(),
|
|
359
|
+
decimals: z.number(),
|
|
360
|
+
recipient: z.string(),
|
|
361
|
+
}),
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
function mockReceipt() {
|
|
366
|
+
return {
|
|
367
|
+
method: 'mock',
|
|
368
|
+
reference: 'tx-ref',
|
|
369
|
+
status: 'success' as const,
|
|
370
|
+
timestamp: new Date().toISOString(),
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
test('attaches Payment-Receipt header to response', async () => {
|
|
375
|
+
const mockMethod = Method.toServer(mockCharge, {
|
|
376
|
+
async verify() {
|
|
377
|
+
return mockReceipt()
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const handler = Mppx.create({ methods: [mockMethod], realm, secretKey })
|
|
382
|
+
const handle = handler.charge({
|
|
383
|
+
amount: '1000',
|
|
384
|
+
currency: '0x0000000000000000000000000000000000000001',
|
|
385
|
+
decimals: 6,
|
|
386
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
387
|
+
recipient: '0x0000000000000000000000000000000000000002',
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
const firstResult = await handle(new Request('https://example.com/resource'))
|
|
391
|
+
expect(firstResult.status).toBe(402)
|
|
392
|
+
if (firstResult.status !== 402) throw new Error()
|
|
393
|
+
|
|
394
|
+
const challenge = Challenge.fromResponse(firstResult.challenge)
|
|
395
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
396
|
+
|
|
397
|
+
const result = await handle(
|
|
398
|
+
new Request('https://example.com/resource', {
|
|
399
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
400
|
+
}),
|
|
401
|
+
)
|
|
402
|
+
expect(result.status).toBe(200)
|
|
403
|
+
if (result.status !== 200) throw new Error()
|
|
404
|
+
|
|
405
|
+
const response = result.withReceipt(Response.json({ data: 'ok' }))
|
|
406
|
+
expect(response.headers.get('Payment-Receipt')).toBeTruthy()
|
|
407
|
+
const body = await response.json()
|
|
408
|
+
expect(body).toEqual({ data: 'ok' })
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
test('throws when called without response arg and no management response', async () => {
|
|
412
|
+
const mockMethod = Method.toServer(mockCharge, {
|
|
413
|
+
async verify() {
|
|
414
|
+
return mockReceipt()
|
|
415
|
+
},
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
const handler = Mppx.create({ methods: [mockMethod], realm, secretKey })
|
|
419
|
+
const handle = handler.charge({
|
|
420
|
+
amount: '1000',
|
|
421
|
+
currency: '0x0000000000000000000000000000000000000001',
|
|
422
|
+
decimals: 6,
|
|
423
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
424
|
+
recipient: '0x0000000000000000000000000000000000000002',
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
const firstResult = await handle(new Request('https://example.com/resource'))
|
|
428
|
+
if (firstResult.status !== 402) throw new Error()
|
|
429
|
+
|
|
430
|
+
const challenge = Challenge.fromResponse(firstResult.challenge)
|
|
431
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
432
|
+
|
|
433
|
+
const result = await handle(
|
|
434
|
+
new Request('https://example.com/resource', {
|
|
435
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
436
|
+
}),
|
|
437
|
+
)
|
|
438
|
+
expect(result.status).toBe(200)
|
|
439
|
+
if (result.status !== 200) throw new Error()
|
|
440
|
+
|
|
441
|
+
expect(() => result.withReceipt()).toThrow('withReceipt() requires a response argument')
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
test('returns management response when respond hook returns Response', async () => {
|
|
445
|
+
const mockMethodWithRespond = Method.toServer(mockCharge, {
|
|
446
|
+
async verify() {
|
|
447
|
+
return mockReceipt()
|
|
448
|
+
},
|
|
449
|
+
respond() {
|
|
450
|
+
return new Response(null, { status: 204 })
|
|
451
|
+
},
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
const handler = Mppx.create({ methods: [mockMethodWithRespond], realm, secretKey })
|
|
455
|
+
const handle = handler.charge({
|
|
456
|
+
amount: '1000',
|
|
457
|
+
currency: '0x0000000000000000000000000000000000000001',
|
|
458
|
+
decimals: 6,
|
|
459
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
460
|
+
recipient: '0x0000000000000000000000000000000000000002',
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
const firstResult = await handle(new Request('https://example.com/resource'))
|
|
464
|
+
if (firstResult.status !== 402) throw new Error()
|
|
465
|
+
|
|
466
|
+
const challenge = Challenge.fromResponse(firstResult.challenge)
|
|
467
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
468
|
+
|
|
469
|
+
const result = await handle(
|
|
470
|
+
new Request('https://example.com/resource', {
|
|
471
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
472
|
+
}),
|
|
473
|
+
)
|
|
474
|
+
expect(result.status).toBe(200)
|
|
475
|
+
if (result.status !== 200) throw new Error()
|
|
476
|
+
|
|
477
|
+
const response = result.withReceipt()
|
|
478
|
+
expect(response.status).toBe(204)
|
|
479
|
+
expect(response.headers.get('Payment-Receipt')).toBeTruthy()
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
test('toNodeListener sets Payment-Receipt header on 200', async () => {
|
|
483
|
+
const mockMethod = Method.toServer(mockCharge, {
|
|
484
|
+
async verify() {
|
|
485
|
+
return mockReceipt()
|
|
486
|
+
},
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
const handler = Mppx.create({ methods: [mockMethod], realm, secretKey })
|
|
490
|
+
|
|
491
|
+
const server = await Http.createServer(async (req, res) => {
|
|
492
|
+
const result = await Mppx.toNodeListener(
|
|
493
|
+
handler.charge({
|
|
494
|
+
amount: '1000',
|
|
495
|
+
currency: '0x0000000000000000000000000000000000000001',
|
|
496
|
+
decimals: 6,
|
|
497
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
498
|
+
recipient: '0x0000000000000000000000000000000000000002',
|
|
499
|
+
}),
|
|
500
|
+
)(req, res)
|
|
501
|
+
if (result.status === 402) return
|
|
502
|
+
res.end('OK')
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
const firstResponse = await fetch(server.url)
|
|
506
|
+
expect(firstResponse.status).toBe(402)
|
|
507
|
+
|
|
508
|
+
const challenge = Challenge.fromResponse(firstResponse)
|
|
509
|
+
const credential = Credential.from({ challenge, payload: { token: 'valid' } })
|
|
510
|
+
|
|
511
|
+
const response = await fetch(server.url, {
|
|
512
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
513
|
+
})
|
|
514
|
+
expect(response.status).toBe(200)
|
|
515
|
+
expect(response.headers.get('Payment-Receipt')).toBeTruthy()
|
|
516
|
+
|
|
517
|
+
server.close()
|
|
518
|
+
})
|
|
519
|
+
})
|
package/src/server/Mppx.ts
CHANGED
|
@@ -126,14 +126,14 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
126
126
|
const { defaults, method, realm, respond, secretKey, transport, verify } = parameters
|
|
127
127
|
|
|
128
128
|
return (options) => {
|
|
129
|
-
const
|
|
129
|
+
const methodMeta = {
|
|
130
130
|
...method,
|
|
131
131
|
...defaults,
|
|
132
132
|
...options,
|
|
133
133
|
}
|
|
134
134
|
return Object.assign(
|
|
135
135
|
async (input: Transport.InputOf): Promise<MethodFn.Response> => {
|
|
136
|
-
const { description, ...rest } = options
|
|
136
|
+
const { description, meta, ...rest } = options
|
|
137
137
|
const expires = 'expires' in options ? (options.expires as string | undefined) : undefined
|
|
138
138
|
|
|
139
139
|
// Merge defaults with per-request options
|
|
@@ -164,6 +164,7 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
164
164
|
const challenge = Challenge.fromMethod(method, {
|
|
165
165
|
description,
|
|
166
166
|
expires,
|
|
167
|
+
meta,
|
|
167
168
|
realm,
|
|
168
169
|
request,
|
|
169
170
|
secretKey,
|
|
@@ -261,7 +262,7 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
261
262
|
},
|
|
262
263
|
}
|
|
263
264
|
},
|
|
264
|
-
{ _internal:
|
|
265
|
+
{ _internal: methodMeta },
|
|
265
266
|
)
|
|
266
267
|
}
|
|
267
268
|
}
|
|
@@ -309,6 +310,8 @@ declare namespace MethodFn {
|
|
|
309
310
|
description?: string | undefined
|
|
310
311
|
/** Optional challenge expiration timestamp (ISO 8601). */
|
|
311
312
|
expires?: string | undefined
|
|
313
|
+
/** Optional server-defined correlation data (serialized as `opaque` in the request). Flat string-to-string map; clients MUST NOT modify. */
|
|
314
|
+
meta?: Record<string, string> | undefined
|
|
312
315
|
} & Method.WithDefaults<z.input<method['schema']['request']>, defaults>
|
|
313
316
|
|
|
314
317
|
export type Response<transport extends Transport.AnyTransport = Transport.Http> =
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import * as FetchServer from '@remix-run/node-fetch-server'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Writes a Fetch API `Response` to a Node.js `ServerResponse`.
|
|
5
|
+
*
|
|
6
|
+
* Delegates to `@remix-run/node-fetch-server`. Useful when bridging
|
|
7
|
+
* Fetch API handlers with Node.js HTTP servers.
|
|
8
|
+
*/
|
|
3
9
|
export const sendResponse = FetchServer.sendResponse
|
package/src/server/Response.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import * as Challenge from '../Challenge.js'
|
|
2
2
|
import type * as Errors from '../Errors.js'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Creates a 402 Payment Required response with a `WWW-Authenticate: Payment` header.
|
|
6
|
+
*
|
|
7
|
+
* Optionally includes RFC 9457 Problem Details in the response body when an error is provided.
|
|
8
|
+
*
|
|
9
|
+
* @param parameters - The challenge and optional error.
|
|
10
|
+
* @returns A 402 Response suitable for returning from a route handler.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { Challenge } from 'mppx'
|
|
15
|
+
* import { Response } from 'mppx/server'
|
|
16
|
+
*
|
|
17
|
+
* const challenge = Challenge.from({ id: '...', realm: 'api.example.com', method: 'tempo', intent: 'charge', request: { ... } })
|
|
18
|
+
* return Response.requirePayment({ challenge })
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
4
21
|
export function requirePayment(parameters: requirePayment.Parameters): Response {
|
|
5
22
|
const { challenge, error } = parameters
|
|
6
23
|
|
|
@@ -43,7 +43,7 @@ describe('http', () => {
|
|
|
43
43
|
{
|
|
44
44
|
"challenge": {
|
|
45
45
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
46
|
-
"id": "
|
|
46
|
+
"id": "4XKyFaMO73Ypu-wOofzu3F8pRIt8vb7zxmWB2GgHAsE",
|
|
47
47
|
"intent": "charge",
|
|
48
48
|
"method": "tempo",
|
|
49
49
|
"realm": "api.example.com",
|
|
@@ -93,7 +93,7 @@ describe('http', () => {
|
|
|
93
93
|
{
|
|
94
94
|
"headers": {
|
|
95
95
|
"cache-control": "no-store",
|
|
96
|
-
"www-authenticate": "Payment id="
|
|
96
|
+
"www-authenticate": "Payment id="4XKyFaMO73Ypu-wOofzu3F8pRIt8vb7zxmWB2GgHAsE", realm="api.example.com", method="tempo", intent="charge", request="eyJhbW91bnQiOiIxMDAwMDAwMDAwIiwiY3VycmVuY3kiOiIweDIwYzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEiLCJleHBpcmVzIjoiMjAyNS0wMS0wMVQwMDowMDowMC4wMDBaIiwicmVjaXBpZW50IjoiMHg3NDJkMzVDYzY2MzRDMDUzMjkyNWEzYjg0NEJjOWU3NTk1ZjhmRTAwIn0", expires="2025-01-01T00:00:00.000Z"",
|
|
97
97
|
},
|
|
98
98
|
"status": 402,
|
|
99
99
|
}
|
|
@@ -183,7 +183,7 @@ describe('mcp', () => {
|
|
|
183
183
|
{
|
|
184
184
|
"challenge": {
|
|
185
185
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
186
|
-
"id": "
|
|
186
|
+
"id": "4XKyFaMO73Ypu-wOofzu3F8pRIt8vb7zxmWB2GgHAsE",
|
|
187
187
|
"intent": "charge",
|
|
188
188
|
"method": "tempo",
|
|
189
189
|
"realm": "api.example.com",
|
|
@@ -221,7 +221,7 @@ describe('mcp', () => {
|
|
|
221
221
|
"challenges": [
|
|
222
222
|
{
|
|
223
223
|
"expires": "2025-01-01T00:00:00.000Z",
|
|
224
|
-
"id": "
|
|
224
|
+
"id": "4XKyFaMO73Ypu-wOofzu3F8pRIt8vb7zxmWB2GgHAsE",
|
|
225
225
|
"intent": "charge",
|
|
226
226
|
"method": "tempo",
|
|
227
227
|
"realm": "api.example.com",
|
|
@@ -262,7 +262,7 @@ describe('mcp', () => {
|
|
|
262
262
|
"result": {
|
|
263
263
|
"_meta": {
|
|
264
264
|
"org.paymentauth/receipt": {
|
|
265
|
-
"challengeId": "
|
|
265
|
+
"challengeId": "4XKyFaMO73Ypu-wOofzu3F8pRIt8vb7zxmWB2GgHAsE",
|
|
266
266
|
"method": "tempo",
|
|
267
267
|
"reference": "0xtxhash",
|
|
268
268
|
"status": "success",
|
|
@@ -47,7 +47,7 @@ export function resolveEscrow(
|
|
|
47
47
|
const escrow =
|
|
48
48
|
challengeEscrow ??
|
|
49
49
|
escrowContractOverride ??
|
|
50
|
-
|
|
50
|
+
defaults.escrowContract[chainId as keyof typeof defaults.escrowContract]
|
|
51
51
|
if (!escrow)
|
|
52
52
|
throw new Error(
|
|
53
53
|
'No `escrowContract` available. Provide it in parameters or ensure the server challenge includes it.',
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
chainId,
|
|
4
|
+
currency,
|
|
5
|
+
decimals,
|
|
6
|
+
escrowContract,
|
|
7
|
+
resolveCurrency,
|
|
8
|
+
rpcUrl,
|
|
9
|
+
tokens,
|
|
10
|
+
} from './defaults.js'
|
|
11
|
+
|
|
12
|
+
describe('chain ID constants', () => {
|
|
13
|
+
test('mainnet is 4217', () => {
|
|
14
|
+
expect(chainId.mainnet).toBe(4217)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('testnet is 42431', () => {
|
|
18
|
+
expect(chainId.testnet).toBe(42431)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('token address constants', () => {
|
|
23
|
+
test('usdc address', () => {
|
|
24
|
+
expect(tokens.usdc).toBe('0x20C000000000000000000000b9537d11c60E8b50')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('pathUsd address', () => {
|
|
28
|
+
expect(tokens.pathUsd).toBe('0x20c0000000000000000000000000000000000000')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('usdc and pathUsd are different addresses', () => {
|
|
32
|
+
expect(tokens.usdc).not.toBe(tokens.pathUsd)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('decimals is 6', () => {
|
|
36
|
+
expect(decimals).toBe(6)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('rpcUrl', () => {
|
|
41
|
+
test('mainnet RPC URL', () => {
|
|
42
|
+
expect(rpcUrl[chainId.mainnet]).toBe('https://rpc.tempo.xyz')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('testnet RPC URL', () => {
|
|
46
|
+
expect(rpcUrl[chainId.testnet]).toBe('https://rpc.moderato.tempo.xyz')
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('escrowContract', () => {
|
|
51
|
+
test('mainnet escrow contract', () => {
|
|
52
|
+
expect(escrowContract[chainId.mainnet]).toBe('0x0901aED692C755b870F9605E56BAA66C35BEfF69')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('testnet escrow contract', () => {
|
|
56
|
+
expect(escrowContract[chainId.testnet]).toBe('0x542831e3E4Ace07559b7C8787395f4Fb99F70787')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('currency', () => {
|
|
61
|
+
test('mainnet (4217) returns USDC', () => {
|
|
62
|
+
expect(currency[chainId.mainnet]).toBe(tokens.usdc)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('testnet (42431) returns pathUSD', () => {
|
|
66
|
+
expect(currency[chainId.testnet]).toBe(tokens.pathUsd)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('mainnet and testnet return different currencies', () => {
|
|
70
|
+
expect(currency[chainId.mainnet]).not.toBe(currency[chainId.testnet])
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('resolveCurrency', () => {
|
|
75
|
+
test('defaults to USDC (mainnet)', () => {
|
|
76
|
+
expect(resolveCurrency({})).toBe(tokens.usdc)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('testnet: true returns pathUSD', () => {
|
|
80
|
+
expect(resolveCurrency({ testnet: true })).toBe(tokens.pathUsd)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('testnet: false returns USDC', () => {
|
|
84
|
+
expect(resolveCurrency({ testnet: false })).toBe(tokens.usdc)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('chainId takes precedence over testnet', () => {
|
|
88
|
+
expect(resolveCurrency({ chainId: chainId.testnet, testnet: false })).toBe(tokens.pathUsd)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('unknown chainId falls back to pathUSD', () => {
|
|
92
|
+
expect(resolveCurrency({ chainId: 999999 })).toBe(tokens.pathUsd)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -1,20 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { ValueOf } from '../../internal/types.js'
|
|
2
|
+
|
|
3
|
+
export const chainId = {
|
|
4
|
+
mainnet: 4217,
|
|
5
|
+
testnet: 42431,
|
|
4
6
|
} as const
|
|
7
|
+
export type ChainId = ValueOf<typeof chainId>
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
/** Token addresses. */
|
|
10
|
+
export const tokens = {
|
|
11
|
+
/** USDC (USDC.e) token address. */
|
|
12
|
+
usdc: '0x20C000000000000000000000b9537d11c60E8b50',
|
|
13
|
+
/** pathUSD token address. */
|
|
14
|
+
pathUsd: '0x20c0000000000000000000000000000000000000',
|
|
9
15
|
} as const
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
/** Chain ID → default currency. */
|
|
18
|
+
export const currency = {
|
|
19
|
+
[chainId.mainnet]: tokens.usdc,
|
|
20
|
+
[chainId.testnet]: tokens.pathUsd,
|
|
21
|
+
} as const satisfies Record<ChainId, string>
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
|
-
* Default token decimals for TIP-20 stablecoins (e.g. pathUSD).
|
|
24
|
+
* Default token decimals for TIP-20 stablecoins (e.g. pathUSD, USDC).
|
|
15
25
|
*
|
|
16
26
|
* All TIP-20 tokens on Tempo use 6 decimals, so there is no risk of
|
|
17
27
|
* client/server mismatch within the Tempo ecosystem. Other chains and
|
|
18
28
|
* runtimes should set `decimals` explicitly to match their token.
|
|
19
29
|
*/
|
|
20
30
|
export const decimals = 6
|
|
31
|
+
|
|
32
|
+
/** Default payment-channel escrow contract addresses per chain. */
|
|
33
|
+
export const escrowContract = {
|
|
34
|
+
[chainId.mainnet]: '0x0901aED692C755b870F9605E56BAA66C35BEfF69',
|
|
35
|
+
[chainId.testnet]: '0x542831e3E4Ace07559b7C8787395f4Fb99F70787',
|
|
36
|
+
} as const satisfies Record<ChainId, string>
|
|
37
|
+
|
|
38
|
+
/** Default RPC URLs for each Tempo chain. */
|
|
39
|
+
export const rpcUrl = {
|
|
40
|
+
[chainId.mainnet]: 'https://rpc.tempo.xyz',
|
|
41
|
+
[chainId.testnet]: 'https://rpc.moderato.tempo.xyz',
|
|
42
|
+
} as const satisfies Record<ChainId, string>
|
|
43
|
+
|
|
44
|
+
/** Resolves the default currency. */
|
|
45
|
+
export function resolveCurrency(parameters: {
|
|
46
|
+
/** Chain ID. */
|
|
47
|
+
chainId?: number | undefined
|
|
48
|
+
/** Whether in testnet mode. */
|
|
49
|
+
testnet?: boolean | undefined
|
|
50
|
+
}): string {
|
|
51
|
+
const id = parameters.chainId ?? (parameters.testnet ? chainId.testnet : chainId.mainnet)
|
|
52
|
+
return currency[id as keyof typeof currency] ?? tokens.pathUsd
|
|
53
|
+
}
|
|
@@ -656,6 +656,156 @@ describe('tempo', () => {
|
|
|
656
656
|
})
|
|
657
657
|
})
|
|
658
658
|
|
|
659
|
+
describe('default currency resolution', () => {
|
|
660
|
+
test('mainnet (default) resolves to USDC', () => {
|
|
661
|
+
const method = tempo_server.charge({
|
|
662
|
+
getClient: () => client,
|
|
663
|
+
account: accounts[0].address,
|
|
664
|
+
})
|
|
665
|
+
expect((method.defaults as Record<string, unknown>)?.currency).toBe(
|
|
666
|
+
'0x20C000000000000000000000b9537d11c60E8b50',
|
|
667
|
+
)
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
test('testnet: true defaults to pathUSD', () => {
|
|
671
|
+
const method = tempo_server.charge({
|
|
672
|
+
getClient: () => client,
|
|
673
|
+
account: accounts[0].address,
|
|
674
|
+
testnet: true,
|
|
675
|
+
})
|
|
676
|
+
expect((method.defaults as Record<string, unknown>)?.currency).toBe(
|
|
677
|
+
'0x20c0000000000000000000000000000000000000',
|
|
678
|
+
)
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
test('unknown chain defaults to pathUSD', () => {
|
|
682
|
+
const method = tempo_server.charge({
|
|
683
|
+
getClient: () => client,
|
|
684
|
+
account: accounts[0].address,
|
|
685
|
+
chainId: 69420,
|
|
686
|
+
})
|
|
687
|
+
expect((method.defaults as Record<string, unknown>)?.currency).toBe(
|
|
688
|
+
'0x20c0000000000000000000000000000000000000',
|
|
689
|
+
)
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
test('explicit currency overrides default', () => {
|
|
693
|
+
const method = tempo_server.charge({
|
|
694
|
+
getClient: () => client,
|
|
695
|
+
account: accounts[0].address,
|
|
696
|
+
testnet: false,
|
|
697
|
+
currency: '0xcustom',
|
|
698
|
+
})
|
|
699
|
+
expect(method.defaults?.currency).toBe('0xcustom')
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
test('decimals defaults to 6', () => {
|
|
703
|
+
const method = tempo_server.charge({
|
|
704
|
+
getClient: () => client,
|
|
705
|
+
account: accounts[0].address,
|
|
706
|
+
})
|
|
707
|
+
expect((method.defaults as Record<string, unknown>)?.decimals).toBe(6)
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
test('challenge contains USDC currency (mainnet default)', async () => {
|
|
711
|
+
const handler = Mppx_server.create({
|
|
712
|
+
methods: [
|
|
713
|
+
tempo_server.charge({
|
|
714
|
+
getClient: () => client,
|
|
715
|
+
account: accounts[0].address,
|
|
716
|
+
}),
|
|
717
|
+
],
|
|
718
|
+
realm,
|
|
719
|
+
secretKey,
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
const result = await (handler.charge as Function)({ amount: '1' })(
|
|
723
|
+
new Request('https://example.com'),
|
|
724
|
+
)
|
|
725
|
+
expect(result.status).toBe(402)
|
|
726
|
+
if (result.status !== 402) throw new Error()
|
|
727
|
+
|
|
728
|
+
const challenge = Challenge.fromResponse(result.challenge, {
|
|
729
|
+
methods: [tempo_client.charge()],
|
|
730
|
+
})
|
|
731
|
+
expect(challenge.request.currency).toBe('0x20C000000000000000000000b9537d11c60E8b50')
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
test('challenge contains pathUSD currency when testnet: true', async () => {
|
|
735
|
+
const handler = Mppx_server.create({
|
|
736
|
+
methods: [
|
|
737
|
+
tempo_server.charge({
|
|
738
|
+
getClient: () => client,
|
|
739
|
+
account: accounts[0].address,
|
|
740
|
+
testnet: true,
|
|
741
|
+
}),
|
|
742
|
+
],
|
|
743
|
+
realm,
|
|
744
|
+
secretKey,
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
const result = await (handler.charge as Function)({ amount: '1', chainId: chain.id })(
|
|
748
|
+
new Request('https://example.com'),
|
|
749
|
+
)
|
|
750
|
+
expect(result.status).toBe(402)
|
|
751
|
+
if (result.status !== 402) throw new Error()
|
|
752
|
+
|
|
753
|
+
const challenge = Challenge.fromResponse(result.challenge, {
|
|
754
|
+
methods: [tempo_client.charge()],
|
|
755
|
+
})
|
|
756
|
+
expect(challenge.request.currency).toBe('0x20c0000000000000000000000000000000000000')
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
test('challenge contains pathUSD currency (unknown chain)', async () => {
|
|
760
|
+
const handler = Mppx_server.create({
|
|
761
|
+
methods: [
|
|
762
|
+
tempo_server.charge({
|
|
763
|
+
getClient: () => client,
|
|
764
|
+
account: accounts[0].address,
|
|
765
|
+
chainId: 69420,
|
|
766
|
+
}),
|
|
767
|
+
],
|
|
768
|
+
realm,
|
|
769
|
+
secretKey,
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
const result = await (handler.charge as Function)({ amount: '1' })(
|
|
773
|
+
new Request('https://example.com'),
|
|
774
|
+
)
|
|
775
|
+
expect(result.status).toBe(402)
|
|
776
|
+
if (result.status !== 402) throw new Error()
|
|
777
|
+
|
|
778
|
+
const challenge = Challenge.fromResponse(result.challenge, {
|
|
779
|
+
methods: [tempo_client.charge()],
|
|
780
|
+
})
|
|
781
|
+
expect(challenge.request.currency).toBe('0x20c0000000000000000000000000000000000000')
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
test('explicit currency in challenge overrides testnet default', async () => {
|
|
785
|
+
const handler = Mppx_server.create({
|
|
786
|
+
methods: [
|
|
787
|
+
tempo_server.charge({
|
|
788
|
+
getClient: () => client,
|
|
789
|
+
account: accounts[0].address,
|
|
790
|
+
testnet: false,
|
|
791
|
+
currency: asset,
|
|
792
|
+
}),
|
|
793
|
+
],
|
|
794
|
+
realm,
|
|
795
|
+
secretKey,
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
const result = await handler.charge({ amount: '1' })(new Request('https://example.com'))
|
|
799
|
+
expect(result.status).toBe(402)
|
|
800
|
+
if (result.status !== 402) throw new Error()
|
|
801
|
+
|
|
802
|
+
const challenge = Challenge.fromResponse(result.challenge, {
|
|
803
|
+
methods: [tempo_client.charge()],
|
|
804
|
+
})
|
|
805
|
+
expect(challenge.request.currency).toBe(asset)
|
|
806
|
+
})
|
|
807
|
+
})
|
|
808
|
+
|
|
659
809
|
describe('attribution memo', () => {
|
|
660
810
|
test('client always generates attribution memo (hash credential)', async () => {
|
|
661
811
|
const httpServer = await Http.createServer(async (req, res) => {
|
|
@@ -40,7 +40,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
40
40
|
) {
|
|
41
41
|
const {
|
|
42
42
|
amount,
|
|
43
|
-
currency,
|
|
43
|
+
currency = defaults.resolveCurrency(parameters),
|
|
44
44
|
decimals = defaults.decimals,
|
|
45
45
|
description,
|
|
46
46
|
externalId,
|
|
@@ -71,7 +71,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
71
71
|
async request({ credential, request }) {
|
|
72
72
|
const chainId = await (async () => {
|
|
73
73
|
if (request.chainId) return request.chainId
|
|
74
|
-
if (parameters.testnet) return defaults.
|
|
74
|
+
if (parameters.testnet) return defaults.chainId.testnet
|
|
75
75
|
return (await getClient({})).chain?.id
|
|
76
76
|
})()
|
|
77
77
|
|