Skip to content

Commit

Permalink
add validator calls
Browse files Browse the repository at this point in the history
  • Loading branch information
tyleroooo committed Dec 16, 2024
1 parent 2487d1d commit 2bbe7af
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 26 deletions.
8 changes: 8 additions & 0 deletions src/abacus-ts/lib/mapLoadable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Loadable } from './loadable';

export function mapLoadableData<T, R>(load: Loadable<T>, map: (obj: T) => R): Loadable<R> {
return {
...load,
data: load.data != null ? map(load.data) : undefined,
} as Loadable<R>;
}
59 changes: 59 additions & 0 deletions src/abacus-ts/rest/height.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { timeUnits } from '@/constants/time';

import { type RootStore } from '@/state/_store';
import { setIndexerHeightRaw, setValidatorHeightRaw } from '@/state/raw';

import { loadableIdle } from '../lib/loadable';
import { mapLoadableData } from '../lib/mapLoadable';
import {
createIndexerQueryStoreEffect,
createValidatorQueryStoreEffect,
} from './lib/indexerQueryStoreEffect';
import { queryResultToLoadable } from './lib/queryResultToLoadable';

export function setUpIndexerHeightQuery(store: RootStore) {
const cleanupEffect = createIndexerQueryStoreEffect(store, {
selector: () => true,
getQueryKey: () => ['height'],
getQueryFn: (indexerClient) => {
return () => indexerClient.utility.getHeight();
},
onResult: (height) => {
store.dispatch(setIndexerHeightRaw(queryResultToLoadable(height)));
},
onNoQuery: () => store.dispatch(setIndexerHeightRaw(loadableIdle())),
refetchInterval: timeUnits.second * 10,
staleTime: timeUnits.second * 10,
});
return () => {
cleanupEffect();
store.dispatch(setIndexerHeightRaw(loadableIdle()));
};
}

export function setUpValidatorHeightQuery(store: RootStore) {
const cleanupEffect = createValidatorQueryStoreEffect(store, {
selector: () => true,
getQueryKey: () => ['height'],
getQueryFn: (compositeClient) => {
return () => compositeClient.validatorClient.get.latestBlock();
},
onResult: (height) => {
store.dispatch(
setValidatorHeightRaw(
mapLoadableData(queryResultToLoadable(height), (d) => ({
height: d.header.height,
time: d.header.time,
}))
)
);
},
onNoQuery: () => store.dispatch(setValidatorHeightRaw(loadableIdle())),
refetchInterval: timeUnits.second * 10,
staleTime: timeUnits.second * 10,
});
return () => {
cleanupEffect();
store.dispatch(setValidatorHeightRaw(loadableIdle()));
};
}
149 changes: 149 additions & 0 deletions src/abacus-ts/rest/lib/compositeClientManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { createStoreEffect } from '@/abacus-ts/lib/createStoreEffect';
import { ResourceCacheManager } from '@/abacus-ts/lib/resourceCacheManager';
import {
CompositeClient,
IndexerClient,
IndexerConfig,
Network,
NetworkOptimizer,
ValidatorConfig,
} from '@dydxprotocol/v4-client-js';

import { DEFAULT_TRANSACTION_MEMO } from '@/constants/analytics';
import {
DydxChainId,
DydxNetwork,
ENVIRONMENT_CONFIG_MAP,
TOKEN_CONFIG_MAP,
} from '@/constants/networks';

import { type RootStore } from '@/state/_store';
import { getSelectedNetwork } from '@/state/appSelectors';
import { setNetworkStateRaw } from '@/state/raw';

import { getStatsigConfigAsync } from '@/lib/statsig';

type CompositeClientWrapper = {
dead?: boolean;
compositeClient?: CompositeClient;
indexer?: IndexerClient;
tearDown: () => void;
};

function makeCompositeClient({
network,
store,
}: {
network: DydxNetwork;
store: RootStore;
}): CompositeClientWrapper {
const networkConfig = ENVIRONMENT_CONFIG_MAP[network];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!networkConfig) {
throw new Error(`Unknown network: ${network}`);
}
const chainId = networkConfig.dydxChainId as DydxChainId;
const tokens = TOKEN_CONFIG_MAP[chainId];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (tokens == null) {
throw new Error(`Unknown chain id: ${chainId}`);
}

