mppx 0.8.13 → 0.8.15
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 +37 -0
- package/dist/Errors.d.ts +10 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +11 -2
- package/dist/Errors.js.map +1 -1
- package/dist/Method.d.ts +105 -3
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js +102 -2
- package/dist/Method.js.map +1 -1
- package/dist/cli/validate/payment.d.ts.map +1 -1
- package/dist/cli/validate/payment.js +18 -4
- 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 +98 -5
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodChallenge.d.ts +19 -0
- package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
- package/dist/client/internal/MethodChallenge.js +15 -0
- package/dist/client/internal/MethodChallenge.js.map +1 -0
- package/dist/client/internal/MethodResponse.d.ts +15 -0
- package/dist/client/internal/MethodResponse.d.ts.map +1 -1
- package/dist/client/internal/MethodResponse.js +10 -0
- package/dist/client/internal/MethodResponse.js.map +1 -1
- package/dist/server/Mppx.d.ts +23 -4
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +91 -82
- package/dist/server/Mppx.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 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +1 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +7 -2
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.js +2 -1
- package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +46 -77
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +289 -209
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +12 -1
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +4 -0
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Relay.d.ts +57 -0
- package/dist/tempo/server/Relay.d.ts.map +1 -0
- package/dist/tempo/server/Relay.js +179 -0
- package/dist/tempo/server/Relay.js.map +1 -0
- package/dist/tempo/server/SponsorBudget.d.ts +59 -0
- package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
- package/dist/tempo/server/SponsorBudget.js +150 -0
- package/dist/tempo/server/SponsorBudget.js.map +1 -0
- 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/client/ChannelOps.d.ts +7 -1
- package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/session/client/ChannelOps.js +9 -4
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +5 -1
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +37 -19
- package/dist/tempo/session/client/CredentialState.js.map +1 -1
- package/dist/tempo/session/client/Runtime.d.ts +8 -0
- package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
- package/dist/tempo/session/client/Runtime.js +17 -0
- package/dist/tempo/session/client/Runtime.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts +15 -5
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +216 -45
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +8 -1
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +114 -44
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +16 -6
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +84 -37
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +96 -8
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/precompile/Channel.d.ts +2 -0
- package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Channel.js +4 -0
- package/dist/tempo/session/precompile/Channel.js.map +1 -1
- package/dist/tempo/session/server/RequestState.js +3 -1
- package/dist/tempo/session/server/RequestState.js.map +1 -1
- package/dist/tempo/session/server/Ws.d.ts.map +1 -1
- package/dist/tempo/session/server/Ws.js +3 -0
- package/dist/tempo/session/server/Ws.js.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +1 -829
- package/dist/tempo/subscription/KeyAuthorization.d.ts.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.js +14 -3
- package/dist/tempo/subscription/KeyAuthorization.js.map +1 -1
- package/dist/viem/Client.d.ts.map +1 -1
- package/dist/viem/Client.js +6 -1
- package/dist/viem/Client.js.map +1 -1
- package/package.json +2 -2
- package/src/Errors.test.ts +23 -0
- package/src/Errors.ts +21 -2
- package/src/Method.test.ts +102 -1
- package/src/Method.ts +241 -5
- package/src/cli/validate/payment.ts +18 -4
- package/src/cli/validate.test.ts +16 -0
- package/src/client/Mppx.test-d.ts +3 -3
- package/src/client/internal/Fetch.test.ts +77 -0
- package/src/client/internal/Fetch.ts +122 -11
- package/src/client/internal/MethodChallenge.ts +35 -0
- package/src/client/internal/MethodResponse.ts +28 -0
- package/src/server/Mppx.test-d.ts +2 -0
- package/src/server/Mppx.test.ts +205 -0
- package/src/server/Mppx.ts +167 -103
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/PublicExports.test-d.ts +20 -0
- package/src/tempo/internal/fee-payer.ts +7 -3
- package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
- package/src/tempo/legacy/client/ChannelOps.ts +2 -1
- package/src/tempo/server/Charge.test.ts +337 -63
- package/src/tempo/server/Charge.ts +443 -267
- package/src/tempo/server/Methods.ts +11 -0
- package/src/tempo/server/Relay.test.ts +677 -0
- package/src/tempo/server/Relay.ts +297 -0
- package/src/tempo/server/SponsorBudget.test.ts +154 -0
- package/src/tempo/server/SponsorBudget.ts +219 -0
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/README.md +7 -0
- package/src/tempo/session/client/ChannelOps.test.ts +17 -2
- package/src/tempo/session/client/ChannelOps.ts +12 -4
- package/src/tempo/session/client/CredentialState.ts +61 -20
- package/src/tempo/session/client/Runtime.test.ts +12 -0
- package/src/tempo/session/client/Runtime.ts +22 -0
- package/src/tempo/session/client/Session.test.ts +506 -7
- package/src/tempo/session/client/Session.ts +279 -54
- package/src/tempo/session/client/SessionManager.test.ts +318 -13
- package/src/tempo/session/client/SessionManager.ts +132 -42
- package/src/tempo/session/client/Transports.test.ts +54 -1
- package/src/tempo/session/client/Transports.ts +137 -57
- package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
- package/src/tempo/session/precompile/Chain.test.ts +197 -10
- package/src/tempo/session/precompile/Chain.ts +102 -8
- package/src/tempo/session/precompile/Channel.test.ts +9 -0
- package/src/tempo/session/precompile/Channel.ts +5 -0
- package/src/tempo/session/server/RequestState.test.ts +8 -1
- package/src/tempo/session/server/RequestState.ts +2 -2
- package/src/tempo/session/server/Session.test.ts +66 -4
- package/src/tempo/session/server/Ws.ts +3 -0
- package/src/tempo/subscription/KeyAuthorization.test.ts +70 -1
- package/src/tempo/subscription/KeyAuthorization.ts +14 -3
- package/src/viem/Client.test.ts +18 -1
- package/src/viem/Client.ts +7 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Account, Address, Client, Hex } from 'viem'
|
|
2
|
-
import { encodeFunctionData, isAddressEqual, parseEventLogs } from 'viem'
|
|
2
|
+
import { decodeFunctionData, encodeFunctionData, isAddressEqual, parseEventLogs } from 'viem'
|
|
3
3
|
import {
|
|
4
4
|
call,
|
|
5
5
|
prepareTransactionRequest,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
signTransaction,
|
|
11
11
|
waitForTransactionReceipt,
|
|
12
12
|
} from 'viem/actions'
|
|
13
|
-
import { Transaction } from 'viem/tempo'
|
|
13
|
+
import { Abis, Addresses, Transaction } from 'viem/tempo'
|
|
14
14
|
|
|
15
15
|
import { BadRequestError, VerificationFailedError } from '../../../Errors.js'
|
|
16
16
|
import * as FeePayer from '../../internal/fee-payer.js'
|
|
@@ -401,6 +401,7 @@ export type ChannelTransactionOptions = {
|
|
|
401
401
|
|
|
402
402
|
type ParsedPrecompileCredentialTransaction = {
|
|
403
403
|
call: Transaction.TransactionTempo['calls'][number] & { data: Hex; to: Address }
|
|
404
|
+
prefixCalls: readonly Transaction.TransactionTempo['calls'][number][]
|
|
404
405
|
transaction: ReturnType<(typeof Transaction)['deserialize']>
|
|
405
406
|
}
|
|
406
407
|
|
|
@@ -418,11 +419,11 @@ function parsePrecompileCredentialTransaction(parameters: {
|
|
|
418
419
|
serializedTransaction as Transaction.TransactionSerializedTempo,
|
|
419
420
|
)
|
|
420
421
|
const calls = transaction.calls
|
|
421
|
-
if (calls.length !== 1)
|
|
422
|
+
if (calls.length !== 1 && calls.length !== 3)
|
|
422
423
|
throw new VerificationFailedError({
|
|
423
|
-
reason: `TIP-1034 ${label} transaction must contain
|
|
424
|
+
reason: `TIP-1034 ${label} transaction must contain one management call, optionally preceded by an auto-swap`,
|
|
424
425
|
})
|
|
425
|
-
const call = calls[
|
|
426
|
+
const call = calls[calls.length - 1]!
|
|
426
427
|
if (!call.to || !isAddressEqual(call.to, escrowContract))
|
|
427
428
|
throw new VerificationFailedError({
|
|
428
429
|
reason: `TIP-1034 ${label} transaction targets the wrong address`,
|
|
@@ -431,7 +432,88 @@ function parsePrecompileCredentialTransaction(parameters: {
|
|
|
431
432
|
throw new VerificationFailedError({
|
|
432
433
|
reason: `TIP-1034 ${label} transaction is missing calldata`,
|
|
433
434
|
})
|
|
434
|
-
return {
|
|
435
|
+
return {
|
|
436
|
+
transaction,
|
|
437
|
+
call: { ...call, data: call.data, to: call.to },
|
|
438
|
+
prefixCalls: calls.slice(0, -1),
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function validateAutoSwapPrefix(parameters: {
|
|
443
|
+
amountOut: bigint
|
|
444
|
+
currency: Address
|
|
445
|
+
label: 'open' | 'topUp'
|
|
446
|
+
prefixCalls: readonly Transaction.TransactionTempo['calls'][number][]
|
|
447
|
+
}) {
|
|
448
|
+
const { amountOut, currency, label, prefixCalls } = parameters
|
|
449
|
+
if (prefixCalls.length === 0) return
|
|
450
|
+
|
|
451
|
+
const fail = (reason: string): never => {
|
|
452
|
+
throw new VerificationFailedError({ reason: `TIP-1034 ${label} auto-swap ${reason}` })
|
|
453
|
+
}
|
|
454
|
+
if (prefixCalls.length !== 2) fail('must contain exactly approve and swap calls')
|
|
455
|
+
|
|
456
|
+
const approveCall = prefixCalls[0]!
|
|
457
|
+
const swapCall = prefixCalls[1]!
|
|
458
|
+
const approveTo = approveCall.to
|
|
459
|
+
const approveData = approveCall.data
|
|
460
|
+
const swapTo = swapCall.to
|
|
461
|
+
const swapData = swapCall.data
|
|
462
|
+
if (!approveTo || !approveData || !swapTo || !swapData)
|
|
463
|
+
fail('call is missing a target or calldata')
|
|
464
|
+
const checkedApproveTo = approveTo as Address
|
|
465
|
+
const checkedApproveData = approveData as Hex
|
|
466
|
+
const checkedSwapTo = swapTo as Address
|
|
467
|
+
const checkedSwapData = swapData as Hex
|
|
468
|
+
if ((approveCall.value ?? 0n) !== 0n || (swapCall.value ?? 0n) !== 0n)
|
|
469
|
+
fail('calls must not transfer native value')
|
|
470
|
+
if (!isAddressEqual(checkedSwapTo, Addresses.stablecoinDex)) fail('targets the wrong DEX')
|
|
471
|
+
|
|
472
|
+
const approve = (() => {
|
|
473
|
+
try {
|
|
474
|
+
return decodeFunctionData({ abi: Abis.tip20, data: checkedApproveData })
|
|
475
|
+
} catch {
|
|
476
|
+
return fail('approval calldata is invalid')
|
|
477
|
+
}
|
|
478
|
+
})()
|
|
479
|
+
const swap = (() => {
|
|
480
|
+
try {
|
|
481
|
+
return decodeFunctionData({ abi: Abis.stablecoinDex, data: checkedSwapData })
|
|
482
|
+
} catch {
|
|
483
|
+
return fail('swap calldata is invalid')
|
|
484
|
+
}
|
|
485
|
+
})()
|
|
486
|
+
if (approve.functionName !== 'approve' || swap.functionName !== 'swapExactAmountOut')
|
|
487
|
+
fail('must contain approve followed by swapExactAmountOut')
|
|
488
|
+
|
|
489
|
+
const [spender, approvedAmount] = approve.args as readonly [Address, bigint]
|
|
490
|
+
const [tokenIn, tokenOut, swapAmountOut, maxAmountIn] = swap.args as readonly [
|
|
491
|
+
Address,
|
|
492
|
+
Address,
|
|
493
|
+
bigint,
|
|
494
|
+
bigint,
|
|
495
|
+
]
|
|
496
|
+
if (!isAddressEqual(checkedApproveTo, tokenIn)) fail('approval token does not match swap input')
|
|
497
|
+
if (!isAddressEqual(spender, Addresses.stablecoinDex)) fail('approval spender is not the DEX')
|
|
498
|
+
if (approvedAmount !== maxAmountIn) fail('approval amount does not match swap maximum input')
|
|
499
|
+
if (!isAddressEqual(tokenOut, currency)) fail('output token does not match channel currency')
|
|
500
|
+
if (swapAmountOut !== amountOut) fail('output amount does not match channel deposit')
|
|
501
|
+
if (isAddressEqual(tokenIn, tokenOut)) fail('input and output tokens must differ')
|
|
502
|
+
|
|
503
|
+
const canonicalApprove = encodeFunctionData({
|
|
504
|
+
abi: Abis.tip20,
|
|
505
|
+
functionName: 'approve',
|
|
506
|
+
args: [spender, approvedAmount],
|
|
507
|
+
})
|
|
508
|
+
const canonicalSwap = encodeFunctionData({
|
|
509
|
+
abi: Abis.stablecoinDex,
|
|
510
|
+
functionName: 'swapExactAmountOut',
|
|
511
|
+
args: [tokenIn, tokenOut, swapAmountOut, maxAmountIn],
|
|
512
|
+
})
|
|
513
|
+
if (checkedApproveData.toLowerCase() !== canonicalApprove.toLowerCase())
|
|
514
|
+
fail('approval calldata is not canonical')
|
|
515
|
+
if (checkedSwapData.toLowerCase() !== canonicalSwap.toLowerCase())
|
|
516
|
+
fail('swap calldata is not canonical')
|
|
435
517
|
}
|
|
436
518
|
|
|
437
519
|
async function simulateTempoTransaction(client: Client, request: unknown) {
|
|
@@ -796,7 +878,7 @@ export type BroadcastOpenTransactionParameters = {
|
|
|
796
878
|
export async function broadcastOpenTransaction(
|
|
797
879
|
parameters: BroadcastOpenTransactionParameters,
|
|
798
880
|
): Promise<BroadcastOpenTransactionResult> {
|
|
799
|
-
const { transaction, call } = parsePrecompileCredentialTransaction({
|
|
881
|
+
const { transaction, call, prefixCalls } = parsePrecompileCredentialTransaction({
|
|
800
882
|
escrowContract: parameters.escrowContract,
|
|
801
883
|
feePayer: parameters.feePayer,
|
|
802
884
|
label: 'open',
|
|
@@ -812,6 +894,12 @@ export async function broadcastOpenTransaction(
|
|
|
812
894
|
authorizedSigner: parameters.expectedAuthorizedSigner,
|
|
813
895
|
},
|
|
814
896
|
})
|
|
897
|
+
validateAutoSwapPrefix({
|
|
898
|
+
amountOut: open.deposit,
|
|
899
|
+
currency: parameters.expectedCurrency,
|
|
900
|
+
label: 'open',
|
|
901
|
+
prefixCalls,
|
|
902
|
+
})
|
|
815
903
|
const descriptor = ChannelOps.descriptorFromOpen({
|
|
816
904
|
chainId: parameters.chainId,
|
|
817
905
|
escrow: parameters.escrowContract,
|
|
@@ -920,7 +1008,7 @@ export type BroadcastTopUpTransactionParameters = {
|
|
|
920
1008
|
export async function broadcastTopUpTransaction(
|
|
921
1009
|
parameters: BroadcastTopUpTransactionParameters,
|
|
922
1010
|
): Promise<BroadcastTopUpTransactionResult> {
|
|
923
|
-
const { transaction, call } = parsePrecompileCredentialTransaction({
|
|
1011
|
+
const { transaction, call, prefixCalls } = parsePrecompileCredentialTransaction({
|
|
924
1012
|
escrowContract: parameters.escrowContract,
|
|
925
1013
|
feePayer: parameters.feePayer,
|
|
926
1014
|
label: 'topUp',
|
|
@@ -933,6 +1021,12 @@ export async function broadcastTopUpTransaction(
|
|
|
933
1021
|
additionalDeposit: parameters.additionalDeposit,
|
|
934
1022
|
},
|
|
935
1023
|
})
|
|
1024
|
+
validateAutoSwapPrefix({
|
|
1025
|
+
amountOut: parameters.additionalDeposit,
|
|
1026
|
+
currency: parameters.expectedCurrency,
|
|
1027
|
+
label: 'topUp',
|
|
1028
|
+
prefixCalls,
|
|
1029
|
+
})
|
|
936
1030
|
const receipt = await sendCredentialTransaction({
|
|
937
1031
|
challengeExpires: parameters.challengeExpires,
|
|
938
1032
|
chainId: parameters.chainId,
|
|
@@ -27,6 +27,15 @@ const transaction = {
|
|
|
27
27
|
} as const satisfies Channel.ExpiringNonceTransaction
|
|
28
28
|
|
|
29
29
|
describe('precompile Channel.computeId', () => {
|
|
30
|
+
test('uses the payer when the descriptor delegates to the zero address', () => {
|
|
31
|
+
expect(
|
|
32
|
+
Channel.resolveAuthorizedSigner({
|
|
33
|
+
...descriptor,
|
|
34
|
+
authorizedSigner: '0x0000000000000000000000000000000000000000',
|
|
35
|
+
}),
|
|
36
|
+
).toBe(descriptor.payer)
|
|
37
|
+
})
|
|
38
|
+
|
|
30
39
|
test('returns deterministic 32-byte hash for fixed inputs', () => {
|
|
31
40
|
const id = Channel.computeId({ ...descriptor, chainId })
|
|
32
41
|
expect(Channel.computeId({ ...descriptor, chainId })).toBe(id)
|
|
@@ -9,6 +9,11 @@ import type { ChannelDescriptor } from './Protocol.js'
|
|
|
9
9
|
/** Re-export of the TIP-1034 channel descriptor shape. */
|
|
10
10
|
export type { ChannelDescriptor } from './Protocol.js'
|
|
11
11
|
|
|
12
|
+
/** Resolves the descriptor's effective voucher signer; zero delegates to the payer. */
|
|
13
|
+
export function resolveAuthorizedSigner(descriptor: ChannelDescriptor): Address {
|
|
14
|
+
return BigInt(descriptor.authorizedSigner) === 0n ? descriptor.payer : descriptor.authorizedSigner
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
/** Tempo transaction shape used to derive the TIP-1034 `expiringNonceHash`. */
|
|
13
18
|
export type ExpiringNonceTransaction = (
|
|
14
19
|
| z_TransactionSerializableTempo
|
|
@@ -544,10 +544,17 @@ describe('SessionSnapshotHints', () => {
|
|
|
544
544
|
}
|
|
545
545
|
})
|
|
546
546
|
|
|
547
|
-
test('omits hints for missing, non-precompile, or finalized channels', async () => {
|
|
547
|
+
test('omits hints for missing, non-precompile, closing, or finalized channels', async () => {
|
|
548
548
|
await expect(
|
|
549
549
|
resolveSessionSnapshot({ amount: 1n, channelId, store: store(null) }),
|
|
550
550
|
).resolves.toBe(undefined)
|
|
551
|
+
await expect(
|
|
552
|
+
resolveSessionSnapshot({
|
|
553
|
+
amount: 1n,
|
|
554
|
+
channelId,
|
|
555
|
+
store: store(channel({ closeRequestedAt: 1n })),
|
|
556
|
+
}),
|
|
557
|
+
).resolves.toBe(undefined)
|
|
551
558
|
await expect(
|
|
552
559
|
resolveSessionSnapshot({
|
|
553
560
|
amount: 1n,
|
|
@@ -143,6 +143,7 @@ export async function resolveSessionSnapshot(
|
|
|
143
143
|
const channel = await store.getChannel(ChannelStore.normalizeChannelId(channelId))
|
|
144
144
|
if (!channel || !ChannelStore.isPrecompileState(channel)) return undefined
|
|
145
145
|
if (channel.finalized) return undefined
|
|
146
|
+
if (channel.closeRequestedAt !== 0n) return undefined
|
|
146
147
|
if (!channel.highestVoucher) return undefined
|
|
147
148
|
if (channel.highestVoucher.cumulativeAmount !== channel.highestVoucherAmount) return undefined
|
|
148
149
|
if (expected && !matchesSnapshotPaymentFields(channel, expected)) return undefined
|
|
@@ -151,8 +152,7 @@ export async function resolveSessionSnapshot(
|
|
|
151
152
|
acceptedCumulative: channel.highestVoucherAmount.toString(),
|
|
152
153
|
chainId: channel.chainId,
|
|
153
154
|
channelId: channel.channelId,
|
|
154
|
-
closeRequestedAt:
|
|
155
|
-
channel.closeRequestedAt === 0n ? undefined : channel.closeRequestedAt.toString(),
|
|
155
|
+
closeRequestedAt: undefined,
|
|
156
156
|
deposit: channel.deposit.toString(),
|
|
157
157
|
descriptor: channel.descriptor,
|
|
158
158
|
escrow: channel.escrowContract,
|
|
@@ -2421,12 +2421,15 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2421
2421
|
})
|
|
2422
2422
|
|
|
2423
2423
|
describe('WebSocket parity', () => {
|
|
2424
|
-
async function createManagedWsHarness(
|
|
2424
|
+
async function createManagedWsHarness(
|
|
2425
|
+
options: { challengeTtlMs?: number; maxDeposit?: bigint } = {},
|
|
2426
|
+
) {
|
|
2425
2427
|
const rawStore = Store.memory()
|
|
2428
|
+
let challengeRequests = 0
|
|
2426
2429
|
let currentPayload: SessionCredentialPayload | undefined
|
|
2427
2430
|
let voucherPosts = 0
|
|
2428
2431
|
const maxDeposit = options.maxDeposit ?? 3n
|
|
2429
|
-
const
|
|
2432
|
+
const payment = Mppx_server.create({
|
|
2430
2433
|
methods: [
|
|
2431
2434
|
tempo_server.session({
|
|
2432
2435
|
amount: '1',
|
|
@@ -2463,7 +2466,7 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2463
2466
|
],
|
|
2464
2467
|
realm: 'api.example.com',
|
|
2465
2468
|
secretKey: 'test-secret-key-test-secret-key-32',
|
|
2466
|
-
})
|
|
2469
|
+
})
|
|
2467
2470
|
|
|
2468
2471
|
const route = async (request: Request) => {
|
|
2469
2472
|
currentPayload = undefined
|
|
@@ -2472,7 +2475,15 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2472
2475
|
currentPayload = Credential.fromRequest<SessionCredentialPayload>(request).payload
|
|
2473
2476
|
if (currentPayload.action === 'voucher') voucherPosts++
|
|
2474
2477
|
} catch {}
|
|
2475
|
-
}
|
|
2478
|
+
} else challengeRequests++
|
|
2479
|
+
const routeHandler = payment.session({
|
|
2480
|
+
amount: '1',
|
|
2481
|
+
decimals: 0,
|
|
2482
|
+
suggestedDeposit: maxDeposit.toString(),
|
|
2483
|
+
...(options.challengeTtlMs === undefined
|
|
2484
|
+
? {}
|
|
2485
|
+
: { expires: new Date(Date.now() + options.challengeTtlMs) }),
|
|
2486
|
+
})
|
|
2476
2487
|
return routeHandler(request)
|
|
2477
2488
|
}
|
|
2478
2489
|
|
|
@@ -2510,6 +2521,9 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2510
2521
|
get port() {
|
|
2511
2522
|
return port
|
|
2512
2523
|
},
|
|
2524
|
+
get challengeRequests() {
|
|
2525
|
+
return challengeRequests
|
|
2526
|
+
},
|
|
2513
2527
|
get voucherPosts() {
|
|
2514
2528
|
return voucherPosts
|
|
2515
2529
|
},
|
|
@@ -2520,6 +2534,54 @@ describe('precompile server session unit guardrails', () => {
|
|
|
2520
2534
|
}
|
|
2521
2535
|
}
|
|
2522
2536
|
|
|
2537
|
+
test('refreshes an expired challenge before sending a later websocket voucher', async () => {
|
|
2538
|
+
const harness = await createManagedWsHarness({ challengeTtlMs: 2_000, maxDeposit: 2n })
|
|
2539
|
+
harness.wsServer.on('connection', (socket: import('ws').WebSocket) => {
|
|
2540
|
+
void TempoWs.serve({
|
|
2541
|
+
socket,
|
|
2542
|
+
store: harness.rawStore,
|
|
2543
|
+
url: `${harness.server.url}/ws`,
|
|
2544
|
+
route: harness.route,
|
|
2545
|
+
generate: async function* (stream: TempoWs.SessionController) {
|
|
2546
|
+
await stream.charge()
|
|
2547
|
+
yield 'chunk-1'
|
|
2548
|
+
await new Promise((resolve) => setTimeout(resolve, 2_200))
|
|
2549
|
+
await stream.charge()
|
|
2550
|
+
yield 'chunk-2'
|
|
2551
|
+
},
|
|
2552
|
+
})
|
|
2553
|
+
})
|
|
2554
|
+
|
|
2555
|
+
try {
|
|
2556
|
+
const manager = precompileSessionManager({
|
|
2557
|
+
account: payer,
|
|
2558
|
+
client: createSigningClient(),
|
|
2559
|
+
decimals: 0,
|
|
2560
|
+
fetch: globalThis.fetch,
|
|
2561
|
+
maxDeposit: '2',
|
|
2562
|
+
webSocket: WebSocket as never,
|
|
2563
|
+
})
|
|
2564
|
+
const ws = await manager.ws(`ws://localhost:${harness.port}/ws`)
|
|
2565
|
+
const chunks: string[] = []
|
|
2566
|
+
|
|
2567
|
+
await new Promise<void>((resolve, reject) => {
|
|
2568
|
+
ws.addEventListener('message', (event) => {
|
|
2569
|
+
if (typeof event.data === 'string') chunks.push(event.data)
|
|
2570
|
+
})
|
|
2571
|
+
ws.addEventListener('close', () => resolve(), { once: true })
|
|
2572
|
+
ws.addEventListener('error', () => reject(new Error('websocket stream failed')), {
|
|
2573
|
+
once: true,
|
|
2574
|
+
})
|
|
2575
|
+
})
|
|
2576
|
+
|
|
2577
|
+
expect(chunks).toEqual(['chunk-1', 'chunk-2'])
|
|
2578
|
+
expect(harness.challengeRequests).toBeGreaterThan(1)
|
|
2579
|
+
expect(harness.voucherPosts).toBe(1)
|
|
2580
|
+
} finally {
|
|
2581
|
+
harness.close()
|
|
2582
|
+
}
|
|
2583
|
+
})
|
|
2584
|
+
|
|
2523
2585
|
test('open -> stream -> need-voucher -> resume -> close', async () => {
|
|
2524
2586
|
const harness = await createManagedWsHarness({ maxDeposit: 3n })
|
|
2525
2587
|
harness.wsServer.on('connection', (socket: import('ws').WebSocket) => {
|
|
@@ -220,6 +220,9 @@ export async function serve(options: serve.Options): Promise<void> {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
const { receipt } = authorizationResult
|
|
223
|
+
if (payload.action === 'voucher' && runtime.streamContext) {
|
|
224
|
+
runtime.streamContext.challengeId = credential.challenge.id
|
|
225
|
+
}
|
|
223
226
|
await send(socket, formatReceiptMessage(receipt))
|
|
224
227
|
|
|
225
228
|
if (payload.action === 'close') {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Rlp } from 'ox'
|
|
2
|
+
import { KeyAuthorization, SignatureEnvelope } from 'ox/tempo'
|
|
2
3
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
3
4
|
import { describe, expect, test } from 'vp/test'
|
|
4
5
|
|
|
@@ -69,6 +70,25 @@ async function createPayload(request = parseRequest()) {
|
|
|
69
70
|
} as const
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function serializeCompositeSignature(
|
|
74
|
+
type: 'keychain' | 'multisig',
|
|
75
|
+
inner: SignatureEnvelope.Primitive,
|
|
76
|
+
) {
|
|
77
|
+
return SignatureEnvelope.serialize(
|
|
78
|
+
type === 'keychain'
|
|
79
|
+
? {
|
|
80
|
+
inner,
|
|
81
|
+
type,
|
|
82
|
+
userAddress: rootAccount.address,
|
|
83
|
+
}
|
|
84
|
+
: {
|
|
85
|
+
account: rootAccount.address,
|
|
86
|
+
signatures: [inner],
|
|
87
|
+
type,
|
|
88
|
+
},
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
describe('tempo subscription key authorization', () => {
|
|
73
93
|
test('signs and verifies a scoped key authorization', async () => {
|
|
74
94
|
const request = parseRequest()
|
|
@@ -87,6 +107,55 @@ describe('tempo subscription key authorization', () => {
|
|
|
87
107
|
)
|
|
88
108
|
})
|
|
89
109
|
|
|
110
|
+
test.each(['keychain', 'multisig'] as const)(
|
|
111
|
+
'rejects %s authorization signatures',
|
|
112
|
+
async (type) => {
|
|
113
|
+
const request = parseRequest()
|
|
114
|
+
|
|
115
|
+
await expect(
|
|
116
|
+
signSubscriptionKeyAuthorization({
|
|
117
|
+
accessKey,
|
|
118
|
+
account: {
|
|
119
|
+
async sign({ hash }) {
|
|
120
|
+
const inner = SignatureEnvelope.from(await rootAccount.sign({ hash }))
|
|
121
|
+
if (inner.type === 'keychain' || inner.type === 'multisig') {
|
|
122
|
+
throw new Error('expected primitive signature')
|
|
123
|
+
}
|
|
124
|
+
return serializeCompositeSignature(type, inner)
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
chainId: 4217,
|
|
128
|
+
request,
|
|
129
|
+
}),
|
|
130
|
+
).rejects.toThrow('keyAuthorization must use a primitive signature')
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
test.each(['keychain', 'multisig'] as const)(
|
|
135
|
+
'rejects serialized %s authorization signatures',
|
|
136
|
+
async (type) => {
|
|
137
|
+
const request = parseRequest()
|
|
138
|
+
const payload = await createPayload(request)
|
|
139
|
+
const authorization = KeyAuthorization.deserialize(payload.signature)
|
|
140
|
+
const [authorizationTuple] = KeyAuthorization.toTuple(authorization)
|
|
141
|
+
|
|
142
|
+
expect(() =>
|
|
143
|
+
verifySubscriptionKeyAuthorization({
|
|
144
|
+
accessKey,
|
|
145
|
+
chainId: 4217,
|
|
146
|
+
payload: {
|
|
147
|
+
...payload,
|
|
148
|
+
signature: Rlp.fromHex([
|
|
149
|
+
authorizationTuple,
|
|
150
|
+
serializeCompositeSignature(type, authorization.signature!),
|
|
151
|
+
]),
|
|
152
|
+
},
|
|
153
|
+
request,
|
|
154
|
+
}),
|
|
155
|
+
).toThrow('keyAuthorization must use a primitive signature')
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
|
|
90
159
|
test('builds wallet allowed calls from the subscription request', () => {
|
|
91
160
|
const request = parseRequest()
|
|
92
161
|
|
|
@@ -168,8 +168,14 @@ export async function signSubscriptionKeyAuthorization(parameters: {
|
|
|
168
168
|
const signature = await account.sign({
|
|
169
169
|
hash: KeyAuthorization.getSignPayload(authorization),
|
|
170
170
|
})
|
|
171
|
+
const signatureEnvelope = SignatureEnvelope.from(signature)
|
|
172
|
+
if (signatureEnvelope.type === 'keychain' || signatureEnvelope.type === 'multisig') {
|
|
173
|
+
throw new VerificationFailedError({
|
|
174
|
+
reason: 'keyAuthorization must use a primitive signature',
|
|
175
|
+
})
|
|
176
|
+
}
|
|
171
177
|
return KeyAuthorization.from(authorization, {
|
|
172
|
-
signature
|
|
178
|
+
signature,
|
|
173
179
|
})
|
|
174
180
|
}
|
|
175
181
|
|
|
@@ -238,14 +244,19 @@ function createUnsignedAuthorization(parameters: {
|
|
|
238
244
|
function deserializeAuthorization(signature: `0x${string}`) {
|
|
239
245
|
try {
|
|
240
246
|
return KeyAuthorization.deserialize(signature)
|
|
241
|
-
} catch {
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error instanceof KeyAuthorization.InvalidSignatureTypeError) {
|
|
249
|
+
throw new VerificationFailedError({
|
|
250
|
+
reason: 'keyAuthorization must use a primitive signature',
|
|
251
|
+
})
|
|
252
|
+
}
|
|
242
253
|
throw new VerificationFailedError({ reason: 'invalid keyAuthorization payload' })
|
|
243
254
|
}
|
|
244
255
|
}
|
|
245
256
|
|
|
246
257
|
function getPrimitiveSignature(authorization: Authorization) {
|
|
247
258
|
const signature = authorization.signature
|
|
248
|
-
if (!signature
|
|
259
|
+
if (!signature) {
|
|
249
260
|
throw new VerificationFailedError({
|
|
250
261
|
reason: 'keyAuthorization must use a primitive signature',
|
|
251
262
|
})
|
package/src/viem/Client.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createClient, custom, defineChain, type Hex } from 'viem'
|
|
|
2
2
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
3
3
|
import { signTransaction } from 'viem/actions'
|
|
4
4
|
import { Account as TempoAccount, Transaction } from 'viem/tempo'
|
|
5
|
-
import { tempoLocalnet } from 'viem/tempo/chains'
|
|
5
|
+
import { tempo as tempoMainnetChain, tempoLocalnet, tempoModerato } from 'viem/tempo/chains'
|
|
6
6
|
import { afterEach, describe, expect, test, vi } from 'vp/test'
|
|
7
7
|
|
|
8
8
|
import * as Client from './Client.js'
|
|
@@ -48,6 +48,23 @@ describe('getResolver', () => {
|
|
|
48
48
|
expect(client.chain?.name).toBe('test')
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
+
test('behavior: resolves the correct named Tempo chain even when the default chain differs', async () => {
|
|
52
|
+
const getClient = Client.getResolver({
|
|
53
|
+
// Default chain is mainnet, but the resolved chain ID below is testnet.
|
|
54
|
+
chain: tempoMainnetChain as never,
|
|
55
|
+
rpcUrl: {
|
|
56
|
+
[tempoMainnetChain.id]: 'https://rpc.example.com',
|
|
57
|
+
[tempoModerato.id]: 'https://rpc2.example.com',
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const client = await getClient({ chainId: tempoModerato.id })
|
|
62
|
+
|
|
63
|
+
expect(client.chain?.id).toBe(tempoModerato.id)
|
|
64
|
+
expect(client.chain?.name).toBe(tempoModerato.name)
|
|
65
|
+
expect(client.chain?.name).not.toBe(tempoMainnetChain.name)
|
|
66
|
+
})
|
|
67
|
+
|
|
51
68
|
test('error: throws when no rpcUrl provided', () => {
|
|
52
69
|
const getClient = Client.getResolver({})
|
|
53
70
|
|
package/src/viem/Client.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { type Chain, type Client, createClient, createTransport, custom, http } from 'viem'
|
|
2
2
|
import { withFeePayer } from 'viem/tempo'
|
|
3
|
+
import { tempo as tempoMainnetChain, tempoModerato } from 'viem/tempo/chains'
|
|
3
4
|
|
|
4
5
|
import type { MaybePromise } from '../internal/types.js'
|
|
5
6
|
|
|
7
|
+
const knownTempoChains: Record<number, Chain> = {
|
|
8
|
+
[tempoMainnetChain.id]: tempoMainnetChain,
|
|
9
|
+
[tempoModerato.id]: tempoModerato,
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
export function getResolver(
|
|
7
13
|
parameters: getResolver.Parameters & {
|
|
8
14
|
/** Default chain to use if not provided. */
|
|
@@ -73,7 +79,7 @@ export function getResolver(
|
|
|
73
79
|
if (!url) throw new Error(`No \`rpcUrl\` configured for \`chainId\` (${resolvedChainId}).`)
|
|
74
80
|
const transport = feePayerUrl ? withFeePayer(http(url), http(feePayerUrl)) : http(url)
|
|
75
81
|
return createClient({
|
|
76
|
-
chain: { ...chain, id: resolvedChainId } as never,
|
|
82
|
+
chain: (knownTempoChains[resolvedChainId] ?? { ...chain, id: resolvedChainId }) as never,
|
|
77
83
|
transport,
|
|
78
84
|
})
|
|
79
85
|
}
|