@renegade-fi/core
Version:
VanillaJS library for Renegade
253 lines (235 loc) • 9.26 kB
text/typescript
import invariant from "tiny-invariant";
import { type Address, createPublicClient, type Hex, http, type PublicClient } from "viem";
import { arbitrumSepolia } from "viem/chains";
import { persist, subscribeWithSelector } from "zustand/middleware";
import { createStore, type Mutate, type StoreApi } from "zustand/vanilla";
import type { ChainId } from "./constants.js";
import type { ExternalConfig } from "./createExternalKeyConfig.js";
import { createStorage, noopStorage, type Storage } from "./createStorage.js";
import { childLogger, createLogger } from "./logging/logger.js";
import type { Logger, LoggingOptions } from "./logging/types.js";
import type { Evaluate, ExactPartial } from "./types/utils.js";
import { AuthType } from "./utils/websocket.js";
import type * as rustUtils from "./utils.d.ts";
export type CreateConfigParameters = {
chainId: ChainId;
darkPoolAddress: Address;
priceReporterUrl: string;
relayerUrl: string;
httpPort?: number;
pollingInterval?: number;
ssr?: boolean | undefined;
storage?: Storage | null | undefined;
useInsecureTransport?: boolean;
utils?: typeof rustUtils;
websocketPort?: number;
viemClient?: PublicClient;
adminKey?: string;
/** Optional logging configuration (silent by default). */
logging?: LoggingOptions;
};
export function createConfig(parameters: CreateConfigParameters): InternalConfig {
const {
relayerUrl,
priceReporterUrl,
httpPort = 3000,
pollingInterval = 5000,
ssr,
storage = createStorage({
storage:
typeof window !== "undefined" && window.localStorage
? window.localStorage
: noopStorage,
}),
useInsecureTransport = false,
viemClient = createPublicClient({
chain: arbitrumSepolia,
transport: http(),
}),
websocketPort = 4000,
adminKey,
} = parameters;
invariant(
parameters.utils,
"Utils must be provided by the package if not supplied by the user.",
);
/////////////////////////////////////////////////////////////////////////////////////////////////
// Create store
/////////////////////////////////////////////////////////////////////////////////////////////////
function getInitialState(): State {
return {
seed: undefined,
chainId: undefined,
status: "disconnected",
id: undefined,
};
}
const store = createStore(
subscribeWithSelector(
// only use persist middleware if storage exists
storage
? persist(getInitialState, {
name: "store",
partialize(state) {
// Only persist "critical" store properties to preserve storage size.
return {
id: state.id,
seed: state.seed,
chainId: state.chainId,
status: state.status,
} satisfies PartializedState;
},
skipHydration: ssr,
storage: storage as Storage<Record<string, unknown>>,
})
: getInitialState,
),
);
// Initialize logger (silent by default when not provided by consumer)
const baseLogger = createLogger(parameters.logging);
return {
utils: parameters.utils,
renegadeKeyType: "internal" as const,
storage,
relayerUrl,
priceReporterUrl,
darkPoolAddress: parameters.darkPoolAddress,
getLogger(namespace?: string): Logger {
return namespace ? childLogger(baseLogger, { ns: namespace }) : baseLogger;
},
getBaseUrl: (route = "") => {
const protocol =
useInsecureTransport || parameters.relayerUrl.includes("localhost")
? "http"
: "https";
const baseUrl = parameters.relayerUrl.includes("localhost")
? `127.0.0.1:${httpPort}/v0`
: `${parameters.relayerUrl}:${httpPort}/v0`;
const formattedRoute = route.startsWith("/") ? route : `/${route}`;
return `${protocol}://${baseUrl}${formattedRoute}`;
},
getPriceReporterBaseUrl: () => {
const baseUrl = parameters.priceReporterUrl.includes("localhost")
? `ws://127.0.0.1:${websocketPort}/`
: `wss://${parameters.priceReporterUrl}:${websocketPort}/`;
return baseUrl;
},
getPriceReporterHTTPBaseUrl: (route = "") => {
const baseUrl = parameters.priceReporterUrl.includes("localhost")
? `http://127.0.0.1:${httpPort}`
: `https://${parameters.priceReporterUrl}:${httpPort}`;
const formattedRoute = route.startsWith("/") ? route : `/${route}`;
return `${baseUrl}${formattedRoute}`;
},
getWebsocketBaseUrl: () => {
const protocol =
useInsecureTransport || parameters.relayerUrl.includes("localhost") ? "ws" : "wss";
const baseUrl = parameters.relayerUrl.includes("localhost")
? `127.0.0.1:${websocketPort}`
: `${parameters.relayerUrl}:${websocketPort}`;
return `${protocol}://${baseUrl}`;
},
getSymmetricKey(type?: AuthType) {
invariant(parameters.utils, "Utils are required");
if (type === AuthType.Admin) {
invariant(parameters.adminKey, "Admin key is required");
const symmetricKey = parameters.utils.b64_to_hex_hmac_key(
parameters.adminKey,
) as Hex;
invariant(symmetricKey, "Admin key is required");
return symmetricKey;
}
const seed = store.getState().seed;
invariant(seed, "Seed is required");
return parameters.utils.get_symmetric_key(seed) as Hex;
},
pollingInterval,
get state() {
return store.getState();
},
setState(value) {
let newState: State;
if (typeof value === "function") newState = value(store.getState() as any);
else newState = value;
// Reset state if it got set to something not matching the base state
const initialState = getInitialState();
if (typeof newState !== "object") newState = initialState;
const isCorrupt = Object.keys(initialState).some((x) => !(x in newState));
if (isCorrupt) newState = initialState;
store.setState(newState, true);
},
subscribe(selector, listener, options) {
return store.subscribe(
selector as unknown as (state: State) => any,
listener,
options ? { ...options, fireImmediately: options.emitImmediately } : undefined,
);
},
viemClient,
adminKey,
_internal: {
store,
ssr: Boolean(ssr),
},
chainId: parameters.chainId,
};
}
export type BaseConfig = {
utils: typeof rustUtils;
getWebsocketBaseUrl: () => string;
getBaseUrl: (route?: string) => string;
getSymmetricKey: (type?: AuthType) => Hex;
/** Retrieve a logger, optionally namespaced. */
getLogger: (namespace?: string) => Logger;
};
export type Config = BaseConfig & {
chainId: ChainId;
renegadeKeyType: "internal";
readonly storage: Storage | null;
darkPoolAddress: Address;
getPriceReporterBaseUrl: () => string;
getPriceReporterHTTPBaseUrl: (route?: string) => string;
pollingInterval: number;
priceReporterUrl: string;
relayerUrl: string;
setState(value: State | ((state: State) => State)): void;
state: State;
subscribe<state>(
selector: (state: State) => state,
listener: (state: state, previousState: state) => void,
options?:
| {
emitImmediately?: boolean | undefined;
equalityFn?: ((a: state, b: state) => boolean) | undefined;
}
| undefined,
): () => void;
viemClient: PublicClient;
adminKey?: string;
/**
* Not part of versioned API, proceed with caution.
* @internal
*/
_internal: {
readonly store: Mutate<StoreApi<any>, [["zustand/persist", any]]>;
readonly ssr: boolean;
};
};
// For backwards-compatibility
export type InternalConfig = Config;
export type RenegadeConfig = InternalConfig | ExternalConfig;
export interface State {
seed?: Hex | undefined;
chainId?: number;
status?: "in relayer" | "disconnected" | "looking up" | "creating wallet" | "connecting";
id?: string | undefined;
}
// The type of keychain a config is using
export const keyTypes = {
EXTERNAL: "external",
INTERNAL: "internal",
NONE: "none",
} as const;
export type PartializedState = Evaluate<
ExactPartial<Pick<State, "id" | "seed" | "chainId" | "status">>
>;