Skip to content

Commit 49263cb

Browse files
committed
chore(tempo): keep split payment PR narrowly scoped
1 parent 832b693 commit 49263cb

File tree

3 files changed

+14
-219
lines changed

3 files changed

+14
-219
lines changed

src/tempo/client/Charge.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type * as Hex from 'ox/Hex'
22
import type { Address } from 'viem'
3-
import { prepareTransactionRequest, sendTransactionSync, signTransaction } from 'viem/actions'
3+
import { prepareTransactionRequest, sendCallsSync, signTransaction } from 'viem/actions'
44
import { tempo as tempo_chain } from 'viem/chains'
55
import { Actions } from 'viem/tempo'
66

@@ -112,17 +112,16 @@ export function charge(parameters: charge.Parameters = {}) {
112112
})()
113113

114114
if (mode === 'push') {
115-
if (methodDetails?.feePayer)
116-
throw new Error('`feePayer: true` requires pull mode so the server can co-sign it.')
117-
118-
const receipt = await sendTransactionSync(client, {
115+
const { receipts } = await sendCallsSync(client, {
119116
account,
120-
calls,
121-
validBefore,
122-
} as never)
117+
calls: calls as never,
118+
experimental_fallback: true,
119+
})
120+
const hash = receipts?.[0]?.transactionHash
121+
if (!hash) throw new Error('No transaction receipt returned.')
123122
return Credential.serialize({
124123
challenge,
125-
payload: { hash: receipt.transactionHash, type: 'hash' },
124+
payload: { hash, type: 'hash' },
126125
source: `did:pkh:eip155:${chainId}:${account.address}`,
127126
})
128127
}

src/tempo/server/Charge.test.ts

Lines changed: 1 addition & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@ import type { Hex } from 'ox'
55
import { TxEnvelopeTempo } from 'ox/tempo'
66
import { Handler } from 'tempo.ts/server'
77
import { createClient, custom, encodeFunctionData, parseUnits } from 'viem'
8-
import {
9-
getTransaction,
10-
getTransactionReceipt,
11-
prepareTransactionRequest,
12-
signTransaction,
13-
} from 'viem/actions'
8+
import { getTransactionReceipt, prepareTransactionRequest, signTransaction } from 'viem/actions'
149
import { Abis, Account, Actions, Addresses, Secp256k1, Tick, Transaction } from 'viem/tempo'
1510
import { beforeAll, describe, expect, test } from 'vp/test'
1611
import * as Http from '~test/Http.js'
@@ -118,161 +113,6 @@ describe('tempo', () => {
118113
httpServer.close()
119114
})
120115

