Skip to content

Add Revolut fiat payment provider#211

Open
claudinethelobster wants to merge 1 commit intoEdgeApp:masterfrom
claudinethelobster:claudine/add-revolut-plugin
Open

Add Revolut fiat payment provider#211
claudinethelobster wants to merge 1 commit intoEdgeApp:masterfrom
claudinethelobster:claudine/add-revolut-plugin

Conversation

@claudinethelobster
Copy link
Copy Markdown

@claudinethelobster claudinethelobster commented Feb 12, 2026

Summary

Implements Revolut fiat payment provider following the moonpay.ts pattern.

Changes

  • Created src/partners/revolut.ts - Partner plugin for Revolut API integration
  • Created src/routes/v1/revolut.ts - REST endpoint for Revolut transactions
  • Updated src/indexApi.ts - Registered revolut router
  • Updated src/demo/partners.ts - Added revolut entry (type: fiat, color: #191C33)
  • Updated src/queryEngine.ts - Registered revolut plugin

Implementation Details

  • Follows moonpay.ts architecture
  • Uses cleaners for all external data
  • Types derived from cleaners
  • No unused code
  • Debug logging guarded

Testing

  • Implementation follows edge-reports-server conventions
  • Pattern verified against moonpay.ts reference

@samholmes
Copy link
Copy Markdown
Collaborator

The fixup commit should have been squashed. If the fixup was apart of review feedback then it should be prefixed with fixup!.

- 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
@claudinethelobster
Copy link
Copy Markdown
Author

Done — the fixup was squashed into the original commit (now 515be6a). The PR shows a single commit.

Copy link
Copy Markdown
Collaborator

@samholmes samholmes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See inline comments for specific issues.

Comment thread src/partners/revolut.ts
Comment on lines +84 to +126
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
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
  }
}

Comment thread src/partners/revolut.ts
Comment on lines +158 to +195
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
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
  })
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants