diff --git a/src/tempo/server/Charge.test.ts b/src/tempo/server/Charge.test.ts index 68319da0..c68bcf6d 100644 --- a/src/tempo/server/Charge.test.ts +++ b/src/tempo/server/Charge.test.ts @@ -25,6 +25,7 @@ import { signVoucher } from '../session/Voucher.js' const realm = 'api.example.com' const secretKey = 'test-secret-key' +const coinflowSender = '0xbd5354a0eb27b574dfaad556c13787ff634a0e65' as const type ProofAccessKeyContext = { accessKey: ReturnType @@ -4037,6 +4038,86 @@ describe('tempo', () => { httpServer.close() }) + test('server accepts hash transfers from the default Coinflow sender', async () => { + let useCoinflowReceiptSender = false + const coinflowClient = createClient({ + chain: client.chain, + transport: custom({ + async request(args: any) { + const result = await client.transport.request(args) + if (useCoinflowReceiptSender && args?.method === 'eth_getTransactionReceipt') { + const fromTopic = `0x${accounts[1].address.slice(2).toLowerCase().padStart(64, '0')}` + const coinflowTopic = `0x${coinflowSender.slice(2).toLowerCase().padStart(64, '0')}` + return { + ...(result as any), + from: coinflowSender, + logs: (result as any).logs.map((log: any) => ({ + ...log, + topics: log.topics.map((topic: string) => + topic.toLowerCase() === fromTopic ? coinflowTopic : topic, + ), + })), + } + } + return result + }, + }), + }) + const coinflowServer = Mppx_server.create({ + methods: [ + tempo_server.charge({ + getClient() { + return coinflowClient + }, + currency: asset, + account: accounts[0], + }), + ], + realm, + secretKey, + }) + const httpServer = await Http.createServer(async (req, res) => { + const result = await Mppx_server.toNodeListener( + coinflowServer.charge({ amount: '1', decimals: 6 }), + )(req, res) + if (result.status === 402) return + res.end('OK') + }) + + const response = await fetch(httpServer.url) + expect(response.status).toBe(402) + + const challenge = Challenge.fromResponse(response, { + methods: [tempo_client.charge()], + }) + const memo = Attribution.encode({ challengeId: challenge.id, serverId: challenge.realm }) + + const { receipt } = await Actions.token.transferSync(client, { + account: accounts[1], + amount: BigInt(challenge.request.amount), + memo: memo as Hex.Hex, + to: challenge.request.recipient as Hex.Hex, + token: challenge.request.currency as Hex.Hex, + }) + + useCoinflowReceiptSender = true + + const credential = Credential.from({ + challenge, + payload: { hash: receipt.transactionHash, type: 'hash' as const }, + source: `did:pkh:eip155:${chain.id}:${accounts[2].address}`, + }) + + { + const response = await fetch(httpServer.url, { + headers: { Authorization: Credential.serialize(credential) }, + }) + expect(response.status).toBe(200) + } + + httpServer.close() + }) + test('server rejects hash transfers that reuse one transferWithMemo for duplicate expected transfers', async () => { const validatingServer = Mppx_server.create({ methods: [ diff --git a/src/tempo/server/Charge.ts b/src/tempo/server/Charge.ts index f1ef2113..c82a33f7 100644 --- a/src/tempo/server/Charge.ts +++ b/src/tempo/server/Charge.ts @@ -38,6 +38,10 @@ import type * as types from '../internal/types.js' import * as Methods from '../Methods.js' import { html as htmlContent } from './internal/html.gen.js' +const defaultAllowedSenders = [ + '0xbd5354a0eb27b574dfaad556c13787ff634a0e65', // Coinflow +] as const + /** * Creates a Tempo charge method intent for usage on the server. * @@ -905,6 +909,8 @@ async function isValidTransferSender(parameters: { validateSender?: charge.ValidateSender | undefined }): Promise { if (TempoAddress.isEqual(parameters.sender, parameters.expectedSender)) return true + if (defaultAllowedSenders.some((sender) => TempoAddress.isEqual(parameters.sender, sender))) + return true if (!parameters.validateSender) return false return parameters.validateSender({ expectedSender: parameters.expectedSender,