mppx 0.8.5 → 0.8.7
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 +25 -0
- package/dist/Receipt.d.ts +20 -2
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js +19 -12
- package/dist/Receipt.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +6 -4
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/internal.js +2 -2
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/plugins/evm.d.ts +2 -0
- package/dist/cli/plugins/evm.d.ts.map +1 -0
- package/dist/cli/plugins/evm.js +51 -0
- package/dist/cli/plugins/evm.js.map +1 -0
- package/dist/cli/plugins/index.d.ts +1 -0
- package/dist/cli/plugins/index.d.ts.map +1 -1
- package/dist/cli/plugins/index.js +1 -0
- package/dist/cli/plugins/index.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +4 -2
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/cli/validate/challenge.d.ts +2 -0
- package/dist/cli/validate/challenge.d.ts.map +1 -1
- package/dist/cli/validate/challenge.js +184 -78
- package/dist/cli/validate/challenge.js.map +1 -1
- package/dist/cli/validate/discovery.d.ts +1 -0
- package/dist/cli/validate/discovery.d.ts.map +1 -1
- package/dist/cli/validate/discovery.js +99 -35
- package/dist/cli/validate/discovery.js.map +1 -1
- package/dist/cli/validate/helpers.d.ts +14 -1
- package/dist/cli/validate/helpers.d.ts.map +1 -1
- package/dist/cli/validate/helpers.js +0 -13
- package/dist/cli/validate/helpers.js.map +1 -1
- package/dist/cli/validate/index.d.ts +1 -0
- package/dist/cli/validate/index.d.ts.map +1 -1
- package/dist/cli/validate/index.js +98 -151
- package/dist/cli/validate/index.js.map +1 -1
- package/dist/cli/validate/payment.d.ts +1 -0
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +215 -144
- package/dist/cli/validate/payment.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +6 -1
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/evm/Chains.d.ts +4 -0
- package/dist/evm/Chains.d.ts.map +1 -1
- package/dist/evm/Chains.js +4 -0
- package/dist/evm/Chains.js.map +1 -1
- package/dist/internal/AcceptPayment.d.ts.map +1 -1
- package/dist/internal/AcceptPayment.js +7 -1
- package/dist/internal/AcceptPayment.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/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +6 -2
- package/dist/tempo/client/Charge.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/server/internal/request-body.d.ts.map +1 -1
- package/dist/tempo/server/internal/request-body.js +2 -0
- package/dist/tempo/server/internal/request-body.js.map +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts +26 -2
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +38 -5
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/server/ChannelStore.js +4 -4
- package/dist/tempo/session/server/ChannelStore.js.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.d.ts +8 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
- package/dist/tempo/session/server/CredentialVerification.js +21 -1
- package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
- package/dist/tempo/session/server/Session.d.ts.map +1 -1
- package/dist/tempo/session/server/Session.js +1 -0
- package/dist/tempo/session/server/Session.js.map +1 -1
- package/dist/validation/core.d.ts +79 -0
- package/dist/validation/core.d.ts.map +1 -0
- package/dist/validation/core.js +256 -0
- package/dist/validation/core.js.map +1 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/x402/Assets.d.ts +9 -0
- package/dist/x402/Assets.d.ts.map +1 -1
- package/dist/x402/Assets.js +38 -0
- package/dist/x402/Assets.js.map +1 -1
- package/package.json +9 -4
- package/src/Receipt.test.ts +53 -0
- package/src/Receipt.ts +26 -13
- package/src/cli/cli.test.ts +41 -5
- package/src/cli/cli.ts +7 -4
- package/src/cli/internal.ts +2 -2
- package/src/cli/plugins/evm.ts +55 -0
- package/src/cli/plugins/index.ts +1 -0
- package/src/cli/plugins/tempo.ts +6 -2
- package/src/cli/validate/challenge.ts +334 -121
- package/src/cli/validate/discovery.test.ts +307 -0
- package/src/cli/validate/discovery.ts +104 -38
- package/src/cli/validate/helpers.ts +15 -13
- package/src/cli/validate/index.ts +117 -240
- package/src/cli/validate/payment.ts +261 -178
- package/src/cli/validate.test.ts +143 -3
- package/src/client/internal/Fetch.test.ts +186 -2
- package/src/client/internal/Fetch.ts +5 -1
- package/src/evm/Chains.ts +6 -0
- package/src/evm/PublicInterface.test-d.ts +7 -0
- package/src/internal/AcceptPayment.test.ts +45 -0
- package/src/internal/AcceptPayment.ts +11 -1
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/client/Charge.test.ts +61 -0
- package/src/tempo/client/Charge.ts +6 -1
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/request-body.test.ts +9 -0
- package/src/tempo/server/internal/request-body.ts +2 -0
- package/src/tempo/session/precompile/Chain.test.ts +121 -2
- package/src/tempo/session/precompile/Chain.ts +56 -6
- package/src/tempo/session/server/ChannelStore.test.ts +17 -0
- package/src/tempo/session/server/ChannelStore.ts +4 -4
- package/src/tempo/session/server/CredentialVerification.test.ts +118 -1
- package/src/tempo/session/server/CredentialVerification.ts +30 -0
- package/src/tempo/session/server/Session.integration.test.ts +5 -0
- package/src/tempo/session/server/Session.test.ts +119 -53
- package/src/tempo/session/server/Session.ts +1 -0
- package/src/validation/core.ts +348 -0
- package/src/validation/index.ts +11 -0
- package/src/x402/Assets.test.ts +42 -0
- package/src/x402/Assets.ts +40 -0
- package/src/x402/Exact.localnet.test.ts +475 -0
package/src/x402/Assets.test.ts
CHANGED
|
@@ -208,4 +208,46 @@ describe('x402 assets', () => {
|
|
|
208
208
|
},
|
|
209
209
|
})
|
|
210
210
|
})
|
|
211
|
+
|
|
212
|
+
test('exports Celo USDC metadata', () => {
|
|
213
|
+
expect(Assets.isAsset(Assets.celo.USDC)).toBe(true)
|
|
214
|
+
expect(Assets.celo.USDC).toMatchObject({
|
|
215
|
+
address: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C',
|
|
216
|
+
decimals: 6,
|
|
217
|
+
network: 'eip155:42220',
|
|
218
|
+
transfer: {
|
|
219
|
+
name: 'USDC',
|
|
220
|
+
type: 'eip3009',
|
|
221
|
+
version: '2',
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('exports Celo USDT metadata', () => {
|
|
227
|
+
expect(Assets.isAsset(Assets.celo.USDT)).toBe(true)
|
|
228
|
+
expect(Assets.celo.USDT).toMatchObject({
|
|
229
|
+
address: '0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e',
|
|
230
|
+
decimals: 6,
|
|
231
|
+
network: 'eip155:42220',
|
|
232
|
+
transfer: {
|
|
233
|
+
name: 'Tether USD',
|
|
234
|
+
type: 'eip3009',
|
|
235
|
+
version: '1',
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('exports Celo Sepolia USDC metadata', () => {
|
|
241
|
+
expect(Assets.isAsset(Assets.celoSepolia.USDC)).toBe(true)
|
|
242
|
+
expect(Assets.celoSepolia.USDC).toMatchObject({
|
|
243
|
+
address: '0x01C5C0122039549AD1493B8220cABEdD739BC44E',
|
|
244
|
+
decimals: 6,
|
|
245
|
+
network: 'eip155:11142220',
|
|
246
|
+
transfer: {
|
|
247
|
+
name: 'USDC',
|
|
248
|
+
type: 'eip3009',
|
|
249
|
+
version: '2',
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
})
|
|
211
253
|
})
|
package/src/x402/Assets.ts
CHANGED
|
@@ -91,6 +91,46 @@ export const baseSepolia = {
|
|
|
91
91
|
}),
|
|
92
92
|
} as const
|
|
93
93
|
|
|
94
|
+
/** Celo network known assets. */
|
|
95
|
+
export const celo = {
|
|
96
|
+
USDC: define({
|
|
97
|
+
address: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C',
|
|
98
|
+
decimals: 6,
|
|
99
|
+
network: 'eip155:42220',
|
|
100
|
+
transfer: {
|
|
101
|
+
// Celo USDC signs with the shorter EIP-712 domain name.
|
|
102
|
+
name: 'USDC',
|
|
103
|
+
type: 'eip3009',
|
|
104
|
+
version: '2',
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
USDT: define({
|
|
108
|
+
address: '0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e',
|
|
109
|
+
decimals: 6,
|
|
110
|
+
network: 'eip155:42220',
|
|
111
|
+
transfer: {
|
|
112
|
+
// Celo USDT signs with domain version "1", unlike Circle's FiatToken "2".
|
|
113
|
+
name: 'Tether USD',
|
|
114
|
+
type: 'eip3009',
|
|
115
|
+
version: '1',
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
} as const
|
|
119
|
+
|
|
120
|
+
/** Celo Sepolia known assets. */
|
|
121
|
+
export const celoSepolia = {
|
|
122
|
+
USDC: define({
|
|
123
|
+
address: '0x01C5C0122039549AD1493B8220cABEdD739BC44E',
|
|
124
|
+
decimals: 6,
|
|
125
|
+
network: 'eip155:11142220',
|
|
126
|
+
transfer: {
|
|
127
|
+
name: 'USDC',
|
|
128
|
+
type: 'eip3009',
|
|
129
|
+
version: '2',
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
} as const
|
|
133
|
+
|
|
94
134
|
/** Returns true when a value is known x402 asset metadata. */
|
|
95
135
|
export function isAsset(value: unknown): value is KnownAsset {
|
|
96
136
|
if (typeof value !== 'object' || value === null) return false
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import type { IncomingMessage } from 'node:http'
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
4
|
+
|
|
5
|
+
import { evm as evmClient, Mppx as ClientMppx } from 'mppx/client'
|
|
6
|
+
import { evm, Mppx as ServerMppx, NodeListener, Request as ServerRequest } from 'mppx/server'
|
|
7
|
+
import type { Abi, Address, Hex } from 'viem'
|
|
8
|
+
import {
|
|
9
|
+
createClient,
|
|
10
|
+
defineChain,
|
|
11
|
+
getAddress,
|
|
12
|
+
http as viem_http,
|
|
13
|
+
parseSignature,
|
|
14
|
+
parseUnits,
|
|
15
|
+
recoverTypedDataAddress,
|
|
16
|
+
} from 'viem'
|
|
17
|
+
import { mnemonicToAccount } from 'viem/accounts'
|
|
18
|
+
import {
|
|
19
|
+
deployContract,
|
|
20
|
+
readContract,
|
|
21
|
+
waitForTransactionReceipt,
|
|
22
|
+
writeContract,
|
|
23
|
+
} from 'viem/actions'
|
|
24
|
+
import { describe, expect, test } from 'vp/test'
|
|
25
|
+
import * as Http from '~test/Http.js'
|
|
26
|
+
|
|
27
|
+
import * as evm_Types from '../evm/Types.js'
|
|
28
|
+
import * as Header from './Header.js'
|
|
29
|
+
import * as x402_ChallengeBrand from './internal/ChallengeBrand.js'
|
|
30
|
+
import * as Types from './Types.js'
|
|
31
|
+
|
|
32
|
+
const runLocalnet = process.env.X402_LOCALNET === 'true'
|
|
33
|
+
const describeLocalnet = runLocalnet ? describe : describe.skip
|
|
34
|
+
|
|
35
|
+
const chainId = 31_337
|
|
36
|
+
const rpcUrl = process.env.X402_ANVIL_RPC_URL ?? 'http://127.0.0.1:18546'
|
|
37
|
+
const mnemonic = 'test test test test test test test test test test test junk'
|
|
38
|
+
const payer = mnemonicToAccount(mnemonic, { accountIndex: 0 })
|
|
39
|
+
const recipient = mnemonicToAccount(mnemonic, { addressIndex: 1 })
|
|
40
|
+
const facilitatorAccount = mnemonicToAccount(mnemonic, { addressIndex: 2 })
|
|
41
|
+
const paymentAmount = parseUnits('0.01', 6)
|
|
42
|
+
|
|
43
|
+
const chain = defineChain({
|
|
44
|
+
id: chainId,
|
|
45
|
+
name: 'Anvil',
|
|
46
|
+
nativeCurrency: { decimals: 18, name: 'Ether', symbol: 'ETH' },
|
|
47
|
+
rpcUrls: { default: { http: [rpcUrl] } },
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const transport = viem_http(rpcUrl, { retryCount: 0, timeout: 10_000 })
|
|
51
|
+
const payerClient = createClient({ account: payer, chain, transport })
|
|
52
|
+
const facilitatorClient = createClient({ account: facilitatorAccount, chain, transport })
|
|
53
|
+
|
|
54
|
+
type ForgeArtifact = {
|
|
55
|
+
abi: Abi
|
|
56
|
+
bytecode: { object: Hex }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type Harness = {
|
|
60
|
+
artifact: ForgeArtifact
|
|
61
|
+
asset: ReturnType<typeof evm.assets.define>
|
|
62
|
+
facilitator: Http.TestServer
|
|
63
|
+
token: Address
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describeLocalnet('x402 exact localnet settlement', () => {
|
|
67
|
+
test('settles x402 exact EIP-3009 payment on localnet', async () => {
|
|
68
|
+
const harness = await setupHarness()
|
|
69
|
+
const paidServer = await createPaidServer(harness)
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const challenge = await fetch(`${paidServer.url}/paid`)
|
|
73
|
+
expect(challenge.status).toBe(402)
|
|
74
|
+
const paymentRequiredHeader = challenge.headers.get(Types.paymentRequiredHeader)
|
|
75
|
+
expect(paymentRequiredHeader).toBeTruthy()
|
|
76
|
+
|
|
77
|
+
const paymentRequired = Header.decodePaymentRequired(paymentRequiredHeader!)
|
|
78
|
+
expect(paymentRequired.x402Version).toBe(2)
|
|
79
|
+
expect(paymentRequired.accepts[0]).toMatchObject({
|
|
80
|
+
amount: paymentAmount.toString(),
|
|
81
|
+
asset: getAddress(harness.token),
|
|
82
|
+
network: `eip155:${chainId}`,
|
|
83
|
+
payTo: recipient.address,
|
|
84
|
+
scheme: 'exact',
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const payerBefore = await balanceOf(harness, payer.address)
|
|
88
|
+
const recipientBefore = await balanceOf(harness, recipient.address)
|
|
89
|
+
const payment = createX402Client(harness.asset)
|
|
90
|
+
const response = await payment.fetch(`${paidServer.url}/paid`)
|
|
91
|
+
|
|
92
|
+
if (response.status !== 200)
|
|
93
|
+
throw new Error(`Expected paid response, got ${response.status}: ${await response.text()}`)
|
|
94
|
+
expect(response.status).toBe(200)
|
|
95
|
+
expect(await response.text()).toBe('paid')
|
|
96
|
+
|
|
97
|
+
const paymentResponseHeader = response.headers.get(Types.paymentResponseHeader)
|
|
98
|
+
expect(paymentResponseHeader).toBeTruthy()
|
|
99
|
+
const paymentResponse = Header.decodePaymentResponse(paymentResponseHeader!)
|
|
100
|
+
expect(paymentResponse).toMatchObject({
|
|
101
|
+
network: `eip155:${chainId}`,
|
|
102
|
+
payer: getAddress(payer.address),
|
|
103
|
+
success: true,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const receipt = await waitForTransactionReceipt(payerClient, {
|
|
107
|
+
hash: paymentResponse.transaction as Hex,
|
|
108
|
+
})
|
|
109
|
+
expect(receipt.status).toBe('success')
|
|
110
|
+
expect(await balanceOf(harness, payer.address)).toBe(payerBefore - paymentAmount)
|
|
111
|
+
expect(await balanceOf(harness, recipient.address)).toBe(recipientBefore + paymentAmount)
|
|
112
|
+
} finally {
|
|
113
|
+
paidServer.close()
|
|
114
|
+
harness.facilitator.close()
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('rejects replayed localnet x402 payment', async () => {
|
|
119
|
+
const harness = await setupHarness()
|
|
120
|
+
const paidServer = await createPaidServer(harness)
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const payment = createX402Client(harness.asset)
|
|
124
|
+
const challenge = await payment.rawFetch(`${paidServer.url}/paid`)
|
|
125
|
+
expect(challenge.status).toBe(402)
|
|
126
|
+
|
|
127
|
+
const credential = await payment.createCredential(pureX402Challenge(challenge))
|
|
128
|
+
const recipientBefore = await balanceOf(harness, recipient.address)
|
|
129
|
+
|
|
130
|
+
const first = await payment.rawFetch(`${paidServer.url}/paid`, {
|
|
131
|
+
headers: { [Types.paymentSignatureHeader]: credential },
|
|
132
|
+
})
|
|
133
|
+
expect(first.status).toBe(200)
|
|
134
|
+
|
|
135
|
+
const second = await payment.rawFetch(`${paidServer.url}/paid`, {
|
|
136
|
+
headers: { [Types.paymentSignatureHeader]: credential },
|
|
137
|
+
})
|
|
138
|
+
expect(second.status).toBe(402)
|
|
139
|
+
expect(await balanceOf(harness, recipient.address)).toBe(recipientBefore + paymentAmount)
|
|
140
|
+
expect(facilitatorStats(harness.facilitator).settleRequests).toBe(1)
|
|
141
|
+
} finally {
|
|
142
|
+
paidServer.close()
|
|
143
|
+
harness.facilitator.close()
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
async function setupHarness(): Promise<Harness> {
|
|
149
|
+
const artifact = await loadArtifact()
|
|
150
|
+
const token = await deployToken(artifact)
|
|
151
|
+
const asset = evm.assets.define({
|
|
152
|
+
address: token,
|
|
153
|
+
decimals: 6,
|
|
154
|
+
network: `eip155:${chainId}`,
|
|
155
|
+
transfer: {
|
|
156
|
+
name: 'USDC',
|
|
157
|
+
type: 'eip3009',
|
|
158
|
+
version: '2',
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
await mint(artifact, token, payer.address, parseUnits('1000', 6))
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
artifact,
|
|
166
|
+
asset,
|
|
167
|
+
facilitator: await createFacilitator({ artifact, token }),
|
|
168
|
+
token,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function loadArtifact(): Promise<ForgeArtifact> {
|
|
173
|
+
try {
|
|
174
|
+
const path = new URL('../../_/foundry/out/TestUSDC.sol/TestUSDC.json', import.meta.url)
|
|
175
|
+
const artifact = JSON.parse(await readFile(path, 'utf8')) as ForgeArtifact
|
|
176
|
+
if (!artifact.bytecode.object) throw new Error()
|
|
177
|
+
return artifact
|
|
178
|
+
} catch {
|
|
179
|
+
throw new Error('Missing TestUSDC Forge artifact. Run `forge build` before localnet tests.')
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function deployToken(artifact: ForgeArtifact): Promise<Address> {
|
|
184
|
+
const hash = await deployContract(payerClient, {
|
|
185
|
+
abi: artifact.abi,
|
|
186
|
+
bytecode: artifact.bytecode.object,
|
|
187
|
+
})
|
|
188
|
+
const receipt = await waitForTransactionReceipt(payerClient, { hash })
|
|
189
|
+
if (!receipt.contractAddress) throw new Error('TestUSDC deploy did not return a contract.')
|
|
190
|
+
return receipt.contractAddress
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function mint(
|
|
194
|
+
artifact: ForgeArtifact,
|
|
195
|
+
token: Address,
|
|
196
|
+
to: Address,
|
|
197
|
+
amount: bigint,
|
|
198
|
+
): Promise<void> {
|
|
199
|
+
const hash = await writeContract(payerClient, {
|
|
200
|
+
abi: artifact.abi,
|
|
201
|
+
address: token,
|
|
202
|
+
functionName: 'mint',
|
|
203
|
+
args: [to, amount],
|
|
204
|
+
})
|
|
205
|
+
await waitForTransactionReceipt(payerClient, { hash })
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function balanceOf(harness: Harness, address: Address): Promise<bigint> {
|
|
209
|
+
return readContract(payerClient, {
|
|
210
|
+
abi: harness.artifact.abi,
|
|
211
|
+
address: harness.token,
|
|
212
|
+
functionName: 'balanceOf',
|
|
213
|
+
args: [address],
|
|
214
|
+
}) as Promise<bigint>
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function authorizationState(parameters: {
|
|
218
|
+
artifact: ForgeArtifact
|
|
219
|
+
from: Address
|
|
220
|
+
nonce: Hex
|
|
221
|
+
token: Address
|
|
222
|
+
}): Promise<boolean> {
|
|
223
|
+
return readContract(payerClient, {
|
|
224
|
+
abi: parameters.artifact.abi,
|
|
225
|
+
address: parameters.token,
|
|
226
|
+
functionName: 'authorizationState',
|
|
227
|
+
args: [parameters.from, parameters.nonce],
|
|
228
|
+
}) as Promise<boolean>
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function createPaidServer(harness: Harness): Promise<Http.TestServer> {
|
|
232
|
+
const payment = ServerMppx.create({
|
|
233
|
+
methods: [
|
|
234
|
+
evm.charge({
|
|
235
|
+
currency: harness.asset,
|
|
236
|
+
recipient: recipient.address,
|
|
237
|
+
x402: { facilitator: harness.facilitator.url },
|
|
238
|
+
}),
|
|
239
|
+
],
|
|
240
|
+
secretKey: 'x402-localnet-secret-key-32-bytes',
|
|
241
|
+
})
|
|
242
|
+
const paid = payment.evm.charge({ amount: '0.01', description: 'localnet x402' })
|
|
243
|
+
|
|
244
|
+
return Http.createServer(async (req, res) => {
|
|
245
|
+
if (req.url !== '/paid') {
|
|
246
|
+
return NodeListener.sendResponse(res, new Response('not found', { status: 404 }))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const result = await paid(ServerRequest.fromNodeListener(req, res))
|
|
250
|
+
if (result.status === 402) return NodeListener.sendResponse(res, result.challenge)
|
|
251
|
+
return NodeListener.sendResponse(res, result.withReceipt(new Response('paid')))
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function createX402Client(asset: ReturnType<typeof evm.assets.define>) {
|
|
256
|
+
return ClientMppx.create({
|
|
257
|
+
methods: [
|
|
258
|
+
evmClient.charge({
|
|
259
|
+
account: payer,
|
|
260
|
+
currencies: [asset],
|
|
261
|
+
maxAmount: '0.02',
|
|
262
|
+
networks: [chainId],
|
|
263
|
+
}),
|
|
264
|
+
],
|
|
265
|
+
orderChallenges: (candidates) =>
|
|
266
|
+
[...candidates].sort(
|
|
267
|
+
(a, b) =>
|
|
268
|
+
Number(x402_ChallengeBrand.is(b.challenge)) - Number(x402_ChallengeBrand.is(a.challenge)),
|
|
269
|
+
),
|
|
270
|
+
polyfill: false,
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function createFacilitator(parameters: {
|
|
275
|
+
artifact: ForgeArtifact
|
|
276
|
+
token: Address
|
|
277
|
+
}): Promise<Http.TestServer> {
|
|
278
|
+
const stats = { settleRequests: 0, verifyRequests: 0 }
|
|
279
|
+
const server = await Http.createServer(async (req, res) => {
|
|
280
|
+
if (req.method !== 'POST') {
|
|
281
|
+
return NodeListener.sendResponse(res, new Response('not found', { status: 404 }))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const body = await readJson(req)
|
|
285
|
+
if (req.url === '/verify') {
|
|
286
|
+
stats.verifyRequests++
|
|
287
|
+
const verified = await verifyPayment({ ...parameters, body })
|
|
288
|
+
return NodeListener.sendResponse(res, Response.json(verified))
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (req.url === '/settle') {
|
|
292
|
+
stats.settleRequests++
|
|
293
|
+
const settled = await settlePayment({ ...parameters, body })
|
|
294
|
+
return NodeListener.sendResponse(res, Response.json(settled))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return NodeListener.sendResponse(res, new Response('not found', { status: 404 }))
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
return Object.assign(server, { x402LocalnetStats: stats })
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function readJson(req: IncomingMessage) {
|
|
304
|
+
const chunks: Buffer[] = []
|
|
305
|
+
for await (const chunk of req) chunks.push(Buffer.from(chunk))
|
|
306
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function verifyPayment(parameters: {
|
|
310
|
+
artifact: ForgeArtifact
|
|
311
|
+
body: unknown
|
|
312
|
+
token: Address
|
|
313
|
+
}): Promise<Types.VerifyResponse> {
|
|
314
|
+
try {
|
|
315
|
+
const { paymentPayload, paymentRequirements, x402Version } = parseFacilitatorRequest(
|
|
316
|
+
parameters.body,
|
|
317
|
+
)
|
|
318
|
+
if (x402Version !== 2) return invalid('unsupported x402 version')
|
|
319
|
+
if (!isDeepStrictEqual(paymentPayload.accepted, paymentRequirements))
|
|
320
|
+
return invalid('payment payload accepted requirements mismatch')
|
|
321
|
+
if (!('authorization' in paymentPayload.payload))
|
|
322
|
+
return invalid('expected EIP-3009 authorization payload')
|
|
323
|
+
|
|
324
|
+
const authorization = paymentPayload.payload.authorization
|
|
325
|
+
if (getAddress(paymentRequirements.payTo as Address) !== getAddress(authorization.to))
|
|
326
|
+
return invalid('authorization recipient mismatch')
|
|
327
|
+
if (paymentRequirements.amount !== authorization.value)
|
|
328
|
+
return invalid('authorization amount mismatch')
|
|
329
|
+
|
|
330
|
+
const now = BigInt(Math.floor(Date.now() / 1000))
|
|
331
|
+
if (BigInt(authorization.validAfter) > now) return invalid('authorization is not valid yet')
|
|
332
|
+
if (BigInt(authorization.validBefore) <= now) return invalid('authorization has expired')
|
|
333
|
+
|
|
334
|
+
const recovered = await recoverTypedDataAddress({
|
|
335
|
+
domain: {
|
|
336
|
+
chainId,
|
|
337
|
+
name: stringExtra(paymentRequirements, 'name'),
|
|
338
|
+
verifyingContract: getAddress(paymentRequirements.asset as Address),
|
|
339
|
+
version: stringExtra(paymentRequirements, 'version'),
|
|
340
|
+
},
|
|
341
|
+
message: {
|
|
342
|
+
from: getAddress(authorization.from),
|
|
343
|
+
nonce: authorization.nonce as Hex,
|
|
344
|
+
to: getAddress(authorization.to),
|
|
345
|
+
validAfter: BigInt(authorization.validAfter),
|
|
346
|
+
validBefore: BigInt(authorization.validBefore),
|
|
347
|
+
value: BigInt(authorization.value),
|
|
348
|
+
},
|
|
349
|
+
primaryType: 'TransferWithAuthorization',
|
|
350
|
+
signature: paymentPayload.payload.signature as Hex,
|
|
351
|
+
types: evm_Types.authorizationTypes,
|
|
352
|
+
})
|
|
353
|
+
if (getAddress(recovered) !== getAddress(authorization.from))
|
|
354
|
+
return invalid('authorization signature mismatch')
|
|
355
|
+
|
|
356
|
+
const used = await authorizationState({
|
|
357
|
+
artifact: parameters.artifact,
|
|
358
|
+
from: authorization.from as Address,
|
|
359
|
+
nonce: authorization.nonce as Hex,
|
|
360
|
+
token: parameters.token,
|
|
361
|
+
})
|
|
362
|
+
if (used) return invalid('authorization nonce already used')
|
|
363
|
+
|
|
364
|
+
return { isValid: true, payer: getAddress(authorization.from) }
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return invalid(error instanceof Error ? error.message : 'invalid payment')
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function settlePayment(parameters: {
|
|
371
|
+
artifact: ForgeArtifact
|
|
372
|
+
body: unknown
|
|
373
|
+
token: Address
|
|
374
|
+
}): Promise<Types.SettleResponse> {
|
|
375
|
+
const verified = await verifyPayment(parameters)
|
|
376
|
+
const { paymentPayload, paymentRequirements } = parseFacilitatorRequest(parameters.body)
|
|
377
|
+
const network = paymentRequirements.network
|
|
378
|
+
|
|
379
|
+
if (!verified.isValid) {
|
|
380
|
+
return {
|
|
381
|
+
errorReason: verified.invalidReason ?? verified.invalidMessage ?? 'verification failed',
|
|
382
|
+
network,
|
|
383
|
+
success: false,
|
|
384
|
+
transaction: '',
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (!('authorization' in paymentPayload.payload)) {
|
|
388
|
+
return {
|
|
389
|
+
errorReason: 'expected EIP-3009 authorization payload',
|
|
390
|
+
network,
|
|
391
|
+
success: false,
|
|
392
|
+
transaction: '',
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
const { authorization, signature } = paymentPayload.payload
|
|
398
|
+
const { r, s, v, yParity } = parseSignature(signature as Hex)
|
|
399
|
+
const hash = await writeContract(facilitatorClient, {
|
|
400
|
+
abi: parameters.artifact.abi,
|
|
401
|
+
address: parameters.token,
|
|
402
|
+
functionName: 'transferWithAuthorization',
|
|
403
|
+
args: [
|
|
404
|
+
authorization.from,
|
|
405
|
+
authorization.to,
|
|
406
|
+
BigInt(authorization.value),
|
|
407
|
+
BigInt(authorization.validAfter),
|
|
408
|
+
BigInt(authorization.validBefore),
|
|
409
|
+
authorization.nonce,
|
|
410
|
+
Number(v ?? (yParity === 0 ? 27 : 28)),
|
|
411
|
+
r,
|
|
412
|
+
s,
|
|
413
|
+
],
|
|
414
|
+
})
|
|
415
|
+
await waitForTransactionReceipt(facilitatorClient, { hash })
|
|
416
|
+
return {
|
|
417
|
+
amount: authorization.value,
|
|
418
|
+
network,
|
|
419
|
+
payer: getAddress(authorization.from),
|
|
420
|
+
success: true,
|
|
421
|
+
transaction: hash,
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
return {
|
|
425
|
+
errorReason: error instanceof Error ? error.message : 'settlement failed',
|
|
426
|
+
network,
|
|
427
|
+
success: false,
|
|
428
|
+
transaction: '',
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function parseFacilitatorRequest(body: unknown): {
|
|
434
|
+
paymentPayload: Types.PaymentPayload
|
|
435
|
+
paymentRequirements: Types.PaymentRequirements
|
|
436
|
+
x402Version: unknown
|
|
437
|
+
} {
|
|
438
|
+
if (!body || typeof body !== 'object') throw new Error('invalid facilitator request')
|
|
439
|
+
const record = body as Record<string, unknown>
|
|
440
|
+
return {
|
|
441
|
+
paymentPayload: Types.PaymentPayloadSchema.parse(record.paymentPayload),
|
|
442
|
+
paymentRequirements: Types.PaymentRequirementsSchema.parse(record.paymentRequirements),
|
|
443
|
+
x402Version: record.x402Version,
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function stringExtra(paymentRequirements: Types.PaymentRequirements, key: string): string {
|
|
448
|
+
const value = paymentRequirements.extra?.[key]
|
|
449
|
+
if (typeof value !== 'string') throw new Error(`missing ${key} metadata`)
|
|
450
|
+
return value
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function invalid(reason: string): Types.VerifyResponse {
|
|
454
|
+
return { invalidReason: reason, isValid: false }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function pureX402Challenge(response: Response): Response {
|
|
458
|
+
const paymentRequired = response.headers.get(Types.paymentRequiredHeader)
|
|
459
|
+
if (!paymentRequired) throw new Error('Missing PAYMENT-REQUIRED header.')
|
|
460
|
+
return new Response(null, {
|
|
461
|
+
headers: { [Types.paymentRequiredHeader]: paymentRequired },
|
|
462
|
+
status: 402,
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function facilitatorStats(server: Http.TestServer): {
|
|
467
|
+
settleRequests: number
|
|
468
|
+
verifyRequests: number
|
|
469
|
+
} {
|
|
470
|
+
return (
|
|
471
|
+
server as Http.TestServer & {
|
|
472
|
+
x402LocalnetStats: { settleRequests: number; verifyRequests: number }
|
|
473
|
+
}
|
|
474
|
+
).x402LocalnetStats
|
|
475
|
+
}
|