feat: typescript bindings (#191)

* basic ethers provider setup

* add getCode

* add call

* add estimateGas

* add gas pricing methods

* add sendRawTransaction

* add getTransactionReceipt

* add getLogs

* add net_version

* decouple ethers from lib

* add config options

* fmt
This commit is contained in:
Noah Citron 2023-02-09 14:32:17 -05:00 committed by GitHub
parent 4066828387
commit 8e006d623b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 4409 additions and 0 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
.DS_Store
target
*.env
helios-ts/node_modules
helios-ts/dist

41
Cargo.lock generated
View File

@ -702,6 +702,16 @@ dependencies = [
"wasm-timer",
]
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "const-cstr"
version = "0.3.0"
@ -1985,6 +1995,26 @@ dependencies = [
"tracing-test",
]
[[package]]
name = "helios-ts"
version = "0.1.0"
dependencies = [
"client",
"common",
"config",
"consensus",
"console_error_panic_hook",
"ethers",
"execution",
"hex",
"serde",
"serde-wasm-bindgen",
"serde_json",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -3884,6 +3914,17 @@ dependencies = [
"serde_json",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_derive"
version = "1.0.152"

View File

@ -12,6 +12,7 @@ members = [
"config",
"consensus",
"execution",
"helios-ts",
]
[dependencies]

32
helios-ts/Cargo.toml Normal file
View File

@ -0,0 +1,32 @@
[package]
name = "helios-ts"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.83"
wasm-bindgen-futures = "0.4.33"
serde-wasm-bindgen = "0.4.5"
console_error_panic_hook = "0.1.7"
ethers = "1.0.0"
hex = "0.4.3"
serde = { version = "1.0.143", features = ["derive"] }
serde_json = "1.0.85"
client = { path = "../client" }
common = { path = "../common" }
consensus = { path = "../consensus" }
execution = { path = "../execution" }
config = { path = "../config" }
[dependencies.web-sys]
version = "0.3"
features = [
"console",
]

26
helios-ts/index.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>hello-wasm example</title>
</head>
<body>
<script src="./dist/bundle.js"></script>
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js"></script>
<script>
helios.then((helios) => {
const config = {
executionRpc:"http://localhost:9001/proxy",
consensusRpc: "http://localhost:9002/proxy",
checkpoint: "0x372342db81e3a42527e08dc19e33cd4f91f440f45b9ddb0a9865d407eceb08e4",
}
const heliosProvider = new helios.HeliosProvider(config);
heliosProvider.sync().then(() => {
window.provider = new ethers.providers.Web3Provider(heliosProvider);
});
});
</script>
</body>
</html>

103
helios-ts/lib.ts Normal file
View File

@ -0,0 +1,103 @@
import { Client } from "./pkg";
/// An EIP-1193 compliant Ethereum provider. Treat this the same as you
/// would window.ethereum when constructing an ethers or web3 provider.
export class HeliosProvider {
#client;
#chainId;
constructor(config: Config) {
const executionRpc = config.executionRpc;
const consensusRpc = config.consensusRpc;
const checkpoint = config.checkpoint;
const network = config.network ?? Network.MAINNET;
this.#client = new Client(executionRpc, consensusRpc, network, checkpoint);
this.#chainId = this.#client.chain_id();
}
async sync() {
await this.#client.sync();
}
async request(req: Request): Promise<any> {
switch(req.method) {
case "eth_getBalance": {
return this.#client.get_balance(req.params[0], req.params[1]);
};
case "eth_chainId": {
return this.#chainId;
};
case "eth_blockNumber": {
return this.#client.get_block_number();
};
case "eth_getTransactionByHash": {
let tx = await this.#client.get_transaction_by_hash(req.params[0]);
return mapToObj(tx);
};
case "eth_getTransactionCount": {
return this.#client.get_transaction_count(req.params[0], req.params[1]);
};
case "eth_getBlockTransactionCountByHash": {
return this.#client.get_block_transaction_count_by_hash(req.params[0]);
};
case "eth_getBlockTransactionCountByNumber": {
return this.#client.get_block_transaction_count_by_number(req.params[0]);
};
case "eth_getCode": {
return this.#client.get_code(req.params[0], req.params[1]);
};
case "eth_call": {
return this.#client.call(req.params[0], req.params[1]);
};
case "eth_estimateGas": {
return this.#client.estimate_gas(req.params[0]);
};
case "eth_gasPrice": {
return this.#client.gas_price();
};
case "eth_maxPriorityFeePerGas": {
return this.#client.max_priority_fee_per_gas();
};
case "eth_sendRawTransaction": {
return this.#client.send_raw_transaction(req.params[0]);
};
case "eth_getTransactionReceipt": {
return this.#client.get_transaction_receipt(req.params[0]);
};
case "eth_getLogs": {
return this.#client.get_logs(req.params[0]);
};
case "net_version": {
return this.#chainId;
};
}
}
}
export type Config = {
executionRpc: string,
consensusRpc?: string,
checkpoint?: string,
network?: Network,
}
export enum Network {
MAINNET = "mainnet",
GOERLI = "goerli",
}
type Request = {
method: string,
params: any[],
}
function mapToObj(map: Map<any, any> | undefined): Object | undefined {
if(!map) return undefined;
return Array.from(map).reduce((obj: any, [key, value]) => {
obj[key] = value;
return obj;
}, {});
}

3951
helios-ts/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
helios-ts/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "helios",
"version": "0.1.0",
"main": "lib.js",
"scripts": {
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"ts-loader": "^9.4.1",
"typescript": "^4.9.3",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0"
},
"dependencies": {
"ethers": "^5.7.2"
}
}

8
helios-ts/run.sh Executable file
View File

@ -0,0 +1,8 @@
set -e
(&>/dev/null lcp --proxyUrl https://eth-mainnet.g.alchemy.com/v2/23IavJytUwkTtBMpzt_TZKwgwAarocdT --port 9001 &)
(&>/dev/null lcp --proxyUrl https://www.lightclientdata.org --port 9002 &)
wasm-pack build
npm run build
simple-http-server

185
helios-ts/src/lib.rs Normal file
View File

@ -0,0 +1,185 @@
extern crate console_error_panic_hook;
extern crate web_sys;
use std::str::FromStr;
use common::types::BlockTag;
use ethers::types::{Address, Filter, H256};
use execution::types::CallOpts;
use wasm_bindgen::prelude::*;
use client::database::ConfigDB;
use config::{networks, Config};
#[allow(unused_macros)]
macro_rules! log {
( $( $t:tt )* ) => {
web_sys::console::log_1(&format!( $( $t )* ).into());
}
}
#[wasm_bindgen]
pub struct Client {
inner: client::Client<ConfigDB>,
chain_id: u64,
}
#[wasm_bindgen]
impl Client {
#[wasm_bindgen(constructor)]
pub fn new(
execution_rpc: String,
consensus_rpc: Option<String>,
network: String,
checkpoint: Option<String>,
) -> Self {
console_error_panic_hook::set_once();
let base = match network.as_str() {
"mainnet" => networks::mainnet(),
"goerli" => networks::goerli(),
_ => panic!("invalid network"),
};
let chain_id = base.chain.chain_id;
let checkpoint = Some(
checkpoint
.as_ref()
.map(|c| c.strip_prefix("0x").unwrap_or(c.as_str()))
.map(|c| hex::decode(c).unwrap())
.unwrap_or(base.default_checkpoint),
);
let consensus_rpc = consensus_rpc.unwrap_or(base.consensus_rpc.unwrap());
let config = Config {
execution_rpc,
consensus_rpc,
checkpoint,
chain: base.chain,
forks: base.forks,
..Default::default()
};
let inner: client::Client<ConfigDB> =
client::ClientBuilder::new().config(config).build().unwrap();
Self { inner, chain_id }
}
#[wasm_bindgen]
pub async fn sync(&mut self) {
self.inner.start().await.unwrap()
}
#[wasm_bindgen]
pub fn chain_id(&self) -> u32 {
self.chain_id as u32
}
#[wasm_bindgen]
pub async fn get_block_number(&self) -> u32 {
self.inner.get_block_number().await.unwrap() as u32
}
#[wasm_bindgen]
pub async fn get_balance(&self, addr: JsValue, block: JsValue) -> String {
let addr: Address = serde_wasm_bindgen::from_value(addr).unwrap();
let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap();
self.inner
.get_balance(&addr, block)
.await
.unwrap()
.to_string()
}
#[wasm_bindgen]
pub async fn get_transaction_by_hash(&self, hash: String) -> JsValue {
let hash = H256::from_str(&hash).unwrap();
let tx = self.inner.get_transaction_by_hash(&hash).await.unwrap();
serde_wasm_bindgen::to_value(&tx).unwrap()
}
#[wasm_bindgen]
pub async fn get_transaction_count(&self, addr: JsValue, block: JsValue) -> u32 {
let addr: Address = serde_wasm_bindgen::from_value(addr).unwrap();
let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap();
self.inner.get_nonce(&addr, block).await.unwrap() as u32
}
#[wasm_bindgen]
pub async fn get_block_transaction_count_by_hash(&self, hash: JsValue) -> u32 {
let hash: H256 = serde_wasm_bindgen::from_value(hash).unwrap();
self.inner
.get_block_transaction_count_by_hash(&hash.as_bytes().to_vec())
.await
.unwrap() as u32
}
#[wasm_bindgen]
pub async fn get_block_transaction_count_by_number(&self, block: JsValue) -> u32 {
let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap();
self.inner
.get_block_transaction_count_by_number(block)
.await
.unwrap() as u32
}
#[wasm_bindgen]
pub async fn get_code(&self, addr: JsValue, block: JsValue) -> String {
let addr: Address = serde_wasm_bindgen::from_value(addr).unwrap();
let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap();
let code = self.inner.get_code(&addr, block).await.unwrap();
format!("0x{}", hex::encode(code))
}
#[wasm_bindgen]
pub async fn call(&self, opts: JsValue, block: JsValue) -> String {
let opts: CallOpts = serde_wasm_bindgen::from_value(opts).unwrap();
let block: BlockTag = serde_wasm_bindgen::from_value(block).unwrap();
let res = self.inner.call(&opts, block).await.unwrap();
format!("0x{}", hex::encode(res))
}
#[wasm_bindgen]
pub async fn estimate_gas(&self, opts: JsValue) -> u32 {
let opts: CallOpts = serde_wasm_bindgen::from_value(opts).unwrap();
self.inner.estimate_gas(&opts).await.unwrap() as u32
}
#[wasm_bindgen]
pub async fn gas_price(&self) -> JsValue {
let price = self.inner.get_gas_price().await.unwrap();
serde_wasm_bindgen::to_value(&price).unwrap()
}
#[wasm_bindgen]
pub async fn max_priority_fee_per_gas(&self) -> JsValue {
let price = self.inner.get_priority_fee().await.unwrap();
serde_wasm_bindgen::to_value(&price).unwrap()
}
#[wasm_bindgen]
pub async fn send_raw_transaction(&self, tx: String) -> JsValue {
let tx = hex::decode(tx).unwrap();
let hash = self.inner.send_raw_transaction(&tx).await.unwrap();
serde_wasm_bindgen::to_value(&hash).unwrap()
}
#[wasm_bindgen]
pub async fn get_transaction_receipt(&self, tx: JsValue) -> JsValue {
let tx: H256 = serde_wasm_bindgen::from_value(tx).unwrap();
let receipt = self.inner.get_transaction_receipt(&tx).await.unwrap();
serde_wasm_bindgen::to_value(&receipt).unwrap()
}
#[wasm_bindgen]
pub async fn get_logs(&self, filter: JsValue) -> JsValue {
let filter: Filter = serde_wasm_bindgen::from_value(filter).unwrap();
let logs = self.inner.get_logs(&filter).await.unwrap();
serde_wasm_bindgen::to_value(&logs).unwrap()
}
}

11
helios-ts/tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es6",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node"
}
}

View File

@ -0,0 +1,28 @@
const path = require("path");
module.exports = {
entry: "./lib.ts",
module: {
rules: [
{
test: /\.ts?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
library: {
name: "helios",
type: "umd",
}
},
experiments: {
asyncWebAssembly: true,
}
};