Skip to content

Commit 0086464

Browse files
committed
Add Revolut fiat payment provider
Recreate the Revolut partner plugin following the moonpay pattern: - src/partners/revolut.ts queries the Revolut partner transactions API, cleans results, and maps completed buy/sell txs to StandardTx. - Register the plugin in queryEngine.ts. - Add the revolut demo entry (fiat, #191C33).
1 parent d9c6f34 commit 0086464

4 files changed

Lines changed: 229 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- added: Add Revolut fiat payment provider
56
- changed: Update sideshift plugin with new optional API fields
67
- changed: Add signature header support to Exolix
78
- changed: Add index for orderId

src/demo/partners.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ export default {
109109
type: 'swap',
110110
color: '#5891EE'
111111
},
112+
revolut: {
113+
type: 'fiat',
114+
color: '#191C33'
115+
},
112116
safello: {
113117
type: 'fiat',
114118
color: deprecated

src/partners/revolut.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import {
2+
asArray,
3+
asDate,
4+
asNumber,
5+
asObject,
6+
asOptional,
7+
asString,
8+
asUnknown,
9+
asValue
10+
} from 'cleaners'
11+
12+
import {
13+
asStandardPluginParams,
14+
EDGE_APP_START_DATE,
15+
FiatPaymentType,
16+
PartnerPlugin,
17+
PluginParams,
18+
PluginResult,
19+
StandardTx
20+
} from '../types'
21+
import { datelog, retryFetch, smartIsoDateFromTimestamp, snooze } from '../util'
22+
23+
const asRevolutTx = asObject({
24+
id: asString,
25+
type: asValue('buy', 'sell'),
26+
created_at: asDate,
27+
fiat_amount: asNumber,
28+
fiat_currency: asString,
29+
crypto_amount: asNumber,
30+
crypto_currency: asString,
31+
wallet_address: asOptional(asString),
32+
tx_hash: asOptional(asString),
33+
country_code: asOptional(asString),
34+
payment_method: asOptional(asString)
35+
})
36+
37+
type RevolutTx = ReturnType<typeof asRevolutTx>
38+
39+
const asPreRevolutTx = asObject({
40+
state: asString
41+
})
42+
43+
const asRevolutResult = asObject({
44+
transactions: asArray(asUnknown),
45+
next_cursor: asOptional(asString)
46+
})
47+
48+
const PLUGIN_START_DATE = '2024-01-01T00:00:00.000Z'
49+
const QUERY_LOOKBACK = 1000 * 60 * 60 * 24 * 7 // 7 days
50+
const QUERY_TIME_BLOCK_MS = QUERY_LOOKBACK
51+
const QUERY_LIMIT = 100
52+
const MAX_RETRIES = 5
53+
54+
export async function queryRevolut(
55+
pluginParams: PluginParams
56+
): Promise<PluginResult> {
57+
const { settings, apiKeys } = asStandardPluginParams(pluginParams)
58+
const { apiKey } = apiKeys
59+
60+
if (apiKey == null) {
61+
return {
62+
settings: { latestIsoDate: settings.latestIsoDate },
63+
transactions: []
64+
}
65+
}
66+
67+
const now = Date.now()
68+
let { latestIsoDate } = settings
69+
70+
if (latestIsoDate === EDGE_APP_START_DATE) {
71+
latestIsoDate = PLUGIN_START_DATE
72+
}
73+
74+
let startTime = new Date(latestIsoDate).getTime() - QUERY_LOOKBACK
75+
if (startTime < 0) startTime = 0
76+
77+
const standardTxs: StandardTx[] = []
78+
let retry = 0
79+
80+
while (true) {
81+
const endTime = startTime + QUERY_TIME_BLOCK_MS
82+
83+
try {
84+
let cursor: string | undefined
85+
86+
while (true) {
87+
const from = new Date(startTime).toISOString()
88+
const to = new Date(endTime).toISOString()
89+
90+
let url = `https://api.revolut.com/partner/v1/transactions?from=${from}&to=${to}&limit=${QUERY_LIMIT}`
91+
if (cursor != null) url += `&cursor=${cursor}`
92+
93+
datelog(`Querying Revolut from:${from} to:${to}`)
94+
95+
const response = await retryFetch(url, {
96+
headers: {
97+
Authorization: `Bearer ${apiKey}`
98+
}
99+
})
100+
if (!response.ok) {
101+
const text = await response.text()
102+
throw new Error(text)
103+
}
104+
105+
const jsonObj = await response.json()
106+
const result = asRevolutResult(jsonObj)
107+
cursor = result.next_cursor
108+
109+
for (const rawTx of result.transactions) {
110+
if (asPreRevolutTx(rawTx).state === 'completed') {
111+
const standardTx = processRevolutTx(rawTx)
112+
standardTxs.push(standardTx)
113+
if (standardTx.isoDate > latestIsoDate) {
114+
latestIsoDate = standardTx.isoDate
115+
}
116+
}
117+
}
118+
119+
if (result.transactions.length > 0) {
120+
datelog(`Revolut txs ${result.transactions.length}`)
121+
}
122+
123+
if (cursor == null) {
124+
break
125+
}
126+
}
127+
128+
startTime = endTime
129+
if (endTime > now) {
130+
break
131+
}
132+
retry = 0
133+
} catch (e) {
134+
datelog(e)
135+
retry++
136+
if (retry <= MAX_RETRIES) {
137+
datelog(`Snoozing ${60 * retry}s`)
138+
await snooze(60000 * retry)
139+
} else {
140+
break
141+
}
142+
}
143+
await snooze(1000)
144+
}
145+
146+
return {
147+
settings: { latestIsoDate },
148+
transactions: standardTxs
149+
}
150+
}
151+
152+
export const revolut: PartnerPlugin = {
153+
queryFunc: queryRevolut,
154+
pluginName: 'Revolut',
155+
pluginId: 'revolut'
156+
}
157+
158+
export function processRevolutTx(rawTx: unknown): StandardTx {
159+
const tx = asRevolutTx(rawTx)
160+
const { isoDate, timestamp } = smartIsoDateFromTimestamp(
161+
tx.created_at.getTime()
162+
)
163+
164+
const direction = tx.type
165+
const depositTxid = direction === 'sell' ? tx.tx_hash : undefined
166+
const payoutTxid = direction === 'buy' ? tx.tx_hash : undefined
167+
168+
const standardTx: StandardTx = {
169+
status: 'complete',
170+
orderId: tx.id,
171+
countryCode: tx.country_code ?? null,
172+
depositTxid,
173+
depositAddress: undefined,
174+
depositCurrency:
175+
direction === 'buy'
176+
? tx.fiat_currency.toUpperCase()
177+
: tx.crypto_currency.toUpperCase(),
178+
depositChainPluginId: undefined,
179+
depositEvmChainId: undefined,
180+
depositTokenId: undefined,
181+
depositAmount: direction === 'buy' ? tx.fiat_amount : tx.crypto_amount,
182+
direction,
183+
exchangeType: 'fiat',
184+
paymentType: getRevolutPaymentType(tx),
185+
payoutTxid,
186+
payoutAddress: tx.wallet_address,
187+
payoutCurrency:
188+
direction === 'buy'
189+
? tx.crypto_currency.toUpperCase()
190+
: tx.fiat_currency.toUpperCase(),
191+
payoutChainPluginId: undefined,
192+
payoutEvmChainId: undefined,
193+
payoutTokenId: undefined,
194+
payoutAmount: direction === 'buy' ? tx.crypto_amount : tx.fiat_amount,
195+
timestamp,
196+
isoDate,
197+
usdValue: -1,
198+
rawTx
199+
}
200+
return standardTx
201+
}
202+
203+
function getRevolutPaymentType(tx: RevolutTx): FiatPaymentType | null {
204+
switch (tx.payment_method) {
205+
case undefined:
206+
return null
207+
case 'revolut':
208+
return 'revolut'
209+
case 'card':
210+
return 'credit'
211+
case 'bank_transfer':
212+
return 'banktransfer'
213+
case 'apple_pay':
214+
return 'applepay'
215+
case 'google_pay':
216+
return 'googlepay'
217+
default:
218+
throw new Error(
219+
`Unknown payment method: ${tx.payment_method} for ${tx.id}`
220+
)
221+
}
222+
}

src/queryEngine.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { moonpay } from './partners/moonpay'
2525
import { paybis } from './partners/paybis'
2626
import { paytrie } from './partners/paytrie'
2727
import { rango } from './partners/rango'
28+
import { revolut } from './partners/revolut'
2829
import { safello } from './partners/safello'
2930
import { sideshift } from './partners/sideshift'
3031
import { simplex } from './partners/simplex'
@@ -77,6 +78,7 @@ const plugins = [
7778
paybis,
7879
paytrie,
7980
rango,
81+
revolut,
8082
safello,
8183
sideshift,
8284
simplex,

0 commit comments

Comments
 (0)