Skip to content

Commit 93d5700

Browse files
committed
Add Swapter partner plugin
Adds the Swapter exchange partner integration: querySwapter pages the /personal/exchange/tool-history endpoint with retry handling and latestIsoDate tracking, and processSwapterTx converts Swapter deposit/ withdraw records into StandardTx. Registers the swapter plugin in the query engine.
1 parent d9c6f34 commit 93d5700

3 files changed

Lines changed: 238 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 Swapter reporting
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/partners/swapter.ts

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import {
2+
asArray,
3+
asMaybe,
4+
asNumber,
5+
asObject,
6+
asString,
7+
asUnknown,
8+
asValue
9+
} from 'cleaners'
10+
11+
import {
12+
asStandardPluginParams,
13+
PartnerPlugin,
14+
PluginParams,
15+
PluginResult,
16+
StandardTx,
17+
Status
18+
} from '../types'
19+
import { datelog, retryFetch, smartIsoDateFromTimestamp, snooze } from '../util'
20+
21+
const asSwapterStatus = asMaybe(
22+
asValue(
23+
'Waiting',
24+
'Confirmation',
25+
'Exchanging',
26+
'Sending',
27+
'Success',
28+
'Frozen',
29+
'Refunded',
30+
'Overdue',
31+
'Suspended'
32+
),
33+
'other'
34+
)
35+
36+
const asSwapterTx = asObject({
37+
info: asObject({
38+
uid: asString,
39+
status: asSwapterStatus,
40+
type: asString,
41+
link: asString,
42+
equivalent: asNumber
43+
}),
44+
deposit: asObject({
45+
coin: asString,
46+
network: asString,
47+
amount: asNumber,
48+
actual: asMaybe(asNumber),
49+
address: asString,
50+
memo: asMaybe(asString)
51+
}),
52+
withdraw: asObject({
53+
coin: asString,
54+
network: asString,
55+
amount: asNumber,
56+
address: asString,
57+
memo: asMaybe(asString)
58+
}),
59+
time: asObject({
60+
create: asNumber,
61+
confirmation: asMaybe(asNumber),
62+
exchanging: asMaybe(asNumber),
63+
send: asMaybe(asNumber),
64+
success: asMaybe(asNumber),
65+
overdue: asMaybe(asNumber)
66+
}),
67+
partner: asObject({
68+
name: asString,
69+
profit: asObject({
70+
amount: asNumber,
71+
percent: asNumber
72+
})
73+
})
74+
})
75+
76+
const asSwapterResult = asObject({
77+
page: asNumber,
78+
total: asNumber,
79+
data: asArray(asUnknown)
80+
})
81+
82+
type SwapterTx = ReturnType<typeof asSwapterTx>
83+
type SwapterStatus = ReturnType<typeof asSwapterStatus>
84+
85+
const MAX_RETRIES = 5
86+
const LIMIT = 200
87+
const QUERY_LOOKBACK = 1000 * 60 * 60 * 24 * 5 // 5 days
88+
89+
const statusMap: { [key in SwapterStatus]: Status } = {
90+
Waiting: 'pending',
91+
Confirmation: 'pending',
92+
Exchanging: 'pending',
93+
Sending: 'pending',
94+
Success: 'complete',
95+
Overdue: 'expired',
96+
Refunded: 'refunded',
97+
Frozen: 'other',
98+
Suspended: 'other',
99+
other: 'other'
100+
}
101+
102+
export const querySwapter = async (
103+
pluginParams: PluginParams
104+
): Promise<PluginResult> => {
105+
const { settings, apiKeys } = asStandardPluginParams(pluginParams)
106+
const { apiKey } = apiKeys
107+
let latestIsoDate =
108+
typeof settings.latestIsoDate === 'string'
109+
? settings.latestIsoDate
110+
: new Date(0).toISOString()
111+
112+
if (apiKey == null) {
113+
return { settings: { latestIsoDate }, transactions: [] }
114+
}
115+
116+
const standardTxs: StandardTx[] = []
117+
118+
let previousTimestamp = new Date(latestIsoDate).getTime() - QUERY_LOOKBACK
119+
if (previousTimestamp < 0) previousTimestamp = 0
120+
121+
let page = 1
122+
let retry = 0
123+
124+
while (true) {
125+
try {
126+
const response = await retryFetch(
127+
'https://api.swapter.io/personal/exchange/tool-history',
128+
{
129+
method: 'POST',
130+
headers: {
131+
'X-Api-Key': apiKey,
132+
'Content-Type': 'application/json'
133+
},
134+
body: JSON.stringify({
135+
page,
136+
items: LIMIT,
137+
timeFrom: previousTimestamp,
138+
timeTo: Date.now()
139+
})
140+
}
141+
)
142+
143+
if (!response.ok) {
144+
const text = await response.text()
145+
datelog(`Swapter error on page:${page}`)
146+
throw new Error(text)
147+
}
148+
149+
const result = asSwapterResult(await response.json())
150+
const txs = result.data
151+
152+
if (txs.length === 0) break
153+
154+
for (const rawTx of txs) {
155+
const standardTx = processSwapterTx(rawTx)
156+
157+
if (standardTx.isoDate <= latestIsoDate) continue
158+
159+
standardTxs.push(standardTx)
160+
161+
if (standardTx.isoDate > latestIsoDate) {
162+
latestIsoDate = standardTx.isoDate
163+
}
164+
}
165+
166+
datelog(`Swapter page ${page} latestIsoDate ${latestIsoDate}`)
167+
168+
const loaded = page * LIMIT
169+
if (loaded >= result.total) break
170+
171+
page++
172+
retry = 0
173+
} catch (e) {
174+
datelog(e)
175+
176+
retry++
177+
if (retry <= MAX_RETRIES) {
178+
datelog(`Snoozing ${5 * retry}s`)
179+
await snooze(5000 * retry)
180+
} else {
181+
break
182+
}
183+
}
184+
}
185+
186+
return {
187+
settings: { latestIsoDate },
188+
transactions: standardTxs
189+
}
190+
}
191+
192+
export const swapter: PartnerPlugin = {
193+
queryFunc: querySwapter,
194+
pluginName: 'Swapter',
195+
pluginId: 'swapter'
196+
}
197+
198+
export function processSwapterTx(rawTx: unknown): StandardTx {
199+
const tx: SwapterTx = asSwapterTx(rawTx)
200+
201+
const { timestamp, isoDate } = smartIsoDateFromTimestamp(tx.time.create)
202+
203+
return {
204+
status: statusMap[tx.info.status],
205+
orderId: tx.info.uid,
206+
countryCode: null,
207+
208+
depositTxid: undefined,
209+
depositAddress: tx.deposit.address,
210+
depositCurrency: tx.deposit.coin.toUpperCase(),
211+
depositChainPluginId: undefined,
212+
depositEvmChainId: undefined,
213+
depositTokenId: undefined,
214+
depositAmount: tx.deposit.actual ?? tx.deposit.amount,
215+
216+
direction: null,
217+
exchangeType: 'swap',
218+
paymentType: null,
219+
220+
payoutTxid: undefined,
221+
payoutAddress: tx.withdraw.address,
222+
payoutCurrency: tx.withdraw.coin.toUpperCase(),
223+
payoutChainPluginId: undefined,
224+
payoutEvmChainId: undefined,
225+
payoutTokenId: undefined,
226+
payoutAmount: tx.withdraw.amount,
227+
228+
timestamp,
229+
isoDate,
230+
231+
usdValue: tx.info.equivalent > 0 ? tx.info.equivalent : -1,
232+
233+
rawTx
234+
}
235+
}

src/queryEngine.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { rango } from './partners/rango'
2828
import { safello } from './partners/safello'
2929
import { sideshift } from './partners/sideshift'
3030
import { simplex } from './partners/simplex'
31+
import { swapter } from './partners/swapter'
3132
import { swapuz } from './partners/swapuz'
3233
import { switchain } from './partners/switchain'
3334
import { maya, thorchain } from './partners/thorchain'
@@ -80,6 +81,7 @@ const plugins = [
8081
safello,
8182
sideshift,
8283
simplex,
84+
swapter,
8385
swapuz,
8486
switchain,
8587
thorchain,

0 commit comments

Comments
 (0)