mppx 0.4.11 → 0.5.0
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 +21 -0
- package/dist/Expires.d.ts +7 -0
- package/dist/Expires.d.ts.map +1 -1
- package/dist/Expires.js +21 -0
- package/dist/Expires.js.map +1 -1
- package/dist/internal/env.d.ts +1 -1
- package/dist/internal/env.d.ts.map +1 -1
- package/dist/internal/env.js +2 -6
- package/dist/internal/env.js.map +1 -1
- package/dist/internal/types.d.ts +23 -0
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/server/Mppx.d.ts +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +55 -7
- package/dist/server/Mppx.js.map +1 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +3 -3
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/tempo/Methods.d.ts +18 -0
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +28 -3
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts +24 -0
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +51 -9
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +18 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/account.d.ts +5 -11
- package/dist/tempo/internal/account.d.ts.map +1 -1
- package/dist/tempo/internal/charge.d.ts +20 -0
- package/dist/tempo/internal/charge.d.ts.map +1 -0
- package/dist/tempo/internal/charge.js +23 -0
- package/dist/tempo/internal/charge.js.map +1 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +15 -3
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/internal/proof.d.ts +23 -0
- package/dist/tempo/internal/proof.d.ts.map +1 -0
- package/dist/tempo/internal/proof.js +17 -0
- package/dist/tempo/internal/proof.js.map +1 -0
- package/dist/tempo/server/Charge.d.ts +20 -2
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +180 -103
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +20 -2
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +4 -1
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +9 -4
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +18 -3
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts +18 -2
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +18 -14
- package/dist/tempo/session/Chain.js.map +1 -1
- package/package.json +1 -1
- package/src/Expires.ts +25 -0
- package/src/cli/cli.test.ts +230 -1
- package/src/internal/env.test.ts +12 -12
- package/src/internal/env.ts +2 -6
- package/src/internal/types.ts +25 -0
- package/src/middlewares/elysia.test.ts +127 -4
- package/src/middlewares/express.test.ts +120 -54
- package/src/middlewares/hono.test.ts +73 -34
- package/src/middlewares/nextjs.test.ts +159 -36
- package/src/server/Mppx.test.ts +373 -0
- package/src/server/Mppx.ts +64 -10
- package/src/stripe/server/Charge.ts +3 -7
- package/src/tempo/Methods.test.ts +105 -0
- package/src/tempo/Methods.ts +54 -17
- package/src/tempo/client/Charge.ts +67 -11
- package/src/tempo/internal/account.ts +7 -14
- package/src/tempo/internal/charge.test.ts +66 -0
- package/src/tempo/internal/charge.ts +43 -0
- package/src/tempo/internal/fee-payer.test.ts +33 -14
- package/src/tempo/internal/fee-payer.ts +21 -6
- package/src/tempo/internal/proof.test.ts +36 -0
- package/src/tempo/internal/proof.ts +19 -0
- package/src/tempo/server/Charge.test.ts +593 -1
- package/src/tempo/server/Charge.ts +233 -126
- package/src/tempo/server/Methods.ts +4 -1
- package/src/tempo/server/Session.test.ts +1152 -54
- package/src/tempo/server/Session.ts +26 -17
- package/src/tempo/server/internal/transport.test.ts +32 -0
- package/src/tempo/session/Chain.test.ts +60 -5
- package/src/tempo/session/Chain.ts +30 -14
- package/src/tempo/session/Sse.test.ts +31 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type Address,
|
|
15
15
|
type Hex,
|
|
16
16
|
parseUnits,
|
|
17
|
+
zeroAddress,
|
|
17
18
|
type Account as viem_Account,
|
|
18
19
|
type Client as viem_Client,
|
|
19
20
|
} from 'viem'
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
VerificationFailedError,
|
|
31
32
|
} from '../../Errors.js'
|
|
32
33
|
import type { Challenge, Credential } from '../../index.js'
|
|
33
|
-
import type { LooseOmit } from '../../internal/types.js'
|
|
34
|
+
import type { LooseOmit, NoExtraKeys } from '../../internal/types.js'
|
|
34
35
|
import * as Method from '../../Method.js'
|
|
35
36
|
import * as Store from '../../Store.js'
|
|
36
37
|
import * as Client from '../../viem/Client.js'
|
|
@@ -82,7 +83,9 @@ type SessionMethodDetails = {
|
|
|
82
83
|
* })
|
|
83
84
|
* ```
|
|
84
85
|
*/
|
|
85
|
-
export function session<const parameters extends session.Parameters>(
|
|
86
|
+
export function session<const parameters extends session.Parameters>(
|
|
87
|
+
p?: NoExtraKeys<parameters, session.Parameters>,
|
|
88
|
+
) {
|
|
86
89
|
const parameters = p as parameters
|
|
87
90
|
const {
|
|
88
91
|
amount,
|
|
@@ -340,8 +343,10 @@ export async function settle(
|
|
|
340
343
|
channelId: Hex,
|
|
341
344
|
options?: {
|
|
342
345
|
escrowContract?: Address | undefined
|
|
343
|
-
|
|
344
|
-
|
|
346
|
+
} & (
|
|
347
|
+
| { feePayer: viem_Account; account: viem_Account }
|
|
348
|
+
| { feePayer?: undefined; account?: viem_Account | undefined }
|
|
349
|
+
),
|
|
345
350
|
): Promise<Hex> {
|
|
346
351
|
const channel = await store.getChannel(channelId)
|
|
347
352
|
if (!channel) throw new ChannelNotFoundError({ reason: 'channel not found' })
|
|
@@ -354,12 +359,11 @@ export async function settle(
|
|
|
354
359
|
if (!resolvedEscrow) throw new Error(`No escrow contract for chainId ${chainId}.`)
|
|
355
360
|
|
|
356
361
|
const settledAmount = channel.highestVoucher.cumulativeAmount
|
|
357
|
-
const txHash = await settleOnChain(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
)
|
|
362
|
+
const txHash = await settleOnChain(client, resolvedEscrow, channel.highestVoucher, {
|
|
363
|
+
...(options?.feePayer && options?.account
|
|
364
|
+
? { feePayer: options.feePayer, account: options.account }
|
|
365
|
+
: { account: options?.account }),
|
|
366
|
+
})
|
|
363
367
|
|
|
364
368
|
await store.updateChannel(channelId, (current) => {
|
|
365
369
|
if (!current) return null
|
|
@@ -456,6 +460,15 @@ async function verifyAndAcceptVoucher(parameters: {
|
|
|
456
460
|
if (onChain.closeRequestedAt !== 0n) {
|
|
457
461
|
throw new ChannelClosedError({ reason: 'channel has a pending close request' })
|
|
458
462
|
}
|
|
463
|
+
// Treat a zero deposit on an existing channel as settled/closed.
|
|
464
|
+
// During settlement the escrow contract may zero the deposit before
|
|
465
|
+
// setting the finalized flag, creating a brief window where
|
|
466
|
+
// finalized=false but deposit=0. Without this guard the voucher
|
|
467
|
+
// check below would return a 402 (AmountExceedsDepositError) instead
|
|
468
|
+
// of the correct 410 (ChannelClosedError).
|
|
469
|
+
if (onChain.deposit === 0n && onChain.payer !== zeroAddress) {
|
|
470
|
+
throw new ChannelClosedError({ reason: 'channel deposit is zero (settled)' })
|
|
471
|
+
}
|
|
459
472
|
|
|
460
473
|
if (voucher.cumulativeAmount <= onChain.settled) {
|
|
461
474
|
throw new VerificationFailedError({
|
|
@@ -843,13 +856,9 @@ async function handleClose(
|
|
|
843
856
|
throw new InvalidSignatureError({ reason: 'invalid voucher signature' })
|
|
844
857
|
}
|
|
845
858
|
|
|
846
|
-
const txHash = await closeOnChain(
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
voucher,
|
|
850
|
-
account,
|
|
851
|
-
feePayer,
|
|
852
|
-
)
|
|
859
|
+
const txHash = await closeOnChain(client, methodDetails.escrowContract, voucher, {
|
|
860
|
+
...(feePayer && account ? { feePayer, account } : { account }),
|
|
861
|
+
})
|
|
853
862
|
|
|
854
863
|
const updated = await store.updateChannel(payload.channelId, (current) => {
|
|
855
864
|
if (!current) return null
|
|
@@ -82,6 +82,19 @@ function makeReceipt() {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
async function readResponseText(response: Response): Promise<string> {
|
|
86
|
+
if (!response.body) return ''
|
|
87
|
+
const reader = response.body.getReader()
|
|
88
|
+
const decoder = new TextDecoder()
|
|
89
|
+
let result = ''
|
|
90
|
+
while (true) {
|
|
91
|
+
const { done, value } = await reader.read()
|
|
92
|
+
if (done) break
|
|
93
|
+
result += decoder.decode(value, { stream: true })
|
|
94
|
+
}
|
|
95
|
+
return result
|
|
96
|
+
}
|
|
97
|
+
|
|
85
98
|
describe('sse transport', () => {
|
|
86
99
|
test('getCredential returns null when no Authorization header', () => {
|
|
87
100
|
const store = memoryStore()
|
|
@@ -152,6 +165,19 @@ describe('sse transport', () => {
|
|
|
152
165
|
challengeId,
|
|
153
166
|
})
|
|
154
167
|
expect(response.headers.get('Content-Type')).toContain('text/event-stream')
|
|
168
|
+
|
|
169
|
+
const body = await readResponseText(response)
|
|
170
|
+
const receiptRaw = body.split('event: payment-receipt\ndata: ')[1]?.split('\n\n')[0]
|
|
171
|
+
const terminalReceipt = JSON.parse(receiptRaw!)
|
|
172
|
+
|
|
173
|
+
expect(response.headers.get('Payment-Receipt')).toBeNull()
|
|
174
|
+
expect(body).toContain('event: message\ndata: hello\n\n')
|
|
175
|
+
expect(body).toContain('event: message\ndata: world\n\n')
|
|
176
|
+
expect(body).toContain('event: payment-receipt\n')
|
|
177
|
+
expect(terminalReceipt.challengeId).toBe(challengeId)
|
|
178
|
+
expect(terminalReceipt.channelId).toBe(channelId)
|
|
179
|
+
expect(terminalReceipt.units).toBe(2)
|
|
180
|
+
expect(terminalReceipt.spent).toBe('2000000')
|
|
155
181
|
})
|
|
156
182
|
|
|
157
183
|
test('respondReceipt with AsyncGeneratorFunction passes stream controller', async () => {
|
|
@@ -197,6 +223,12 @@ describe('sse transport', () => {
|
|
|
197
223
|
challengeId,
|
|
198
224
|
})
|
|
199
225
|
expect(response.headers.get('Content-Type')).toContain('text/event-stream')
|
|
226
|
+
|
|
227
|
+
const body = await readResponseText(response)
|
|
228
|
+
expect(response.headers.get('Payment-Receipt')).toBeNull()
|
|
229
|
+
expect(body).toContain('event: message\ndata: chunk1\n\n')
|
|
230
|
+
expect(body).toContain('event: message\ndata: chunk2\n\n')
|
|
231
|
+
expect(body).toContain('event: payment-receipt\n')
|
|
200
232
|
})
|
|
201
233
|
|
|
202
234
|
test('respondReceipt with plain Response delegates to base http transport', () => {
|
|
@@ -539,6 +539,41 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
539
539
|
).rejects.toThrow('topUp transaction amount')
|
|
540
540
|
})
|
|
541
541
|
|
|
542
|
+
test('rejects when post-broadcast deposit does not exceed declared previousDeposit', async () => {
|
|
543
|
+
const salt = nextSalt()
|
|
544
|
+
const deposit = 5_000_000n
|
|
545
|
+
const topUpAmount = 1_000_000n
|
|
546
|
+
|
|
547
|
+
const { channelId } = await openChannel({
|
|
548
|
+
escrow: escrowContract,
|
|
549
|
+
payer,
|
|
550
|
+
payee: recipient,
|
|
551
|
+
token: currency,
|
|
552
|
+
deposit,
|
|
553
|
+
salt,
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
const { serializedTransaction } = await signTopUpChannel({
|
|
557
|
+
escrow: escrowContract,
|
|
558
|
+
payer,
|
|
559
|
+
channelId,
|
|
560
|
+
token: currency,
|
|
561
|
+
amount: topUpAmount,
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
await expect(
|
|
565
|
+
broadcastTopUpTransaction({
|
|
566
|
+
client,
|
|
567
|
+
serializedTransaction,
|
|
568
|
+
escrowContract,
|
|
569
|
+
channelId,
|
|
570
|
+
currency: asset,
|
|
571
|
+
declaredDeposit: topUpAmount,
|
|
572
|
+
previousDeposit: deposit + topUpAmount,
|
|
573
|
+
}),
|
|
574
|
+
).rejects.toThrow('channel deposit did not increase after topUp')
|
|
575
|
+
})
|
|
576
|
+
|
|
542
577
|
test('successful broadcast returns txHash and newDeposit', async () => {
|
|
543
578
|
const salt = nextSalt()
|
|
544
579
|
const deposit = 5_000_000n
|
|
@@ -730,7 +765,9 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
730
765
|
expect(channel.finalized).toBe(false)
|
|
731
766
|
})
|
|
732
767
|
|
|
733
|
-
test('settles
|
|
768
|
+
test.todo('settles with distinct feePayer != account (fee-sponsored settle)')
|
|
769
|
+
|
|
770
|
+
test('settles with explicit account (no fee payer)', async () => {
|
|
734
771
|
const salt = nextSalt()
|
|
735
772
|
const deposit = 10_000_000n
|
|
736
773
|
const settleAmount = 5_000_000n
|
|
@@ -752,6 +789,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
752
789
|
chain.id,
|
|
753
790
|
)
|
|
754
791
|
|
|
792
|
+
// Pass account explicitly — should use it as sender instead of client.account
|
|
755
793
|
const txHash = await settleOnChain(
|
|
756
794
|
client,
|
|
757
795
|
escrowContract,
|
|
@@ -760,7 +798,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
760
798
|
cumulativeAmount: settleAmount,
|
|
761
799
|
signature,
|
|
762
800
|
},
|
|
763
|
-
accounts[0],
|
|
801
|
+
{ account: accounts[0] },
|
|
764
802
|
)
|
|
765
803
|
|
|
766
804
|
expect(txHash).toBeDefined()
|
|
@@ -769,6 +807,21 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
769
807
|
expect(channel.settled).toBe(settleAmount)
|
|
770
808
|
expect(channel.finalized).toBe(false)
|
|
771
809
|
})
|
|
810
|
+
|
|
811
|
+
test('throws when no account available', async () => {
|
|
812
|
+
const noAccountClient = { chain: { id: 42431 } } as any
|
|
813
|
+
const dummyEscrow = '0x0000000000000000000000000000000000000001' as Address
|
|
814
|
+
const dummyChannelId =
|
|
815
|
+
'0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
|
|
816
|
+
|
|
817
|
+
await expect(
|
|
818
|
+
settleOnChain(noAccountClient, dummyEscrow, {
|
|
819
|
+
channelId: dummyChannelId,
|
|
820
|
+
cumulativeAmount: 1_000_000n,
|
|
821
|
+
signature: '0xsig' as Hex,
|
|
822
|
+
}),
|
|
823
|
+
).rejects.toThrow('no account available')
|
|
824
|
+
})
|
|
772
825
|
})
|
|
773
826
|
|
|
774
827
|
describe('closeOnChain', () => {
|
|
@@ -806,7 +859,9 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
806
859
|
expect(channel.finalized).toBe(true)
|
|
807
860
|
})
|
|
808
861
|
|
|
809
|
-
test('closes
|
|
862
|
+
test.todo('closes with distinct feePayer != account (fee-sponsored close)')
|
|
863
|
+
|
|
864
|
+
test('closes with explicit account (no fee payer)', async () => {
|
|
810
865
|
const salt = nextSalt()
|
|
811
866
|
const deposit = 10_000_000n
|
|
812
867
|
const closeAmount = 5_000_000n
|
|
@@ -828,6 +883,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
828
883
|
chain.id,
|
|
829
884
|
)
|
|
830
885
|
|
|
886
|
+
// Pass account explicitly — should use it as sender instead of client.account
|
|
831
887
|
const txHash = await closeOnChain(
|
|
832
888
|
client,
|
|
833
889
|
escrowContract,
|
|
@@ -836,8 +892,7 @@ describe.runIf(isLocalnet)('on-chain', () => {
|
|
|
836
892
|
cumulativeAmount: closeAmount,
|
|
837
893
|
signature,
|
|
838
894
|
},
|
|
839
|
-
|
|
840
|
-
accounts[0],
|
|
895
|
+
{ account: accounts[0] },
|
|
841
896
|
)
|
|
842
897
|
|
|
843
898
|
expect(txHash).toBeDefined()
|
|
@@ -93,6 +93,11 @@ function assertUint128(amount: bigint): void {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** Options for {@link settleOnChain}. */
|
|
97
|
+
export type SettleOptions =
|
|
98
|
+
| { feePayer: Account; account: Account }
|
|
99
|
+
| { feePayer?: undefined; account?: Account | undefined }
|
|
100
|
+
|
|
96
101
|
/**
|
|
97
102
|
* Submit a settle transaction on-chain.
|
|
98
103
|
*/
|
|
@@ -100,16 +105,21 @@ export async function settleOnChain(
|
|
|
100
105
|
client: Client,
|
|
101
106
|
escrowContract: Address,
|
|
102
107
|
voucher: SignedVoucher,
|
|
103
|
-
|
|
108
|
+
options?: SettleOptions,
|
|
104
109
|
): Promise<Hex> {
|
|
105
110
|
assertUint128(voucher.cumulativeAmount)
|
|
111
|
+
const resolved = options?.account ?? client.account
|
|
112
|
+
if (!resolved)
|
|
113
|
+
throw new Error(
|
|
114
|
+
'Cannot settle channel: no account available. Pass an `account` to tempo.settle(), or provide a `getClient` that returns an account-bearing client.',
|
|
115
|
+
)
|
|
106
116
|
const args = [voucher.channelId, voucher.cumulativeAmount, voucher.signature] as const
|
|
107
|
-
if (feePayer) {
|
|
117
|
+
if (options?.feePayer) {
|
|
108
118
|
const data = encodeFunctionData({ abi: escrowAbi, functionName: 'settle', args })
|
|
109
|
-
return sendFeePayerTx(client, feePayer, escrowContract, data, 'settle')
|
|
119
|
+
return sendFeePayerTx(client, resolved, options.feePayer, escrowContract, data, 'settle')
|
|
110
120
|
}
|
|
111
121
|
return writeContract(client, {
|
|
112
|
-
account:
|
|
122
|
+
account: resolved,
|
|
113
123
|
chain: client.chain,
|
|
114
124
|
address: escrowContract,
|
|
115
125
|
abi: escrowAbi,
|
|
@@ -118,6 +128,11 @@ export async function settleOnChain(
|
|
|
118
128
|
})
|
|
119
129
|
}
|
|
120
130
|
|
|
131
|
+
/** Options for {@link closeOnChain}. */
|
|
132
|
+
export type CloseOptions =
|
|
133
|
+
| { feePayer: Account; account: Account }
|
|
134
|
+
| { feePayer?: undefined; account?: Account | undefined }
|
|
135
|
+
|
|
121
136
|
/**
|
|
122
137
|
* Submit a close transaction on-chain.
|
|
123
138
|
*/
|
|
@@ -125,19 +140,18 @@ export async function closeOnChain(
|
|
|
125
140
|
client: Client,
|
|
126
141
|
escrowContract: Address,
|
|
127
142
|
voucher: SignedVoucher,
|
|
128
|
-
|
|
129
|
-
feePayer?: Account | undefined,
|
|
143
|
+
options?: CloseOptions,
|
|
130
144
|
): Promise<Hex> {
|
|
131
145
|
assertUint128(voucher.cumulativeAmount)
|
|
132
|
-
const resolved = account ?? client.account
|
|
146
|
+
const resolved = options?.account ?? client.account
|
|
133
147
|
if (!resolved)
|
|
134
148
|
throw new Error(
|
|
135
149
|
'Cannot close channel: no account available. Pass an `account` (viem Account, e.g. privateKeyToAccount("0x...")) to tempo.session(), or provide a `getClient` that returns an account-bearing client.',
|
|
136
150
|
)
|
|
137
151
|
const args = [voucher.channelId, voucher.cumulativeAmount, voucher.signature] as const
|
|
138
|
-
if (feePayer) {
|
|
152
|
+
if (options?.feePayer) {
|
|
139
153
|
const data = encodeFunctionData({ abi: escrowAbi, functionName: 'close', args })
|
|
140
|
-
return sendFeePayerTx(client, feePayer, escrowContract, data, 'close')
|
|
154
|
+
return sendFeePayerTx(client, resolved, options.feePayer, escrowContract, data, 'close')
|
|
141
155
|
}
|
|
142
156
|
return writeContract(client, {
|
|
143
157
|
account: resolved,
|
|
@@ -155,9 +169,13 @@ export async function closeOnChain(
|
|
|
155
169
|
* Follows the same signTransaction + sendRawTransactionSync pattern used
|
|
156
170
|
* by broadcastOpenTransaction / broadcastTopUpTransaction, but originates
|
|
157
171
|
* the transaction server-side (estimating gas and fees first).
|
|
172
|
+
*
|
|
173
|
+
* @param account - The logical sender / msg.sender (e.g. the payee).
|
|
174
|
+
* @param feePayer - The gas sponsor — only co-signs to cover fees.
|
|
158
175
|
*/
|
|
159
176
|
async function sendFeePayerTx(
|
|
160
177
|
client: Client,
|
|
178
|
+
account: Account,
|
|
161
179
|
feePayer: Account,
|
|
162
180
|
to: Address,
|
|
163
181
|
data: Hex,
|
|
@@ -167,12 +185,10 @@ async function sendFeePayerTx(
|
|
|
167
185
|
// token. `feePayer: true` tells the prepare hook to use expiring nonces but
|
|
168
186
|
// does NOT set feeToken automatically, so we must provide it explicitly.
|
|
169
187
|
const chainId = client.chain?.id
|
|
170
|
-
const feeToken = chainId
|
|
171
|
-
? defaults.currency[chainId as keyof typeof defaults.currency]
|
|
172
|
-
: undefined
|
|
188
|
+
const feeToken = chainId ? defaults.resolveCurrency({ chainId }) : undefined
|
|
173
189
|
|
|
174
190
|
const prepared = await prepareTransactionRequest(client, {
|
|
175
|
-
account
|
|
191
|
+
account,
|
|
176
192
|
calls: [{ to, data }],
|
|
177
193
|
feePayer: true,
|
|
178
194
|
...(feeToken ? { feeToken } : {}),
|
|
@@ -180,7 +196,7 @@ async function sendFeePayerTx(
|
|
|
180
196
|
|
|
181
197
|
const serialized = (await signTransaction(client, {
|
|
182
198
|
...prepared,
|
|
183
|
-
account
|
|
199
|
+
account,
|
|
184
200
|
feePayer,
|
|
185
201
|
} as never)) as Hex
|
|
186
202
|
|
|
@@ -366,6 +366,37 @@ describe('serve', () => {
|
|
|
366
366
|
expect(receipt.challengeId).toBe(challengeId)
|
|
367
367
|
})
|
|
368
368
|
|
|
369
|
+
test('emits exactly one terminal payment-receipt event at stream end', async () => {
|
|
370
|
+
const storage = memoryStore()
|
|
371
|
+
await seedChannel(storage, 2000000n)
|
|
372
|
+
|
|
373
|
+
const stream = serve({
|
|
374
|
+
store: storage,
|
|
375
|
+
channelId,
|
|
376
|
+
challengeId,
|
|
377
|
+
tickCost: 1000000n,
|
|
378
|
+
generate: generate(['one', 'two']),
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const output = await readStream(stream)
|
|
382
|
+
const events = output
|
|
383
|
+
.trim()
|
|
384
|
+
.split('\n\n')
|
|
385
|
+
.filter((chunk) => chunk.length > 0)
|
|
386
|
+
.map((chunk) => parseEvent(`${chunk}\n\n`))
|
|
387
|
+
.filter((event): event is NonNullable<typeof event> => event !== null)
|
|
388
|
+
|
|
389
|
+
const terminal = events.at(-1)
|
|
390
|
+
expect(terminal?.type).toBe('payment-receipt')
|
|
391
|
+
if (terminal?.type !== 'payment-receipt') throw new Error('expected terminal payment receipt')
|
|
392
|
+
|
|
393
|
+
expect(events.filter((event) => event.type === 'payment-receipt')).toHaveLength(1)
|
|
394
|
+
expect(terminal.data.challengeId).toBe(challengeId)
|
|
395
|
+
expect(terminal.data.channelId).toBe(channelId)
|
|
396
|
+
expect(terminal.data.units).toBe(2)
|
|
397
|
+
expect(terminal.data.spent).toBe('2000000')
|
|
398
|
+
})
|
|
399
|
+
|
|
369
400
|
test('handles empty generator', async () => {
|
|
370
401
|
const storage = memoryStore()
|
|
371
402
|
await seedChannel(storage, 1000000n)
|