Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/tempo/server/Charge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof Account.fromSecp256k1>
Expand Down Expand Up @@ -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: [
Expand Down
6 changes: 6 additions & 0 deletions src/tempo/server/Charge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -905,6 +909,8 @@ async function isValidTransferSender(parameters: {
validateSender?: charge.ValidateSender | undefined
}): Promise<boolean> {
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,
Expand Down
Loading