@sovryn-zero/lib-ethers
Version:
Sovryn Zero SDK Ethers-based implementation
327 lines (273 loc) • 10.3 kB
text/typescript
import { BigNumber } from "@ethersproject/bignumber";
import { Block, BlockTag } from "@ethersproject/abstract-provider";
import { Signer } from "@ethersproject/abstract-signer";
import devOrNull from "../deployments/dev.json";
import rsktestnet from "../deployments/default/rsktestnet.json";
import rskdev from "../deployments/rskdev.json";
import rskMainnet from "../deployments/default/rsksovrynmainnet.json";
import { EthersProvider, EthersSigner } from "./types";
import {
_connectToContracts,
_LiquityContractAddresses,
_LiquityContracts,
_LiquityDeploymentJSON
} from "./contracts";
import { _connectToMulticall, _Multicall } from "./_Multicall";
const dev = devOrNull as _LiquityDeploymentJSON | null;
const deployments = {
[rsktestnet.chainId]: rsktestnet,
[rskMainnet.chainId]: rskMainnet,
...(rskdev ? { [rskdev.chainId]: rskdev } : {}),
...(dev !== null ? { [dev.chainId]: dev } : {})
};
declare const brand: unique symbol;
const branded = <T>(t: Omit<T, typeof brand>): T => t as T;
/**
* Information about a connection to the Zero protocol.
*
* @remarks
* Provided for debugging / informational purposes.
*
* Exposed through {@link ReadableEthersLiquity.connection} and {@link EthersLiquity.connection}.
*
* @public
*/
export interface EthersLiquityConnection extends EthersLiquityConnectionOptionalParams {
/** Ethers `Provider` used for connecting to the network. */
readonly provider: EthersProvider;
/** Ethers `Signer` used for sending transactions. */
readonly signer?: EthersSigner;
/** Chain ID of the connected network. */
readonly chainId: number;
/** Version of the Zero contracts (Git commit hash). */
readonly version: string;
/** Date when the Zero contracts were deployed. */
readonly deploymentDate: Date;
/** Number of block in which the first Zero contract was deployed. */
readonly startBlock: number;
/** Time period (in seconds) after `deploymentDate` during which redemptions are disabled. */
readonly bootstrapPeriod: number;
/** A mapping of Zero contracts' names to their addresses. */
readonly addresses: Record<string, string>;
/** @internal */
readonly _priceFeedIsTestnet: boolean;
/** @internal */
readonly _isDev: boolean;
/** @internal */
readonly [brand]: unique symbol;
}
/** @internal */
export interface _InternalEthersLiquityConnection extends EthersLiquityConnection {
readonly addresses: _LiquityContractAddresses;
readonly _contracts: _LiquityContracts;
readonly _multicall?: _Multicall;
}
const connectionFrom = (
provider: EthersProvider,
signer: EthersSigner | undefined,
_contracts: _LiquityContracts,
_multicall: _Multicall | undefined,
{ deploymentDate, ...deployment }: _LiquityDeploymentJSON,
optionalParams?: EthersLiquityConnectionOptionalParams
): _InternalEthersLiquityConnection => {
if (
optionalParams &&
optionalParams.useStore !== undefined &&
!validStoreOptions.includes(optionalParams.useStore)
) {
throw new Error(`Invalid useStore value ${optionalParams.useStore}`);
}
return branded({
provider,
signer,
_contracts,
_multicall,
deploymentDate: new Date(deploymentDate),
...deployment,
...optionalParams
});
};
/** @internal */
export const _getContracts = (connection: EthersLiquityConnection): _LiquityContracts =>
(connection as _InternalEthersLiquityConnection)._contracts;
const getMulticall = (connection: EthersLiquityConnection): _Multicall | undefined =>
(connection as _InternalEthersLiquityConnection)._multicall;
const numberify = (bigNumber: BigNumber) => bigNumber.toNumber();
const getTimestampFromBlock = ({ timestamp }: Block) => timestamp;
/** @internal */
export const _getBlockTimestamp = (
connection: EthersLiquityConnection,
blockTag: BlockTag = "latest"
): Promise<number> =>
// Get the timestamp via a contract call whenever possible, to make it batchable with other calls
getMulticall(connection)?.getCurrentBlockTimestamp({ blockTag }).then(numberify) ??
_getProvider(connection).getBlock(blockTag).then(getTimestampFromBlock);
const panic = <T>(e: unknown): T => {
throw e;
};
/** @internal */
export const _requireSigner = (connection: EthersLiquityConnection): EthersSigner =>
connection.signer ?? panic(new Error("Must be connected through a Signer"));
/** @internal */
export const _getProvider = (connection: EthersLiquityConnection): EthersProvider =>
connection.provider;
// TODO parameterize error message?
/** @internal */
export const _requireAddress = (
connection: EthersLiquityConnection,
overrides?: { from?: string }
): string =>
overrides?.from ?? connection.userAddress ?? panic(new Error("A user address is required"));
/** @internal */
export const _requireFrontendAddress = (connection: EthersLiquityConnection): string =>
connection.frontendTag ?? panic(new Error("A frontend address is required"));
/** @internal */
export const _usingStore = (
connection: EthersLiquityConnection
): connection is EthersLiquityConnection & { useStore: EthersLiquityStoreOption } =>
connection.useStore !== undefined;
/**
* Thrown when trying to connect to a network where Zero is not deployed.
*
* @remarks
* Thrown by {@link ReadableEthersLiquity.(connect:2)} and {@link EthersLiquity.(connect:2)}.
*
* @public
*/
export class UnsupportedNetworkError extends Error {
/** Chain ID of the unsupported network. */
readonly chainId: number;
/** @internal */
constructor(chainId: number) {
super(`Unsupported network (chainId = ${chainId})`);
this.name = "UnsupportedNetworkError";
this.chainId = chainId;
}
}
const getProviderAndSigner = (
signerOrProvider: EthersSigner | EthersProvider
): [provider: EthersProvider, signer: EthersSigner | undefined] => {
const provider: EthersProvider = Signer.isSigner(signerOrProvider)
? signerOrProvider.provider ?? panic(new Error("Signer must have a Provider"))
: signerOrProvider;
const signer = Signer.isSigner(signerOrProvider) ? signerOrProvider : undefined;
return [provider, signer];
};
/** @internal */
export const _connectToDeployment = (
deployment: _LiquityDeploymentJSON,
signerOrProvider: EthersSigner | EthersProvider,
optionalParams?: EthersLiquityConnectionOptionalParams
): EthersLiquityConnection =>
connectionFrom(
...getProviderAndSigner(signerOrProvider),
_connectToContracts(signerOrProvider, deployment),
undefined,
deployment,
optionalParams
);
/**
* Possible values for the optional
* {@link EthersLiquityConnectionOptionalParams.useStore | useStore}
* connection parameter.
*
* @remarks
* Currently, the only supported value is `"blockPolled"`, in which case a
* {@link BlockPolledLiquityStore} will be created.
*
* @public
*/
export type EthersLiquityStoreOption = "blockPolled";
const validStoreOptions = ["blockPolled"];
/**
* Optional parameters of {@link ReadableEthersLiquity.(connect:2)} and
* {@link EthersLiquity.(connect:2)}.
*
* @public
*/
export interface EthersLiquityConnectionOptionalParams {
/**
* Address whose Trove, Stability Deposit, ZERO Stake and balances will be read by default.
*
* @remarks
* For example {@link EthersLiquity.getTrove | getTrove(address?)} will return the Trove owned by
* `userAddress` when the `address` parameter is omitted.
*
* Should be omitted when connecting through a {@link EthersSigner | Signer}. Instead `userAddress`
* will be automatically determined from the `Signer`.
*/
readonly userAddress?: string;
/**
* Address that will receive ZERO rewards from newly created Stability Deposits by default.
*
* @remarks
* For example
* {@link EthersLiquity.depositZUSDInStabilityPool | depositZUSDInStabilityPool(amount, frontendTag?)}
* will tag newly made Stability Deposits with this address when its `frontendTag` parameter is
* omitted.
*/
readonly frontendTag?: string;
/**
* Create a {@link @sovryn-zero/lib-base#LiquityStore} and expose it as the `store` property.
*
* @remarks
* When set to one of the available {@link EthersLiquityStoreOption | options},
* {@link ReadableEthersLiquity.(connect:2) | ReadableEthersLiquity.connect()} will return a
* {@link ReadableEthersLiquityWithStore}, while
* {@link EthersLiquity.(connect:2) | EthersLiquity.connect()} will return an
* {@link EthersLiquityWithStore}.
*
* Note that the store won't start monitoring the blockchain until its
* {@link @sovryn-zero/lib-base#LiquityStore.start | start()} function is called.
*/
readonly useStore?: EthersLiquityStoreOption;
}
/** @internal */
export function _connectByChainId<T>(
provider: EthersProvider,
signer: EthersSigner | undefined,
chainId: number,
optionalParams: EthersLiquityConnectionOptionalParams & { useStore: T }
): EthersLiquityConnection & { useStore: T };
/** @internal */
export function _connectByChainId(
provider: EthersProvider,
signer: EthersSigner | undefined,
chainId: number,
optionalParams?: EthersLiquityConnectionOptionalParams
): EthersLiquityConnection;
/** @internal */
export function _connectByChainId(
provider: EthersProvider,
signer: EthersSigner | undefined,
chainId: number,
optionalParams?: EthersLiquityConnectionOptionalParams
): EthersLiquityConnection {
const deployment: _LiquityDeploymentJSON =
(deployments[chainId] as _LiquityDeploymentJSON) ?? panic(new UnsupportedNetworkError(chainId));
return connectionFrom(
provider,
signer,
_connectToContracts(signer ?? provider, deployment),
_connectToMulticall(signer ?? provider, chainId),
deployment,
optionalParams
);
}
/** @internal */
export const _connect = async (
signerOrProvider: EthersSigner | EthersProvider,
optionalParams?: EthersLiquityConnectionOptionalParams
): Promise<EthersLiquityConnection> => {
const [provider, signer] = getProviderAndSigner(signerOrProvider);
if (signer) {
if (optionalParams?.userAddress !== undefined) {
throw new Error("Can't override userAddress when connecting through Signer");
}
optionalParams = {
...optionalParams,
userAddress: await signer.getAddress()
};
}
return _connectByChainId(provider, signer, (await provider.getNetwork()).chainId, optionalParams);
};