Add Revolut fiat payment provider#211
Conversation
|
The fixup commit should have been squashed. If the fixup was apart of review feedback then it should be prefixed with |
bb46b01 to
515be6a
Compare
|
Done — the fixup was squashed into the original commit (now |
samholmes
left a comment
There was a problem hiding this comment.
See inline comments for specific issues.
| let cursor: string | undefined | ||
|
|
||
| while (true) { | ||
| const from = new Date(startTime).toISOString() | ||
| const to = new Date(endTime).toISOString() | ||
|
|
||
| let url = `https://api.revolut.com/partner/v1/transactions?from=${from}&to=${to}&limit=${QUERY_LIMIT}` | ||
| if (cursor != null) url += `&cursor=${cursor}` | ||
|
|
||
| datelog(`Querying Revolut from:${from} to:${to}`) | ||
|
|
||
| const response = await retryFetch(url, { | ||
| headers: { | ||
| Authorization: `Bearer ${apiKey}` | ||
| } | ||
| }) | ||
| if (!response.ok) { | ||
| const text = await response.text() | ||
| throw new Error(text) | ||
| } | ||
|
|
||
| const jsonObj = await response.json() | ||
| const result = asRevolutResult(jsonObj) | ||
| cursor = result.next_cursor | ||
|
|
||
| for (const rawTx of result.transactions) { | ||
| if (asPreRevolutTx(rawTx).state === 'completed') { | ||
| const standardTx = processRevolutTx(rawTx) | ||
| standardTxs.push(standardTx) | ||
| if (standardTx.isoDate > latestIsoDate) { | ||
| latestIsoDate = standardTx.isoDate | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (result.transactions.length > 0) { | ||
| datelog(`Revolut txs ${result.transactions.length}`) | ||
| } | ||
|
|
||
| if (cursor == null) { | ||
| break | ||
| } | ||
| } |
There was a problem hiding this comment.
Issue: The inner pagination loop only exits when cursor == null. If the API returns a repeated, empty-string, or non-advancing cursor, this loop runs indefinitely until the outer promiseTimeout kills it—delaying progress and producing noisy retries.
Recommendation: Add a max-pages guard and track the previous cursor to break on non-advancing values:
const MAX_PAGES = 1000
let cursor: string | undefined
let pages = 0
while (true) {
// ... existing fetch logic ...
const prevCursor = cursor
cursor = result.next_cursor
// ... existing tx processing ...
pages++
if (cursor == null || cursor === prevCursor || pages >= MAX_PAGES) {
break
}
}| export function processRevolutTx(rawTx: unknown): StandardTx { | ||
| const tx = asRevolutTx(rawTx) | ||
| const { isoDate, timestamp } = smartIsoDateFromTimestamp( | ||
| tx.created_at.getTime() | ||
| ) | ||
|
|
||
| const direction = tx.type | ||
| const depositTxid = direction === 'sell' ? tx.tx_hash : undefined | ||
| const payoutTxid = direction === 'buy' ? tx.tx_hash : undefined | ||
|
|
||
| const standardTx: StandardTx = { | ||
| status: 'complete', | ||
| orderId: tx.id, | ||
| countryCode: tx.country_code ?? null, | ||
| depositTxid, | ||
| depositAddress: undefined, | ||
| depositCurrency: | ||
| direction === 'buy' | ||
| ? tx.fiat_currency.toUpperCase() | ||
| : tx.crypto_currency.toUpperCase(), | ||
| depositAmount: direction === 'buy' ? tx.fiat_amount : tx.crypto_amount, | ||
| direction, | ||
| exchangeType: 'fiat', | ||
| paymentType: getRevolutPaymentType(tx), | ||
| payoutTxid, | ||
| payoutAddress: tx.wallet_address, | ||
| payoutCurrency: | ||
| direction === 'buy' | ||
| ? tx.crypto_currency.toUpperCase() | ||
| : tx.fiat_currency.toUpperCase(), | ||
| payoutAmount: direction === 'buy' ? tx.crypto_amount : tx.fiat_amount, | ||
| timestamp, | ||
| isoDate, | ||
| usdValue: -1, | ||
| rawTx | ||
| } | ||
| return standardTx | ||
| } |
There was a problem hiding this comment.
Suggestion: This mapping logic is high-impact for reporting accuracy and currently untested. Consider adding focused unit tests covering buy/sell direction, deposit/payout currency assignment, txid placement, and payment-method conversion.
Recommendation: Add a test file such as test/partners/revolut.test.ts:
import { expect } from 'chai'
import { processRevolutTx, getRevolutPaymentType } from '../../src/partners/revolut'
describe('processRevolutTx', () => {
const baseTx = {
id: 'order-1',
type: 'buy',
state: 'completed',
created_at: '2024-06-01T12:00:00.000Z',
fiat_amount: 100,
fiat_currency: 'usd',
crypto_amount: 0.005,
crypto_currency: 'btc',
wallet_address: 'bc1abc',
tx_hash: '0xabc',
country_code: 'US',
payment_method: 'card'
}
it('should map a buy tx with fiat deposit and crypto payout', () => {
const result = processRevolutTx(baseTx)
expect(result.direction).to.equal('buy')
expect(result.depositCurrency).to.equal('USD')
expect(result.payoutCurrency).to.equal('BTC')
expect(result.payoutTxid).to.equal('0xabc')
expect(result.depositTxid).to.be.undefined
})
it('should map a sell tx with crypto deposit and fiat payout', () => {
const result = processRevolutTx({ ...baseTx, type: 'sell' })
expect(result.direction).to.equal('sell')
expect(result.depositCurrency).to.equal('BTC')
expect(result.payoutCurrency).to.equal('USD')
expect(result.depositTxid).to.equal('0xabc')
expect(result.payoutTxid).to.be.undefined
})
})- Created src/partners/revolut.ts following moonpay.ts pattern - Created src/routes/v1/revolut.ts REST endpoint - Updated src/indexApi.ts to register revolut router - Updated src/demo/partners.ts with revolut entry (fiat, #191C33) - Updated src/queryEngine.ts to register revolut plugin
515be6a to
727da5d
Compare
|
Updated for Sam’s review feedback in
I could not run the repo checks locally because dependencies are unavailable in this workspace ( |
|
Updated for the Cursor Bugbot follow-up in
Verification:
|
|
Updated for the latest Cursor Bugbot follow-up in
Verification:
|
|
Updated for the latest Cursor Bugbot follow-up in
Verification:
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 134bdb3. Configure here.
| const to = new Date(endTime).toISOString() | ||
|
|
||
| let url = `https://api.revolut.com/partner/v1/transactions?from=${from}&to=${to}&limit=${QUERY_LIMIT}` | ||
| if (cursor != null) url += `&cursor=${cursor}` |
There was a problem hiding this comment.
Cursor left unencoded in URL
Medium Severity
Pagination appends cursor with raw string concatenation, so reserved characters in a cursor token are not escaped. That can alter the query string, fetch the wrong page, or break pagination after the first page. paybis builds the same params through a query helper that encodes values.
Reviewed by Cursor Bugbot for commit 134bdb3. Configure here.
| } finally { | ||
| ;(util as any).retryFetch = oldRetryFetch | ||
| ;(util as any).snooze = oldSnooze | ||
| } |
There was a problem hiding this comment.
Retry test misses frozen time
Medium Severity
The retry test never stubs Date.now, so after the first window succeeds queryRevolut keeps walking later windows until it reaches the real current time. That adds extra fetches and can append the same mocked orderId again, so the callCount === 3 and single-transaction assertions do not match the implementation under today’s date.
Reviewed by Cursor Bugbot for commit 134bdb3. Configure here.


Summary
Implements Revolut fiat payment provider following the moonpay.ts pattern.
Changes
src/partners/revolut.ts- Partner plugin for Revolut API integrationsrc/routes/v1/revolut.ts- REST endpoint for Revolut transactionssrc/indexApi.ts- Registered revolut routersrc/demo/partners.ts- Added revolut entry (type: fiat, color: #191C33)src/queryEngine.ts- Registered revolut pluginImplementation Details
Testing
Note
Medium Risk
New third-party API ingestion affects transaction data and progress watermarks, though it follows existing partner patterns and includes targeted tests for pagination/retry behavior.
Overview
Adds Revolut as a new fiat partner so the query engine can pull completed buy/sell transactions from Revolut’s partner API and store them as standard report transactions.
The new
queryRevolutflow walks time windows with a 7-day lookback, paginates with cursor guards (malformed/repeated cursors, max pages), retries on failures, and only advanceslatestIsoDatewhen pagination completes—otherwise it keeps fetched rows but does not move the watermark.processRevolutTxmaps buy/sell orders, payment methods (card, bank transfer, Apple/Google Pay, etc.), and optional fields intoStandardTx. Revolut is registered inqueryEngineand listed in demo partner metadata (fiat,#191C33).Tests cover mapping for buy/sell and payment types, plus query behavior for cursor bugs, retries without duplicate appends, and advancing progress on empty responses.
Reviewed by Cursor Bugbot for commit 134bdb3. Bugbot is set up for automated code reviews on this repo. Configure here.