Skip to content

Commit b21225d

Browse files
committed
Query both old and new Sideshift affiliate accounts and merge completed orders
Loop the per-account completed-orders query over each configured affiliate account and merge the streams so a Sideshift affiliate-account rotation keeps reporting the old account's historical and in-flight shifts. Track latestIsoDate per account so neither account's cursor skips the other's orders, falling back to the legacy single cursor for backward compatibility. Single-account config is unchanged. Add a mocha test covering the dual-account merge and per-account cursor behavior. Claude-Session: https://claude.ai/code/session_01YNwNii1LxjqaeowP5d7dgJ
1 parent 458438a commit b21225d

3 files changed

Lines changed: 417 additions & 31 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+
- changed: Query both old and new Sideshift affiliate accounts and merge completed orders to preserve full shift history across an affiliate-account rotation
56
- changed: Add signature header support to Exolix
67
- changed: Add index for orderId
78
- changed: Add EVM chainId, pluginId, and tokenId fields to StandardTx

src/partners/sideshift.ts

Lines changed: 158 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
asArray,
3+
asMap,
34
asMaybe,
45
asObject,
56
asOptional,
@@ -13,6 +14,7 @@ import {
1314
PartnerPlugin,
1415
PluginParams,
1516
PluginResult,
17+
ScopedLog,
1618
StandardTx,
1719
Status
1820
} from '../types'
@@ -166,20 +168,53 @@ const asSideshiftTx = asObject({
166168
createdAt: asString
167169
})
168170

171+
// apiKeys reaching a partner plugin are a flat string->string map
172+
// (see asPartnerInfo in types.ts), so additional accounts are carried as
173+
// explicit string fields rather than a nested array. The primary
174+
// sideshiftAffiliateId/Secret pair is the active (new) account; the optional
175+
// *Old pair is the previous affiliate account kept during a rotation so its
176+
// historical and in-flight completed orders are still reported. Backward
177+
// compatible: when the *Old fields are absent only the primary pair is queried.
178+
const asSideshiftApiKeys = asObject({
179+
sideshiftAffiliateId: asString,
180+
sideshiftAffiliateSecret: asString,
181+
sideshiftAffiliateIdOld: asOptional(asString),
182+
sideshiftAffiliateSecretOld: asOptional(asString)
183+
})
184+
185+
// `accounts` is a per-affiliateId cursor map. Legacy progress docs only carry
186+
// the top-level `latestIsoDate`; accounts with no map entry fall back to it on
187+
// the first run after deploy so neither account's cursor skips the other's.
188+
const asSideshiftSettings = asObject({
189+
latestIsoDate: asOptional(asString, '1970-01-01T00:00:00.000Z'),
190+
accounts: asOptional(asMap(asString), () => ({}))
191+
})
192+
169193
const asSideshiftPluginParams = asObject({
170-
apiKeys: asObject({
171-
sideshiftAffiliateId: asString,
172-
sideshiftAffiliateSecret: asString
173-
}),
174-
settings: asObject({
175-
latestIsoDate: asOptional(asString, '1970-01-01T00:00:00.000Z')
176-
})
194+
apiKeys: asSideshiftApiKeys,
195+
settings: asSideshiftSettings
177196
})
178197

198+
interface SideshiftAccount {
199+
affiliateId: string
200+
affiliateSecret: string
201+
}
202+
203+
type SideshiftApiKeys = ReturnType<typeof asSideshiftApiKeys>
179204
type SideshiftTx = ReturnType<typeof asSideshiftTx>
180205
type SideshiftStatus = ReturnType<typeof asSideshiftStatus>
181206
const asSideshiftResult = asArray(asUnknown)
182207

208+
// Fetches one raw page of completed orders for an account, and processes a raw
209+
// order into a StandardTx. Both are injectable so the multi-account merge and
210+
// cursor logic can be exercised in tests without the live Sideshift API.
211+
type FetchSideshiftOrders = (
212+
account: SideshiftAccount,
213+
startTime: number,
214+
now: number
215+
) => Promise<unknown[]>
216+
type ProcessSideshiftOrder = (rawTx: unknown) => Promise<StandardTx>
217+
183218
const MAX_RETRIES = 5
184219
const QUERY_LOOKBACK = 1000 * 60 * 60 * 24 * 5 // 5 days
185220
const QUERY_TIME_BLOCK_MS = QUERY_LOOKBACK
@@ -209,13 +244,72 @@ function affiliateSignature(
209244
.digest('hex')
210245
}
211246