const clientWrapper: CompositeClientWrapper = {
tearDown: () => {
clientWrapper.dead = true;
store.dispatch(
setNetworkStateRaw({
networkId: network,
stateToMerge: { compositeClientReady: false, indexerClientReady: false },
})
);
},
};

store.dispatch(
setNetworkStateRaw({
networkId: network,
stateToMerge: { compositeClientReady: false, indexerClientReady: false },
})
);

(async () => {
const networkOptimizer = new NetworkOptimizer();
const indexerUrl = networkConfig.endpoints.indexers[0];
if (indexerUrl == null) {
throw new Error('No indexer urls found');
}
const validatorUrl = await networkOptimizer.findOptimalNode(
networkConfig.endpoints.validators,
chainId
);
if (clientWrapper.dead) {
return;
}
const indexerConfig = new IndexerConfig(indexerUrl.api, indexerUrl.socket);
clientWrapper.indexer = new IndexerClient(indexerConfig);
store.dispatch(
setNetworkStateRaw({
networkId: network,
stateToMerge: { indexerClientReady: true },
})
);
const statsigFlags = await getStatsigConfigAsync();
const compositeClient = await CompositeClient.connect(
new Network(
chainId,
indexerConfig,
new ValidatorConfig(
validatorUrl,
chainId,
{
USDC_DENOM: tokens.usdc.denom,
USDC_DECIMALS: tokens.usdc.decimals,
USDC_GAS_DENOM: tokens.usdc.gasDenom,
CHAINTOKEN_DENOM: tokens.chain.denom,
CHAINTOKEN_DECIMALS: tokens.chain.decimals,
},
{
broadcastPollIntervalMs: 3_000,
broadcastTimeoutMs: 60_000,
},
DEFAULT_TRANSACTION_MEMO,
statsigFlags.ff_enable_timestamp_nonce
)
)
);
// this shouldn't be necessary - can actually be false
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (clientWrapper.dead) {
return;
}
clientWrapper.compositeClient = compositeClient;
store.dispatch(
setNetworkStateRaw({
networkId: network,
stateToMerge: { compositeClientReady: true },
})
);
})();
return clientWrapper;
}

export const CompositeClientManager = new ResourceCacheManager({
constructor: (config: { network: DydxNetwork; store: RootStore }) => makeCompositeClient(config),
destroyer: (instance) => {
instance.tearDown();
},
// store not part of serialization, assumed immutable
keySerializer: ({ network }) => network,
});

// this just makes things simpler
export function alwaysUseCurrentNetworkClient(store: RootStore) {
return createStoreEffect(store, getSelectedNetwork, (network) => {
CompositeClientManager.use({ network, store });
return () => {
CompositeClientManager.markDone({ network, store });
};
});
}
10 changes: 0 additions & 10 deletions src/abacus-ts/rest/lib/indexerClientManager.ts

This file was deleted.

99 changes: 83 additions & 16 deletions src/abacus-ts/rest/lib/indexerQueryStoreEffect.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { logAbacusTsError } from '@/abacus-ts/logs';
import { IndexerClient } from '@dydxprotocol/v4-client-js';
import { selectCompositeClientReady, selectIndexerReady } from '@/abacus-ts/socketSelectors';
import { CompositeClient, IndexerClient } from '@dydxprotocol/v4-client-js';
import { QueryObserver, QueryObserverOptions, QueryObserverResult } from '@tanstack/react-query';

import { timeUnits } from '@/constants/time';

import { type RootState, type RootStore } from '@/state/_store';
import { appQueryClient } from '@/state/appQueryClient';
import { getSelectedNetwork } from '@/state/appSelectors';
import { createAppSelector } from '@/state/appTypes';

import { createStoreEffect } from '../../lib/createStoreEffect';
import { selectIndexerUrl, selectWebsocketUrl } from '../../socketSelectors';
import { IndexerClientManager } from './indexerClientManager';
import { CompositeClientManager } from './compositeClientManager';

type PassedQueryOptions<R> = Pick<
QueryObserverOptions<R>,
Expand All @@ -23,10 +24,10 @@ type PassedQueryOptions<R> = Pick<
| 'refetchOnMount'
>;

