mppx 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js.map +1 -1
- package/dist/tempo/internal/defaults.d.ts +1 -1
- package/dist/tempo/internal/defaults.js +1 -1
- package/dist/tempo/server/Session.js +3 -3
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/session/Chain.js +2 -2
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/escrow.abi.d.ts +24 -24
- package/dist/tempo/session/escrow.abi.js +30 -30
- package/dist/tempo/session/escrow.abi.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js +24 -2
- package/dist/viem/Client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/Mppx.test-d.ts +4 -4
- package/src/client/internal/Fetch.test-d.ts +2 -2
- package/src/client/internal/Fetch.test.ts +57 -1
- package/src/server/Mppx.test-d.ts +2 -2
- package/src/server/Mppx.ts +7 -9
- package/src/tempo/client/ChannelOps.test.ts +5 -1
- package/src/tempo/client/Session.test.ts +2 -1
- package/src/tempo/internal/defaults.test.ts +1 -1
- package/src/tempo/internal/defaults.ts +1 -1
- package/src/tempo/server/Session.test.ts +5 -1
- package/src/tempo/server/Session.ts +3 -3
- package/src/tempo/server/Sse.test.ts +2 -1
- package/src/tempo/server/internal/transport.test.ts +2 -1
- package/src/tempo/session/Chain.ts +2 -2
- package/src/tempo/session/ChannelStore.test.ts +2 -1
- package/src/tempo/session/Sse.test.ts +2 -1
- package/src/tempo/session/escrow.abi.ts +30 -30
- package/src/viem/Client.test.ts +196 -0
- package/src/viem/Client.ts +23 -1
|
@@ -47,9 +47,9 @@ describe('Fetch.from', () => {
|
|
|
47
47
|
|
|
48
48
|
describe('Fetch.from.RequestInit', () => {
|
|
49
49
|
test('behavior: has context property typed to method context', () => {
|
|
50
|
-
const
|
|
50
|
+
const _method = charge()
|
|
51
51
|
|
|
52
|
-
type Methods = [typeof
|
|
52
|
+
type Methods = [typeof _method]
|
|
53
53
|
type Init = Fetch.from.RequestInit<Methods>
|
|
54
54
|
|
|
55
55
|
expectTypeOf<Init>().toHaveProperty('context')
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Receipt } from 'mppx'
|
|
2
2
|
import { tempo } from 'mppx/client'
|
|
3
3
|
import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
|
|
4
|
-
import { createClient } from 'viem'
|
|
4
|
+
import { createClient, defineChain } from 'viem'
|
|
5
5
|
import { describe, expect, test, vi } from 'vitest'
|
|
6
6
|
import * as Http from '~test/Http.js'
|
|
7
|
+
import { rpcUrl } from '~test/tempo/prool.js'
|
|
7
8
|
import { accounts, asset, chain, client, http } from '~test/tempo/viem.js'
|
|
8
9
|
import * as Fetch from './Fetch.js'
|
|
9
10
|
|
|
@@ -233,6 +234,61 @@ describe('Fetch.from', () => {
|
|
|
233
234
|
httpServer.close()
|
|
234
235
|
})
|
|
235
236
|
|
|
237
|
+
test('behavior: fee payer with plain chain client (no Tempo serializers)', async () => {
|
|
238
|
+
const plainChain = defineChain({
|
|
239
|
+
id: chain.id,
|
|
240
|
+
name: chain.name,
|
|
241
|
+
nativeCurrency: chain.nativeCurrency,
|
|
242
|
+
rpcUrls: chain.rpcUrls,
|
|
243
|
+
})
|
|
244
|
+
const plainClient = createClient({
|
|
245
|
+
account: accounts[1],
|
|
246
|
+
chain: plainChain,
|
|
247
|
+
transport: http(rpcUrl),
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const serverWithFeePayer = Mppx_server.create({
|
|
251
|
+
methods: [
|
|
252
|
+
tempo_server.charge({
|
|
253
|
+
feePayer: accounts[0],
|
|
254
|
+
getClient: () => client,
|
|
255
|
+
}),
|
|
256
|
+
],
|
|
257
|
+
realm,
|
|
258
|
+
secretKey,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const fetch = Fetch.from({
|
|
262
|
+
methods: [
|
|
263
|
+
tempo.charge({
|
|
264
|
+
account: accounts[1],
|
|
265
|
+
getClient: () => plainClient,
|
|
266
|
+
}),
|
|
267
|
+
],
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
271
|
+
const result = await Mppx_server.toNodeListener(
|
|
272
|
+
serverWithFeePayer.charge({
|
|
273
|
+
amount: '1',
|
|
274
|
+
currency: asset,
|
|
275
|
+
expires: new Date(Date.now() + 60_000).toISOString(),
|
|
276
|
+
recipient: accounts[0].address,
|
|
277
|
+
}),
|
|
278
|
+
)(req, res)
|
|
279
|
+
if (result.status === 402) return
|
|
280
|
+
res.end('OK')
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const response = await fetch(httpServer.url)
|
|
284
|
+
expect(response.status).toBe(200)
|
|
285
|
+
|
|
286
|
+
const receipt = Receipt.fromResponse(response)
|
|
287
|
+
expect(receipt.status).toBe('success')
|
|
288
|
+
|
|
289
|
+
httpServer.close()
|
|
290
|
+
})
|
|
291
|
+
|
|
236
292
|
test('behavior: onChallenge can create credential', async () => {
|
|
237
293
|
const onChallenge = vi.fn(async (_challenge, { createCredential }) =>
|
|
238
294
|
createCredential({ account: accounts[1] }),
|
|
@@ -122,8 +122,8 @@ describe('Mppx type tests', () => {
|
|
|
122
122
|
recipient: '0x02',
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
const
|
|
126
|
-
type HandlerReturn = ReturnType<typeof
|
|
125
|
+
const _handler = mppx.compose([alphaMethod, opts])
|
|
126
|
+
type HandlerReturn = ReturnType<typeof _handler>
|
|
127
127
|
|
|
128
128
|
assertType<Promise<{ status: 402; challenge: Response } | { status: 200; withReceipt: any }>>(
|
|
129
129
|
{} as Awaited<HandlerReturn> as any,
|
package/src/server/Mppx.ts
CHANGED
|
@@ -82,16 +82,14 @@ type EffectiveTransportOf<mi, defaultTransport extends Transport.AnyTransport> =
|
|
|
82
82
|
: TransportOverrideOf<mi>
|
|
83
83
|
|
|
84
84
|
/** True when exactly one method has the given intent (no name collision). */
|
|
85
|
-
type IsUniqueIntent<methods extends readonly Method.AnyServer[], intent extends string> =
|
|
86
|
-
methods[number],
|
|
87
|
-
|
|
88
|
-
> extends
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
: false
|
|
85
|
+
type IsUniqueIntent<methods extends readonly Method.AnyServer[], intent extends string> =
|
|
86
|
+
Extract<methods[number], { intent: intent }> extends infer M
|
|
87
|
+
? M extends M
|
|
88
|
+
? [Exclude<Extract<methods[number], { intent: intent }>, M>] extends [never]
|
|
89
|
+
? true
|
|
90
|
+
: false
|
|
91
|
+
: never
|
|
93
92
|
: never
|
|
94
|
-
: never
|
|
95
93
|
|
|
96
94
|
/** Only includes shorthand intent keys when the intent is unique across methods. */
|
|
97
95
|
type UniqueIntentHandlers<
|
|
@@ -7,6 +7,10 @@ import { deployEscrow, openChannel } from '~test/tempo/session.js'
|
|
|
7
7
|
import { accounts, asset, chain, client, fundAccount, http } from '~test/tempo/viem.js'
|
|
8
8
|
import type { Challenge } from '../../Challenge.js'
|
|
9
9
|
import * as Credential from '../../Credential.js'
|
|
10
|
+
import {
|
|
11
|
+
chainId as chainIdDefaults,
|
|
12
|
+
escrowContract as escrowContractDefaults,
|
|
13
|
+
} from '../internal/defaults.js'
|
|
10
14
|
import { verifyVoucher } from '../session/Voucher.js'
|
|
11
15
|
import {
|
|
12
16
|
createClosePayload,
|
|
@@ -17,7 +21,7 @@ import {
|
|
|
17
21
|
tryRecoverChannel,
|
|
18
22
|
} from './ChannelOps.js'
|
|
19
23
|
|
|
20
|
-
const escrow42431 =
|
|
24
|
+
const escrow42431 = escrowContractDefaults[chainIdDefaults.testnet] as Address
|
|
21
25
|
|
|
22
26
|
const localAccount = privateKeyToAccount(
|
|
23
27
|
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
|
|
@@ -6,6 +6,7 @@ import { deployEscrow, openChannel } from '~test/tempo/session.js'
|
|
|
6
6
|
import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
|
|
7
7
|
import * as Challenge from '../../Challenge.js'
|
|
8
8
|
import * as Credential from '../../Credential.js'
|
|
9
|
+
import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
|
|
9
10
|
import type { SessionCredentialPayload } from '../session/Types.js'
|
|
10
11
|
import { session } from './Session.js'
|
|
11
12
|
|
|
@@ -22,7 +23,7 @@ const pureClient = createClient({
|
|
|
22
23
|
transport: http('http://127.0.0.1'),
|
|
23
24
|
})
|
|
24
25
|
|
|
25
|
-
const escrowAddress =
|
|
26
|
+
const escrowAddress = escrowContractDefaults[chainId.testnet] as Address
|
|
26
27
|
const recipient = '0x2222222222222222222222222222222222222222' as Address
|
|
27
28
|
const currency = '0x3333333333333333333333333333333333333333' as Address
|
|
28
29
|
|
|
@@ -53,7 +53,7 @@ describe('escrowContract', () => {
|
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
test('testnet escrow contract', () => {
|
|
56
|
-
expect(escrowContract[chainId.testnet]).toBe('
|
|
56
|
+
expect(escrowContract[chainId.testnet]).toBe('0xe1c4d3dce17bc111181ddf716f75bae49e61a336')
|
|
57
57
|
})
|
|
58
58
|
})
|
|
59
59
|
|
|
@@ -32,7 +32,7 @@ export const decimals = 6
|
|
|
32
32
|
/** Default payment-channel escrow contract addresses per chain. */
|
|
33
33
|
export const escrowContract = {
|
|
34
34
|
[chainId.mainnet]: '0x33b901018174DDabE4841042ab76ba85D4e24f25',
|
|
35
|
-
[chainId.testnet]: '
|
|
35
|
+
[chainId.testnet]: '0xe1c4d3dce17bc111181ddf716f75bae49e61a336',
|
|
36
36
|
} as const satisfies Record<ChainId, string>
|
|
37
37
|
|
|
38
38
|
/** Default RPC URLs for each Tempo chain. */
|
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
InvalidSignatureError,
|
|
19
19
|
} from '../../Errors.js'
|
|
20
20
|
import * as Store from '../../Store.js'
|
|
21
|
+
import {
|
|
22
|
+
chainId as chainIdDefaults,
|
|
23
|
+
escrowContract as escrowContractDefaults,
|
|
24
|
+
} from '../internal/defaults.js'
|
|
21
25
|
import type * as Methods from '../Methods.js'
|
|
22
26
|
import * as ChannelStore from '../session/ChannelStore.js'
|
|
23
27
|
import type { SessionReceipt } from '../session/Types.js'
|
|
@@ -1309,7 +1313,7 @@ describe('monotonicity and TOCTOU (unit tests)', () => {
|
|
|
1309
1313
|
token: '0x0000000000000000000000000000000000000003' as Address,
|
|
1310
1314
|
authorizedSigner: '0x0000000000000000000000000000000000000004' as Address,
|
|
1311
1315
|
chainId: 42431,
|
|
1312
|
-
escrowContract:
|
|
1316
|
+
escrowContract: escrowContractDefaults[chainIdDefaults.testnet] as Address,
|
|
1313
1317
|
deposit: 10000000n,
|
|
1314
1318
|
settledOnChain: 0n,
|
|
1315
1319
|
highestVoucherAmount: 5000000n,
|
|
@@ -723,14 +723,14 @@ async function handleVoucher(
|
|
|
723
723
|
lastOnChainVerified.set(payload.channelId, Date.now())
|
|
724
724
|
} else {
|
|
725
725
|
cachedOnChain = {
|
|
726
|
+
finalized: channel.finalized,
|
|
727
|
+
closeRequestedAt: 0n,
|
|
726
728
|
payer: channel.payer,
|
|
727
729
|
payee: channel.payee,
|
|
728
730
|
token: channel.token,
|
|
731
|
+
authorizedSigner: channel.authorizedSigner,
|
|
729
732
|
deposit: channel.deposit,
|
|
730
733
|
settled: channel.settledOnChain,
|
|
731
|
-
finalized: channel.finalized,
|
|
732
|
-
authorizedSigner: channel.authorizedSigner,
|
|
733
|
-
closeRequestedAt: 0n,
|
|
734
734
|
}
|
|
735
735
|
}
|
|
736
736
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Address, Hex } from 'viem'
|
|
2
2
|
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
|
|
3
4
|
import type * as ChannelStore from '../session/ChannelStore.js'
|
|
4
5
|
import { serve, toResponse } from '../session/Sse.js'
|
|
5
6
|
|
|
@@ -33,7 +34,7 @@ function seedChannel(
|
|
|
33
34
|
token: '0x0000000000000000000000000000000000000003' as Address,
|
|
34
35
|
authorizedSigner: '0x0000000000000000000000000000000000000004' as Address,
|
|
35
36
|
chainId: 42431,
|
|
36
|
-
escrowContract:
|
|
37
|
+
escrowContract: escrowContractDefaults[chainId.testnet] as Address,
|
|
37
38
|
deposit: balance,
|
|
38
39
|
settledOnChain: 0n,
|
|
39
40
|
highestVoucherAmount: balance,
|
|
@@ -2,6 +2,7 @@ import { Challenge, Credential } from 'mppx'
|
|
|
2
2
|
import type { Address, Hex } from 'viem'
|
|
3
3
|
import { describe, expect, test } from 'vitest'
|
|
4
4
|
import * as Store from '../../../Store.js'
|
|
5
|
+
import { chainId, escrowContract as escrowContractDefaults } from '../../internal/defaults.js'
|
|
5
6
|
import * as ChannelStore from '../../session/ChannelStore.js'
|
|
6
7
|
import { sse } from './transport.js'
|
|
7
8
|
|
|
@@ -23,7 +24,7 @@ function seedChannel(
|
|
|
23
24
|
token: '0x0000000000000000000000000000000000000003' as Address,
|
|
24
25
|
authorizedSigner: '0x0000000000000000000000000000000000000004' as Address,
|
|
25
26
|
chainId: 42431,
|
|
26
|
-
escrowContract:
|
|
27
|
+
escrowContract: escrowContractDefaults[chainId.testnet] as Address,
|
|
27
28
|
deposit: balance,
|
|
28
29
|
settledOnChain: 0n,
|
|
29
30
|
highestVoucherAmount: balance,
|
|
@@ -305,14 +305,14 @@ export async function broadcastOpenTransaction(parameters: {
|
|
|
305
305
|
return {
|
|
306
306
|
txHash,
|
|
307
307
|
onChain: {
|
|
308
|
+
finalized: false,
|
|
309
|
+
closeRequestedAt: 0n,
|
|
308
310
|
payer: transaction.from,
|
|
309
311
|
payee,
|
|
310
312
|
token,
|
|
311
313
|
authorizedSigner,
|
|
312
314
|
deposit,
|
|
313
315
|
settled: 0n,
|
|
314
|
-
closeRequestedAt: 0n,
|
|
315
|
-
finalized: false,
|
|
316
316
|
} as OnChainChannel,
|
|
317
317
|
}
|
|
318
318
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Address, Hex } from 'viem'
|
|
2
2
|
import { describe, expect, test } from 'vitest'
|
|
3
3
|
import * as Store from '../../Store.js'
|
|
4
|
+
import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
|
|
4
5
|
import * as ChannelStore from './ChannelStore.js'
|
|
5
6
|
|
|
6
7
|
const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
|
|
@@ -14,7 +15,7 @@ function makeChannel(overrides?: Partial<ChannelStore.State>): ChannelStore.Stat
|
|
|
14
15
|
token: '0x0000000000000000000000000000000000000003' as Address,
|
|
15
16
|
authorizedSigner: '0x0000000000000000000000000000000000000004' as Address,
|
|
16
17
|
chainId: 42431,
|
|
17
|
-
escrowContract:
|
|
18
|
+
escrowContract: escrowContractDefaults[chainId.testnet] as Address,
|
|
18
19
|
deposit: 10_000_000n,
|
|
19
20
|
settledOnChain: 0n,
|
|
20
21
|
highestVoucherAmount: 10_000_000n,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Address, Hex } from 'viem'
|
|
2
2
|
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import { chainId, escrowContract as escrowContractDefaults } from '../internal/defaults.js'
|
|
3
4
|
import type * as ChannelStore from './ChannelStore.js'
|
|
4
5
|
import { formatNeedVoucherEvent, formatReceiptEvent, parseEvent, serve } from './Sse.js'
|
|
5
6
|
import type { NeedVoucherEvent, SessionReceipt } from './Types.js'
|
|
@@ -218,7 +219,7 @@ describe('serve', () => {
|
|
|
218
219
|
token: '0x0000000000000000000000000000000000000003' as Address,
|
|
219
220
|
authorizedSigner: '0x0000000000000000000000000000000000000004' as Address,
|
|
220
221
|
chainId: 42431,
|
|
221
|
-
escrowContract:
|
|
222
|
+
escrowContract: escrowContractDefaults[chainId.testnet] as Address,
|
|
222
223
|
deposit: balance,
|
|
223
224
|
settledOnChain: 0n,
|
|
224
225
|
highestVoucherAmount: balance,
|
|
@@ -36,6 +36,16 @@ export const escrowAbi = [
|
|
|
36
36
|
},
|
|
37
37
|
],
|
|
38
38
|
outputs: [
|
|
39
|
+
{
|
|
40
|
+
name: 'finalized',
|
|
41
|
+
type: 'bool',
|
|
42
|
+
internalType: 'bool',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'closeRequestedAt',
|
|
46
|
+
type: 'uint64',
|
|
47
|
+
internalType: 'uint64',
|
|
48
|
+
},
|
|
39
49
|
{
|
|
40
50
|
name: 'payer',
|
|
41
51
|
type: 'address',
|
|
@@ -66,16 +76,6 @@ export const escrowAbi = [
|
|
|
66
76
|
type: 'uint128',
|
|
67
77
|
internalType: 'uint128',
|
|
68
78
|
},
|
|
69
|
-
{
|
|
70
|
-
name: 'closeRequestedAt',
|
|
71
|
-
type: 'uint64',
|
|
72
|
-
internalType: 'uint64',
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: 'finalized',
|
|
76
|
-
type: 'bool',
|
|
77
|
-
internalType: 'bool',
|
|
78
|
-
},
|
|
79
79
|
],
|
|
80
80
|
stateMutability: 'view',
|
|
81
81
|
},
|
|
@@ -213,6 +213,16 @@ export const escrowAbi = [
|
|
|
213
213
|
type: 'tuple',
|
|
214
214
|
internalType: 'struct TempoStreamChannel.Channel',
|
|
215
215
|
components: [
|
|
216
|
+
{
|
|
217
|
+
name: 'finalized',
|
|
218
|
+
type: 'bool',
|
|
219
|
+
internalType: 'bool',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'closeRequestedAt',
|
|
223
|
+
type: 'uint64',
|
|
224
|
+
internalType: 'uint64',
|
|
225
|
+
},
|
|
216
226
|
{
|
|
217
227
|
name: 'payer',
|
|
218
228
|
type: 'address',
|
|
@@ -243,16 +253,6 @@ export const escrowAbi = [
|
|
|
243
253
|
type: 'uint128',
|
|
244
254
|
internalType: 'uint128',
|
|
245
255
|
},
|
|
246
|
-
{
|
|
247
|
-
name: 'closeRequestedAt',
|
|
248
|
-
type: 'uint64',
|
|
249
|
-
internalType: 'uint64',
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
name: 'finalized',
|
|
253
|
-
type: 'bool',
|
|
254
|
-
internalType: 'bool',
|
|
255
|
-
},
|
|
256
256
|
],
|
|
257
257
|
},
|
|
258
258
|
],
|
|
@@ -274,6 +274,16 @@ export const escrowAbi = [
|
|
|
274
274
|
type: 'tuple[]',
|
|
275
275
|
internalType: 'struct TempoStreamChannel.Channel[]',
|
|
276
276
|
components: [
|
|
277
|
+
{
|
|
278
|
+
name: 'finalized',
|
|
279
|
+
type: 'bool',
|
|
280
|
+
internalType: 'bool',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: 'closeRequestedAt',
|
|
284
|
+
type: 'uint64',
|
|
285
|
+
internalType: 'uint64',
|
|
286
|
+
},
|
|
277
287
|
{
|
|
278
288
|
name: 'payer',
|
|
279
289
|
type: 'address',
|
|
@@ -304,16 +314,6 @@ export const escrowAbi = [
|
|
|
304
314
|
type: 'uint128',
|
|
305
315
|
internalType: 'uint128',
|
|
306
316
|
},
|
|
307
|
-
{
|
|
308
|
-
name: 'closeRequestedAt',
|
|
309
|
-
type: 'uint64',
|
|
310
|
-
internalType: 'uint64',
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
name: 'finalized',
|
|
314
|
-
type: 'bool',
|
|
315
|
-
internalType: 'bool',
|
|
316
|
-
},
|
|
317
317
|
],
|
|
318
318
|
},
|
|
319
319
|
],
|
package/src/viem/Client.test.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { createClient, custom, defineChain, type Hex } from 'viem'
|
|
2
|
+
import { privateKeyToAccount } from 'viem/accounts'
|
|
3
|
+
import { signTransaction } from 'viem/actions'
|
|
4
|
+
import { tempoLocalnet } from 'viem/chains'
|
|
5
|
+
import { Transaction } from 'viem/tempo'
|
|
1
6
|
import { describe, expect, test } from 'vitest'
|
|
2
7
|
import * as Client from './Client.js'
|
|
3
8
|
|
|
@@ -56,3 +61,194 @@ describe('getResolver', () => {
|
|
|
56
61
|
)
|
|
57
62
|
})
|
|
58
63
|
})
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Serializer injection – ensures user-provided clients without Tempo chain
|
|
67
|
+
// config get Tempo serializers merged in by getResolver.
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
const testAccount = privateKeyToAccount(
|
|
71
|
+
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const chainIdHex = `0x${tempoLocalnet.id.toString(16)}`
|
|
75
|
+
const mockTransport = custom({
|
|
76
|
+
async request({ method }: { method: string }) {
|
|
77
|
+
if (method === 'eth_chainId') return chainIdHex
|
|
78
|
+
throw new Error(`Unexpected RPC call: ${method}`)
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const tempoClient = createClient({
|
|
83
|
+
account: testAccount,
|
|
84
|
+
chain: tempoLocalnet,
|
|
85
|
+
transport: mockTransport,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
function createPlainClient() {
|
|
89
|
+
const plainChain = defineChain({
|
|
90
|
+
id: tempoLocalnet.id,
|
|
91
|
+
name: 'Tempo (no serializer)',
|
|
92
|
+
nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
|
|
93
|
+
rpcUrls: { default: { http: ['http://127.0.0.1:1'] } },
|
|
94
|
+
})
|
|
95
|
+
return createClient({
|
|
96
|
+
account: testAccount,
|
|
97
|
+
chain: plainChain,
|
|
98
|
+
transport: mockTransport,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const feePayer_prepared = {
|
|
103
|
+
chainId: tempoLocalnet.id,
|
|
104
|
+
calls: [
|
|
105
|
+
{
|
|
106
|
+
to: '0x20c0000000000000000000000000000000000001' as const,
|
|
107
|
+
data: '0x1234' as Hex,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
feePayer: true as const,
|
|
111
|
+
feeToken: '0x20c0000000000000000000000000000000000001' as const,
|
|
112
|
+
gas: 100_000n,
|
|
113
|
+
maxFeePerGas: 1_000_000_000n,
|
|
114
|
+
maxPriorityFeePerGas: 1_000_000n,
|
|
115
|
+
nonce: 0,
|
|
116
|
+
nonceKey: 115792089237316195423570985008687907853269984665640564039457584007913129639935n,
|
|
117
|
+
validBefore: Math.floor(Date.now() / 1000) + 25,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe('feePayer transaction serialization', () => {
|
|
121
|
+
test('behavior: signTransaction with Tempo chain and feePayer: true', async () => {
|
|
122
|
+
const serialized = await signTransaction(tempoClient, {
|
|
123
|
+
account: testAccount,
|
|
124
|
+
...feePayer_prepared,
|
|
125
|
+
} as never)
|
|
126
|
+
expect(serialized).toMatch(/^0x7[68]/)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('behavior: signTransaction with gasPrice + type legacy', async () => {
|
|
130
|
+
const serialized = await signTransaction(tempoClient, {
|
|
131
|
+
account: testAccount,
|
|
132
|
+
...feePayer_prepared,
|
|
133
|
+
gasPrice: 1_000_000_000n,
|
|
134
|
+
type: 'legacy' as const,
|
|
135
|
+
} as never)
|
|
136
|
+
expect(serialized).toMatch(/^0x7[68]/)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('behavior: fee-payer co-sign with Account object', async () => {
|
|
140
|
+
const feePayerAccount = privateKeyToAccount(
|
|
141
|
+
'0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d',
|
|
142
|
+
)
|
|
143
|
+
const serialized = await signTransaction(tempoClient, {
|
|
144
|
+
account: feePayerAccount,
|
|
145
|
+
...feePayer_prepared,
|
|
146
|
+
feePayer: feePayerAccount,
|
|
147
|
+
} as never)
|
|
148
|
+
expect(serialized).toMatch(/^0x7[68]/)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('behavior: deserialized + re-signed tx (server charge flow)', async () => {
|
|
152
|
+
const feePayerAccount = privateKeyToAccount(
|
|
153
|
+
'0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d',
|
|
154
|
+
)
|
|
155
|
+
const clientSigned = await signTransaction(tempoClient, {
|
|
156
|
+
account: testAccount,
|
|
157
|
+
...feePayer_prepared,
|
|
158
|
+
} as never)
|
|
159
|
+
const deserialized = Transaction.deserialize(
|
|
160
|
+
clientSigned as Transaction.TransactionSerializedTempo,
|
|
161
|
+
)
|
|
162
|
+
const serverSigned = await signTransaction(tempoClient, {
|
|
163
|
+
...deserialized,
|
|
164
|
+
account: feePayerAccount,
|
|
165
|
+
feePayer: feePayerAccount,
|
|
166
|
+
feeToken: '0x20c0000000000000000000000000000000000001' as const,
|
|
167
|
+
} as never)
|
|
168
|
+
expect(serverSigned).toMatch(/^0x7[68]/)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('getResolver serializer injection', () => {
|
|
173
|
+
test('behavior: injects Tempo serializer onto plain clients', async () => {
|
|
174
|
+
const plainClient = createPlainClient()
|
|
175
|
+
expect(plainClient.chain?.serializers?.transaction).toBeUndefined()
|
|
176
|
+
|
|
177
|
+
const resolver = Client.getResolver({
|
|
178
|
+
chain: tempoLocalnet,
|
|
179
|
+
getClient: () => plainClient,
|
|
180
|
+
})
|
|
181
|
+
const resolvedClient = await resolver({})
|
|
182
|
+
|
|
183
|
+
expect(resolvedClient.chain?.serializers?.transaction).toBeDefined()
|
|
184
|
+
const serialized = await signTransaction(resolvedClient, {
|
|
185
|
+
account: testAccount,
|
|
186
|
+
...feePayer_prepared,
|
|
187
|
+
} as never)
|
|
188
|
+
expect(serialized).toMatch(/^0x7[68]/)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('behavior: fixes legacy type + feePayer + maxFeePerGas', async () => {
|
|
192
|
+
const resolver = Client.getResolver({
|
|
193
|
+
chain: tempoLocalnet,
|
|
194
|
+
getClient: () => createPlainClient(),
|
|
195
|
+
})
|
|
196
|
+
const resolvedClient = await resolver({})
|
|
197
|
+
|
|
198
|
+
const serialized = await signTransaction(resolvedClient, {
|
|
199
|
+
account: testAccount,
|
|
200
|
+
...feePayer_prepared,
|
|
201
|
+
type: 'legacy' as const,
|
|
202
|
+
} as never)
|
|
203
|
+
expect(serialized).toMatch(/^0x7[68]/)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('behavior: preserves existing serializers', async () => {
|
|
207
|
+
const resolver = Client.getResolver({
|
|
208
|
+
chain: tempoLocalnet,
|
|
209
|
+
getClient: () => tempoClient,
|
|
210
|
+
})
|
|
211
|
+
const resolvedClient = await resolver({})
|
|
212
|
+
|
|
213
|
+
expect(resolvedClient.chain?.serializers?.transaction).toBe(
|
|
214
|
+
tempoClient.chain?.serializers?.transaction,
|
|
215
|
+
)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('behavior: passes through getClient when chain has no serializers', async () => {
|
|
219
|
+
const getClient = () => createPlainClient()
|
|
220
|
+
const resolver = Client.getResolver({
|
|
221
|
+
chain: undefined,
|
|
222
|
+
getClient,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
expect(resolver).toBe(getClient)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('behavior: does not mutate the original client', async () => {
|
|
229
|
+
const plainClient = createPlainClient()
|
|
230
|
+
const originalChain = plainClient.chain
|
|
231
|
+
|
|
232
|
+
const resolver = Client.getResolver({
|
|
233
|
+
chain: tempoLocalnet,
|
|
234
|
+
getClient: () => plainClient,
|
|
235
|
+
})
|
|
236
|
+
const resolvedClient = await resolver({})
|
|
237
|
+
|
|
238
|
+
expect(plainClient.chain).toBe(originalChain)
|
|
239
|
+
expect(resolvedClient).not.toBe(plainClient)
|
|
240
|
+
expect(resolvedClient.chain?.serializers?.transaction).toBeDefined()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('error: plain client without resolver throws on feePayer tx', async () => {
|
|
244
|
+
const plainClient = createPlainClient()
|
|
245
|
+
|
|
246
|
+
await expect(
|
|
247
|
+
signTransaction(plainClient, {
|
|
248
|
+
account: testAccount,
|
|
249
|
+
...feePayer_prepared,
|
|
250
|
+
type: 'legacy' as const,
|
|
251
|
+
} as never),
|
|
252
|
+
).rejects.toThrow()
|
|
253
|
+
})
|
|
254
|
+
})
|
package/src/viem/Client.ts
CHANGED
|
@@ -11,7 +11,29 @@ export function getResolver(
|
|
|
11
11
|
): (parameters: { chainId?: number | undefined }) => MaybePromise<Client> {
|
|
12
12
|
const { chain, getClient, rpcUrl } = parameters
|
|
13
13
|
|
|
14
|
-
if (getClient)
|
|
14
|
+
if (getClient) {
|
|
15
|
+
// When a default chain with serializers is provided (e.g. Tempo chain config),
|
|
16
|
+
// ensure user-provided clients inherit those serializers. Without this, clients
|
|
17
|
+
// created without the Tempo chain config will use the default viem serializer,
|
|
18
|
+
// causing errors like "maxFeePerGas is not a valid Legacy Transaction attribute".
|
|
19
|
+
if (!chain?.serializers) return getClient
|
|
20
|
+
return async (params) => {
|
|
21
|
+
const client = await getClient(params)
|
|
22
|
+
if (client.chain?.serializers?.transaction) return client
|
|
23
|
+
return Object.assign({}, client, {
|
|
24
|
+
chain: {
|
|
25
|
+
...chain,
|
|
26
|
+
...client.chain,
|
|
27
|
+
formatters: client.chain?.formatters ?? chain.formatters,
|
|
28
|
+
prepareTransactionRequest:
|
|
29
|
+
client.chain?.prepareTransactionRequest ?? chain.prepareTransactionRequest,
|
|
30
|
+
serializers: client.chain?.serializers?.transaction
|
|
31
|
+
? client.chain.serializers
|
|
32
|
+
: chain.serializers,
|
|
33
|
+
} as typeof client.chain,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
15
37
|
|
|
16
38
|
return ({ chainId }: { chainId?: number | undefined }) => {
|
|
17
39
|
if (!rpcUrl) throw new Error('No `rpcUrl` provided.')
|