121-
test('behavior: push mode sets validBefore no later than challenge expiry', async () => {
122-
const expires = new Date(Date.now() + 60_000).toISOString()
123-
const mppx = Mppx_client.create({
124-
polyfill: false,
125-
methods: [
126-
tempo_client({
127-
account: accounts[1],
128-
mode: 'push',
129-
getClient: () => client,
130-
}),
131-
],
132-
})
133-
134-
const httpServer = await Http.createServer(async (req, res) => {
135-
const result = await Mppx_server.toNodeListener(
136-
server.charge({
137-
amount: '1',
138-
currency: asset,
139-
expires,
140-
recipient: accounts[0].address,
141-
}),
142-
)(req, res)
143-
if (result.status === 402) return
144-
res.end('OK')
145-
})
146-
147-
const response = await mppx.fetch(httpServer.url)
148-
expect(response.status).toBe(200)
149-
150-
const receipt = Receipt.fromResponse(response)
151-
const transaction = await getTransaction(client, {
152-
hash: receipt.reference as Hex.Hex,
153-
})
154-
155-
expect(transaction.validBefore).toBeTypeOf('number')
156-
expect(transaction.validBefore!).toBeLessThanOrEqual(
157-
Math.floor(new Date(expires).getTime() / 1000),
158-
)
159-
160-
httpServer.close()
161-
})
162-
163-
test('behavior: rejects hash credentials for fee-payer challenges', async () => {
164-
const httpServer = await Http.createServer(async (req, res) => {
165-
const result = await Mppx_server.toNodeListener(
166-
server.charge({
167-
feePayer: accounts[0],
168-
amount: '1',
169-
currency: asset,
170-
recipient: accounts[0].address,
171-
}),
172-
)(req, res)
173-
if (result.status === 402) return
174-
res.end('OK')
175-
})
176-
177-
const response = await fetch(httpServer.url)
178-
expect(response.status).toBe(402)
179-
180-
const challenge = Challenge.fromResponse(response, {
181-
methods: [tempo_client.charge()],
182-
})
183-
184-
const { receipt } = await Actions.token.transferSync(client, {
185-
account: accounts[1],
186-
amount: BigInt(challenge.request.amount),
187-
to: challenge.request.recipient as Hex.Hex,
188-
token: challenge.request.currency as Hex.Hex,
189-
})
190-
191-
const credential = Credential.from({
192-
challenge,
193-
payload: { hash: receipt.transactionHash, type: 'hash' as const },
194-
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
195-
})
196-
197-
const authResponse = await fetch(httpServer.url, {
198-
headers: { Authorization: Credential.serialize(credential) },
199-
})
200-
expect(authResponse.status).toBe(402)
201-
202-
const body = (await authResponse.json()) as { detail: string }
203-
expect(body.detail).toContain('Hash credentials cannot be used when `feePayer` is true.')
204-
205-
httpServer.close()
206-
})
207-
208-
test('behavior: verifies payment effects even when receipt.from differs', async () => {
209-
const interceptingClient = createClient({
210-
chain: client.chain,
211-
transport: custom({
212-
async request(args: any) {
213-
const result = await client.transport.request(args)
214-
if (args?.method === 'eth_getTransactionReceipt') {
215-
return {
216-
...(result as any),
217-
from: accounts[0].address,
218-
}
219-
}
220-
return result
221-
},
222-
}),
223-
})
224-
225-
const serverProxy = Mppx_server.create({
226-
methods: [
227-
tempo_server.charge({
228-
getClient() {
229-
return interceptingClient
230-
},
231-
currency: asset,
232-
account: accounts[0],
233-
}),
234-
],
235-
realm,
236-
secretKey,
237-
})
238-
239-
const httpServer = await Http.createServer(async (req, res) => {
240-
const result = await Mppx_server.toNodeListener(serverProxy.charge({ amount: '1' }))(
241-
req,
242-
res,
243-
)
244-
if (result.status === 402) return
245-
res.end('OK')
246-
})
247-
248-
const response = await fetch(httpServer.url)
249-
expect(response.status).toBe(402)
250-
251-
const challenge = Challenge.fromResponse(response, {
252-
methods: [tempo_client.charge()],
253-
})
254-
255-
const { receipt } = await Actions.token.transferSync(client, {
256-
account: accounts[1],
257-
amount: BigInt(challenge.request.amount),
258-
to: challenge.request.recipient as Hex.Hex,
259-
token: challenge.request.currency as Hex.Hex,
260-
})
261-
262-
const credential = Credential.from({
263-
challenge,
264-
payload: { hash: receipt.transactionHash, type: 'hash' as const },
265-
source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
266-
})
267-
268-
const authResponse = await fetch(httpServer.url, {
269-
headers: { Authorization: Credential.serialize(credential) },
270-
})
271-
expect(authResponse.status).toBe(200)
272-
273-
httpServer.close()
274-
})
275-
276116
test('behavior: rejects replayed transaction hash', async () => {
277117
const dedupServer = Mppx_server.create({
278118
methods: [
@@ -1417,43 +1257,6 @@ describe('tempo', () => {
14171257
httpServer.close()
14181258
})
14191259

1420-
test('error: rejects push mode for fee-payer challenges', async () => {
1421-
const mppx = Mppx_client.create({
1422-
polyfill: false,
1423-
methods: [
1424-
tempo_client({
1425-
account: accounts[1],
1426-
mode: 'push',
1427-
getClient() {
1428-
return client
1429-
},
1430-
}),
1431-
],
1432-
})
1433-
1434-
const httpServer = await Http.createServer(async (req, res) => {
1435-
const result = await Mppx_server.toNodeListener(
1436-
server.charge({
1437-
feePayer: accounts[0],
1438-
amount: '1',
1439-
currency: asset,
1440-
recipient: accounts[0].address,
1441-
}),
1442-
)(req, res)
1443-
if (result.status === 402) return
1444-
res.end('OK')
1445-
})
1446-
1447-
const response = await fetch(httpServer.url)
1448-
expect(response.status).toBe(402)
1449-
1450-
await expect(mppx.createCredential(response)).rejects.toThrow(
1451-
'`feePayer: true` requires pull mode so the server can co-sign it.',
1452-
)
1453-
1454-
httpServer.close()
1455-
})
1456-
14571260
test('behavior: fee payer with splits', async () => {
14581261
const mppx = Mppx_client.create({
14591262
polyfill: false,

src/tempo/server/Charge.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export function charge<const parameters extends charge.Parameters>(
103103
},
104104

105105
async verify({ credential, request }) {
106-
const { challenge, source } = credential
106+
const { challenge } = credential
107107
const { chainId, feePayer } = request
108108

109109
const client = await getClient({ chainId })
@@ -123,17 +123,14 @@ export function charge<const parameters extends charge.Parameters>(
123123

124124
switch (payload.type) {
125125
case 'hash': {
126-
if (methodDetails?.feePayer)
127-
throw new MismatchError('Hash credentials cannot be used when `feePayer` is true.', {})
128-
129126
const hash = payload.hash as `0x${string}`
130127
await assertHashUnused(store, hash)
131128

132129
const expectedTransfers = getExpectedTransfers({ amount, memo, methodDetails, recipient })
133130
const receipt = await getTransactionReceipt(client, { hash })
134131
assertTransferLogs(receipt, {
135132
currency,
136-
sender: getCredentialSourceAddress(source) ?? receipt.from,
133+
sender: receipt.from,
137134
transfers: expectedTransfers,
138135
})
139136

@@ -192,6 +189,7 @@ export function charge<const parameters extends charge.Parameters>(
192189
})
193190
assertTransferLogs(receipt, {
194191
currency,
192+
sender: transaction.from! as `0x${string}`,
195193
transfers,
196194
})
197195
// Post-broadcast dedup: catch malleable input variants
@@ -407,7 +405,7 @@ function assertTransferLogs(
407405
receipt: TransactionReceipt,
408406
parameters: {
409407
currency: `0x${string}`
410-
sender?: `0x${string}` | undefined
408+
sender: `0x${string}`
411409
transfers: readonly ExpectedTransfer[]
412410
},
413411
) {
@@ -438,7 +436,7 @@ function assertTransferLogs(
438436
const matchIndex = logs.findIndex((log, index) => {
439437
if (used.has(index)) return false
440438
if (!TempoAddress.isEqual(log.address, parameters.currency)) return false
441-
if (parameters.sender && !TempoAddress.isEqual(log.args.from, parameters.sender)) return false
439+
if (!TempoAddress.isEqual(log.args.from, parameters.sender)) return false
442440
if (!TempoAddress.isEqual(log.args.to, transfer.recipient)) return false
443441
if (log.args.amount.toString() !== transfer.amount) return false
444442
if (transfer.memo) {
@@ -460,11 +458,6 @@ function assertTransferLogs(
460458
}
461459
}
462460

463-
function getCredentialSourceAddress(source: string | undefined): `0x${string}` | undefined {
464-
const match = source?.match(/^did:pkh:eip155:\d+:(0x[0-9a-fA-F]{40})$/)
465-
return match?.[1] as `0x${string}` | undefined
466-
}
467-
468461
/** @internal */
469462
function getHashStoreKey(hash: `0x${string}`): `mppx:charge:${string}` {
470463
return `mppx:charge:${hash.toLowerCase()}`

0 commit comments

Comments
 (0)