mppx 0.3.5 → 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/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/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/internal/types.ts +11 -0
- package/src/proxy/internal/Headers.ts +2 -0
- package/src/proxy/internal/Route.ts +4 -0
- package/src/server/NodeListener.ts +6 -0
- package/src/server/Response.ts +17 -0
- 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 +189 -1
- package/src/tempo/server/Session.ts +8 -3
- package/src/tempo/session/Voucher.test.ts +46 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const httpMethods = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'])
|
|
2
2
|
|
|
3
|
+
/** Extracts the pathname from a URL, stripping the optional `basePath` prefix. Returns `null` if the path doesn't match. */
|
|
3
4
|
export function pathname(url: URL, basePath?: string): string | null {
|
|
4
5
|
let pathname = url.pathname
|
|
5
6
|
if (basePath) {
|
|
@@ -10,6 +11,7 @@ export function pathname(url: URL, basePath?: string): string | null {
|
|
|
10
11
|
return pathname
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/** Splits a `/{serviceId}/rest/of/path` pathname into its service ID and upstream path. */
|
|
13
15
|
export function parse(pathname: string): { serviceId: string; upstreamPath: string } | null {
|
|
14
16
|
const segments = pathname.split('/').filter(Boolean)
|
|
15
17
|
const serviceId = segments[0]
|
|
@@ -19,6 +21,7 @@ export function parse(pathname: string): { serviceId: string; upstreamPath: stri
|
|
|
19
21
|
return { serviceId, upstreamPath }
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/** Finds the first route matching both the HTTP method and path (via `URLPattern`). */
|
|
22
25
|
export function match(
|
|
23
26
|
routes: Record<string, unknown>,
|
|
24
27
|
method: string,
|
|
@@ -33,6 +36,7 @@ export function match(
|
|
|
33
36
|
return null
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
/** Finds the first route matching just the path, ignoring the HTTP method. Used for management POST fallback. */
|
|
36
40
|
export function matchPath(
|
|
37
41
|
routes: Record<string, unknown>,
|
|
38
42
|
path: string,
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { z } from 'mppx'
|
|
2
|
+
import { Challenge } from 'mppx'
|
|
2
3
|
import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
|
|
3
4
|
import { type Address, createClient, type Hex } from 'viem'
|
|
4
5
|
import { Addresses } from 'viem/tempo'
|
|
@@ -1334,6 +1335,193 @@ describe('monotonicity and TOCTOU (unit tests)', () => {
|
|
|
1334
1335
|
})
|
|
1335
1336
|
})
|
|
1336
1337
|
|
|
1338
|
+
describe('session default currency resolution', () => {
|
|
1339
|
+
const mockClient = createClient({ transport: http('http://localhost:1') })
|
|
1340
|
+
const mockMainnetClient = createClient({
|
|
1341
|
+
chain: {
|
|
1342
|
+
id: 4217,
|
|
1343
|
+
name: 'Tempo',
|
|
1344
|
+
nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
|
|
1345
|
+
rpcUrls: { default: { http: ['http://localhost:1'] } },
|
|
1346
|
+
},
|
|
1347
|
+
transport: http('http://localhost:1'),
|
|
1348
|
+
})
|
|
1349
|
+
const mockTestnetClient = createClient({
|
|
1350
|
+
chain: {
|
|
1351
|
+
id: 42431,
|
|
1352
|
+
name: 'Tempo Testnet',
|
|
1353
|
+
nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
|
|
1354
|
+
rpcUrls: { default: { http: ['http://localhost:1'] } },
|
|
1355
|
+
},
|
|
1356
|
+
transport: http('http://localhost:1'),
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
test('mainnet (default) resolves to USDC', () => {
|
|
1360
|
+
const server = session({
|
|
1361
|
+
store: Store.memory(),
|
|
1362
|
+
getClient: () => mockClient,
|
|
1363
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1364
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1365
|
+
} as session.Parameters)
|
|
1366
|
+
expect(server.defaults?.currency).toBe('0x20C000000000000000000000b9537d11c60E8b50')
|
|
1367
|
+
})
|
|
1368
|
+
|
|
1369
|
+
test('testnet: true defaults to pathUSD', () => {
|
|
1370
|
+
const server = session({
|
|
1371
|
+
store: Store.memory(),
|
|
1372
|
+
getClient: () => mockClient,
|
|
1373
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1374
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1375
|
+
testnet: true,
|
|
1376
|
+
} as session.Parameters)
|
|
1377
|
+
expect(server.defaults?.currency).toBe('0x20c0000000000000000000000000000000000000')
|
|
1378
|
+
})
|
|
1379
|
+
|
|
1380
|
+
test('unknown chain defaults to pathUSD', () => {
|
|
1381
|
+
const server = session({
|
|
1382
|
+
store: Store.memory(),
|
|
1383
|
+
getClient: () => mockClient,
|
|
1384
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1385
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1386
|
+
chainId: 69420,
|
|
1387
|
+
} as session.Parameters)
|
|
1388
|
+
expect(server.defaults?.currency).toBe('0x20c0000000000000000000000000000000000000')
|
|
1389
|
+
})
|
|
1390
|
+
|
|
1391
|
+
test('explicit currency overrides default', () => {
|
|
1392
|
+
const server = session({
|
|
1393
|
+
store: Store.memory(),
|
|
1394
|
+
getClient: () => mockClient,
|
|
1395
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1396
|
+
currency: '0xcustom',
|
|
1397
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1398
|
+
chainId: 4217,
|
|
1399
|
+
testnet: false,
|
|
1400
|
+
} as session.Parameters)
|
|
1401
|
+
expect(server.defaults?.currency).toBe('0xcustom')
|
|
1402
|
+
})
|
|
1403
|
+
|
|
1404
|
+
test('decimals defaults to 6', () => {
|
|
1405
|
+
const server = session({
|
|
1406
|
+
store: Store.memory(),
|
|
1407
|
+
getClient: () => mockClient,
|
|
1408
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1409
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1410
|
+
chainId: 42431,
|
|
1411
|
+
} as session.Parameters)
|
|
1412
|
+
expect(server.defaults?.decimals).toBe(6)
|
|
1413
|
+
})
|
|
1414
|
+
|
|
1415
|
+
test('challenge contains USDC currency (mainnet default)', async () => {
|
|
1416
|
+
const handler = Mppx_server.create({
|
|
1417
|
+
methods: [
|
|
1418
|
+
tempo_server.session({
|
|
1419
|
+
store: Store.memory(),
|
|
1420
|
+
getClient: () => mockMainnetClient,
|
|
1421
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1422
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1423
|
+
chainId: 4217,
|
|
1424
|
+
testnet: false,
|
|
1425
|
+
}),
|
|
1426
|
+
],
|
|
1427
|
+
realm: 'api.example.com',
|
|
1428
|
+
secretKey: 'secret',
|
|
1429
|
+
})
|
|
1430
|
+
|
|
1431
|
+
const result = await (handler.session as Function)({
|
|
1432
|
+
amount: '1',
|
|
1433
|
+
decimals: 6,
|
|
1434
|
+
unitType: 'token',
|
|
1435
|
+
})(new Request('https://example.com'))
|
|
1436
|
+
expect(result.status).toBe(402)
|
|
1437
|
+
|
|
1438
|
+
const challenge = Challenge.fromResponse(result.challenge)
|
|
1439
|
+
expect(challenge.request.currency).toBe('0x20C000000000000000000000b9537d11c60E8b50')
|
|
1440
|
+
})
|
|
1441
|
+
|
|
1442
|
+
test('challenge contains pathUSD currency when testnet: true', async () => {
|
|
1443
|
+
const handler = Mppx_server.create({
|
|
1444
|
+
methods: [
|
|
1445
|
+
tempo_server.session({
|
|
1446
|
+
store: Store.memory(),
|
|
1447
|
+
getClient: () => mockTestnetClient,
|
|
1448
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1449
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1450
|
+
testnet: true,
|
|
1451
|
+
}),
|
|
1452
|
+
],
|
|
1453
|
+
realm: 'api.example.com',
|
|
1454
|
+
secretKey: 'secret',
|
|
1455
|
+
})
|
|
1456
|
+
|
|
1457
|
+
const result = await (handler.session as Function)({
|
|
1458
|
+
amount: '1',
|
|
1459
|
+
decimals: 6,
|
|
1460
|
+
unitType: 'token',
|
|
1461
|
+
chainId: 42431,
|
|
1462
|
+
})(new Request('https://example.com'))
|
|
1463
|
+
expect(result.status).toBe(402)
|
|
1464
|
+
|
|
1465
|
+
const challenge = Challenge.fromResponse(result.challenge)
|
|
1466
|
+
expect(challenge.request.currency).toBe('0x20c0000000000000000000000000000000000000')
|
|
1467
|
+
})
|
|
1468
|
+
|
|
1469
|
+
test('challenge contains pathUSD currency (unknown chain)', async () => {
|
|
1470
|
+
const handler = Mppx_server.create({
|
|
1471
|
+
methods: [
|
|
1472
|
+
tempo_server.session({
|
|
1473
|
+
store: Store.memory(),
|
|
1474
|
+
getClient: () => mockTestnetClient,
|
|
1475
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1476
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1477
|
+
chainId: 69420,
|
|
1478
|
+
}),
|
|
1479
|
+
],
|
|
1480
|
+
realm: 'api.example.com',
|
|
1481
|
+
secretKey: 'secret',
|
|
1482
|
+
})
|
|
1483
|
+
|
|
1484
|
+
const result = await (handler.session as Function)({
|
|
1485
|
+
amount: '1',
|
|
1486
|
+
decimals: 6,
|
|
1487
|
+
unitType: 'token',
|
|
1488
|
+
})(new Request('https://example.com'))
|
|
1489
|
+
expect(result.status).toBe(402)
|
|
1490
|
+
|
|
1491
|
+
const challenge = Challenge.fromResponse(result.challenge)
|
|
1492
|
+
expect(challenge.request.currency).toBe('0x20c0000000000000000000000000000000000000')
|
|
1493
|
+
})
|
|
1494
|
+
|
|
1495
|
+
test('explicit currency in challenge overrides testnet default', async () => {
|
|
1496
|
+
const handler = Mppx_server.create({
|
|
1497
|
+
methods: [
|
|
1498
|
+
tempo_server.session({
|
|
1499
|
+
store: Store.memory(),
|
|
1500
|
+
getClient: () => mockClient,
|
|
1501
|
+
account: '0x0000000000000000000000000000000000000001',
|
|
1502
|
+
currency: '0xcustom',
|
|
1503
|
+
escrowContract: '0x0000000000000000000000000000000000000002',
|
|
1504
|
+
chainId: 4217,
|
|
1505
|
+
testnet: false,
|
|
1506
|
+
}),
|
|
1507
|
+
],
|
|
1508
|
+
realm: 'api.example.com',
|
|
1509
|
+
secretKey: 'secret',
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
const result = await handler.session({
|
|
1513
|
+
amount: '1',
|
|
1514
|
+
decimals: 6,
|
|
1515
|
+
unitType: 'token',
|
|
1516
|
+
})(new Request('https://example.com'))
|
|
1517
|
+
expect(result.status).toBe(402)
|
|
1518
|
+
if (result.status !== 402) throw new Error()
|
|
1519
|
+
|
|
1520
|
+
const challenge = Challenge.fromResponse(result.challenge)
|
|
1521
|
+
expect(challenge.request.currency).toBe('0xcustom')
|
|
1522
|
+
})
|
|
1523
|
+
})
|
|
1524
|
+
|
|
1337
1525
|
function nextSalt(): Hex {
|
|
1338
1526
|
saltCounter++
|
|
1339
1527
|
return `0x${saltCounter.toString(16).padStart(64, '0')}` as Hex
|
|
@@ -85,7 +85,7 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
85
85
|
const parameters = p as parameters
|
|
86
86
|
const {
|
|
87
87
|
amount,
|
|
88
|
-
currency,
|
|
88
|
+
currency = defaults.resolveCurrency(parameters),
|
|
89
89
|
decimals = defaults.decimals,
|
|
90
90
|
store: rawStore = Store.memory(),
|
|
91
91
|
suggestedDeposit,
|
|
@@ -127,7 +127,7 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
127
127
|
// Extract chainId from request or default.
|
|
128
128
|
const chainId = await (async () => {
|
|
129
129
|
if (request.chainId) return request.chainId
|
|
130
|
-
if (parameters.testnet) return defaults.
|
|
130
|
+
if (parameters.testnet) return defaults.chainId.testnet
|
|
131
131
|
return (await getClient({})).chain?.id
|
|
132
132
|
})()
|
|
133
133
|
|
|
@@ -156,7 +156,12 @@ export function session<const parameters extends session.Parameters>(p?: paramet
|
|
|
156
156
|
return undefined
|
|
157
157
|
})()
|
|
158
158
|
|
|
159
|
-
return {
|
|
159
|
+
return {
|
|
160
|
+
...request,
|
|
161
|
+
chainId,
|
|
162
|
+
escrowContract: resolvedEscrow,
|
|
163
|
+
feePayer: resolvedFeePayer,
|
|
164
|
+
}
|
|
160
165
|
},
|
|
161
166
|
|
|
162
167
|
async verify({ credential }) {
|
|
@@ -131,4 +131,50 @@ describe('Voucher', () => {
|
|
|
131
131
|
expect(voucher.cumulativeAmount).toBe(5000000n)
|
|
132
132
|
expect(voucher.signature).toBe(sig)
|
|
133
133
|
})
|
|
134
|
+
|
|
135
|
+
test('parseVoucherFromPayload with zero amount', () => {
|
|
136
|
+
const sig =
|
|
137
|
+
'0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab' as const
|
|
138
|
+
const voucher = parseVoucherFromPayload(channelId, '0', sig)
|
|
139
|
+
expect(voucher.cumulativeAmount).toBe(0n)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('verifyVoucher rejects wrong escrow contract', async () => {
|
|
143
|
+
const signature = await signVoucher(
|
|
144
|
+
client,
|
|
145
|
+
account,
|
|
146
|
+
{ channelId, cumulativeAmount },
|
|
147
|
+
escrowContract,
|
|
148
|
+
chainId,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const wrongEscrow = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as const
|
|
152
|
+
const isValid = await verifyVoucher(
|
|
153
|
+
wrongEscrow,
|
|
154
|
+
chainId,
|
|
155
|
+
{ channelId, cumulativeAmount, signature },
|
|
156
|
+
account.address,
|
|
157
|
+
)
|
|
158
|
+
expect(isValid).toBe(false)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('signVoucher and verifyVoucher round-trip with zero amount', async () => {
|
|
162
|
+
const zeroAmount = 0n
|
|
163
|
+
const signature = await signVoucher(
|
|
164
|
+
client,
|
|
165
|
+
account,
|
|
166
|
+
{ channelId, cumulativeAmount: zeroAmount },
|
|
167
|
+
escrowContract,
|
|
168
|
+
chainId,
|
|
169
|
+
)
|
|
170
|
+
expect(signature).toMatch(/^0x/)
|
|
171
|
+
|
|
172
|
+
const isValid = await verifyVoucher(
|
|
173
|
+
escrowContract,
|
|
174
|
+
chainId,
|
|
175
|
+
{ channelId, cumulativeAmount: zeroAmount, signature },
|
|
176
|
+
account.address,
|
|
177
|
+
)
|
|
178
|
+
expect(isValid).toBe(true)
|
|
179
|
+
})
|
|
134
180
|
})
|