mppx 0.8.12 → 0.8.13
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/CHANGELOG.md +18 -0
- package/README.md +16 -1
- package/dist/cli/account.d.ts +88 -0
- package/dist/cli/account.d.ts.map +1 -1
- package/dist/cli/account.js +39 -9
- package/dist/cli/account.js.map +1 -1
- package/dist/cli/cli.d.ts +2 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +59 -2
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/sessions/Manager.d.ts +31 -0
- package/dist/cli/sessions/Manager.d.ts.map +1 -0
- package/dist/cli/sessions/Manager.js +56 -0
- package/dist/cli/sessions/Manager.js.map +1 -0
- package/dist/cli/sessions/commands.d.ts +36 -0
- package/dist/cli/sessions/commands.d.ts.map +1 -0
- package/dist/cli/sessions/commands.js +407 -0
- package/dist/cli/sessions/commands.js.map +1 -0
- package/dist/cli/sessions/request.d.ts +35 -0
- package/dist/cli/sessions/request.d.ts.map +1 -0
- package/dist/cli/sessions/request.js +271 -0
- package/dist/cli/sessions/request.js.map +1 -0
- package/dist/cli/sessions/store.d.ts +134 -0
- package/dist/cli/sessions/store.d.ts.map +1 -0
- package/dist/cli/sessions/store.js +723 -0
- package/dist/cli/sessions/store.js.map +1 -0
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +12 -0
- package/dist/cli/utils.js.map +1 -1
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +12 -6
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/messages.d.ts +2 -0
- package/dist/cli/validate/messages.d.ts.map +1 -0
- package/dist/cli/validate/messages.js +8 -0
- package/dist/cli/validate/messages.js.map +1 -0
- package/dist/client/node.d.ts +29 -0
- package/dist/client/node.d.ts.map +1 -0
- package/dist/client/node.js +177 -0
- package/dist/client/node.js.map +1 -0
- package/dist/internal/types.d.ts +24 -0
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts +8 -4
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +1 -1
- package/dist/tempo/internal/types.d.ts +10 -1
- package/dist/tempo/internal/types.d.ts.map +1 -1
- package/dist/tempo/legacy/client/Session.d.ts +2 -2
- package/dist/tempo/legacy/server/Session.d.ts +3 -3
- package/dist/tempo/legacy/server/Session.d.ts.map +1 -1
- package/dist/tempo/legacy/server/Session.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +3 -2
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +2 -23
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +2 -2
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts +3 -2
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js.map +1 -1
- package/dist/tempo/server/index.d.ts +1 -1
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/session/Snapshot.d.ts +9 -0
- package/dist/tempo/session/Snapshot.d.ts.map +1 -1
- package/dist/tempo/session/Snapshot.js +6 -0
- package/dist/tempo/session/Snapshot.js.map +1 -1
- package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/session/client/ChannelOps.js +11 -2
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +21 -1
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +66 -1
- package/dist/tempo/session/client/CredentialState.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +2 -2
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +128 -30
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +11 -2
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +33 -9
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/client/internal/SessionManager.d.ts +19 -0
- package/dist/tempo/session/client/internal/SessionManager.d.ts.map +1 -0
- package/dist/tempo/session/client/internal/SessionManager.js +13 -0
- package/dist/tempo/session/client/internal/SessionManager.js.map +1 -0
- package/dist/tempo/session/precompile/Chain.js +13 -4
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.d.ts +3 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +14 -0
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/tempo/session/server/MeteredStream.d.ts +7 -1
- package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -1
- package/dist/tempo/session/server/MeteredStream.js +5 -4
- package/dist/tempo/session/server/MeteredStream.js.map +1 -1
- package/dist/tempo/session/server/RequestState.d.ts +5 -1
- package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
- package/dist/tempo/session/server/RequestState.js +10 -4
- package/dist/tempo/session/server/RequestState.js.map +1 -1
- package/dist/tempo/session/server/Session.d.ts +21 -6
- package/dist/tempo/session/server/Session.d.ts.map +1 -1
- package/dist/tempo/session/server/Session.js +4 -1
- package/dist/tempo/session/server/Session.js.map +1 -1
- package/dist/tempo/session/server/Settlement.d.ts +20 -0
- package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
- package/dist/tempo/session/server/Settlement.js +20 -0
- package/dist/tempo/session/server/Settlement.js.map +1 -1
- package/dist/tempo/session/server/Ws.d.ts +2 -0
- package/dist/tempo/session/server/Ws.d.ts.map +1 -1
- package/dist/tempo/session/server/Ws.js.map +1 -1
- package/dist/tempo/session/server/index.d.ts +1 -1
- package/dist/tempo/session/server/index.d.ts.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +21 -21
- package/dist/validation/core.d.ts.map +1 -1
- package/dist/validation/core.js +6 -2
- package/dist/validation/core.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js +24 -17
- package/dist/viem/Client.js.map +1 -1
- package/package.json +6 -1
- package/src/cli/account.ts +45 -10
- package/src/cli/cli.test.ts +165 -70
- package/src/cli/cli.ts +67 -2
- package/src/cli/mcp.test.ts +11 -0
- package/src/cli/sessions/Manager.test.ts +249 -0
- package/src/cli/sessions/Manager.ts +93 -0
- package/src/cli/sessions/commands.ts +444 -0
- package/src/cli/sessions/request.test.ts +51 -0
- package/src/cli/sessions/request.ts +353 -0
- package/src/cli/sessions/store.test.ts +581 -0
- package/src/cli/sessions/store.ts +940 -0
- package/src/cli/utils.test.ts +23 -0
- package/src/cli/utils.ts +10 -0
- package/src/cli/validate/index.ts +12 -14
- package/src/cli/validate/messages.ts +7 -0
- package/src/cli/validate.test.ts +38 -5
- package/src/client/node.test.ts +115 -0
- package/src/client/node.ts +247 -0
- package/src/internal/types.test-d.ts +21 -0
- package/src/internal/types.ts +33 -0
- package/src/server/Mppx.test-d.ts +42 -0
- package/src/stripe/Methods.test.ts +10 -0
- package/src/stripe/server/Charge.test-d.ts +66 -0
- package/src/stripe/server/Charge.ts +8 -5
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/internal/types.ts +13 -2
- package/src/tempo/legacy/server/Defaults.test-d.ts +9 -0
- package/src/tempo/legacy/server/Session.ts +6 -5
- package/src/tempo/server/Charge.ts +6 -4
- package/src/tempo/server/Methods.ts +5 -4
- package/src/tempo/server/Subscription.ts +6 -4
- package/src/tempo/server/index.ts +5 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/README.md +201 -0
- package/src/tempo/session/Snapshot.ts +21 -0
- package/src/tempo/session/client/ChannelOps.test.ts +40 -0
- package/src/tempo/session/client/ChannelOps.ts +12 -2
- package/src/tempo/session/client/CredentialState.test.ts +2 -2
- package/src/tempo/session/client/CredentialState.ts +103 -1
- package/src/tempo/session/client/Session.test.ts +3 -3
- package/src/tempo/session/client/SessionManager.test.ts +137 -4
- package/src/tempo/session/client/SessionManager.ts +139 -33
- package/src/tempo/session/client/Transports.test.ts +150 -8
- package/src/tempo/session/client/Transports.ts +48 -10
- package/src/tempo/session/client/internal/SessionManager.ts +36 -0
- package/src/tempo/session/precompile/Chain.test.ts +26 -0
- package/src/tempo/session/precompile/Chain.ts +17 -4
- package/src/tempo/session/server/CredentialVerification.ts +18 -1
- package/src/tempo/session/server/MeteredStream.ts +12 -5
- package/src/tempo/session/server/RequestState.test.ts +39 -3
- package/src/tempo/session/server/RequestState.ts +13 -6
- package/src/tempo/session/server/Session.test.ts +461 -0
- package/src/tempo/session/server/Session.ts +31 -8
- package/src/tempo/session/server/Settlement.ts +45 -0
- package/src/tempo/session/server/Ws.test.ts +113 -0
- package/src/tempo/session/server/Ws.ts +2 -0
- package/src/tempo/session/server/index.ts +2 -0
- package/src/validation/core.ts +7 -1
- package/src/viem/Client.test.ts +44 -1
- package/src/viem/Client.ts +31 -17
package/src/cli/utils.test.ts
CHANGED
|
@@ -3,12 +3,35 @@ import { afterEach, describe, expect, test, vi } from 'vp/test'
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
fetchTokenInfo,
|
|
6
|
+
isAgentEnvironment,
|
|
6
7
|
networkRpcUrls,
|
|
7
8
|
resolveChain,
|
|
8
9
|
resolveFundingNetwork,
|
|
9
10
|
resolveRpcUrl,
|
|
10
11
|
} from './utils.js'
|
|
11
12
|
|
|
13
|
+
describe('isAgentEnvironment', () => {
|
|
14
|
+
test('detects Claude Code', () => {
|
|
15
|
+
expect(isAgentEnvironment({ CLAUDECODE: '1' })).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('detects Codex by exact env var', () => {
|
|
19
|
+
expect(isAgentEnvironment({ CODEX: '1' })).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('detects Codex wrapper env vars', () => {
|
|
23
|
+
expect(isAgentEnvironment({ CODEX_CI: '1' })).toBe(true)
|
|
24
|
+
expect(isAgentEnvironment({ CODEX_SANDBOX: 'seatbelt' })).toBe(true)
|
|
25
|
+
expect(isAgentEnvironment({ CODEX_THREAD_ID: '019f6bc0-b80f-7061-ac1b-7af8ba1ea513' })).toBe(
|
|
26
|
+
true,
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('ignores empty and unrelated env vars', () => {
|
|
31
|
+
expect(isAgentEnvironment({ CODEX_CI: '', CI: '1' })).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
12
35
|
describe('resolveRpcUrl', () => {
|
|
13
36
|
afterEach(() => {
|
|
14
37
|
delete process.env.MPPX_RPC_URL
|
package/src/cli/utils.ts
CHANGED
|
@@ -179,6 +179,16 @@ export function parseMethodOpts(raw: string | string[] | undefined): Record<stri
|
|
|
179
179
|
return result
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/** Returns true when the CLI is running inside an agent wrapper that prefers machine-readable output. */
|
|
183
|
+
export function isAgentEnvironment(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
184
|
+
if (env.CLAUDECODE) return true
|
|
185
|
+
for (const [key, value] of Object.entries(env)) {
|
|
186
|
+
if (!value) continue
|
|
187
|
+
if (key === 'CODEX' || key.startsWith('CODEX_')) return true
|
|
188
|
+
}
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
|
|
182
192
|
export function isTempoAccount(accountName: string): boolean {
|
|
183
193
|
return accountName.startsWith('tempo:')
|
|
184
194
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Cli, z } from 'incur'
|
|
2
2
|
|
|
3
3
|
import { validate as validateCore, validateStream } from '../../validation/core.js'
|
|
4
|
-
import { pc } from '../utils.js'
|
|
4
|
+
import { isAgentEnvironment, pc } from '../utils.js'
|
|
5
5
|
import type { Counts } from './helpers.js'
|
|
6
6
|
import { printResults, printSection } from './helpers.js'
|
|
7
|
+
import { missingDiscoverySuggestion } from './messages.js'
|
|
7
8
|
|
|
8
9
|
const validate = Cli.create('validate', {
|
|
9
10
|
description: 'Validate an MPP server implementation end-to-end',
|
|
@@ -22,7 +23,10 @@ const validate = Cli.create('validate', {
|
|
|
22
23
|
header: z.array(z.string()).optional().describe('Request header (key:value, repeatable)'),
|
|
23
24
|
verbose: z.number().default(0).meta({ count: true }).describe('Verbosity level'),
|
|
24
25
|
yes: z.boolean().default(false).describe('Auto-approve mainnet payments'),
|
|
25
|
-
outputJson: z
|
|
26
|
+
outputJson: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.default(false)
|
|
29
|
+
.describe('Output results as JSON (auto-enabled in known agent environments)'),
|
|
26
30
|
}),
|
|
27
31
|
alias: {
|
|
28
32
|
endpoint: 'e',
|
|
@@ -32,8 +36,10 @@ const validate = Cli.create('validate', {
|
|
|
32
36
|
outputJson: 'j',
|
|
33
37
|
},
|
|
34
38
|
async run(c) {
|
|
39
|
+
const outputJson = c.options.outputJson || isAgentEnvironment()
|
|
40
|
+
|
|
35
41
|
// JSON mode: batch everything
|
|
36
|
-
if (
|
|
42
|
+
if (outputJson) {
|
|
37
43
|
const result = await validateCore({
|
|
38
44
|
url: c.args.url,
|
|
39
45
|
endpoint: c.options.endpoint,
|
|
@@ -82,17 +88,9 @@ const validate = Cli.create('validate', {
|
|
|
82
88
|
discoveryFound = event.discovery.found
|
|
83
89
|
if (!discoveryFound && !c.options.endpoint) {
|
|
84
90
|
console.log('')
|
|
85
|
-
|
|
86
|
-
console.log(
|
|
87
|
-
|
|
88
|
-
' MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.',
|
|
89
|
-
),
|
|
90
|
-
)
|
|
91
|
-
console.log(
|
|
92
|
-
pc.dim(
|
|
93
|
-
' To test a specific endpoint: mppx validate <url> --endpoint POST:/your/path',
|
|
94
|
-
),
|
|
95
|
-
)
|
|
91
|
+
const [headline, ...lines] = missingDiscoverySuggestion.split('\n')
|
|
92
|
+
console.log(pc.yellow(` ${headline}`))
|
|
93
|
+
for (const line of lines) console.log(line ? pc.dim(` ${line}`) : '')
|
|
96
94
|
console.log('')
|
|
97
95
|
process.exit(1)
|
|
98
96
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const missingDiscoverySuggestion = [
|
|
2
|
+
'No discovery document found, endpoints could not be automatically found.',
|
|
3
|
+
'MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.',
|
|
4
|
+
'The MPP SDKs can automatically expose this endpoint. See https://mpp.dev/sdk.',
|
|
5
|
+
'',
|
|
6
|
+
'To test a specific endpoint: mppx validate <url> --endpoint POST:/your/path',
|
|
7
|
+
].join('\n')
|
package/src/cli/validate.test.ts
CHANGED
|
@@ -6,6 +6,10 @@ import * as Http from '~test/Http.js'
|
|
|
6
6
|
import * as Challenge from '../Challenge.js'
|
|
7
7
|
import * as Constants from '../Constants.js'
|
|
8
8
|
import * as Receipt from '../Receipt.js'
|
|
9
|
+
import { missingDiscoverySuggestion } from './validate/messages.js'
|
|
10
|
+
|
|
11
|
+
// Keep validate tests out of JSON mode by default, even when the suite runs inside an agent.
|
|
12
|
+
const isAgentEnvironmentMock = vi.fn(() => false)
|
|
9
13
|
|
|
10
14
|
// Keep CLI orchestration tests independent of the public Tempo RPC and faucet.
|
|
11
15
|
vi.doMock('viem/tempo', async () => {
|
|
@@ -28,11 +32,20 @@ vi.doMock('viem/actions', async () => {
|
|
|
28
32
|
}
|
|
29
33
|
})
|
|
30
34
|
|
|
35
|
+
vi.doMock('./utils.js', async () => {
|
|
36
|
+
const actual = await vi.importActual<typeof import('./utils.js')>('./utils.js')
|
|
37
|
+
return {
|
|
38
|
+
...actual,
|
|
39
|
+
isAgentEnvironment: isAgentEnvironmentMock,
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
31
43
|
// Auto-cleanup for test servers
|
|
32
44
|
const servers: Http.TestServer[] = []
|
|
33
45
|
afterEach(() => {
|
|
34
46
|
servers.forEach((s) => s.close())
|
|
35
47
|
servers.length = 0
|
|
48
|
+
isAgentEnvironmentMock.mockReturnValue(false)
|
|
36
49
|
})
|
|
37
50
|
|
|
38
51
|
async function testServer(handler: http.RequestListener) {
|
|
@@ -167,11 +180,8 @@ describe('validate: discovery', () => {
|
|
|
167
180
|
})
|
|
168
181
|
const { output, exitCode } = await serve(['validate', server.url])
|
|
169
182
|
expect(exitCode).toBe(1)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
expect(output).toContain(
|
|
173
|
-
'To test a specific endpoint: mppx validate <url> --endpoint POST:/your/path',
|
|
174
|
-
)
|
|
183
|
+
for (const line of missingDiscoverySuggestion.split('\n').filter(Boolean))
|
|
184
|
+
expect(output).toContain(line)
|
|
175
185
|
})
|
|
176
186
|
|
|
177
187
|
test('strips /openapi.json from input URL', { timeout: 15_000 }, async () => {
|
|
@@ -774,6 +784,19 @@ describe('validate: JSON mode', () => {
|
|
|
774
784
|
expect(allPayment.every((r: { severity: string }) => r.severity === 'skip')).toBe(true)
|
|
775
785
|
})
|
|
776
786
|
|
|
787
|
+
test(
|
|
788
|
+
'auto-enables JSON output when an agent environment is detected',
|
|
789
|
+
{ timeout: 15_000 },
|
|
790
|
+
async () => {
|
|
791
|
+
const server = await mppServer(mainnetChallenge())
|
|
792
|
+
isAgentEnvironmentMock.mockReturnValue(true)
|
|
793
|
+
const { output } = await serve(['validate', server.url])
|
|
794
|
+
const result = parseJson(output)
|
|
795
|
+
expect(result.summary.skipped).toBeGreaterThan(0)
|
|
796
|
+
expect(result.endpoints.length).toBeGreaterThan(0)
|
|
797
|
+
},
|
|
798
|
+
)
|
|
799
|
+
|
|
777
800
|
test('no console leaks before JSON', { timeout: 15_000 }, async () => {
|
|
778
801
|
const server = await mppServer(mainnetChallenge())
|
|
779
802
|
const { output } = await serve(['validate', server.url, '--outputJson', '--yes'])
|
|
@@ -791,4 +814,14 @@ describe('validate: JSON mode', () => {
|
|
|
791
814
|
const result = parseJson(output)
|
|
792
815
|
expect(Array.isArray(result.suggestions)).toBe(true)
|
|
793
816
|
})
|
|
817
|
+
|
|
818
|
+
test('includes missing discovery suggestions', { timeout: 15_000 }, async () => {
|
|
819
|
+
const server = await testServer((_req, res) => {
|
|
820
|
+
res.writeHead(404)
|
|
821
|
+
res.end()
|
|
822
|
+
})
|
|
823
|
+
const { output } = await serve(['validate', server.url, '--outputJson'])
|
|
824
|
+
const result = parseJson(output)
|
|
825
|
+
expect(result.suggestions).toEqual([missingDiscoverySuggestion])
|
|
826
|
+
})
|
|
794
827
|
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
2
|
+
import { homedir, tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'vp/test'
|
|
7
|
+
|
|
8
|
+
import type { ChannelEntry } from '../tempo/session/client/ChannelOps.js'
|
|
9
|
+
import { entryKey } from '../tempo/session/client/ChannelStore.js'
|
|
10
|
+
import { createSqliteChannelStore, defaultChannelDatabasePath } from './node.js'
|
|
11
|
+
|
|
12
|
+
const channel: ChannelEntry = {
|
|
13
|
+
channelId: `0x${'11'.repeat(32)}`,
|
|
14
|
+
cumulativeAmount: 2_000_000n,
|
|
15
|
+
deposit: 10_000_000n,
|
|
16
|
+
descriptor: {
|
|
17
|
+
authorizedSigner: '0x0000000000000000000000000000000000000001',
|
|
18
|
+
expiringNonceHash: `0x${'22'.repeat(32)}`,
|
|
19
|
+
operator: '0x0000000000000000000000000000000000000000',
|
|
20
|
+
payee: '0x0000000000000000000000000000000000000002',
|
|
21
|
+
payer: '0x0000000000000000000000000000000000000003',
|
|
22
|
+
salt: `0x${'33'.repeat(32)}`,
|
|
23
|
+
token: '0x0000000000000000000000000000000000000004',
|
|
24
|
+
},
|
|
25
|
+
escrow: '0x0000000000000000000000000000000000000005',
|
|
26
|
+
chainId: 4217,
|
|
27
|
+
opened: true,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('SQLite ChannelStore', () => {
|
|
31
|
+
test('defaults to the shared Tempo wallet channels database', () => {
|
|
32
|
+
expect(defaultChannelDatabasePath()).toBe(join(homedir(), '.tempo', 'wallet', 'channels.db'))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('persists a namespaced channel across fresh client instances', () => {
|
|
36
|
+
const directory = mkdtempSync(join(tmpdir(), 'mppx-channels-'))
|
|
37
|
+
const path = join(directory, 'channels.db')
|
|
38
|
+
try {
|
|
39
|
+
const first = createSqliteChannelStore({ namespace: 'https://api.example.com', path })
|
|
40
|
+
first.set(channel)
|
|
41
|
+
first.close()
|
|
42
|
+
|
|
43
|
+
const second = createSqliteChannelStore({ namespace: 'https://api.example.com', path })
|
|
44
|
+
expect(second.get(entryKey(channel))).toEqual(channel)
|
|
45
|
+
second.set({ ...channel, cumulativeAmount: 1_000_000n, deposit: 5_000_000n })
|
|
46
|
+
expect(second.get(entryKey(channel))).toMatchObject({
|
|
47
|
+
cumulativeAmount: 2_000_000n,
|
|
48
|
+
deposit: 10_000_000n,
|
|
49
|
+
})
|
|
50
|
+
second.close()
|
|
51
|
+
} finally {
|
|
52
|
+
rmSync(directory, { recursive: true })
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('rehydrates an existing wallet-cli v2 channel row', () => {
|
|
57
|
+
const directory = mkdtempSync(join(tmpdir(), 'mppx-wallet-channels-'))
|
|
58
|
+
const path = join(directory, 'channels.db')
|
|
59
|
+
try {
|
|
60
|
+
const database = new DatabaseSync(path)
|
|
61
|
+
database.exec(`CREATE TABLE channels (
|
|
62
|
+
channel_id TEXT PRIMARY KEY,
|
|
63
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
64
|
+
origin TEXT NOT NULL,
|
|
65
|
+
request_url TEXT NOT NULL DEFAULT '',
|
|
66
|
+
chain_id INTEGER NOT NULL,
|
|
67
|
+
escrow_contract TEXT NOT NULL,
|
|
68
|
+
token TEXT NOT NULL,
|
|
69
|
+
payee TEXT NOT NULL,
|
|
70
|
+
payer TEXT NOT NULL,
|
|
71
|
+
authorized_signer TEXT NOT NULL,
|
|
72
|
+
salt TEXT NOT NULL,
|
|
73
|
+
deposit TEXT NOT NULL,
|
|
74
|
+
cumulative_amount TEXT NOT NULL,
|
|
75
|
+
challenge_echo TEXT NOT NULL,
|
|
76
|
+
state TEXT NOT NULL DEFAULT 'active',
|
|
77
|
+
close_requested_at INTEGER NOT NULL DEFAULT 0,
|
|
78
|
+
grace_ready_at INTEGER NOT NULL DEFAULT 0,
|
|
79
|
+
created_at INTEGER NOT NULL,
|
|
80
|
+
last_used_at INTEGER NOT NULL,
|
|
81
|
+
accepted_cumulative TEXT NOT NULL DEFAULT '0',
|
|
82
|
+
server_spent TEXT NOT NULL DEFAULT '0',
|
|
83
|
+
session_protocol TEXT NOT NULL DEFAULT 'v1',
|
|
84
|
+
descriptor_json TEXT
|
|
85
|
+
)`)
|
|
86
|
+
database
|
|
87
|
+
.prepare(`INSERT INTO channels (
|
|
88
|
+
channel_id, origin, chain_id, escrow_contract, token, payee, payer,
|
|
89
|
+
authorized_signer, salt, deposit, cumulative_amount, challenge_echo,
|
|
90
|
+
state, created_at, last_used_at, session_protocol, descriptor_json
|
|
91
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '{}', 'active', 1, 1, 'v2', ?)`)
|
|
92
|
+
.run(
|
|
93
|
+
channel.channelId,
|
|
94
|
+
'https://api.example.com',
|
|
95
|
+
channel.chainId,
|
|
96
|
+
channel.escrow,
|
|
97
|
+
channel.descriptor.token,
|
|
98
|
+
channel.descriptor.payee,
|
|
99
|
+
channel.descriptor.payer,
|
|
100
|
+
channel.descriptor.authorizedSigner,
|
|
101
|
+
channel.descriptor.salt,
|
|
102
|
+
channel.deposit.toString(),
|
|
103
|
+
channel.cumulativeAmount.toString(),
|
|
104
|
+
JSON.stringify(channel.descriptor),
|
|
105
|
+
)
|
|
106
|
+
database.close()
|
|
107
|
+
|
|
108
|
+
const store = createSqliteChannelStore({ namespace: 'https://api.example.com', path })
|
|
109
|
+
expect(store.get(entryKey(channel))).toEqual(channel)
|
|
110
|
+
store.close()
|
|
111
|
+
} finally {
|
|
112
|
+
rmSync(directory, { recursive: true })
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
5
|
+
|
|
6
|
+
import type { ChannelEntry } from '../tempo/session/client/ChannelOps.js'
|
|
7
|
+
import {
|
|
8
|
+
deserializeEntry,
|
|
9
|
+
entryKey,
|
|
10
|
+
serializeEntry,
|
|
11
|
+
type ChannelStore,
|
|
12
|
+
type StoredChannel,
|
|
13
|
+
} from '../tempo/session/client/ChannelStore.js'
|
|
14
|
+
|
|
15
|
+
const schemaVersion = 2
|
|
16
|
+
|
|
17
|
+
/** Options for the Node SQLite-backed payer channel store. */
|
|
18
|
+
export type SqliteChannelStoreOptions = {
|
|
19
|
+
/** Service namespace, normally the protected API origin. */
|
|
20
|
+
namespace?: string | undefined
|
|
21
|
+
/** SQLite file path. Defaults to Tempo Wallet's shared channel database. */
|
|
22
|
+
path?: string | undefined
|
|
23
|
+
/** Full protected URL retained for CLI session-management requests. */
|
|
24
|
+
requestUrl?: string | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A SQLite-backed channel store that can release its database handle. */
|
|
28
|
+
export type SqliteChannelStore = ChannelStore & {
|
|
29
|
+
/** Absolute or caller-supplied path opened by this store. */
|
|
30
|
+
readonly path: string
|
|
31
|
+
/** Closes the underlying SQLite connection. */
|
|
32
|
+
close(): void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ChannelRow = {
|
|
36
|
+
chain_id: number
|
|
37
|
+
channel_id: string
|
|
38
|
+
cumulative_amount: string
|
|
39
|
+
deposit: string
|
|
40
|
+
descriptor_json: string | null
|
|
41
|
+
entry_json: string | null
|
|
42
|
+
escrow_contract: string
|
|
43
|
+
state: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Returns the channel database shared by Tempo command-line applications. */
|
|
47
|
+
export function defaultChannelDatabasePath(): string {
|
|
48
|
+
return join(homedir(), '.tempo', 'wallet', 'channels.db')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a synchronous Node SQLite implementation of {@link ChannelStore}.
|
|
53
|
+
*
|
|
54
|
+
* The schema is compatible with Tempo Wallet's existing `channels` table, so
|
|
55
|
+
* a fresh MPPx client can reuse v2 session records without a separate migration
|
|
56
|
+
* command. A namespace keeps identical payment scopes at different services
|
|
57
|
+
* isolated from one another.
|
|
58
|
+
*/
|
|
59
|
+
export function createSqliteChannelStore(
|
|
60
|
+
options: SqliteChannelStoreOptions = {},
|
|
61
|
+
): SqliteChannelStore {
|
|
62
|
+
const path = options.path ?? defaultChannelDatabasePath()
|
|
63
|
+
const namespace = options.namespace ?? ''
|
|
64
|
+
const requestUrl = options.requestUrl ?? namespace
|
|
65
|
+
const origin = resolveOrigin(requestUrl, namespace)
|
|
66
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
67
|
+
const database = new DatabaseSync(path)
|
|
68
|
+
database.exec('PRAGMA journal_mode = WAL')
|
|
69
|
+
database.exec('PRAGMA busy_timeout = 5000')
|
|
70
|
+
ensureSchema(database)
|
|
71
|
+
|
|
72
|
+
const getRow = database.prepare(`SELECT channel_id, chain_id, escrow_contract,
|
|
73
|
+
cumulative_amount, deposit, descriptor_json, entry_json, state
|
|
74
|
+
FROM channels
|
|
75
|
+
WHERE scope_key = ?`)
|
|
76
|
+
const deleteScope = database.prepare('DELETE FROM channels WHERE scope_key = ?')
|
|
77
|
+
const deleteOtherScopeChannel = database.prepare(
|
|
78
|
+
'DELETE FROM channels WHERE scope_key = ? AND channel_id <> ?',
|
|
79
|
+
)
|
|
80
|
+
const upsert = database.prepare(`INSERT INTO channels (
|
|
81
|
+
channel_id, version, scope_key, origin, request_url, chain_id, escrow_contract, token,
|
|
82
|
+
payee, payer, authorized_signer, salt, session_protocol, descriptor_json, entry_json,
|
|
83
|
+
deposit, cumulative_amount, accepted_cumulative, challenge_echo, state,
|
|
84
|
+
close_requested_at, grace_ready_at, created_at, last_used_at, server_spent
|
|
85
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'v2', ?, ?, ?, ?, '0', '{}', 'active',
|
|
86
|
+
0, 0, ?, ?, '0')
|
|
87
|
+
ON CONFLICT(channel_id) DO UPDATE SET
|
|
88
|
+
version = excluded.version,
|
|
89
|
+
scope_key = excluded.scope_key,
|
|
90
|
+
origin = excluded.origin,
|
|
91
|
+
request_url = excluded.request_url,
|
|
92
|
+
chain_id = excluded.chain_id,
|
|
93
|
+
escrow_contract = excluded.escrow_contract,
|
|
94
|
+
token = excluded.token,
|
|
95
|
+
payee = excluded.payee,
|
|
96
|
+
payer = excluded.payer,
|
|
97
|
+
authorized_signer = excluded.authorized_signer,
|
|
98
|
+
salt = excluded.salt,
|
|
99
|
+
session_protocol = excluded.session_protocol,
|
|
100
|
+
descriptor_json = excluded.descriptor_json,
|
|
101
|
+
entry_json = excluded.entry_json,
|
|
102
|
+
deposit = excluded.deposit,
|
|
103
|
+
cumulative_amount = excluded.cumulative_amount,
|
|
104
|
+
state = 'active',
|
|
105
|
+
close_requested_at = 0,
|
|
106
|
+
last_used_at = excluded.last_used_at`)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
path,
|
|
110
|
+
get(key) {
|
|
111
|
+
const row = getRow.get(scopedKey(namespace, key)) as ChannelRow | undefined
|
|
112
|
+
return row ? entryFromRow(row) : undefined
|
|
113
|
+
},
|
|
114
|
+
set(entry) {
|
|
115
|
+
const scopeKey = scopedKey(namespace, entryKey(entry))
|
|
116
|
+
database.exec('BEGIN IMMEDIATE')
|
|
117
|
+
try {
|
|
118
|
+
const existing = getRow.get(scopeKey) as ChannelRow | undefined
|
|
119
|
+
const merged = mergeEntry(existing ? entryFromRow(existing) : undefined, entry)
|
|
120
|
+
const stored = serializeEntry(merged)
|
|
121
|
+
const now = Math.floor(Date.now() / 1_000)
|
|
122
|
+
deleteOtherScopeChannel.run(scopeKey, merged.channelId)
|
|
123
|
+
upsert.run(
|
|
124
|
+
merged.channelId,
|
|
125
|
+
schemaVersion,
|
|
126
|
+
scopeKey,
|
|
127
|
+
origin,
|
|
128
|
+
requestUrl,
|
|
129
|
+
merged.chainId,
|
|
130
|
+
merged.escrow,
|
|
131
|
+
merged.descriptor.token,
|
|
132
|
+
merged.descriptor.payee,
|
|
133
|
+
merged.descriptor.payer,
|
|
134
|
+
merged.descriptor.authorizedSigner,
|
|
135
|
+
merged.descriptor.salt,
|
|
136
|
+
JSON.stringify(merged.descriptor),
|
|
137
|
+
JSON.stringify(stored),
|
|
138
|
+
merged.deposit.toString(),
|
|
139
|
+
merged.cumulativeAmount.toString(),
|
|
140
|
+
now,
|
|
141
|
+
now,
|
|
142
|
+
)
|
|
143
|
+
database.exec('COMMIT')
|
|
144
|
+
} catch (error) {
|
|
145
|
+
database.exec('ROLLBACK')
|
|
146
|
+
throw error
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
delete(key) {
|
|
150
|
+
deleteScope.run(scopedKey(namespace, key))
|
|
151
|
+
},
|
|
152
|
+
close() {
|
|
153
|
+
database.close()
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveOrigin(requestUrl: string, fallback: string): string {
|
|
159
|
+
try {
|
|
160
|
+
return new URL(requestUrl).origin
|
|
161
|
+
} catch {
|
|
162
|
+
return fallback
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function scopedKey(namespace: string, key: string): string {
|
|
167
|
+
return `${namespace}\n${key}`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function mergeEntry(current: ChannelEntry | undefined, incoming: ChannelEntry): ChannelEntry {
|
|
171
|
+
if (!current || current.channelId.toLowerCase() !== incoming.channelId.toLowerCase())
|
|
172
|
+
return incoming
|
|
173
|
+
return {
|
|
174
|
+
...incoming,
|
|
175
|
+
cumulativeAmount:
|
|
176
|
+
current.cumulativeAmount > incoming.cumulativeAmount
|
|
177
|
+
? current.cumulativeAmount
|
|
178
|
+
: incoming.cumulativeAmount,
|
|
179
|
+
deposit: current.deposit > incoming.deposit ? current.deposit : incoming.deposit,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function entryFromRow(row: ChannelRow): ChannelEntry {
|
|
184
|
+
if (row.entry_json) {
|
|
185
|
+
const stored = JSON.parse(row.entry_json) as StoredChannel
|
|
186
|
+
return deserializeEntry({ ...stored, opened: row.state === 'active' })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!row.descriptor_json) throw new Error('v2 channel row is missing its descriptor')
|
|
190
|
+
const descriptor = JSON.parse(row.descriptor_json) as ChannelEntry['descriptor']
|
|
191
|
+
return {
|
|
192
|
+
channelId: row.channel_id as ChannelEntry['channelId'],
|
|
193
|
+
cumulativeAmount: BigInt(row.cumulative_amount),
|
|
194
|
+
deposit: BigInt(row.deposit),
|
|
195
|
+
descriptor,
|
|
196
|
+
escrow: row.escrow_contract as ChannelEntry['escrow'],
|
|
197
|
+
chainId: row.chain_id,
|
|
198
|
+
opened: row.state === 'active',
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function ensureSchema(database: DatabaseSync): void {
|
|
203
|
+
database.exec(`CREATE TABLE IF NOT EXISTS channels (
|
|
204
|
+
channel_id TEXT PRIMARY KEY,
|
|
205
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
206
|
+
scope_key TEXT,
|
|
207
|
+
origin TEXT NOT NULL,
|
|
208
|
+
request_url TEXT NOT NULL DEFAULT '',
|
|
209
|
+
chain_id INTEGER NOT NULL,
|
|
210
|
+
escrow_contract TEXT NOT NULL,
|
|
211
|
+
token TEXT NOT NULL,
|
|
212
|
+
payee TEXT NOT NULL,
|
|
213
|
+
payer TEXT NOT NULL,
|
|
214
|
+
authorized_signer TEXT NOT NULL,
|
|
215
|
+
salt TEXT NOT NULL,
|
|
216
|
+
deposit TEXT NOT NULL,
|
|
217
|
+
cumulative_amount TEXT NOT NULL,
|
|
218
|
+
challenge_echo TEXT NOT NULL,
|
|
219
|
+
state TEXT NOT NULL DEFAULT 'active',
|
|
220
|
+
close_requested_at INTEGER NOT NULL DEFAULT 0,
|
|
221
|
+
grace_ready_at INTEGER NOT NULL DEFAULT 0,
|
|
222
|
+
created_at INTEGER NOT NULL,
|
|
223
|
+
last_used_at INTEGER NOT NULL,
|
|
224
|
+
accepted_cumulative TEXT NOT NULL DEFAULT '0',
|
|
225
|
+
server_spent TEXT NOT NULL DEFAULT '0',
|
|
226
|
+
session_protocol TEXT NOT NULL DEFAULT 'v1',
|
|
227
|
+
descriptor_json TEXT,
|
|
228
|
+
entry_json TEXT
|
|
229
|
+
)`)
|
|
230
|
+
addColumn(database, 'scope_key TEXT')
|
|
231
|
+
addColumn(database, 'entry_json TEXT')
|
|
232
|
+
database.exec(`UPDATE channels
|
|
233
|
+
SET scope_key = origin || char(10) || lower(payee) || ':' || lower(token) || ':' ||
|
|
234
|
+
lower(escrow_contract) || ':' || chain_id
|
|
235
|
+
WHERE scope_key IS NULL AND session_protocol = 'v2' AND descriptor_json IS NOT NULL`)
|
|
236
|
+
database.exec(
|
|
237
|
+
'CREATE UNIQUE INDEX IF NOT EXISTS idx_channels_scope_key ON channels(scope_key) WHERE scope_key IS NOT NULL',
|
|
238
|
+
)
|
|
239
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_channels_origin ON channels(origin)')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function addColumn(database: DatabaseSync, definition: string): void {
|
|
243
|
+
const name = definition.slice(0, definition.indexOf(' '))
|
|
244
|
+
const columns = database.prepare('PRAGMA table_info(channels)').all() as Array<{ name: string }>
|
|
245
|
+
if (columns.some((column) => column.name === name)) return
|
|
246
|
+
database.exec(`ALTER TABLE channels ADD COLUMN ${definition}`)
|
|
247
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { expectTypeOf, test } from 'vp/test'
|
|
2
|
+
|
|
3
|
+
import type { ConfiguredDefaults } from './types.js'
|
|
4
|
+
|
|
5
|
+
test('ConfiguredDefaults retains only known defined values', () => {
|
|
6
|
+
type Parameters = {
|
|
7
|
+
configured: string
|
|
8
|
+
maybeDefined?: number | undefined
|
|
9
|
+
explicitlyUndefined: boolean | undefined
|
|
10
|
+
unrelated: bigint
|
|
11
|
+
}
|
|
12
|
+
type Defaults = {
|
|
13
|
+
configured?: string | undefined
|
|
14
|
+
maybeDefined?: number | undefined
|
|
15
|
+
explicitlyUndefined?: boolean | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
expectTypeOf<ConfiguredDefaults<Parameters, Defaults>>().toEqualTypeOf<{
|
|
19
|
+
configured: string
|
|
20
|
+
}>()
|
|
21
|
+
})
|
package/src/internal/types.ts
CHANGED
|
@@ -277,6 +277,39 @@ export type ExactPartial<type> = {
|
|
|
277
277
|
[key in keyof type]?: type[key] | undefined
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Keys whose values are known to be present and defined.
|
|
282
|
+
*
|
|
283
|
+
* This is useful when a factory accepts partial configuration and its return
|
|
284
|
+
* type needs to distinguish values supplied as defaults from fields that
|
|
285
|
+
* callers must still provide later. Optional and `undefined`-able properties
|
|
286
|
+
* are excluded because their runtime values cannot be assumed to be defaults.
|
|
287
|
+
*
|
|
288
|
+
* @internal
|
|
289
|
+
*/
|
|
290
|
+
export type DefinedKeys<type> = {
|
|
291
|
+
[key in keyof type]-?: {} extends Pick<type, key>
|
|
292
|
+
? never
|
|
293
|
+
: undefined extends type[key]
|
|
294
|
+
? never
|
|
295
|
+
: key
|
|
296
|
+
}[keyof type]
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Request defaults known to have been configured by a factory caller.
|
|
300
|
+
*
|
|
301
|
+
* Optional and `undefined`-able parameters are omitted: their runtime values
|
|
302
|
+
* are not guaranteed to be usable defaults for a subsequently returned
|
|
303
|
+
* handler. Factories can intersect this type with values they resolve
|
|
304
|
+
* unconditionally at runtime.
|
|
305
|
+
*
|
|
306
|
+
* @internal
|
|
307
|
+
*/
|
|
308
|
+
export type ConfiguredDefaults<parameters, defaults> = Pick<
|
|
309
|
+
parameters,
|
|
310
|
+
Extract<DefinedKeys<parameters>, keyof defaults>
|
|
311
|
+
>
|
|
312
|
+
|
|
280
313
|
/** @internal */
|
|
281
314
|
export type ExactRequired<type> = {
|
|
282
315
|
[key in keyof type]-?: Exclude<type[key], undefined>
|
|
@@ -313,4 +313,46 @@ describe('Mppx type tests', () => {
|
|
|
313
313
|
}),
|
|
314
314
|
).toBeFunction()
|
|
315
315
|
})
|
|
316
|
+
|
|
317
|
+
test('tempo factories expose resolved currency and decimals as defaults', () => {
|
|
318
|
+
const charge = Mppx.create({ methods: [tempo.charge()], realm, secretKey })
|
|
319
|
+
expectTypeOf(charge.tempo.charge({ amount: '1' })).toBeFunction()
|
|
320
|
+
// @ts-expect-error amount is not supplied by Tempo's runtime defaults.
|
|
321
|
+
void charge.tempo.charge({})
|
|
322
|
+
|
|
323
|
+
const subscription = Mppx.create({
|
|
324
|
+
methods: [
|
|
325
|
+
tempo.subscription({
|
|
326
|
+
recipient: '0x1234567890abcdef1234567890abcdef12345678',
|
|
327
|
+
resolve: async () => null,
|
|
328
|
+
}),
|
|
329
|
+
],
|
|
330
|
+
realm,
|
|
331
|
+
secretKey,
|
|
332
|
+
})
|
|
333
|
+
expectTypeOf(
|
|
334
|
+
subscription.tempo.subscription({
|
|
335
|
+
amount: '1',
|
|
336
|
+
periodCount: 1n,
|
|
337
|
+
periodUnit: 'day',
|
|
338
|
+
subscriptionExpires: new Date('2026-01-01T00:00:00Z'),
|
|
339
|
+
}),
|
|
340
|
+
).toBeFunction()
|
|
341
|
+
|
|
342
|
+
const session = Mppx.create({ methods: [tempo.session()], realm, secretKey })
|
|
343
|
+
expectTypeOf(session.tempo.session({ amount: '1', unitType: 'request' })).toBeFunction()
|
|
344
|
+
|
|
345
|
+
const common = Mppx.create({
|
|
346
|
+
methods: [
|
|
347
|
+
tempo.common({
|
|
348
|
+
amount: '1',
|
|
349
|
+
recipient: '0x1234567890abcdef1234567890abcdef12345678',
|
|
350
|
+
}),
|
|
351
|
+
],
|
|
352
|
+
realm,
|
|
353
|
+
secretKey,
|
|
354
|
+
})
|
|
355
|
+
expectTypeOf(common.tempo.charge({})).toBeFunction()
|
|
356
|
+
expectTypeOf(common.tempo.session({ unitType: 'request' })).toBeFunction()
|
|
357
|
+
})
|
|
316
358
|
})
|
|
@@ -35,6 +35,16 @@ describe('charge', () => {
|
|
|
35
35
|
expect(result.success).toBe(false)
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
+
test('schema: requires decimals when no server default supplies it', () => {
|
|
39
|
+
const result = Methods.charge.schema.request.safeParse({
|
|
40
|
+
amount: '1',
|
|
41
|
+
currency: 'usd',
|
|
42
|
+
networkId: 'profile_123',
|
|
43
|
+
paymentMethodTypes: ['card'],
|
|
44
|
+
})
|
|
45
|
+
expect(result.success).toBe(false)
|
|
46
|
+
})
|
|
47
|
+
|
|
38
48
|
test('schema: validates spt payload', () => {
|
|
39
49
|
const result = Methods.charge.schema.credential.payload.safeParse({
|
|
40
50
|
spt: 'spt_test_123',
|