type QuerySetupConfig<T, R> = {
type QuerySetupConfig<ClientType, T, R> = {
selector: (state: RootState) => T;
getQueryKey: (selectorResult: NoInfer<T>) => any[];
getQueryFn: (client: IndexerClient, selectorResult: NoInfer<T>) => (() => Promise<R>) | null;
getQueryFn: (client: ClientType, selectorResult: NoInfer<T>) => (() => Promise<R>) | null;
onResult: (result: NoInfer<QueryObserverResult<R, Error>>) => void;
onNoQuery: () => void;
} & PassedQueryOptions<R>;
Expand All @@ -38,36 +39,41 @@ const baseOptions: PassedQueryOptions<any> = {

export function createIndexerQueryStoreEffect<T, R>(
store: RootStore,
config: QuerySetupConfig<T, R>
config: QuerySetupConfig<IndexerClient, T, R>
) {
const fullSelector = createAppSelector(
[selectWebsocketUrl, selectIndexerUrl, config.selector],
(wsUrl, indexerUrl, selectorResult) => ({
infrastructure: { wsUrl, indexerUrl },
[getSelectedNetwork, selectIndexerReady, config.selector],
(network, indexerReady, selectorResult) => ({
infrastructure: { network, indexerReady },
queryData: selectorResult,
})
);

return createStoreEffect(store, fullSelector, (fullResult) => {
const { infrastructure, queryData } = fullResult;

const indexerClientConfig = {
url: infrastructure.indexerUrl,
wsUrl: infrastructure.wsUrl,
if (!infrastructure.indexerReady) {
config.onNoQuery();
return undefined;
}

const clientConfig = {
network: infrastructure.network,
store,
};
const indexerClient = IndexerClientManager.use(indexerClientConfig);
const indexerClient = CompositeClientManager.use(clientConfig).indexer!;

const queryFn = config.getQueryFn(indexerClient, queryData);
if (!queryFn) {
IndexerClientManager.markDone(indexerClientConfig);
CompositeClientManager.markDone(clientConfig);
config.onNoQuery();
return undefined;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { selector, getQueryKey, getQueryFn, onResult, ...otherOpts } = config;
const observer = new QueryObserver(appQueryClient, {
queryKey: ['indexer', ...config.getQueryKey(queryData), indexerClientConfig],
queryKey: ['indexer', ...config.getQueryKey(queryData), clientConfig.network],
queryFn,
...baseOptions,
...otherOpts,
Expand All @@ -88,7 +94,68 @@ export function createIndexerQueryStoreEffect<T, R>(

return () => {
unsubscribe();
IndexerClientManager.markDone(indexerClientConfig);
CompositeClientManager.markDone(clientConfig);
};
});
}

export function createValidatorQueryStoreEffect<T, R>(
store: RootStore,
config: QuerySetupConfig<CompositeClient, T, R>
) {
const fullSelector = createAppSelector(
[getSelectedNetwork, selectCompositeClientReady, config.selector],
(network, compositeClientReady, selectorResult) => ({
infrastructure: { network, compositeClientReady },
queryData: selectorResult,
})
);

return createStoreEffect(store, fullSelector, (fullResult) => {
const { infrastructure, queryData } = fullResult;

if (!infrastructure.compositeClientReady) {
config.onNoQuery();
return undefined;
}
const clientConfig = {
network: infrastructure.network,
store,
};
const compositeClient = CompositeClientManager.use(clientConfig).compositeClient!;

const queryFn = config.getQueryFn(compositeClient, queryData);
if (!queryFn) {
CompositeClientManager.markDone(clientConfig);
config.onNoQuery();
return undefined;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { selector, getQueryKey, getQueryFn, onResult, ...otherOpts } = config;
const observer = new QueryObserver(appQueryClient, {
queryKey: ['validator', ...config.getQueryKey(queryData), clientConfig.network],
queryFn,
...baseOptions,
...otherOpts,
});

const unsubscribe = observer.subscribe((result) => {
try {
config.onResult(result);
} catch (e) {
logAbacusTsError(
'ValidatorQueryStoreEffect',
'Error handling result from react query store effect',
e,
result
);
}
});

return () => {
unsubscribe();
CompositeClientManager.markDone(clientConfig);
};
});
}
10 changes: 10 additions & 0 deletions src/abacus-ts/rest/lib/queryResultToLoadable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Loadable } from '@/abacus-ts/lib/loadable';
import { QueryObserverResult } from '@tanstack/react-query';

export function queryResultToLoadable<T>(arg: QueryObserverResult<T>): Loadable<T> {
return {
status: arg.status,
data: arg.data,
error: arg.error,
} as Loadable<T>;
}
Loading

0 comments on commit 2bbe7af

Please sign in to comment.