Skip to content

Commit

Permalink
Remove FetchProvider, recommend new module
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Jun 18, 2024
1 parent 4831b1a commit 3907f64
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 221 deletions.
66 changes: 39 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ If you don't like NPM, a standalone [eth-signer.js](https://github.com/paulmillr
- [Transactions: create, sign](#create-and-sign-transactions)
- [Addresses: create, checksum](#create-and-checksum-addresses)
- [Network and smart contracts](#network-and-smart-contracts)
- [Init network](#init-network)
- [Fetch balances and history from an archive node](#fetch-balances-and-history-from-an-archive-node)
- [Fetch Chainlink oracle prices](#fetch-chainlink-oracle-prices)
- [Resolve ENS address](#resolve-ens-address)
Expand Down Expand Up @@ -92,27 +93,47 @@ console.log(

### Network and smart contracts

We don't have _actual network code_ in the package:
all network code is deferred to FetchProvider, which must be initialized with a
method, conforming to built-in `fetch`. This means, you are able to completely control
all network requests.
A common problem in web3 libraries is how complex they are to audit with regards to network calls.

In eth-signer, all network calls are done with user-provided function, conforming to built-in `fetch()`:

1. This makes library network-free, which simplifies auditability
2. User fully controls all network requests

It's recommended to use [micro-ftch](https://github.com/paulmillr/micro-ftch),
which works on top of fetch and implements killswitch, logging, concurrency limits and other features.

#### Init network

Most APIs (chainlink, uniswap) expect instance of ArchiveNodeProvider.
The call stack would look like this:

- `Chainlink` => `ArchiveNodeProvider` => `jsonrpc` => `fetch`

To initialize ArchiveNodeProvider, do the following:

```js
// Requests are made with fetch(), a built-in method
import { jsonrpc } from 'micro-ftch';
import { ArchiveNodeProvider } from 'micro-eth-signer/net';
const RPC_URL = 'http://localhost:8545';
const prov = new ArchiveNodeProvider(jsonrpc(fetch, RPC_URL));

// Example using mewapi RPC
const RPC_URL_2 = 'https://nodes.mewapi.io/rpc/eth';
const prov2 = new ArchiveNodeProvider(
jsonrpc(fetch, RPC_URL_2, { Origin: 'https://www.myetherwallet.com' })
);
```

#### Fetch balances and history from an archive node

```ts
import { ArchiveNodeProvider, FetchProvider } from 'micro-eth-signer/net';
const RPC_URL = 'http://localhost:8545'; // URL to any RPC
const rpc = new FetchProvider(globalThis.fetch, RPC_URL); // use built-in fetch()
const prov = new ArchiveNodeProvider(rpc);
const addr = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';

const block = await prov.blockInfo(await prov.height());
console.log('current block', block.number, block.timestamp, block.baseFeePerGas);
console.log('info for addr', addr, await prov.unspent(addr));

// ENS example:
// import { ENS } from 'micro-eth-signer/net'; const ens = new ENS(rpc), addr = await ens.nameToAddress('vitalik.eth');

// Other methods of ArchiveNodeProvider:
// blockInfo(block: number): Promise<BlockInfo>; // {baseFeePerGas, hash, timestamp...}
// height(): Promise<number>;
Expand All @@ -137,11 +158,8 @@ Historical balances, transactions and others can only be fetched from an archive
#### Fetch Chainlink oracle prices

```ts
import { FetchProvider, Chainlink } from 'micro-eth-signer/net';
const provider = new FetchProvider(thisGlobal.fetch, 'https://nodes.mewapi.io/rpc/eth', {
Origin: 'https://www.myetherwallet.com',
});
const link = new Chainlink(provider);
import { Chainlink } from 'micro-eth-signer/net';
const link = new Chainlink(prov);
const btc = await link.coinPrice('BTC');
const bat = await link.tokenPrice('BAT');
console.log({ btc, bat }); // BTC 19188.68870991, BAT 0.39728989 in USD
Expand All @@ -150,10 +168,8 @@ console.log({ btc, bat }); // BTC 19188.68870991, BAT 0.39728989 in USD
#### Resolve ENS address

```ts
import { FetchProvider, ENS } from 'micro-eth-signer/net';
const RPC_URL = 'http://127.0.0.1:8545';
const provider = new FetchProvider(thisGlobal.fetch, RPC_URL);
const ens = new ENS(provider);
import { ENS } from 'micro-eth-signer/net';
const ens = new ENS(prov);
const vitalikAddr = await ens.nameToAddress('vitalik.eth');
```

Expand All @@ -167,15 +183,11 @@ Swap 12.12 USDT to BAT with uniswap V3 defaults of 0.5% slippage, 30 min expirat

```ts
import { tokenFromSymbol } from 'micro-eth-signer/abi';
import { FetchProvider, UniswapV3 } from 'micro-eth-signer/net'; // or UniswapV2
import { UniswapV3 } from 'micro-eth-signer/net'; // or UniswapV2

const RPC_URL = 'https://nodes.mewapi.io/rpc/eth';
const provider = new FetchProvider(thisGlobal.fetch, RPC_URL, {
Origin: 'https://www.myetherwallet.com',
});
const USDT = tokenFromSymbol('USDT');
const BAT = tokenFromSymbol('BAT');
const u3 = new UniswapV3(provider); // or new UniswapV2(provider)
const u3 = new UniswapV3(prov); // or new UniswapV2(provider)
const fromAddress = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';
const toAddress = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';
const swap = await u3.swap(USDT, BAT, '12.12', { slippagePercent: 0.5, ttl: 30 * 60 });
Expand Down
50 changes: 28 additions & 22 deletions src/net/archive.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Web3Provider, amounts } from '../utils.js';
import { Web3Provider, Web3CallArgs, hexToNumber, amounts } from '../utils.js';
import { Transaction } from '../index.js';
import { TxVersions, legacySig } from '../tx.js';
import { ContractInfo, createContract, events, ERC20, WETH } from '../abi/index.js';
Expand Down Expand Up @@ -148,7 +148,7 @@ export type Unspent = {
symbol: 'ETH';
decimals: number;
balance: bigint;
value?: number;
nonce: number;
// useful for wallets to know if there was transactions related to wallet
// NOTE: even if nonce is zero, there can be transfers to wallet
// can be used to check before fetching all transactions
Expand Down Expand Up @@ -328,32 +328,41 @@ function validateLogOpts(opts: Record<string, unknown>) {
validateCallbacks(opts);
}

export type JsonrpcInterface = {
call: (method: string, ...args: any[]) => Promise<any>;
};

/**
* Transaction-related code around Web3Provider.
* High-level methods are `height`, `unspent`, `transfers`, `allowances` and `tokenBalances`.
*
* Low-level methods are `blockInfo`, `internalTransactions`, `ethLogs`, `tokenTransfers`, `wethTransfers`,
* `tokenInfo` and `txInfo`.
*/
export class ArchiveNodeProvider {
constructor(private provider: Web3Provider) {}
export class ArchiveNodeProvider implements Web3Provider {
constructor(private rpc: JsonrpcInterface) {}

// The low-level place where network calls are done
private rpc(method: string, ...args: any[]) {
return this.provider.call(method, ...args);
call(method: string, ...args: any[]) {
return this.rpc.call(method, ...args);
}
ethCall(args: Web3CallArgs, tag = 'latest') {
return this.rpc.call('eth_call', args, tag);
}
async estimateGas(args: Web3CallArgs, tag = 'latest') {
return hexToNumber(await this.rpc.call('eth_estimateGas', args, tag));
}

// Timestamp is available only inside blocks
async blockInfo(block: number): Promise<BlockInfo> {
const res = await this.rpc('eth_getBlockByNumber', ethNum(block), false);
const res = await this.call('eth_getBlockByNumber', ethNum(block), false);
fixBlock(res);
return res;
}

async unspent(address: string) {
async unspent(address: string): Promise<Unspent> {
let [balance, nonce] = await Promise.all([
this.rpc('eth_getBalance', address, 'latest'),
this.rpc('eth_getTransactionCount', address, 'latest'),
this.call('eth_getBalance', address, 'latest'),
this.call('eth_getTransactionCount', address, 'latest'),
]);
balance = BigInt(balance);
nonce = BigInt(nonce);
Expand All @@ -367,14 +376,11 @@ export class ArchiveNodeProvider {
};
}
async height(): Promise<number> {
return Number.parseInt(await this.rpc('eth_blockNumber'));
}
async maxPriorityFeePerGas(): Promise<BigInt> {
return BigInt(await this.rpc('eth_maxPriorityFeePerGas'));
return Number.parseInt(await this.call('eth_blockNumber'));
}

async traceFilterSingle(address: string, opts: TraceOpts = {}) {
const res = await this.rpc('trace_filter', {
const res = await this.call('trace_filter', {
fromBlock: ethNum(opts.fromBlock),
toBlock: ethNum(opts.toBlock),
toAddress: [address],
Expand Down Expand Up @@ -411,7 +417,7 @@ export class ArchiveNodeProvider {
if (opts.toBlock !== undefined) params.toBlock = ethNum(opts.toBlock);
if (opts.perRequest !== undefined) params.count = opts.perRequest;

const res = await this.rpc('trace_filter', params);
const res = await this.call('trace_filter', params);
if (!res.length) break;
for (const action of res) {
fixAction(action, opts);
Expand All @@ -427,7 +433,7 @@ export class ArchiveNodeProvider {
async ethLogsSingle(topics: Topics, opts: LogOpts): Promise<Log[]> {
const req: Record<string, any> = { topics, fromBlock: ethNum(opts.fromBlock || 0) };
if (opts.toBlock !== undefined) req.toBlock = ethNum(opts.toBlock);
const res = await this.rpc('eth_getLogs', req);
const res = await this.call('eth_getLogs', req);
return res.map((i: any) => fixLog(i, opts));
}

Expand Down Expand Up @@ -466,8 +472,8 @@ export class ArchiveNodeProvider {

async txInfo(txHash: string, opts: TxInfoOpts = {}) {
let [info, receipt] = await Promise.all([
this.rpc('eth_getTransactionByHash', txHash),
this.rpc('eth_getTransactionReceipt', txHash),
this.call('eth_getTransactionByHash', txHash),
this.call('eth_getTransactionReceipt', txHash),
]);
info = fixTxInfo(info);
receipt = fixTxReceipt(receipt);
Expand Down Expand Up @@ -513,7 +519,7 @@ export class ArchiveNodeProvider {
async tokenInfo(address: string): Promise<TokenInfo | undefined> {
// will throw 'Execution reverted' if not ERC20
try {
let c = createContract(ERC20, this.provider, address);
let c = createContract(ERC20, this, address);
const [symbol, decimals] = await Promise.all([c.symbol.call(), c.decimals.call()]);
return { contract: address, abi: 'ERC20', symbol, decimals: Number(decimals) };
} catch (e) {
Expand Down Expand Up @@ -648,7 +654,7 @@ export class ArchiveNodeProvider {

async tokenBalances(address: string, tokens: string[]): Promise<Record<string, bigint>> {
const balances = await Promise.all(
tokens.map((i) => createContract(ERC20, this.provider, i).balanceOf.call(address))
tokens.map((i) => createContract(ERC20, this, i).balanceOf.call(address))
);
return Object.fromEntries(tokens.map((i, j) => [i, balances[j]]));
}
Expand Down
11 changes: 1 addition & 10 deletions src/net/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import Chainlink from './chainlink.js';
import ENS from './ens.js';
import FetchProvider from './provider.js';
import { ArchiveNodeProvider, calcTransfersDiff } from './archive.js';
import UniswapV2 from './uniswap-v2.js';
import UniswapV3 from './uniswap-v3.js';

// There are many low level APIs inside which are not exported yet.
export {
ArchiveNodeProvider,
calcTransfersDiff,
Chainlink,
ENS,
FetchProvider,
UniswapV2,
UniswapV3,
};
export { ArchiveNodeProvider, calcTransfersDiff, Chainlink, ENS, UniswapV2, UniswapV3 };
92 changes: 0 additions & 92 deletions src/net/provider.ts

This file was deleted.

Loading

0 comments on commit 3907f64

Please sign in to comment.