diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 0797c8e..dcc24a6 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -1,4 +1,2 @@ -import queryApiBase from './queryApi'; -import nsync from './nsync'; - -export const queryApi = nsync(queryApiBase); +export { default as queryApi } from './queryApi'; +export * as nsync from './nsync'; diff --git a/packages/api/src/utils/nsync.ts b/packages/api/src/utils/nsync.ts index a551e61..21ec771 100644 --- a/packages/api/src/utils/nsync.ts +++ b/packages/api/src/utils/nsync.ts @@ -1,17 +1,11 @@ import config from '@nftx/config'; -import queryApi from './queryApi'; - -const isApiBehind = () => { - const { network, internal } = config; - const apiBlockNumber = internal.apiBlockNumber[network]; - const requiredBlockNumber = internal.requiredBlockNumber[network]; - - return apiBlockNumber < requiredBlockNumber; -}; +import { query } from '@nftx/utils'; // Wraps an async function. // While the promise is unresolved, all subsequent calls will receive the same promise -const throttleFn = Promise>(fn: F): F => { +const throttleAsyncFn = Promise>( + fn: F +): F => { let p: Promise | undefined; return ((...args: Parameters) => { if (p) { @@ -31,65 +25,93 @@ const throttleFn = Promise>(fn: F): F => { }) as any as F; }; -const fetchLastIndexedBlock = throttleFn(async () => { - const url = `/${config.network}/block`; - type Response = { block: number }; - const response = await queryApi({ - url, - query: { source: 'live' }, - }); - return response?.block; -}); +/** Gets the required block number */ +export const getRequiredBlockNumber = (network = config.network) => { + return config.internal.requiredBlockNumber[network] ?? 0; +}; -const updateApiBlock = ({ source }: { source: 'live' | 'api' }) => { - // Switch to live mode - if (source !== 'live') { - config.internal.source = 'live'; - } - // Wait a few seconds before polling the api again - setTimeout(async () => { - // Get the last indexed block on the api - config.internal.apiBlockNumber[config.network] = - await fetchLastIndexedBlock(); - // Run this fn again until we're up to date... - checkApiBlock(); - }, 5000); +/** Sets the required block number */ +export const setRequiredBlockNumber = ( + value: number, + network: number = config.network +) => { + config.internal.requiredBlockNumber[network] = value; }; -const resetRequiredBlock = () => { - // Reset the required block number - config.internal.requiredBlockNumber[config.network] = 0; - // Switch back to using the api as the SoT - config.internal.source = 'api'; +/** Gets the current block number from the api */ +export const getApiBlockNumber = (network = config.network) => { + return config.internal.apiBlockNumber[network] ?? 0; }; -const checkApiBlock = (): void => { - const requiredBlockNumber = - config.internal.requiredBlockNumber[config.network]; +export const getBlockBuffer = () => { + return config.internal.blockBuffer; +}; - // We don't need to worry about syncing if there's no required block number +// Checks whether the required block number is ahead of the api block number +const isApiBehind = ({ + network, + requiredBlockNumber, +}: { + network: number; + requiredBlockNumber: number; +}) => { if (!requiredBlockNumber) { - return; + // No required block number so we don't need to worry about the api + return false; } + const apiBlockNumber = getApiBlockNumber(network); - const { source } = config.internal; + return apiBlockNumber < requiredBlockNumber; +}; - // The API is behind the required block - if (isApiBehind()) { - // Switch to live mode and start polling the api to see what the last-indexed block is - updateApiBlock({ source }); - } else if (source === 'live') { - // Api has caught up so we no longer need to be in live mode - resetRequiredBlock(); - } +const updateLastIndexedBlock = async ({ network }: { network: number }) => { + // Get the last indexed block on the api + const response = await query<{ block: number }>({ + url: `/${network}/block`, + query: { source: 'live', ebn: 'true' }, + headers: { + 'Content-Type': 'application/json', + Authorization: config.keys.NFTX_API, + }, + }); + const block = response?.block ?? 0; + // Save it (writes to local storage) + config.internal.apiBlockNumber[network] = block; }; -/** Wrap another function so that when it gets called, we first check the last-indexed block from the api */ -const nsync = any>(f: F): F => { - return ((...args: any[]) => { - checkApiBlock(); - return f(...args); - }) as F; +const resetRequiredBlock = ({ network }: { network: number }) => { + // reset the required block number + setRequiredBlockNumber(0, network); + // Switch back to using the api as the SoT + config.internal.source = 'api'; }; -export default nsync; +/** Checks if the api is behind the latest block. + * If it is behind, it will switch to live mode and continue checking the api until it has caught up + **/ +export const syncApiBlock = throttleAsyncFn( + async (network: number = config.network) => { + // We throttle this method so even if 1k requests are made in quick succession, + // we'll only attempt to sync the api one time + + // Keep looping while the api is behind the current block + while ( + isApiBehind({ + network, + requiredBlockNumber: getRequiredBlockNumber(network), + }) + ) { + if (config.internal.source !== 'live') { + // Switch to live mode + config.internal.source = 'live'; + } + // Wait 5s before polling again + await new Promise((res) => setTimeout(res, 5000)); + // Fetch the latest block from the api + await updateLastIndexedBlock({ network }); + } + + // The api has caught up and we no longer need to be in live mode + resetRequiredBlock({ network }); + } +); diff --git a/packages/api/src/utils/queryApi.ts b/packages/api/src/utils/queryApi.ts index cd8fb38..32e4018 100644 --- a/packages/api/src/utils/queryApi.ts +++ b/packages/api/src/utils/queryApi.ts @@ -1,5 +1,6 @@ import config from '@nftx/config'; import { query as sendQuery } from '@nftx/utils'; +import { syncApiBlock } from './nsync'; const queryApi = async ({ url, @@ -10,6 +11,9 @@ const queryApi = async ({ query?: Record; method?: string; }) => { + // Make sure the api is up to date (or switch to live mode if necessary) + syncApiBlock(); + const uri = new URL(url, config.urls.NFTX_API_URL); const query: Record = { ...givenQuery, diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 168e4c3..ffe8b41 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -52,7 +52,6 @@ export interface Config { keys: { /** Your specific nftx.js API key, this must be provided in order to use @nftx/api methods */ NFTX_API: string; - ALCHEMY: Record; RESERVOIR: Record; }; /** Internal config settings managed by nftx.js */ @@ -60,6 +59,8 @@ export interface Config { source: 'api' | 'live'; requiredBlockNumber: Record; apiBlockNumber: Record; + /** The number of blocks to buffer when syncing data from the api */ + blockBuffer: number; }; } @@ -130,7 +131,6 @@ const defaultConfig: Config = { keys: { NFTX_API: null as unknown as string, - ALCHEMY: {}, RESERVOIR: {}, }, @@ -146,6 +146,7 @@ const defaultConfig: Config = { [Network.Goerli]: 0, [Network.Sepolia]: 0, }, + blockBuffer: 10, }, }; diff --git a/packages/react/src/useTransaction/useWrapTransaction.ts b/packages/react/src/useTransaction/useWrapTransaction.ts index 1c947ea..bcd0854 100644 --- a/packages/react/src/useTransaction/useWrapTransaction.ts +++ b/packages/react/src/useTransaction/useWrapTransaction.ts @@ -1,11 +1,12 @@ import { useNftx } from '../contexts/nftx'; import { t } from '../utils'; import { useAddEvent } from '../contexts/events'; -import { config, Transaction } from 'nftx.js'; import { + Transaction, TransactionExceptionError, TransactionFailedError, -} from '@nftx/errors'; + nsync, +} from 'nftx.js'; type Fn = (...args: any) => Promise; @@ -113,9 +114,14 @@ export default function useWrapTransaction( description, }); - config.internal.requiredBlockNumber[network] = Number( - receipt.blockNumber - ); + // We ideally want data to be up to the block for this transaction + // So we store the block number and any subsequent api calls will + // be made with "live mode" enabled until the api has caught up + const txnBlock = Number(receipt.blockNumber); + // For a bit of leeway we add a buffer to the required block number + // so the api must be at least this far ahead before we switch back to "api mode" + const buffer = nsync.getBlockBuffer(); + nsync.setRequiredBlockNumber(txnBlock + buffer); return receipt; },