Skip to content

Commit fd39125

Browse files
committed
Add 0xGasless plugin
1 parent aa136dc commit fd39125

File tree

2 files changed

+253
-1
lines changed

2 files changed

+253
-1
lines changed

src/partners/0xgasless.ts

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import {
2+
asArray,
3+
asEither,
4+
asJSON,
5+
asNull,
6+
asNumber,
7+
asObject,
8+
asString,
9+
asValue
10+
} from 'cleaners'
11+
import URL from 'url-parse'
12+
13+
import {
14+
asStandardPluginParams,
15+
EDGE_APP_START_DATE,
16+
PartnerPlugin,
17+
PluginParams,
18+
PluginResult,
19+
StandardTx,
20+
Status
21+
} from '../types'
22+
import { datelog, retryFetch, smartIsoDateFromTimestamp, snooze } from '../util'
23+
24+
const API_URL = 'https://api.0x.org/trade-analytics/gasless'
25+
const PLUGIN_START_DATE = '2024-05-05T00:00:00.000Z'
26+
/** Max fetch retries before bailing */
27+
const MAX_RETRIES = 5
28+
/**
29+
* How far to rollback from the last successful query
30+
* date when starting a new query
31+
*/
32+
const QUERY_LOOKBACK = 1000 * 60 * 60 * 24 * 30 // 30 Days
33+
/** Time period to query per loop */
34+
const QUERY_TIME_BLOCK_MS = QUERY_LOOKBACK
35+
36+
type PartnerStatuses =
37+
| 'other'
38+
| 'created'
39+
| 'completed'
40+
| 'cancelled'
41+
| 'payment_error'
42+
| 'rejected'
43+
const statusMap: { [key in PartnerStatuses]: Status } = {
44+
created: 'pending',
45+
cancelled: 'refunded',
46+
payment_error: 'refunded',
47+
completed: 'complete',
48+
rejected: 'refunded',
49+
other: 'other'
50+
}
51+
52+
export async function query0xGasless(
53+
pluginParams: PluginParams
54+
): Promise<PluginResult> {
55+
const { settings, apiKeys } = asStandardPluginParams(pluginParams)
56+
57+
if (apiKeys.apiKey == null) {
58+
throw new Error('0xGasless: Missing 0xgasless API key')
59+
}
60+
const nowDate = new Date()
61+
const now = nowDate.getTime()
62+
63+
let { latestIsoDate } = settings
64+
65+
if (latestIsoDate === EDGE_APP_START_DATE) {
66+
latestIsoDate = new Date(PLUGIN_START_DATE).toISOString()
67+
}
68+
69+
let lastCheckedTimestamp = new Date(latestIsoDate).getTime() - QUERY_LOOKBACK
70+
if (lastCheckedTimestamp < 0) lastCheckedTimestamp = 0
71+
72+
const ssFormatTxs: StandardTx[] = []
73+
let retry = 0
74+
75+
while (true) {
76+
const startTimestamp = lastCheckedTimestamp
77+
const endTimestamp = lastCheckedTimestamp + QUERY_TIME_BLOCK_MS
78+
79+
try {
80+
let cursor: string | undefined
81+
82+
while (true) {
83+
const urlObj = new URL(API_URL, true)
84+
85+
const queryParams: {
86+
startTimestamp: string
87+
endTimestamp: string
88+
cursor?: string
89+
} = {
90+
// API expects seconds-based unix timestamps
91+
startTimestamp: Math.floor(startTimestamp / 1000).toString(),
92+
endTimestamp: Math.floor(endTimestamp / 1000).toString()
93+
}
94+
if (cursor != null) queryParams.cursor = cursor
95+
urlObj.set('query', queryParams)
96+
97+
datelog(
98+
`0xGasless Querying from:${new Date(
99+
startTimestamp
100+
).toISOString()} to:${new Date(endTimestamp).toISOString()}`
101+
)
102+
103+
const url = urlObj.href
104+
const response = await retryFetch(url, {
105+
headers: {
106+
'0x-api-key': apiKeys.apiKey,
107+
'0x-version': 'v2'
108+
}
109+
})
110+
const responseJson = await response.text()
111+
if (!response.ok) {
112+
throw new Error(`${url} response ${response.status}: ${responseJson}`)
113+
}
114+
const responseBody = asGetGaslessTradesResponse(responseJson)
115+
116+
for (const trade of responseBody.trades) {
117+
const buySymbol = trade.tokens.find(t => t.address === trade.buyToken)
118+
?.symbol
119+
const sellSymbol = trade.tokens.find(
120+
t => t.address === trade.sellToken
121+
)?.symbol
122+
123+
if (buySymbol == null || sellSymbol == null) {
124+
throw new Error(
125+
`Could not find buy or sell symbol for trade ${trade.zid}; txid: ${trade.transactionHash}`
126+
)
127+
}
128+
129+
const {
130+
isoDate: tradeIsoDate,
131+
timestamp: tradeTimestamp
132+
} = smartIsoDateFromTimestamp(trade.timestamp * 1000)
133+
134+
// If trade is 2 days or older, then it's finalized according to 0x
135+
// documentation.
136+
const status: Status =
137+
tradeTimestamp + 2 * 24 * 60 * 60 * 1000 < now
138+
? 'complete'
139+
: 'pending'
140+
141+
const ssTx: StandardTx = {
142+
status,
143+
orderId: trade.zid,
144+
depositTxid: trade.transactionHash,
145+
depositAddress: undefined,
146+
depositCurrency: sellSymbol,
147+
depositAmount: Number(trade.sellAmount),
148+
payoutTxid: trade.transactionHash,
149+
payoutAddress: trade.taker ?? undefined,
150+
payoutCurrency: buySymbol,
151+
payoutAmount: Number(trade.buyAmount),
152+
timestamp: tradeTimestamp,
153+
isoDate: tradeIsoDate,
154+
usdValue: parseFloat(trade.volumeUsd),
155+
rawTx: trade
156+
}
157+
ssFormatTxs.push(ssTx)
158+
if (ssTx.isoDate > latestIsoDate) {
159+
latestIsoDate = ssTx.isoDate
160+
}
161+
}
162+
163+
datelog(`0xGasless ${responseBody.trades.length} trades processed`)
164+
165+
if (responseBody.nextCursor == null) {
166+
datelog(`0xGasless No cursor from API`)
167+
break
168+
} else {
169+
cursor = responseBody.nextCursor
170+
datelog(`0xGasless Get nextCursor: ${cursor}`)
171+
}
172+
}
173+
174+
lastCheckedTimestamp = endTimestamp
175+
datelog(
176+
`0xGasless endDate:${new Date(
177+
lastCheckedTimestamp
178+
).toISOString()} latestIsoDate:${latestIsoDate}`
179+
)
180+
if (lastCheckedTimestamp > now) {
181+
break
182+
}
183+
retry = 0
184+
} catch (error) {
185+
datelog(error)
186+
// Retry a few times with time delay to prevent throttling
187+
retry++
188+
if (retry <= MAX_RETRIES) {
189+
datelog(`Snoozing ${60 * retry}s`)
190+
await snooze(60000 * retry)
191+
} else {
192+
// We can't safely save our progress since we go from newest to oldest.
193+
throw error
194+
}
195+
}
196+
197+
// Wait before next query, to prevent rate-limiting and thrashing
198+
await snooze(1000)
199+
}
200+
201+
const out = {
202+
settings: { latestIsoDate },
203+
transactions: ssFormatTxs
204+
}
205+
return out
206+
}
207+
208+
export const zeroxgasless: PartnerPlugin = {
209+
queryFunc: query0xGasless,
210+
pluginName: '0xGasless',
211+
pluginId: '0xgasless'
212+
}
213+
214+
const asGetGaslessTradesResponse = asJSON(
215+
asObject({
216+
nextCursor: asEither(asString, asNull),
217+
trades: asArray(v => asGaslessTrade(v))
218+
})
219+
)
220+
221+
const asGaslessTrade = asObject({
222+
appName: asString,
223+
blockNumber: asString,
224+
buyToken: asString,
225+
buyAmount: asString,
226+
chainId: asNumber,
227+
// Fee data is not used.
228+
// fees: {
229+
// "integratorFee": null,
230+
// "zeroExFee": null
231+
// },
232+
gasUsed: asString,
233+
protocolVersion: asString,
234+
sellToken: asString,
235+
sellAmount: asString,
236+
slippageBps: asEither(asString, asNull),
237+
taker: asString,
238+
timestamp: asNumber,
239+
tokens: asArray(v => asGaslessTradeToken(v)),
240+
transactionHash: asString,
241+
volumeUsd: asString,
242+
/** The 0x trade id */
243+
zid: asString,
244+
service: asValue('gasless')
245+
})
246+
247+
const asGaslessTradeToken = asObject({
248+
address: asString,
249+
symbol: asString
250+
})

src/queryEngine.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import nano from 'nano'
33
import { config } from './config'
44
import { pagination } from './dbutils'
55
import { initDbs } from './initDbs'
6+
import { zeroxgasless } from './partners/0xgasless'
67
import { banxa } from './partners/banxa'
78
import { bitaccess } from './partners/bitaccess'
89
import { bitrefill } from './partners/bitrefill'
@@ -68,7 +69,8 @@ const plugins = [
6869
thorchain,
6970
transak,
7071
wyre,
71-
xanpool
72+
xanpool,
73+
zeroxgasless
7274
]
7375
const QUERY_FREQ_MS = 60 * 1000
7476
const MAX_CONCURRENT_QUERIES = 3

0 commit comments

Comments
 (0)