Skip to content

Commit 1725c41

Browse files
authored
Merge pull request #954 from Merit-Systems/feat/base-bitquery-transfers
Switch Base transfer sync to Bitquery
2 parents 0e7f354 + 3896b4b commit 1725c41

8 files changed

Lines changed: 227 additions & 17 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { FACILITATORS_BY_CHAIN } from '@/trigger/lib/facilitators';
2+
import { ONE_MINUTE_IN_SECONDS } from '@/trigger/lib/constants';
3+
import type { SyncConfig } from '@/trigger/types';
4+
import { Network, PaginationStrategy, QueryProvider } from '@/trigger/types';
5+
import { buildQuery, transformResponse } from './query';
6+
7+
export const baseBitqueryConfig: SyncConfig = {
8+
cron: '*/5 * * * *',
9+
maxDurationInSeconds: ONE_MINUTE_IN_SECONDS * 15,
10+
chain: 'base',
11+
provider: QueryProvider.BITQUERY,
12+
apiUrl: 'https://streaming.bitquery.io/graphql',
13+
paginationStrategy: PaginationStrategy.OFFSET,
14+
limit: 25_000,
15+
facilitators: FACILITATORS_BY_CHAIN(Network.BASE),
16+
buildQuery,
17+
transformResponse,
18+
enabled: true,
19+
machine: 'medium-1x',
20+
splitSyncByFacilitator: true,
21+
resumeFromProviders: [QueryProvider.CDP, QueryProvider.BITQUERY],
22+
resumeOverlapMs: ONE_MINUTE_IN_SECONDS * 1000,
23+
};
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type {
2+
EvmBitQueryTransferRow,
3+
Facilitator,
4+
FacilitatorConfig,
5+
SyncConfig,
6+
TransferEventData,
7+
} from '@/trigger/types';
8+
9+
export function buildQuery(
10+
config: SyncConfig,
11+
facilitatorConfig: FacilitatorConfig,
12+
since: Date,
13+
now: Date,
14+
offset = 0
15+
): string {
16+
return `
17+
{
18+
EVM(dataset: combined, network: base) {
19+
Transfers(
20+
limit: { count: ${config.limit}, offset: ${offset} }
21+
where: {
22+
Transfer: {
23+
Success: true
24+
Currency: {
25+
SmartContract: {
26+
is: "${facilitatorConfig.token.address}"
27+
}
28+
}
29+
}
30+
Transaction: {
31+
From: {
32+
is: "${facilitatorConfig.address.toLowerCase()}"
33+
}
34+
}
35+
Block: {
36+
Time: {
37+
since: "${since.toISOString()}"
38+
till: "${now.toISOString()}"
39+
}
40+
}
41+
}
42+
orderBy: {
43+
ascending: [
44+
Block_Number,
45+
Transaction_Index,
46+
Call_Index,
47+
Log_Index,
48+
Transfer_Index,
49+
Transfer_Type
50+
]
51+
}
52+
) {
53+
Block {
54+
Time
55+
Number
56+
}
57+
Transaction {
58+
Hash
59+
From
60+
Index
61+
}
62+
Transfer {
63+
Amount
64+
Sender
65+
Receiver
66+
Index
67+
Type
68+
Success
69+
Currency {
70+
SmartContract
71+
Decimals
72+
Symbol
73+
Name
74+
}
75+
}
76+
Log {
77+
Index
78+
}
79+
Call {
80+
Index
81+
}
82+
}
83+
}
84+
}
85+
`;
86+
}
87+
88+
export function transformResponse(
89+
data: unknown,
90+
config: SyncConfig,
91+
facilitator: Facilitator,
92+
facilitatorConfig: FacilitatorConfig
93+
): TransferEventData[] {
94+
const transfers = (data as { EVM: { Transfers: EvmBitQueryTransferRow[] } })
95+
.EVM.Transfers;
96+
const multiplier = 10 ** facilitatorConfig.token.decimals;
97+
98+
return transfers.map(transfer => {
99+
if (transfer.Log?.Index === undefined || transfer.Log.Index === null) {
100+
throw new Error(
101+
`Bitquery transfer is missing Log.Index for ${transfer.Transaction.Hash}`
102+
);
103+
}
104+
105+
return {
106+
address: transfer.Transfer.Currency.SmartContract.toLowerCase(),
107+
transaction_from: transfer.Transaction.From.toLowerCase(),
108+
sender: transfer.Transfer.Sender.toLowerCase(),
109+
recipient: transfer.Transfer.Receiver.toLowerCase(),
110+
amount: Math.round(parseFloat(transfer.Transfer.Amount) * multiplier),
111+
block_timestamp: new Date(transfer.Block.Time),
112+
tx_hash: transfer.Transaction.Hash.toLowerCase(),
113+
log_index: transfer.Log.Index,
114+
chain: config.chain,
115+
provider: config.provider,
116+
decimals: facilitatorConfig.token.decimals,
117+
facilitator_id: facilitator.id,
118+
};
119+
});
120+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { createChainSyncTask } from '../../../../sync';
2+
import { baseBitqueryConfig } from './config';
3+
4+
export const baseBitquerySyncTransfers =
5+
createChainSyncTask(baseBitqueryConfig);

sync/transfers/trigger/chains/evm/base/cdp/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const baseCdpConfig: SyncConfig = {
1616
facilitators: FACILITATORS_BY_CHAIN(Network.BASE),
1717
buildQuery,
1818
transformResponse,
19-
enabled: true,
19+
enabled: false,
2020
machine: 'medium-1x',
2121
splitSyncByFacilitator: true,
2222
};

sync/transfers/trigger/fetch/bitquery/fetch.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ export async function fetchWithOffsetPagination(
1414
facilitator: Facilitator,
1515
facilitatorConfig: FacilitatorConfig,
1616
since: Date,
17-
now: Date
17+
now: Date,
18+
onBatchFetched?: (batch: TransferEventData[]) => Promise<void>
1819
): Promise<TransferEventData[]> {
1920
const allTransfers = [];
2021
let offset = 0;
@@ -39,6 +40,10 @@ export async function fetchWithOffsetPagination(
3940

4041
allTransfers.push(...transfers);
4142

43+
if (onBatchFetched && transfers.length > 0) {
44+
await onBatchFetched(transfers);
45+
}
46+
4247
if (transfers.length < config.limit) {
4348
hasMore = false;
4449
} else {

sync/transfers/trigger/fetch/fetch.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,10 @@ async function fetchWithOffset(
137137
facilitator,
138138
facilitatorConfig,
139139
since,
140-
now
140+
now,
141+
onBatchFetched
141142
);
142143

143-
if (onBatchFetched && results.length > 0) {
144-
await onBatchFetched(results);
145-
}
146-
147144
return { totalFetched: results.length };
148145
}
149146

sync/transfers/trigger/sync.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ import { fetchTransfers } from './fetch/fetch';
55

66
import type { Facilitator, SyncConfig } from './types';
77

8+
function normalizeTransactionFrom(chain: string, address: string): string {
9+
return chain === Network.SOLANA.toString() ? address : address.toLowerCase();
10+
}
11+
12+
function getResumeSince(
13+
latestTimestamp: Date | undefined,
14+
syncConfig: SyncConfig,
15+
syncStartDate: Date
16+
): Date {
17+
if (!latestTimestamp) {
18+
return syncStartDate;
19+
}
20+
21+
const resumeTimestamp =
22+
syncConfig.resumeOverlapMs !== undefined
23+
? latestTimestamp.getTime() - syncConfig.resumeOverlapMs
24+
: latestTimestamp.getTime() + 1000;
25+
26+
return new Date(Math.max(resumeTimestamp, syncStartDate.getTime()));
27+
}
28+
829
async function syncFacilitator(
930
syncConfig: SyncConfig,
1031
facilitator: Facilitator,
@@ -24,27 +45,32 @@ async function syncFacilitator(
2445
`[${syncConfig.chain}] Getting most recent transfer for ${facilitator.id}:${facilitatorConfig.address}`
2546
);
2647

48+
const resumeFromProviders = syncConfig.resumeFromProviders ?? [
49+
syncConfig.provider,
50+
];
51+
2752
const mostRecentTransfer = await getTransferEvents({
2853
orderBy: { block_timestamp: 'desc' },
2954
take: 1,
3055
where: {
3156
chain: syncConfig.chain,
32-
transaction_from:
33-
syncConfig.chain === Network.SOLANA.toString()
34-
? facilitatorConfig.address
35-
: facilitatorConfig.address.toLowerCase(),
36-
provider: syncConfig.provider,
57+
transaction_from: normalizeTransactionFrom(
58+
syncConfig.chain,
59+
facilitatorConfig.address
60+
),
61+
provider: { in: resumeFromProviders },
3762
},
3863
});
3964

4065
logger.log(
41-
`[${syncConfig.chain}] Most recent transfer: ${mostRecentTransfer[0]?.block_timestamp?.toISOString()}`
66+
`[${syncConfig.chain}] Most recent transfer from ${resumeFromProviders.join(',')}: ${mostRecentTransfer[0]?.block_timestamp?.toISOString()}`
4267
);
4368

44-
// Start from 1 second after the most recent transfer to avoid re-fetching it
45-
const since = mostRecentTransfer[0]?.block_timestamp
46-
? new Date(mostRecentTransfer[0].block_timestamp.getTime() + 1000)
47-
: facilitatorConfig.syncStartDate;
69+
const since = getResumeSince(
70+
mostRecentTransfer[0]?.block_timestamp,
71+
syncConfig,
72+
facilitatorConfig.syncStartDate
73+
);
4874

4975
logger.log(
5076
`[${syncConfig.chain}] Syncing ${facilitator.id}:${facilitatorConfig.address} from ${since.toISOString()} to ${now.toISOString()}`

sync/transfers/trigger/types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export type SyncConfig = QueryConfig & {
8080
enabled: boolean;
8181
machine: 'small-1x' | 'medium-1x' | 'large-2x';
8282
splitSyncByFacilitator?: boolean;
83+
resumeFromProviders?: QueryProvider[];
84+
resumeOverlapMs?: number;
8385
};
8486

8587
export interface EvmChainConfig {
@@ -147,3 +149,35 @@ export interface BitQueryTransferRowStream {
147149
From: string;
148150
};
149151
}
152+
153+
export interface EvmBitQueryTransferRow {
154+
Transfer: {
155+
Amount: string;
156+
Sender: string;
157+
Receiver: string;
158+
Index: number;
159+
Type: string;
160+
Success: boolean;
161+
Currency: {
162+
Name: string;
163+
SmartContract: string;
164+
Symbol: string;
165+
Decimals: number;
166+
};
167+
};
168+
Block: {
169+
Time: string;
170+
Number: string;
171+
};
172+
Transaction: {
173+
Hash: string;
174+
From: string;
175+
Index: number;
176+
};
177+
Log?: {
178+
Index: number | null;
179+
};
180+
Call?: {
181+
Index: number | null;
182+
};
183+
}

0 commit comments

Comments
 (0)