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
|
@@ -47,6 +47,8 @@ export const WebSocketReadyState = {
|
|
|
47
47
|
/** Client-side close code used for payment protocol errors. */
|
|
48
48
|
export const ClientWebSocketProtocolErrorCloseCode = 3008
|
|
49
49
|
|
|
50
|
+
const webSocketChallengeRefreshWindowMs = 5_000
|
|
51
|
+
|
|
50
52
|
type EventType = 'close' | 'error' | 'message' | 'open'
|
|
51
53
|
type ManagedEventMap = {
|
|
52
54
|
close: { code: number; reason: string; type: 'close'; wasClean: boolean }
|
|
@@ -268,6 +270,8 @@ export type ApplyTopUpResultParameters = {
|
|
|
268
270
|
challengeId?: string | undefined
|
|
269
271
|
/** Current active machine state, used to preserve paid unit count when possible. */
|
|
270
272
|
currentState: SessionState
|
|
273
|
+
/** Highest deposit known before the top-up. */
|
|
274
|
+
knownDeposit?: bigint | undefined
|
|
271
275
|
/** Receipt returned by the top-up POST, when present. */
|
|
272
276
|
receipt?: SessionReceipt | undefined
|
|
273
277
|
/** Latest locally observed spend in raw units. */
|
|
@@ -362,26 +366,36 @@ export function resolveManualTopUp(parameters: ResolveManualTopUpParameters): Ma
|
|
|
362
366
|
/**
|
|
363
367
|
* Applies local deposit and state bookkeeping for a top-up response.
|
|
364
368
|
*
|
|
365
|
-
*
|
|
366
|
-
* update accepted spend authorization; they do not replace deposit.
|
|
369
|
+
* Deposit advances from the highest known baseline. Receipt cumulative values
|
|
370
|
+
* only update accepted spend authorization; they do not replace deposit.
|
|
367
371
|
*/
|
|
368
372
|
export function applyTopUpResult(
|
|
369
373
|
parameters: ApplyTopUpResultParameters,
|
|
370
374
|
): AppliedTopUpResult | undefined {
|
|
371
|
-
const {
|
|
372
|
-
|
|
375
|
+
const {
|
|
376
|
+
additionalDeposit,
|
|
377
|
+
channel,
|
|
378
|
+
channelId,
|
|
379
|
+
challengeId,
|
|
380
|
+
currentState,
|
|
381
|
+
knownDeposit,
|
|
382
|
+
receipt,
|
|
383
|
+
spent,
|
|
384
|
+
} = parameters
|
|
373
385
|
if (channel?.channelId !== channelId) return undefined
|
|
374
386
|
|
|
375
|
-
|
|
387
|
+
const baseline =
|
|
388
|
+
knownDeposit !== undefined && knownDeposit > channel.deposit ? knownDeposit : channel.deposit
|
|
389
|
+
const updated = { ...channel, deposit: baseline + additionalDeposit }
|
|
376
390
|
|
|
377
|
-
if (receipt) return { channel, state: activeStateFromReceipt(receipt,
|
|
378
|
-
if (!challengeId) return { channel }
|
|
391
|
+
if (receipt) return { channel: updated, state: activeStateFromReceipt(receipt, updated) }
|
|
392
|
+
if (!challengeId) return { channel: updated }
|
|
379
393
|
|
|
380
394
|
return {
|
|
381
|
-
channel,
|
|
395
|
+
channel: updated,
|
|
382
396
|
state: activeStateFromChannel({
|
|
383
397
|
challengeId,
|
|
384
|
-
entry:
|
|
398
|
+
entry: updated,
|
|
385
399
|
spent: spent.toString(),
|
|
386
400
|
units: currentState.status === 'active' ? currentState.units : 0,
|
|
387
401
|
}),
|
|
@@ -477,7 +491,7 @@ export type RetryHttpPaymentRequiredParameters = {
|
|
|
477
491
|
/** Failed HTTP response that may contain a session challenge. */
|
|
478
492
|
response: Response
|
|
479
493
|
/** Restores local cumulative authorization if the voucher retry fails. */
|
|
480
|
-
restoreCumulative(channelId: Hex.Hex, cumulativeAmount: bigint): void
|
|
494
|
+
restoreCumulative(channelId: Hex.Hex, cumulativeAmount: bigint): void | Promise<void>
|
|
481
495
|
/** Stores the selected follow-up challenge on the manager. */
|
|
482
496
|
setChallenge(challenge: TempoSessionChallenge): void
|
|
483
497
|
/** Performs automatic top-up before the voucher retry when deposit is insufficient. */
|
|
@@ -521,6 +535,19 @@ export function isTempoSessionChallenge(
|
|
|
521
535
|
)
|
|
522
536
|
}
|
|
523
537
|
|
|
538
|
+
/** Returns true when a challenge selects the TIP-1034 session protocol. */
|
|
539
|
+
export function isTip1034SessionChallenge(
|
|
540
|
+
challenge: Challenge.Challenge,
|
|
541
|
+
): challenge is TempoSessionChallenge {
|
|
542
|
+
return (
|
|
543
|
+
isTempoSessionChallenge(challenge) &&
|
|
544
|
+
Constants.getMethodDetail(
|
|
545
|
+
challenge.request.methodDetails,
|
|
546
|
+
Constants.MethodDetailKeys.sessionProtocol,
|
|
547
|
+
) === Constants.SessionProtocols.v2
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
524
551
|
/** Reads a server-provided session snapshot from challenge method details. */
|
|
525
552
|
export function getSessionSnapshot(challenge: TempoSessionChallenge): SessionSnapshot | undefined {
|
|
526
553
|
return Constants.getMethodDetail<SessionSnapshot>(
|
|
@@ -594,15 +621,17 @@ export async function postTopUp(
|
|
|
594
621
|
throw new Error('Cannot top up session: no local channel descriptor available.')
|
|
595
622
|
}
|
|
596
623
|
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
624
|
+
const response = await postManagementCredential({
|
|
625
|
+
challenge: parameters.challenge,
|
|
626
|
+
context: {
|
|
627
|
+
action: 'topUp',
|
|
628
|
+
channelId,
|
|
629
|
+
descriptor: channel.descriptor,
|
|
630
|
+
additionalDepositRaw: parameters.additionalDeposit.toString(),
|
|
631
|
+
},
|
|
632
|
+
createSessionCredential: parameters.createSessionCredential,
|
|
633
|
+
fetch: parameters.fetch,
|
|
634
|
+
input: parameters.input,
|
|
606
635
|
})
|
|
607
636
|
if (!response.ok) throw new Error(`Top-up POST failed with status ${response.status}`)
|
|
608
637
|
|
|
@@ -610,6 +639,30 @@ export async function postTopUp(
|
|
|
610
639
|
return receiptHeader ? deserializeSessionReceipt(receiptHeader) : undefined
|
|
611
640
|
}
|
|
612
641
|
|
|
642
|
+
/** Posts a management credential and retries once with a refreshed TIP-1034 challenge. */
|
|
643
|
+
async function postManagementCredential(parameters: {
|
|
644
|
+
challenge: TempoSessionChallenge
|
|
645
|
+
context: SessionContext
|
|
646
|
+
createSessionCredential: CreateSessionCredential
|
|
647
|
+
fetch: typeof globalThis.fetch
|
|
648
|
+
input: RequestInfo | URL
|
|
649
|
+
}): Promise<Response> {
|
|
650
|
+
const post = async (challenge: TempoSessionChallenge) =>
|
|
651
|
+
parameters.fetch(managementInput(parameters.input), {
|
|
652
|
+
method: 'POST',
|
|
653
|
+
headers: {
|
|
654
|
+
[Constants.Headers.authorization]: await parameters.createSessionCredential(
|
|
655
|
+
challenge,
|
|
656
|
+
parameters.context,
|
|
657
|
+
),
|
|
658
|
+
},
|
|
659
|
+
})
|
|
660
|
+
const response = await post(parameters.challenge)
|
|
661
|
+
if (response.status !== 402) return response
|
|
662
|
+
const challenge = Challenge.fromResponseList(response).find(isTip1034SessionChallenge)
|
|
663
|
+
return challenge ? post(challenge) : response
|
|
664
|
+
}
|
|
665
|
+
|
|
613
666
|
/** Retries an HTTP 402 when the server snapshot asks for more voucher headroom. */
|
|
614
667
|
export async function retryHttpPaymentRequired(
|
|
615
668
|
parameters: RetryHttpPaymentRequiredParameters,
|
|
@@ -641,18 +694,25 @@ export async function retryHttpPaymentRequired(
|
|
|
641
694
|
currentChannel.cumulativeAmount > requiredCumulative
|
|
642
695
|
? currentChannel.cumulativeAmount
|
|
643
696
|
: requiredCumulative
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
697
|
+
const restore = () => parameters.restoreCumulative(snapshot.channelId, cumulativeBeforeVoucher)
|
|
698
|
+
let retry: Response
|
|
699
|
+
try {
|
|
700
|
+
const credential = await parameters.createSessionCredential(challenge, {
|
|
701
|
+
action: 'voucher',
|
|
702
|
+
channelId: snapshot.channelId,
|
|
703
|
+
descriptor: currentChannel.descriptor,
|
|
704
|
+
cumulativeAmountRaw: cumulativeAmount.toString(),
|
|
705
|
+
})
|
|
706
|
+
retry = await parameters.fetch(
|
|
707
|
+
parameters.input,
|
|
708
|
+
requestInitWithAuthorization(parameters.input, parameters.init, credential),
|
|
709
|
+
)
|
|
710
|
+
} catch (error) {
|
|
711
|
+
await restore()
|
|
712
|
+
throw error
|
|
713
|
+
}
|
|
654
714
|
if (!retry.ok && !retry.headers.get(Constants.Headers.paymentReceipt)) {
|
|
655
|
-
|
|
715
|
+
await restore()
|
|
656
716
|
}
|
|
657
717
|
return retry
|
|
658
718
|
}
|
|
@@ -662,7 +722,7 @@ export function resolveRetryHttpPaymentContext(parameters: {
|
|
|
662
722
|
channel: ChannelEntry | null
|
|
663
723
|
response: Response
|
|
664
724
|
}): RetryHttpPaymentContext | undefined {
|
|
665
|
-
const challenge = Challenge.fromResponseList(parameters.response).find(
|
|
725
|
+
const challenge = Challenge.fromResponseList(parameters.response).find(isTip1034SessionChallenge)
|
|
666
726
|
if (!challenge) return undefined
|
|
667
727
|
|
|
668
728
|
const snapshot = getSessionSnapshot(challenge)
|
|
@@ -705,7 +765,7 @@ export async function closeHttpSession(
|
|
|
705
765
|
|
|
706
766
|
let response = await postClose(parameters.target.challenge)
|
|
707
767
|
if (response.status === 402) {
|
|
708
|
-
const challenge = Challenge.fromResponseList(response).find(
|
|
768
|
+
const challenge = Challenge.fromResponseList(response).find(isTip1034SessionChallenge)
|
|
709
769
|
if (challenge) {
|
|
710
770
|
parameters.setChallenge?.(challenge)
|
|
711
771
|
response = await postClose(challenge, true)
|
|
@@ -768,8 +828,6 @@ export type OpenSseSessionParameters = {
|
|
|
768
828
|
getChallenge(): TempoSessionChallenge | null
|
|
769
829
|
/** Validates a cumulative amount against local client policy. */
|
|
770
830
|
assertVoucherWithinLocalLimit(cumulativeAmount: bigint): void
|
|
771
|
-
/** Converts a resource URL to the server's session management URL. */
|
|
772
|
-
managementInput(input: RequestInfo | URL): RequestInfo | URL
|
|
773
831
|
/** Applies an incoming receipt to manager state. */
|
|
774
832
|
acceptReceipt(receipt: SessionReceipt): void
|
|
775
833
|
/** Performs an automatic channel top-up when deposit is insufficient. */
|
|
@@ -783,7 +841,6 @@ export type SsePaymentDriver = Pick<
|
|
|
783
841
|
| 'createSessionCredential'
|
|
784
842
|
| 'fetch'
|
|
785
843
|
| 'getChannel'
|
|
786
|
-
| 'managementInput'
|
|
787
844
|
| 'topUpIfNeeded'
|
|
788
845
|
>
|
|
789
846
|
|
|
@@ -1018,17 +1075,13 @@ export async function handleSseNeedVoucher(
|
|
|
1018
1075
|
})
|
|
1019
1076
|
if (resolution.status !== 'ready') return
|
|
1020
1077
|
|
|
1021
|
-
const
|
|
1022
|
-
parameters.challenge,
|
|
1023
|
-
resolution.context,
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
parameters.
|
|
1027
|
-
|
|
1028
|
-
method: 'POST',
|
|
1029
|
-
headers: { [Constants.Headers.authorization]: credential },
|
|
1030
|
-
},
|
|
1031
|
-
)
|
|
1078
|
+
const voucherResponse = await postManagementCredential({
|
|
1079
|
+
challenge: parameters.challenge,
|
|
1080
|
+
context: resolution.context,
|
|
1081
|
+
createSessionCredential: parameters.driver.createSessionCredential,
|
|
1082
|
+
fetch: parameters.driver.fetch,
|
|
1083
|
+
input: parameters.input,
|
|
1084
|
+
})
|
|
1032
1085
|
if (!voucherResponse.ok) {
|
|
1033
1086
|
throw new Error(`Voucher POST failed with status ${voucherResponse.status}`)
|
|
1034
1087
|
}
|
|
@@ -1066,6 +1119,15 @@ export type PrepareWebSocketSessionParameters = {
|
|
|
1066
1119
|
signal?: AbortSignal | undefined
|
|
1067
1120
|
}
|
|
1068
1121
|
|
|
1122
|
+
/** Inputs for probing a paid WebSocket endpoint without creating a credential. */
|
|
1123
|
+
export type ProbeWebSocketSessionParameters = Omit<
|
|
1124
|
+
PrepareWebSocketSessionParameters,
|
|
1125
|
+
'createSessionCredential'
|
|
1126
|
+
>
|
|
1127
|
+
|
|
1128
|
+
/** Result of resolving the current payment challenge for a WebSocket endpoint. */
|
|
1129
|
+
export type ProbedWebSocketSession = Omit<PreparedWebSocketSession, 'credential'>
|
|
1130
|
+
|
|
1069
1131
|
/** Result of the HTTP probe and opening credential creation for a WebSocket session. */
|
|
1070
1132
|
export type PreparedWebSocketSession = {
|
|
1071
1133
|
/** Selected tempo/session challenge from the HTTP probe. */
|
|
@@ -1129,6 +1191,8 @@ export type OpenWebSocketSessionParameters = {
|
|
|
1129
1191
|
getChannel(): ChannelEntry | null
|
|
1130
1192
|
/** Stores the active socket state in the manager. */
|
|
1131
1193
|
setSocketSession(session: ActiveSocketSession): void
|
|
1194
|
+
/** Fetches the route's current challenge after the active one expires. */
|
|
1195
|
+
refreshChallenge(): Promise<TempoSessionChallenge>
|
|
1132
1196
|
/** Validates a cumulative amount against local client policy. */
|
|
1133
1197
|
assertVoucherWithinLocalLimit(cumulativeAmount: bigint): void
|
|
1134
1198
|
/** Applies an incoming receipt to manager state. */
|
|
@@ -1169,10 +1233,10 @@ export type ValidateSocketPaymentReceiptParameters = ExpectedSocketReceiptParame
|
|
|
1169
1233
|
expectedCloseAmount: string | null
|
|
1170
1234
|
}
|
|
1171
1235
|
|
|
1172
|
-
/** Probes a WebSocket endpoint over HTTP and
|
|
1173
|
-
export async function
|
|
1174
|
-
parameters:
|
|
1175
|
-
): Promise<
|
|
1236
|
+
/** Probes a WebSocket endpoint over HTTP and returns its current session challenge. */
|
|
1237
|
+
export async function probeWebSocketSession(
|
|
1238
|
+
parameters: ProbeWebSocketSessionParameters,
|
|
1239
|
+
): Promise<ProbedWebSocketSession> {
|
|
1176
1240
|
const wsUrl = new URL(parameters.input.toString())
|
|
1177
1241
|
const httpUrl = webSocketProbeUrl(wsUrl)
|
|
1178
1242
|
parameters.onProbeUrl?.(httpUrl)
|
|
@@ -1186,7 +1250,7 @@ export async function prepareWebSocketSession(
|
|
|
1186
1250
|
)
|
|
1187
1251
|
}
|
|
1188
1252
|
|
|
1189
|
-
const challenge = Challenge.fromResponseList(probe).find(
|
|
1253
|
+
const challenge = Challenge.fromResponseList(probe).find(isTip1034SessionChallenge)
|
|
1190
1254
|
if (!challenge) {
|
|
1191
1255
|
throw new Error(
|
|
1192
1256
|
'No payment challenge received from HTTP endpoint for this WebSocket URL. The server may not require payment or did not advertise a challenge.',
|
|
@@ -1195,12 +1259,22 @@ export async function prepareWebSocketSession(
|
|
|
1195
1259
|
|
|
1196
1260
|
return {
|
|
1197
1261
|
challenge,
|
|
1198
|
-
credential: await parameters.createSessionCredential(challenge, {}),
|
|
1199
1262
|
httpUrl,
|
|
1200
1263
|
wsUrl,
|
|
1201
1264
|
}
|
|
1202
1265
|
}
|
|
1203
1266
|
|
|
1267
|
+
/** Probes a WebSocket endpoint over HTTP and creates the opening credential. */
|
|
1268
|
+
export async function prepareWebSocketSession(
|
|
1269
|
+
parameters: PrepareWebSocketSessionParameters,
|
|
1270
|
+
): Promise<PreparedWebSocketSession> {
|
|
1271
|
+
const probed = await probeWebSocketSession(parameters)
|
|
1272
|
+
return {
|
|
1273
|
+
...probed,
|
|
1274
|
+
credential: await parameters.createSessionCredential(probed.challenge, {}),
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1204
1278
|
/** Creates the initial runtime state for a paid WebSocket from its opening credential. */
|
|
1205
1279
|
export function createActiveSocketSession(
|
|
1206
1280
|
parameters: CreateActiveSocketSessionParameters,
|
|
@@ -1408,9 +1482,18 @@ async function handleNeedVoucher(
|
|
|
1408
1482
|
event: NeedVoucherEvent,
|
|
1409
1483
|
) {
|
|
1410
1484
|
try {
|
|
1485
|
+
let challenge = parameters.driver.challenge
|
|
1486
|
+
if (
|
|
1487
|
+
challenge.expires &&
|
|
1488
|
+
new Date(challenge.expires).getTime() <= Date.now() + webSocketChallengeRefreshWindowMs
|
|
1489
|
+
) {
|
|
1490
|
+
challenge = await parameters.driver.refreshChallenge()
|
|
1491
|
+
parameters.driver.challenge = challenge
|
|
1492
|
+
parameters.socketState.challenge = challenge
|
|
1493
|
+
}
|
|
1411
1494
|
const resolution = await resolveNeedVoucherContext({
|
|
1412
1495
|
assertVoucherWithinLocalLimit: parameters.driver.assertVoucherWithinLocalLimit,
|
|
1413
|
-
challenge
|
|
1496
|
+
challenge,
|
|
1414
1497
|
event,
|
|
1415
1498
|
expectedChannelId: parameters.socketState.channelId,
|
|
1416
1499
|
getChannel: parameters.driver.getChannel,
|
|
@@ -1425,10 +1508,7 @@ async function handleNeedVoucher(
|
|
|
1425
1508
|
)
|
|
1426
1509
|
return
|
|
1427
1510
|
}
|
|
1428
|
-
const voucher = await parameters.driver.createSessionCredential(
|
|
1429
|
-
parameters.driver.challenge,
|
|
1430
|
-
resolution.context,
|
|
1431
|
-
)
|
|
1511
|
+
const voucher = await parameters.driver.createSessionCredential(challenge, resolution.context)
|
|
1432
1512
|
parameters.rawSocket.send(Ws.formatAuthorizationMessage(voucher))
|
|
1433
1513
|
} catch (error) {
|
|
1434
1514
|
parameters.failSocketFlow(
|
|
@@ -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 }
|