212-
export async function querySideshift(
213-
pluginParams: PluginParams
214-
): Promise<PluginResult> {
215-
const { log } = pluginParams
216-
const { settings, apiKeys } = asSideshiftPluginParams(pluginParams)
217-
const { sideshiftAffiliateId, sideshiftAffiliateSecret } = apiKeys
218-
let { latestIsoDate } = settings
247+
// Builds the list of affiliate accounts to query from the configured apiKeys.
248+
// Returns the primary (new) account plus the optional old account, deduped by
249+
// affiliateId so a single-account config yields exactly one account.
250+
export function getSideshiftAccounts(
251+
apiKeys: SideshiftApiKeys
252+
): SideshiftAccount[] {
253+
const {
254+
sideshiftAffiliateId,
255+
sideshiftAffiliateSecret,
256+
sideshiftAffiliateIdOld,
257+
sideshiftAffiliateSecretOld
258+
} = apiKeys
259+
260+
const accounts: SideshiftAccount[] = [
261+
{
262+
affiliateId: sideshiftAffiliateId,
263+
affiliateSecret: sideshiftAffiliateSecret
264+
}
265+
]
266+
267+
if (
268+
sideshiftAffiliateIdOld != null &&
269+
sideshiftAffiliateSecretOld != null &&
270+
sideshiftAffiliateIdOld !== sideshiftAffiliateId
271+
) {
272+
accounts.push({
273+
affiliateId: sideshiftAffiliateIdOld,
274+
affiliateSecret: sideshiftAffiliateSecretOld
275+
})
276+
}
277+
278+
return accounts
279+
}
280+
281+
const defaultFetchSideshiftOrders: FetchSideshiftOrders = async (
282+
account,
283+
startTime,
284+
now
285+
): Promise<unknown[]> => {
286+
const signature = affiliateSignature(
287+
account.affiliateId,
288+
account.affiliateSecret,
289+
now
290+
)
291+
const url = `https://sideshift.ai/api/affiliate/completedOrders?affiliateId=${account.affiliateId}&since=${startTime}&currentTime=${now}&signature=${signature}`
292+
const response = await retryFetch(url)
293+
if (!response.ok) {
294+
const text = await response.text()
295+
throw new Error(text)
296+
}
297+
const jsonObj = await response.json()
298+
return asSideshiftResult(jsonObj)
299+
}
300+
301+
// Queries the completed orders for a single affiliate account, advancing its
302+
// own cursor. Mirrors the original per-account query/signature/retry/time-block
303+
// behavior exactly; only the fetch + per-order processing are abstracted so the
304+
// merge can be tested without the live API.
305+
async function querySideshiftAccount(
306+
account: SideshiftAccount,
307+
initialLatestIsoDate: string,
308+
fetchOrders: FetchSideshiftOrders,
309+
processOrder: ProcessSideshiftOrder,
310+
log: ScopedLog
311+
): Promise<{ transactions: StandardTx[]; latestIsoDate: string }> {
312+
let latestIsoDate = initialLatestIsoDate
219313

220314
let lastCheckedTimestamp = new Date(latestIsoDate).getTime() - QUERY_LOOKBACK
221315
if (lastCheckedTimestamp < 0) lastCheckedTimestamp = 0
@@ -228,33 +322,20 @@ export async function querySideshift(
228322
const endTime = startTime + QUERY_TIME_BLOCK_MS
229323
const now = Date.now()
230324

231-
const signature = affiliateSignature(
232-
sideshiftAffiliateId,
233-
sideshiftAffiliateSecret,
234-
now
235-
)
236-
237-
const url = `https://sideshift.ai/api/affiliate/completedOrders?affiliateId=${sideshiftAffiliateId}&since=${startTime}&currentTime=${now}&signature=${signature}`
238325
try {
239-
const response = await retryFetch(url)
240-
if (!response.ok) {
241-
const text = await response.text()
242-
throw new Error(text)
243-
}
244-
const jsonObj = await response.json()
245-
const orders = asSideshiftResult(jsonObj)
326+
const orders = await fetchOrders(account, startTime, now)
246327
if (orders.length === 0) {
247328
break
248329
}
249330
for (const rawTx of orders) {
250-
const standardTx = await processSideshiftTx(rawTx, pluginParams)
331+
const standardTx = await processOrder(rawTx)
251332
standardTxs.push(standardTx)
252333
if (standardTx.isoDate > latestIsoDate) {
253334
latestIsoDate = standardTx.isoDate
254335
}
255336
}
256337
startTime = new Date(latestIsoDate).getTime()
257-
log(`latestIsoDate ${latestIsoDate}`)
338+
log(`${account.affiliateId} latestIsoDate ${latestIsoDate}`)
258339
if (endTime > now) {
259340
break
260341
}
@@ -273,8 +354,54 @@ export async function querySideshift(
273354
}
274355
}
275356

357+
return { transactions: standardTxs, latestIsoDate }
358+
}
359+
360+
export async function querySideshift(
361+
pluginParams: PluginParams,
362+
fetchOrders: FetchSideshiftOrders = defaultFetchSideshiftOrders,
363+
processOrder: ProcessSideshiftOrder = async rawTx =>
364+
await processSideshiftTx(rawTx, pluginParams)
365+
): Promise<PluginResult> {
366+
const { log } = pluginParams
367+
const { settings, apiKeys } = asSideshiftPluginParams(pluginParams)
368+
const {
369+
latestIsoDate: legacyLatestIsoDate,
370+
accounts: accountCursors
371+
} = settings
372+
373+
const accounts = getSideshiftAccounts(apiKeys)
374+
375+
const standardTxs: StandardTx[] = []
376+
const newAccountCursors: { [affiliateId: string]: string } = {}
377+
let overallLatestIsoDate = legacyLatestIsoDate
378+
379+
for (const account of accounts) {
380+
// Per-account cursor, falling back to the legacy single cursor for accounts
381+
// not yet present in the map (first run after deploy / new account).
382+
const startCursor =
383+
accountCursors[account.affiliateId] ?? legacyLatestIsoDate
384+
385+
const { transactions, latestIsoDate } = await querySideshiftAccount(
386+
account,
387+
startCursor,
388+
fetchOrders,
389+
processOrder,
390+
log
391+
)
392+
393+
standardTxs.push(...transactions)
394+
newAccountCursors[account.affiliateId] = latestIsoDate
395+
if (latestIsoDate > overallLatestIsoDate) {
396+
overallLatestIsoDate = latestIsoDate
397+
}
398+
}
399+
276400
const out = {
277-
settings: { latestIsoDate },
401+
settings: {
402+
latestIsoDate: overallLatestIsoDate,
403+
accounts: newAccountCursors
404+
},
278405
transactions: standardTxs
279406
}
280407
return out

0 commit comments

Comments
 (0)