mppx 0.8.13 → 0.8.14
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 +16 -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/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +12 -2
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodChallenge.d.ts +17 -0
- package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
- package/dist/client/internal/MethodChallenge.js +11 -0
- package/dist/client/internal/MethodChallenge.js.map +1 -0
- 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/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 +38 -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 +48 -0
- package/dist/tempo/server/Relay.d.ts.map +1 -0
- package/dist/tempo/server/Relay.js +177 -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 +144 -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 +93 -36
- 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 +54 -26
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +7 -5
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +48 -24
- 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/package.json +1 -1
- 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/client/Mppx.test-d.ts +3 -3
- package/src/client/internal/Fetch.ts +16 -7
- package/src/client/internal/MethodChallenge.ts +30 -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/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 +435 -267
- package/src/tempo/server/Methods.ts +11 -0
- package/src/tempo/server/Relay.test.ts +523 -0
- package/src/tempo/server/Relay.ts +288 -0
- package/src/tempo/server/SponsorBudget.test.ts +125 -0
- package/src/tempo/server/SponsorBudget.ts +213 -0
- package/src/tempo/server/internal/html.gen.ts +1 -1
- 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 +235 -2
- package/src/tempo/session/client/Session.ts +130 -41
- package/src/tempo/session/client/SessionManager.test.ts +102 -4
- package/src/tempo/session/client/SessionManager.ts +71 -27
- package/src/tempo/session/client/Transports.test.ts +32 -1
- package/src/tempo/session/client/Transports.ts +79 -35
- 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
|
@@ -85,6 +85,7 @@ describe('HttpManagement', () => {
|
|
|
85
85
|
methodDetails: {
|
|
86
86
|
chainId: 4217,
|
|
87
87
|
escrowContract: undefined,
|
|
88
|
+
sessionProtocol: Constants.SessionProtocols.v2,
|
|
88
89
|
...(snapshot && { [Constants.MethodDetailKeys.sessionSnapshot]: snapshot }),
|
|
89
90
|
},
|
|
90
91
|
},
|
|
@@ -220,6 +221,36 @@ describe('HttpManagement', () => {
|
|
|
220
221
|
expect(fetch).toHaveBeenCalledOnce()
|
|
221
222
|
})
|
|
222
223
|
|
|
224
|
+
test('postTopUp retries once with the management route challenge', async () => {
|
|
225
|
+
const routeChallenge = { ...challenge(), id: 'management-challenge' } as TempoSessionChallenge
|
|
226
|
+
const createSessionCredential = vi.fn(async (challenge_) => `top-up-${challenge_.id}`)
|
|
227
|
+
const fetch = vi
|
|
228
|
+
.fn()
|
|
229
|
+
.mockResolvedValueOnce(response402(routeChallenge))
|
|
230
|
+
.mockResolvedValueOnce(
|
|
231
|
+
new Response(null, {
|
|
232
|
+
status: 204,
|
|
233
|
+
headers: {
|
|
234
|
+
[Constants.Headers.paymentReceipt]: receiptHeader(8n, 5n, routeChallenge.id),
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const receipt = await postTopUp({
|
|
240
|
+
additionalDeposit: 3n,
|
|
241
|
+
challenge: challenge(),
|
|
242
|
+
channel: channel(),
|
|
243
|
+
channelId,
|
|
244
|
+
createSessionCredential,
|
|
245
|
+
fetch,
|
|
246
|
+
input: 'https://example.test/resource',
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
expect(receipt?.challengeId).toBe(routeChallenge.id)
|
|
250
|
+
expect(createSessionCredential).toHaveBeenCalledTimes(2)
|
|
251
|
+
expect(authorizationHeader(fetch.mock.calls[1]?.[1])).toBe(`top-up-${routeChallenge.id}`)
|
|
252
|
+
})
|
|
253
|
+
|
|
223
254
|
test('retryHttpPaymentRequired signs the server-required cumulative voucher', async () => {
|
|
224
255
|
let entry = channel({ cumulativeAmount: 5n, deposit: 6n })
|
|
225
256
|
const topUpIfNeeded = vi.fn(async () => {
|
|
@@ -874,7 +905,6 @@ describe('SseDriver', () => {
|
|
|
874
905
|
fetch: vi.fn(async () => new Response(null, { status: 204 })),
|
|
875
906
|
getChallenge: () => currentChallenge,
|
|
876
907
|
getChannel: () => channel,
|
|
877
|
-
managementInput: (input) => input,
|
|
878
908
|
async topUpIfNeeded() {},
|
|
879
909
|
})
|
|
880
910
|
|
|
@@ -1011,6 +1041,7 @@ describe('WsDriver', () => {
|
|
|
1011
1041
|
methodDetails: {
|
|
1012
1042
|
chainId: 4217,
|
|
1013
1043
|
escrowContract: tip20ChannelEscrow,
|
|
1044
|
+
sessionProtocol: Constants.SessionProtocols.v2,
|
|
1014
1045
|
},
|
|
1015
1046
|
recipient: '0x742d35cc6634c0532925a3b844bc9e7595f8fe00',
|
|
1016
1047
|
},
|
|
@@ -268,6 +268,8 @@ export type ApplyTopUpResultParameters = {
|
|
|
268
268
|
challengeId?: string | undefined
|
|
269
269
|
/** Current active machine state, used to preserve paid unit count when possible. */
|
|
270
270
|
currentState: SessionState
|
|
271
|
+
/** Highest deposit known before the top-up. */
|
|
272
|
+
knownDeposit?: bigint | undefined
|
|
271
273
|
/** Receipt returned by the top-up POST, when present. */
|
|
272
274
|
receipt?: SessionReceipt | undefined
|
|
273
275
|
/** Latest locally observed spend in raw units. */
|
|
@@ -362,26 +364,36 @@ export function resolveManualTopUp(parameters: ResolveManualTopUpParameters): Ma
|
|
|
362
364
|
/**
|
|
363
365
|
* Applies local deposit and state bookkeeping for a top-up response.
|
|
364
366
|
*
|
|
365
|
-
*
|
|
366
|
-
* update accepted spend authorization; they do not replace deposit.
|
|
367
|
+
* Deposit advances from the highest known baseline. Receipt cumulative values
|
|
368
|
+
* only update accepted spend authorization; they do not replace deposit.
|
|
367
369
|
*/
|
|
368
370
|
export function applyTopUpResult(
|
|
369
371
|
parameters: ApplyTopUpResultParameters,
|
|
370
372
|
): AppliedTopUpResult | undefined {
|
|
371
|
-
const {
|
|
372
|
-
|
|
373
|
+
const {
|
|
374
|
+
additionalDeposit,
|
|
375
|
+
channel,
|
|
376
|
+
channelId,
|
|
377
|
+
challengeId,
|
|
378
|
+
currentState,
|
|
379
|
+
knownDeposit,
|
|
380
|
+
receipt,
|
|
381
|
+
spent,
|
|
382
|
+
} = parameters
|
|
373
383
|
if (channel?.channelId !== channelId) return undefined
|
|
374
384
|
|
|
375
|
-
|
|
385
|
+
const baseline =
|
|
386
|
+
knownDeposit !== undefined && knownDeposit > channel.deposit ? knownDeposit : channel.deposit
|
|
387
|
+
const updated = { ...channel, deposit: baseline + additionalDeposit }
|
|
376
388
|
|
|
377
|
-
if (receipt) return { channel, state: activeStateFromReceipt(receipt,
|
|
378
|
-
if (!challengeId) return { channel }
|
|
389
|
+
if (receipt) return { channel: updated, state: activeStateFromReceipt(receipt, updated) }
|
|
390
|
+
if (!challengeId) return { channel: updated }
|
|
379
391
|
|
|
380
392
|
return {
|
|
381
|
-
channel,
|
|
393
|
+
channel: updated,
|
|
382
394
|
state: activeStateFromChannel({
|
|
383
395
|
challengeId,
|
|
384
|
-
entry:
|
|
396
|
+
entry: updated,
|
|
385
397
|
spent: spent.toString(),
|
|
386
398
|
units: currentState.status === 'active' ? currentState.units : 0,
|
|
387
399
|
}),
|
|
@@ -521,6 +533,19 @@ export function isTempoSessionChallenge(
|
|
|
521
533
|
)
|
|
522
534
|
}
|
|
523
535
|
|
|
536
|
+
/** Returns true when a challenge selects the TIP-1034 session protocol. */
|
|
537
|
+
export function isTip1034SessionChallenge(
|
|
538
|
+
challenge: Challenge.Challenge,
|
|
539
|
+
): challenge is TempoSessionChallenge {
|
|
540
|
+
return (
|
|
541
|
+
isTempoSessionChallenge(challenge) &&
|
|
542
|
+
Constants.getMethodDetail(
|
|
543
|
+
challenge.request.methodDetails,
|
|
544
|
+
Constants.MethodDetailKeys.sessionProtocol,
|
|
545
|
+
) === Constants.SessionProtocols.v2
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
|
|
524
549
|
/** Reads a server-provided session snapshot from challenge method details. */
|
|
525
550
|
export function getSessionSnapshot(challenge: TempoSessionChallenge): SessionSnapshot | undefined {
|
|
526
551
|
return Constants.getMethodDetail<SessionSnapshot>(
|
|
@@ -594,15 +619,17 @@ export async function postTopUp(
|
|
|
594
619
|
throw new Error('Cannot top up session: no local channel descriptor available.')
|
|
595
620
|
}
|
|
596
621
|
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
622
|
+
const response = await postManagementCredential({
|
|
623
|
+
challenge: parameters.challenge,
|
|
624
|
+
context: {
|
|
625
|
+
action: 'topUp',
|
|
626
|
+
channelId,
|
|
627
|
+
descriptor: channel.descriptor,
|
|
628
|
+
additionalDepositRaw: parameters.additionalDeposit.toString(),
|
|
629
|
+
},
|
|
630
|
+
createSessionCredential: parameters.createSessionCredential,
|
|
631
|
+
fetch: parameters.fetch,
|
|
632
|
+
input: parameters.input,
|
|
606
633
|
})
|
|
607
634
|
if (!response.ok) throw new Error(`Top-up POST failed with status ${response.status}`)
|
|
608
635
|
|
|
@@ -610,6 +637,30 @@ export async function postTopUp(
|
|
|
610
637
|
return receiptHeader ? deserializeSessionReceipt(receiptHeader) : undefined
|
|
611
638
|
}
|
|
612
639
|
|
|
640
|
+
/** Posts a management credential and retries once with a refreshed TIP-1034 challenge. */
|
|
641
|
+
async function postManagementCredential(parameters: {
|
|
642
|
+
challenge: TempoSessionChallenge
|
|
643
|
+
context: SessionContext
|
|
644
|
+
createSessionCredential: CreateSessionCredential
|
|
645
|
+
fetch: typeof globalThis.fetch
|
|
646
|
+
input: RequestInfo | URL
|
|
647
|
+
}): Promise<Response> {
|
|
648
|
+
const post = async (challenge: TempoSessionChallenge) =>
|
|
649
|
+
parameters.fetch(managementInput(parameters.input), {
|
|
650
|
+
method: 'POST',
|
|
651
|
+
headers: {
|
|
652
|
+
[Constants.Headers.authorization]: await parameters.createSessionCredential(
|
|
653
|
+
challenge,
|
|
654
|
+
parameters.context,
|
|
655
|
+
),
|
|
656
|
+
},
|
|
657
|
+
})
|
|
658
|
+
const response = await post(parameters.challenge)
|
|
659
|
+
if (response.status !== 402) return response
|
|
660
|
+
const challenge = Challenge.fromResponseList(response).find(isTip1034SessionChallenge)
|
|
661
|
+
return challenge ? post(challenge) : response
|
|
662
|
+
}
|
|
663
|
+
|
|
613
664
|
/** Retries an HTTP 402 when the server snapshot asks for more voucher headroom. */
|
|
614
665
|
export async function retryHttpPaymentRequired(
|
|
615
666
|
parameters: RetryHttpPaymentRequiredParameters,
|
|
@@ -662,7 +713,7 @@ export function resolveRetryHttpPaymentContext(parameters: {
|
|
|
662
713
|
channel: ChannelEntry | null
|
|
663
714
|
response: Response
|
|
664
715
|
}): RetryHttpPaymentContext | undefined {
|
|
665
|
-
const challenge = Challenge.fromResponseList(parameters.response).find(
|
|
716
|
+
const challenge = Challenge.fromResponseList(parameters.response).find(isTip1034SessionChallenge)
|
|
666
717
|
if (!challenge) return undefined
|
|
667
718
|
|
|
668
719
|
const snapshot = getSessionSnapshot(challenge)
|
|
@@ -705,7 +756,7 @@ export async function closeHttpSession(
|
|
|
705
756
|
|
|
706
757
|
let response = await postClose(parameters.target.challenge)
|
|
707
758
|
if (response.status === 402) {
|
|
708
|
-
const challenge = Challenge.fromResponseList(response).find(
|
|
759
|
+
const challenge = Challenge.fromResponseList(response).find(isTip1034SessionChallenge)
|
|
709
760
|
if (challenge) {
|
|
710
761
|
parameters.setChallenge?.(challenge)
|
|
711
762
|
response = await postClose(challenge, true)
|
|
@@ -768,8 +819,6 @@ export type OpenSseSessionParameters = {
|
|
|
768
819
|
getChallenge(): TempoSessionChallenge | null
|
|
769
820
|
/** Validates a cumulative amount against local client policy. */
|
|
770
821
|
assertVoucherWithinLocalLimit(cumulativeAmount: bigint): void
|
|
771
|
-
/** Converts a resource URL to the server's session management URL. */
|
|
772
|
-
managementInput(input: RequestInfo | URL): RequestInfo | URL
|
|
773
822
|
/** Applies an incoming receipt to manager state. */
|
|
774
823
|
acceptReceipt(receipt: SessionReceipt): void
|
|
775
824
|
/** Performs an automatic channel top-up when deposit is insufficient. */
|
|
@@ -783,7 +832,6 @@ export type SsePaymentDriver = Pick<
|
|
|
783
832
|
| 'createSessionCredential'
|
|
784
833
|
| 'fetch'
|
|
785
834
|
| 'getChannel'
|
|
786
|
-
| 'managementInput'
|
|
787
835
|
| 'topUpIfNeeded'
|
|
788
836
|
>
|
|
789
837
|
|
|
@@ -1018,17 +1066,13 @@ export async function handleSseNeedVoucher(
|
|
|
1018
1066
|
})
|
|
1019
1067
|
if (resolution.status !== 'ready') return
|
|
1020
1068
|
|
|
1021
|
-
const
|
|
1022
|
-
parameters.challenge,
|
|
1023
|
-
resolution.context,
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
parameters.
|
|
1027
|
-
|
|
1028
|
-
method: 'POST',
|
|
1029
|
-
headers: { [Constants.Headers.authorization]: credential },
|
|
1030
|
-
},
|
|
1031
|
-
)
|
|
1069
|
+
const voucherResponse = await postManagementCredential({
|
|
1070
|
+
challenge: parameters.challenge,
|
|
1071
|
+
context: resolution.context,
|
|
1072
|
+
createSessionCredential: parameters.driver.createSessionCredential,
|
|
1073
|
+
fetch: parameters.driver.fetch,
|
|
1074
|
+
input: parameters.input,
|
|
1075
|
+
})
|
|
1032
1076
|
if (!voucherResponse.ok) {
|
|
1033
1077
|
throw new Error(`Voucher POST failed with status ${voucherResponse.status}`)
|
|
1034
1078
|
}
|
|
@@ -1186,7 +1230,7 @@ export async function prepareWebSocketSession(
|
|
|
1186
1230
|
)
|
|
1187
1231
|
}
|
|
1188
1232
|
|
|
1189
|
-
const challenge = Challenge.fromResponseList(probe).find(
|
|
1233
|
+
const challenge = Challenge.fromResponseList(probe).find(isTip1034SessionChallenge)
|
|
1190
1234
|
if (!challenge) {
|
|
1191
1235
|
throw new Error(
|
|
1192
1236
|
'No payment challenge received from HTTP endpoint for this WebSocket URL. The server may not require payment or did not advertise a challenge.',
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { Hex } from 'ox'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type Address,
|
|
4
|
+
encodeFunctionData,
|
|
5
|
+
isAddressEqual,
|
|
6
|
+
maxUint256,
|
|
7
|
+
parseEventLogs,
|
|
8
|
+
zeroAddress,
|
|
9
|
+
} from 'viem'
|
|
3
10
|
import { sendTransaction, waitForTransactionReceipt } from 'viem/actions'
|
|
4
11
|
import { Transaction } from 'viem/tempo'
|
|
5
12
|
import { describe, expect, test } from 'vp/test'
|
|
@@ -42,6 +49,15 @@ function getSingleEvent(receipt: { logs: readonly unknown[] }, name: string) {
|
|
|
42
49
|
return logs[0] as unknown as { args: Record<string, unknown> }
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
function expectExpiringNonce(serializedTransaction: Hex.Hex) {
|
|
53
|
+
const transaction = Transaction.deserialize(
|
|
54
|
+
serializedTransaction as Transaction.TransactionSerializedTempo,
|
|
55
|
+
) as unknown as { nonce: number; nonceKey: bigint; validBefore?: bigint }
|
|
56
|
+
expect(transaction.nonce).toBe(0)
|
|
57
|
+
expect(transaction.nonceKey).toBe(maxUint256)
|
|
58
|
+
expect(transaction.validBefore).toBeDefined()
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
async function openChannel(parameters: { deposit?: bigint | undefined } = {}) {
|
|
46
62
|
const deposit = uint96(parameters.deposit ?? 1_000n)
|
|
47
63
|
const salt = Hex.random(32)
|
|
@@ -120,6 +136,7 @@ describe.runIf(isPrecompileTestnet)('TIP20EscrowChannel precompile chain operati
|
|
|
120
136
|
token: asset,
|
|
121
137
|
})
|
|
122
138
|
if (payload.action !== 'open') throw new Error('expected open payload')
|
|
139
|
+
expectExpiringNonce(payload.transaction)
|
|
123
140
|
|
|
124
141
|
const transaction = Transaction.deserialize(
|
|
125
142
|
payload.transaction as Transaction.TransactionSerializedTempo,
|
|
@@ -224,6 +241,7 @@ describe.runIf(isPrecompileTestnet)('TIP20EscrowChannel precompile chain operati
|
|
|
224
241
|
chain.id,
|
|
225
242
|
)
|
|
226
243
|
if (topUp.action !== 'topUp') throw new Error('expected topUp payload')
|
|
244
|
+
expectExpiringNonce(topUp.transaction)
|
|
227
245
|
|
|
228
246
|
const result = await Chain.broadcastTopUpTransaction({
|
|
229
247
|
additionalDeposit,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type Hex,
|
|
13
13
|
} from 'viem'
|
|
14
14
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
15
|
-
import { Transaction } from 'viem/tempo'
|
|
15
|
+
import { Abis, Addresses, Transaction } from 'viem/tempo'
|
|
16
16
|
import { describe, expect, test } from 'vp/test'
|
|
17
17
|
|
|
18
18
|
import { VerificationFailedError } from '../../../Errors.js'
|
|
@@ -36,6 +36,7 @@ const descriptor = {
|
|
|
36
36
|
const deposit = Types.uint96(1_000_000n)
|
|
37
37
|
const chainId = 42431
|
|
38
38
|
const txHash = `0x${'ab'.repeat(32)}` as const
|
|
39
|
+
const sourceToken = '0x6666666666666666666666666666666666666666' as const
|
|
39
40
|
const feePayer = privateKeyToAccount(
|
|
40
41
|
'0x59c6995e998f97a5a0044966f0945389d1fc6e60e7346d6c36c49d32f75b9a1b',
|
|
41
42
|
)
|
|
@@ -162,7 +163,11 @@ function topUpLog(parameters: { channelId: `0x${string}`; newDeposit: bigint })
|
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
async function createSerializedTransaction(parameters: {
|
|
165
|
-
calls: {
|
|
166
|
+
calls: {
|
|
167
|
+
to?: `0x${string}` | undefined
|
|
168
|
+
data?: `0x${string}` | undefined
|
|
169
|
+
value?: bigint | undefined
|
|
170
|
+
}[]
|
|
166
171
|
gas?: bigint | undefined
|
|
167
172
|
signed?: boolean | undefined
|
|
168
173
|
}) {
|
|
@@ -192,6 +197,28 @@ async function createSerializedTransaction(parameters: {
|
|
|
192
197
|
} as never)) as `0x${string}`
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
function autoSwapCalls(amountOut: bigint = deposit) {
|
|
201
|
+
const maxAmountIn = amountOut + 1_000n
|
|
202
|
+
return [
|
|
203
|
+
{
|
|
204
|
+
to: sourceToken,
|
|
205
|
+
data: encodeFunctionData({
|
|
206
|
+
abi: Abis.tip20,
|
|
207
|
+
functionName: 'approve',
|
|
208
|
+
args: [Addresses.stablecoinDex, maxAmountIn],
|
|
209
|
+
}),
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
to: Addresses.stablecoinDex,
|
|
213
|
+
data: encodeFunctionData({
|
|
214
|
+
abi: Abis.stablecoinDex,
|
|
215
|
+
functionName: 'swapExactAmountOut',
|
|
216
|
+
args: [sourceToken, descriptor.token, amountOut, maxAmountIn],
|
|
217
|
+
}),
|
|
218
|
+
},
|
|
219
|
+
] as const
|
|
220
|
+
}
|
|
221
|
+
|
|
195
222
|
async function createOpenTransaction(
|
|
196
223
|
parameters: {
|
|
197
224
|
authorizedSigner?: `0x${string}` | undefined
|
|
@@ -201,6 +228,13 @@ async function createOpenTransaction(
|
|
|
201
228
|
signed?: boolean | undefined
|
|
202
229
|
token?: `0x${string}` | undefined
|
|
203
230
|
to?: `0x${string}` | undefined
|
|
231
|
+
prefixCalls?:
|
|
232
|
+
| readonly {
|
|
233
|
+
to?: `0x${string}` | undefined
|
|
234
|
+
data?: `0x${string}` | undefined
|
|
235
|
+
value?: bigint | undefined
|
|
236
|
+
}[]
|
|
237
|
+
| undefined
|
|
204
238
|
} = {},
|
|
205
239
|
) {
|
|
206
240
|
const data = encodeFunctionData({
|
|
@@ -216,7 +250,7 @@ async function createOpenTransaction(
|
|
|
216
250
|
],
|
|
217
251
|
})
|
|
218
252
|
return createSerializedTransaction({
|
|
219
|
-
calls: [{ to: parameters.to ?? tip20ChannelEscrow, data }],
|
|
253
|
+
calls: [...(parameters.prefixCalls ?? []), { to: parameters.to ?? tip20ChannelEscrow, data }],
|
|
220
254
|
gas: parameters.gas,
|
|
221
255
|
signed: parameters.signed,
|
|
222
256
|
})
|
|
@@ -229,6 +263,13 @@ async function createTopUpTransaction(
|
|
|
229
263
|
gas?: bigint | undefined
|
|
230
264
|
signed?: boolean | undefined
|
|
231
265
|
to?: `0x${string}` | undefined
|
|
266
|
+
prefixCalls?:
|
|
267
|
+
| readonly {
|
|
268
|
+
to?: `0x${string}` | undefined
|
|
269
|
+
data?: `0x${string}` | undefined
|
|
270
|
+
value?: bigint | undefined
|
|
271
|
+
}[]
|
|
272
|
+
| undefined
|
|
232
273
|
} = {},
|
|
233
274
|
) {
|
|
234
275
|
const data = encodeFunctionData({
|
|
@@ -240,7 +281,7 @@ async function createTopUpTransaction(
|
|
|
240
281
|
],
|
|
241
282
|
})
|
|
242
283
|
return createSerializedTransaction({
|
|
243
|
-
calls: [{ to: parameters.to ?? tip20ChannelEscrow, data }],
|
|
284
|
+
calls: [...(parameters.prefixCalls ?? []), { to: parameters.to ?? tip20ChannelEscrow, data }],
|
|
244
285
|
gas: parameters.gas,
|
|
245
286
|
signed: parameters.signed,
|
|
246
287
|
})
|
|
@@ -446,8 +487,152 @@ describe('precompile broadcastOpenTransaction', () => {
|
|
|
446
487
|
expectedPayer: descriptor.payer,
|
|
447
488
|
serializedTransaction,
|
|
448
489
|
}),
|
|
449
|
-
).rejects.toThrow(
|
|
490
|
+
).rejects.toThrow(
|
|
491
|
+
'TIP-1034 open transaction must contain one management call, optionally preceded by an auto-swap',
|
|
492
|
+
)
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
const [validApprove, validSwap] = autoSwapCalls()
|
|
496
|
+
const wrongToken = '0x7777777777777777777777777777777777777777' as const
|
|
497
|
+
const maxAmountIn = deposit + 1_000n
|
|
498
|
+
const approve = (parameters: {
|
|
499
|
+
amount?: bigint | undefined
|
|
500
|
+
spender?: Address | undefined
|
|
501
|
+
token?: Address | undefined
|
|
502
|
+
}) => ({
|
|
503
|
+
to: parameters.token ?? sourceToken,
|
|
504
|
+
data: encodeFunctionData({
|
|
505
|
+
abi: Abis.tip20,
|
|
506
|
+
functionName: 'approve',
|
|
507
|
+
args: [parameters.spender ?? Addresses.stablecoinDex, parameters.amount ?? maxAmountIn],
|
|
508
|
+
}),
|
|
509
|
+
})
|
|
510
|
+
const swap = (parameters: {
|
|
511
|
+
amountOut?: bigint | undefined
|
|
512
|
+
maxAmount?: bigint | undefined
|
|
513
|
+
tokenIn?: Address | undefined
|
|
514
|
+
tokenOut?: Address | undefined
|
|
515
|
+
}) => ({
|
|
516
|
+
to: Addresses.stablecoinDex as Address,
|
|
517
|
+
data: encodeFunctionData({
|
|
518
|
+
abi: Abis.stablecoinDex,
|
|
519
|
+
functionName: 'swapExactAmountOut',
|
|
520
|
+
args: [
|
|
521
|
+
parameters.tokenIn ?? sourceToken,
|
|
522
|
+
parameters.tokenOut ?? descriptor.token,
|
|
523
|
+
parameters.amountOut ?? deposit,
|
|
524
|
+
parameters.maxAmount ?? maxAmountIn,
|
|
525
|
+
],
|
|
526
|
+
}),
|
|
450
527
|
})
|
|
528
|
+
const invalidAutoSwapPrefixes = [
|
|
529
|
+
{
|
|
530
|
+
name: 'missing call target',
|
|
531
|
+
prefixCalls: [{ data: validApprove.data }, validSwap],
|
|
532
|
+
reason: 'call is missing a target or calldata',
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: 'native value',
|
|
536
|
+
prefixCalls: [{ ...validApprove, value: 1n }, validSwap],
|
|
537
|
+
reason: 'calls must not transfer native value',
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: 'wrong DEX target',
|
|
541
|
+
prefixCalls: [validApprove, { ...validSwap, to: wrongToken }],
|
|
542
|
+
reason: 'targets the wrong DEX',
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: 'invalid approval calldata',
|
|
546
|
+
prefixCalls: [{ ...validApprove, data: '0x12345678' as const }, validSwap],
|
|
547
|
+
reason: 'approval calldata is invalid',
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: 'invalid swap calldata',
|
|
551
|
+
prefixCalls: [validApprove, { ...validSwap, data: '0x12345678' as const }],
|
|
552
|
+
reason: 'swap calldata is invalid',
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
name: 'wrong approval function',
|
|
556
|
+
prefixCalls: [
|
|
557
|
+
{
|
|
558
|
+
to: sourceToken,
|
|
559
|
+
data: encodeFunctionData({
|
|
560
|
+
abi: Abis.tip20,
|
|
561
|
+
functionName: 'transfer',
|
|
562
|
+
args: [Addresses.stablecoinDex, maxAmountIn],
|
|
563
|
+
}),
|
|
564
|
+
},
|
|
565
|
+
validSwap,
|
|
566
|
+
],
|
|
567
|
+
reason: 'must contain approve followed by swapExactAmountOut',
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: 'approval token mismatch',
|
|
571
|
+
prefixCalls: [approve({ token: wrongToken }), validSwap],
|
|
572
|
+
reason: 'approval token does not match swap input',
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: 'approval spender mismatch',
|
|
576
|
+
prefixCalls: [approve({ spender: wrongToken }), validSwap],
|
|
577
|
+
reason: 'approval spender is not the DEX',
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
name: 'approval amount mismatch',
|
|
581
|
+
prefixCalls: [approve({ amount: maxAmountIn + 1n }), validSwap],
|
|
582
|
+
reason: 'approval amount does not match swap maximum input',
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: 'output token mismatch',
|
|
586
|
+
prefixCalls: [validApprove, swap({ tokenOut: wrongToken })],
|
|
587
|
+
reason: 'output token does not match channel currency',
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
name: 'output amount mismatch',
|
|
591
|
+
prefixCalls: [
|
|
592
|
+
approve({ amount: maxAmountIn + 1n }),
|
|
593
|
+
swap({ amountOut: deposit + 1n, maxAmount: maxAmountIn + 1n }),
|
|
594
|
+
],
|
|
595
|
+
reason: 'output amount does not match channel deposit',
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: 'identical input and output tokens',
|
|
599
|
+
prefixCalls: [approve({ token: descriptor.token }), swap({ tokenIn: descriptor.token })],
|
|
600
|
+
reason: 'input and output tokens must differ',
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
name: 'non-canonical approval calldata',
|
|
604
|
+
prefixCalls: [{ ...validApprove, data: `${validApprove.data}00` as Hex }, validSwap],
|
|
605
|
+
reason: 'approval calldata is not canonical',
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
name: 'non-canonical swap calldata',
|
|
609
|
+
prefixCalls: [validApprove, { ...validSwap, data: `${validSwap.data}00` as Hex }],
|
|
610
|
+
reason: 'swap calldata is not canonical',
|
|
611
|
+
},
|
|
612
|
+
] as const
|
|
613
|
+
|
|
614
|
+
test.each(invalidAutoSwapPrefixes)(
|
|
615
|
+
'rejects an auto-swap with $name',
|
|
616
|
+
async ({ prefixCalls, reason }) => {
|
|
617
|
+
const serializedTransaction = await createOpenTransaction({ prefixCalls })
|
|
618
|
+
|
|
619
|
+
await expect(
|
|
620
|
+
Chain.broadcastOpenTransaction({
|
|
621
|
+
chainId,
|
|
622
|
+
client: createMockClient(),
|
|
623
|
+
escrowContract: tip20ChannelEscrow,
|
|
624
|
+
expectedAuthorizedSigner: descriptor.authorizedSigner,
|
|
625
|
+
expectedChannelId: `0x${'11'.repeat(32)}`,
|
|
626
|
+
expectedCurrency: descriptor.token,
|
|
627
|
+
expectedExpiringNonceHash: expectedExpiringNonceHash(serializedTransaction),
|
|
628
|
+
expectedOperator: descriptor.operator,
|
|
629
|
+
expectedPayee: descriptor.payee,
|
|
630
|
+
expectedPayer: descriptor.payer,
|
|
631
|
+
serializedTransaction,
|
|
632
|
+
}),
|
|
633
|
+
).rejects.toThrow(`TIP-1034 open auto-swap ${reason}`)
|
|
634
|
+
},
|
|
635
|
+
)
|
|
451
636
|
|
|
452
637
|
test('rejects open transactions targeting the wrong escrow contract', async () => {
|
|
453
638
|
const serializedTransaction = await createOpenTransaction({ to: descriptor.token })
|
|
@@ -715,8 +900,8 @@ describe('precompile broadcastOpenTransaction', () => {
|
|
|
715
900
|
).rejects.toThrow('credential expiringNonceHash does not match transaction')
|
|
716
901
|
})
|
|
717
902
|
|
|
718
|
-
test('
|
|
719
|
-
const serializedTransaction = await createOpenTransaction()
|
|
903
|
+
test('accepts an exact-output auto-swap before open', async () => {
|
|
904
|
+
const serializedTransaction = await createOpenTransaction({ prefixCalls: autoSwapCalls() })
|
|
720
905
|
const expiringNonceHash = expectedExpiringNonceHash(serializedTransaction)
|
|
721
906
|
const expectedDescriptor = { ...descriptor, expiringNonceHash }
|
|
722
907
|
const channelId = Channel.computeId({
|
|
@@ -858,7 +1043,9 @@ describe('precompile broadcastTopUpTransaction', () => {
|
|
|
858
1043
|
expectedCurrency: descriptor.token,
|
|
859
1044
|
serializedTransaction,
|
|
860
1045
|
}),
|
|
861
|
-
).rejects.toThrow(
|
|
1046
|
+
).rejects.toThrow(
|
|
1047
|
+
'TIP-1034 topUp transaction must contain one management call, optionally preceded by an auto-swap',
|
|
1048
|
+
)
|
|
862
1049
|
})
|
|
863
1050
|
|
|
864
1051
|
test('rejects top-up transactions targeting the wrong escrow contract', async () => {
|
|
@@ -1025,8 +1212,8 @@ describe('precompile broadcastTopUpTransaction', () => {
|
|
|
1025
1212
|
).rejects.toThrow('topUp deposit does not match credential')
|
|
1026
1213
|
})
|
|
1027
1214
|
|
|
1028
|
-
test('
|
|
1029
|
-
const serializedTransaction = await createTopUpTransaction()
|
|
1215
|
+
test('accepts an exact-output auto-swap before top-up', async () => {
|
|
1216
|
+
const serializedTransaction = await createTopUpTransaction({ prefixCalls: autoSwapCalls() })
|
|
1030
1217
|
const channelId = Channel.computeId({ ...descriptor, chainId, escrow: tip20ChannelEscrow })
|
|
1031
1218
|
const newDeposit = deposit * 2n
|
|
1032
1219
|
const state = { settled: 0n, deposit: newDeposit, closeRequestedAt: 0 }
|