11import {
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+
169193const 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 >
179204type SideshiftTx = ReturnType < typeof asSideshiftTx >
180205type SideshiftStatus = ReturnType < typeof asSideshiftStatus >
181206const 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+
183218const MAX_RETRIES = 5
184219const QUERY_LOOKBACK = 1000 * 60 * 60 * 24 * 5 // 5 days
185220const 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 } ¤tTime=${ 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 } ¤tTime=${ 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