@derivation-tech/web3-core
v1.0.42
Published
Common core utilities to interact with the web3 world
Downloads
1,364
Readme
Web3-Core
Introduction
Web3-Core offers essential functionalities to facilitate user interactions with EVM-compatible networks. It locally configures assets and chain information, encapsulating transactions throughout their entire lifecycle. The output is presented in a user-friendly format, encompassing parsed function calls, identified addresses, transaction data, emitted events, errors, and more. Interaction with the web3 world begins here!
Installation
yarn add @derivation-tech/web3-core
Getting started with a ChainContext instance
Initialization
Simplify initialization using the network name or ID in the following TypeScript code:
// Initialization by chain ID
const ctx = ChainContext.getInstance(5);
// Initialization by chain name
const ctx = ChainContext.getInstance('goerli');
Web3-Core streamlines the initialization of JsonRpc/WebSocket provider instances by retrieving endpoint information from environment variables: {CHAIN_NAME_UPPERCASE}_RPC and {CHAIN_NAME_UPPERCASE}_WSS, respectively. For instance, if you are working on Goerli network, ensure you configure GOERLI_RPC and, optionally, GOERLI_WSS in your environment variables (you can also specify them in a .env file).
GOERLI_RPC=https://goerli.infura.io/v3/77e667fxxx95476299975383d80a7ce3
# optional
GOERLI_WSS=wss://goerli.infura.io/ws/v3/77e667fxxx5476299975383d80a7ce3
if the rpc endpoint need authentication, you can also set {CHAIN_NAME_UPPERCASE}_RPC_AUTH in your environment variables, for example, GOERLI_RPC_AUTH=xxx:yyy
GOERLI_RPC_AUTH=userName:password
# optional
GOERLI_WSS_AUTH=userName:password
ChainContext is a singleton
The ChainContext is designed following the singleton pattern, enabling you to access it seamlessly throughout your project by invoking ChainContext.getInstance(). Subsequent calls with the same network parameter will consistently yield the same instance.
const ctx1 = ChainContext.getInstance('goerli');
const ctx2 = ChainContext.getInstance('goerli');
const ctx3 = ChainContext.getInstance('goerli');
console.log(ctx1 === ctx2); // true
console.log(ctx2 === ctx3); // true
Prepare Local Network
For local network, you should call prepareLocalNetwork
before getting instance.
await prepareLocalNetwork();
const ctx = ChainContext.getInstance('local');
This is because wrappedNativeToken
, tokenAssistant
and multicall3
and some popular ERC20 tokens are essential contracts for ChainContext
to work.
These essential contracts will be deployed automatically during the prepareLocalNetwork
process.
Again, once you have called await prepareLocalNetwork()
, you can do ChainContext.getInstance('local')
anywhere, which returns the same instance.
Customize Call Options
Also, Web3-Core provides a powerful way to customize your transaction request, you can specify the following options to meet your needs.
export interface CallOption {
// tx.wait or not
waitReceipt?: boolean;
// wait timeout in seconds
waitTimeout?: number;
// gas limit scaler, default: 1.5
gasLimitScaler?: number;
// estimate gas or not
estimateGas?: boolean;
// gas price scaler, default: 1.2
// for legacy tx, gasPrice = gasPrice * gasPriceScaler
// for EIP-1559 tx, maxFeePerGas = maxFeePerGas * gasPriceScaler
gasPriceScaler?: number;
// gas estimator is used for estimating gas price
gasEstimator?: EthGasEstimator;
}
The default call option is:
export const DEFAULT_CALL_OPTION: CallOption = {
waitReceipt: true,
waitTimeout: 3 * 60,
estimateGas: true,
gasLimitScaler: 1.5,
gasPriceScaler: 1.2,
};
By the way, If you want to customize your own gas estimator, simply implement the EthGasEstimator interface and pass the instance through callOption.
Obtaining Signers
Web3-Core provides three straightforward methods for obtaining signers: one involves configured private keys, another entails using a mnemonic, and the last option is to acquire a signer from a Ledger wallet.
- Get Signers by Private Keys
To obtain signers using private keys, set {CUSTOM_IDENTIFIER}_PRIVATE_KEY in your environment variables (excluding the '0x' prefix) and then call ctx.getSigner({CUSTOM_IDENTIFIER}). For example:
ALICE_PRIVATE_KEY=abababababab...
const ctx = ChainContext.getInstance('goerli');
const signer = await ctx.getSigner('alice'); // not case-sensitive. 'ALICE' do the same thing.
- Get Signers by Mnemonic
To obtain signers using a mnemonic, set {CUSTOM_IDENTIFIER}_MNEMONIC in your environment variables. For example,
BOB_MNEMONIC=how are you fine thank you and you
const ctx = ChainContext.getInstance('goerli');
// get first 3 wallets derived by BOB_MNEMONIC with DEFAULT HD_PATH
const signer0 = await ctx.getSigner('BOB:0');
const signer1 = await ctx.getSigner('BOB:1');
const signer2 = await ctx.getSigner('BOB:2');
// get the forth wallet derived from BOB_MNEMONIC with HD_PATH: `m/44'/60'/0'/0/`
const signer3 = await ctx.getSigner(`bob:m/44'/60'/0'/0/3`);
Besides, you can config HD_PATH in environment variables as follows: {CUSTOM_IDENTIFIER}_{CHAIN_NAME_UPPERCASE}_HD_PATH, eg: DAVID_GOERLI_HD_PATH=m/44'/60'/0'/1
const ctx = ChainContext.getInstance('goerli');
const signer0 = await ctx.getSigner('DAVID:0');
- Get Signers from Ledger wallet
Thus,ledger is a key word, it is specifically used for Ledger wallet.
const ctx = ChainContext.getInstance('goerli');
const ledgerSigner0 = await ctx.getSigner('ledger:1');
const ledgerSigner12 = await ctx.getSigner("ledger:m/44'/60'/0'/0/12")
Basic Chain Info
ChainInfo interface provides comprehensive details about the blockchain, including the chain ID, name, aliases, default transaction type, native and wrapped native token information, explorer URL, token assistant URL, multicall3 URL, and a list of ERC20 tokens.
Definition of the ChainInfo interface
export interface ChainInfo {
chainId: CHAIN_ID;
chainName: string;
chainAlias: string[];
defaultTxType: string;
nativeToken: TokenInfo;
wrappedNativeToken: TokenInfo;
explorer: string;
tokenAssistant: string;
multicall3: string;
erc20: TokenInfo[];
}
Example usage to retrieve chain information for the Goerli network:
const ctx = ChainContext.getInstance('goerli');
const info = ctx.info;
Asset Information
Obtain ERC20 or native token information by symbol or address: If the parameter is a symbol, the search is conducted locally. If it is an address, the local search is performed first. If the information is not found locally, an attempt is made to query on-chain.
const ctx = ChainContext.getInstance('goerli');
// By symbol
await ctx.getTokenInfo('USDC');
// By address
await ctx.getTokenInfo('0xA375A26dbb09F5c57fB54264f393Ad6952d1d2de');
This functionality allows you to seamlessly retrieve information about ERC20 or native tokens using either their symbols or addresses, with a preference for local data when available.
Transactions
- Execute an ERC20 transfer using sendTx
const ctx = ChainContext.getInstance('goerli');
const erc20Info = await ctx.getTokenInfo('USDC');
const signer = await ctx.getSigner('alice');
const contract = ERC20__factory.connect(erc20Info.address, signer);
const target = '0x0E038F13d9D5732223cF9b4b61Eed264ccd44641';
// Set the address and its name if you want to identify it in the transaction
ctx.registerAddress(target, 'Alice');
// Optional: If not set, the default parser will be used, and function and event arguments will be parsed as origin
ctx.registerContractParser(contract.address, new ERC20Parser(ERC20__factory.createInterface(), erc20Info));
const unsignedTx = await contract.populateTransaction.transfer(
target,
ethers.utils.parseUnits('4', erc20Info.decimals)
);
await ctx.sendTx(signer, unsignedTx);
Parsers
- Register parsers
Parsers play a crucial role in parsing function calls, events, and errors. They are associated with addresses, and Web3-Core locates the relevant parser by searching for the address in the parser map. You can register a parser using the following syntax:
ctx.registerContractParser(erc20Address, new ERC20Parser(ERC20__factory.createInterface(), erc20Info));
Additionally, you can register an address for the Web3-core to idenitfy it:
ctx.registerAddress(target, 'Alice');
- Customize Parsers
Parsers are optional; if not set, the default parser will be used, and function and event arguments will be parsed as origin. If you wish to customize your parser, you only need to extend the base class: ContractParser and re-implement function parseBaseParam. Now, let me provide you with an example of how an ERC20 parser is implemented.
export class Erc20Parser extends ContractParser {
tokenInfo: TokenInfo;
constructor(
iface: ethers.utils.Interface,
tokenInfo: TokenInfo,
addressParser?: (address: string) => Promise<string>,
) {
super(iface, addressParser);
this.tokenInfo = tokenInfo;
}
override async parseBaseParam(
description: TransactionDescription | LogDescription | ErrorDescription,
paramType: ethers.utils.ParamType,
value: BigNumber | number,
) {
switch (paramType.type) {
case 'uint256':
// different erc20 handles maxed approval differently:
// some reduce the approval gradually as you spend, while others simply check and maintain the maximum approval during spending
// for better human readability, we will simply show MAX if the approval is greater than MAX_UINT256/2
if (
(description.name.toLowerCase() === 'approve' || description.name.toLowerCase() === 'approval') &&
BigNumber.from(value).gte(MAX_UINT256.div(2))
) {
return 'MAX';
}
return `${formatUnits(value, this.tokenInfo.decimals)} ${this.tokenInfo.symbol}`;
default:
return value.toString();
}
}
}
Supported Networks
- ETHEREUM
- ARBITRUM
- OPTIMISM
- BASE
- MANTLE
- LINEA
- BSC
- CONFLUX
- POLYGON
- POLYGON_ZKEVM
- SCROLL
- ZKSYNC_ERA
- MAPO
- KLAYTN
- GOERLI
- SEPOLIA
- BERA_ARTIO