Browse Source

service/eth: Add proof of concept Geth interface.

parazyd 4 năm trước cách đây
mục cha
commit
34cbcfb87b
5 tập tin đã thay đổi với 378 bổ sung3 xóa
  1. 29 0
      Cargo.lock
  2. 8 3
      Cargo.toml
  3. 75 0
      src/bin/eth.rs
  4. 263 0
      src/service/eth.rs
  5. 3 0
      src/service/mod.rs

+ 29 - 0
Cargo.lock

@@ -1259,8 +1259,10 @@ dependencies = [
  "ff",
  "futures 0.3.17",
  "group",
+ "hash-db",
  "hex",
  "jubjub",
+ "keccak-hasher",
  "lazy_static",
  "log",
  "native-tls",
@@ -1939,6 +1941,21 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "hash-db"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a"
+
+[[package]]
+name = "hash256-std-hasher"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2"
+dependencies = [
+ "crunchy",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.9.1"
@@ -2246,6 +2263,17 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7"
 
+[[package]]
+name = "keccak-hasher"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711adba9940a039f4374fc5724c0a5eaca84a2d558cce62256bfe26f0dbef05e"
+dependencies = [
+ "hash-db",
+ "hash256-std-hasher",
+ "tiny-keccak",
+]
+
 [[package]]
 name = "kv-log-macro"
 version = "1.0.7"
@@ -2556,6 +2584,7 @@ dependencies = [
  "num-integer",
  "num-traits",
  "rand 0.7.3",
+ "serde",
 ]
 
 [[package]]

+ 8 - 3
Cargo.toml

@@ -35,7 +35,7 @@ hex = "0.4.2"
 bs58 = "0.4.0"
 prettytable-rs = "0.8"
 num_cpus = "1.13.0"
-num-bigint = {version = "0.3.2", features = ["rand"]}
+num-bigint = {version = "0.3.2", features = ["rand", "serde"]}
 
 smol = "1.2.5"
 futures = "0.3.17"
@@ -73,11 +73,16 @@ spl-token = {version = "3.2.0", features = ["no-entrypoint"], optional = true}
 spl-associated-token-account = {version = "1.0.3", features = ["no-entrypoint"], optional = true}
 
 ## Cashier Bitcoin Dependencies
-bitcoin = {version = "0.27.0", optional = true }
+bitcoin = {version = "0.27.0", optional = true}
 bitcoin_hashes = "0.10.0"
 secp256k1 = {version = "0.20.3", default-features = false, features = ["rand-std"], optional = true}
-electrum-client = {version = "0.8.0", optional = true }
+electrum-client = {version = "0.8.0", optional = true}
+
+## Cashier Ethereum Dependencies
+hash-db = {version = "0.15.2", optional = true}
+keccak-hasher = {version = "0.15.3", optional = true}
 
 [features]
 btc = ["bitcoin", "secp256k1", "electrum-client"]
 sol = ["solana-sdk", "solana-client", "spl-token", "spl-associated-token-account"]
+eth = ["keccak-hasher", "hash-db"]

+ 75 - 0
src/bin/eth.rs

@@ -0,0 +1,75 @@
+use num_bigint::BigUint;
+
+use drk::{
+    service::eth::{erc20_transfer_data, generate_privkey, EthClient, EthTx},
+    util::{decode_base10, encode_base10},
+    Result,
+};
+
+#[async_std::main]
+async fn main() -> Result<()> {
+    simple_logger::init_with_level(log::Level::Debug)?;
+
+    let eth = EthClient::new("/home/parazyd/.ethereum/ropsten/geth.ipc".to_string());
+
+    //let key = generate_privkey();
+    //let passphrase = "foobar".to_string();
+    //let rep = eth.import_privkey(&key, &passphrase).await?;
+    //println!("{:#?}", rep);
+
+    let acc = "0x113b6648f34f4d0340d04ff171cbcf0b49d47827".to_string();
+    let _key = "67cbb73cb293eea5fa2a7025d5479dbd50319010c03fd8821917ad0d9d53276c".to_string();
+    let passphrase = "foobar".to_string();
+
+    // Recipient address
+    let dest = "0xcD640A363305c21255c58Ba9C8c1C508e6997a12".to_string();
+
+    // Latest known block, used to calculate present balance.
+    let block = eth.block_number().await?;
+    let block = block.as_str().unwrap();
+
+    // Native ETH balance
+    let hexbalance = eth.get_eth_balance(&acc, &block).await?;
+    let hexbalance = hexbalance.as_str().unwrap().trim_start_matches("0x");
+    let balance = BigUint::parse_bytes(hexbalance.as_bytes(), 16).unwrap();
+    println!("{}", encode_base10(balance, 18));
+
+    /*
+    // Transfer native ETH
+    let tx = EthTx::new(
+        &acc,
+        &dest,
+        None,
+        None,
+        Some(decode_base10("0.051", 18, true)?),
+        None,
+        None,
+    );
+
+    let rep = eth.send_transaction(&tx, &passphrase).await?;
+    println!("TXID: {}", rep.as_str().unwrap());
+    */
+
+    // ERC20 Token balance
+    let mint = "0xad6d458402f60fd3bd25163575031acdce07538d"; // Ropsten DAI (get on Uniswap)
+    let hexbalance = eth.get_erc20_balance(&acc, mint).await?;
+    let hexbalance = hexbalance.as_str().unwrap().trim_start_matches("0x");
+    let balance = BigUint::parse_bytes(hexbalance.as_bytes(), 16).unwrap();
+    println!("{}", encode_base10(balance, 18));
+
+    // Transfer ERC20 token
+    let tx = EthTx::new(
+        &acc,
+        mint,
+        None,
+        None,
+        None,
+        Some(erc20_transfer_data(&dest, decode_base10("1", 18, true)?)),
+        None,
+    );
+
+    let rep = eth.send_transaction(&tx, &passphrase).await?;
+    println!("TXID: {}", rep.as_str().unwrap());
+
+    Ok(())
+}

+ 263 - 0
src/service/eth.rs

@@ -0,0 +1,263 @@
+use std::convert::TryInto;
+
+use hash_db::Hasher;
+use keccak_hasher::KeccakHasher;
+use lazy_static::lazy_static;
+use log::debug;
+use num_bigint::{BigUint, RandBigInt};
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value};
+
+use crate::{rpc::jsonrpc, rpc::jsonrpc::JsonResult, Error, Result};
+
+// An ERC-20 token transfer transaction's data is as follows:
+//
+// 1. The first 4 bytes of the keccak256 hash of "transfer(address,uint256)".
+// 2. The address of the recipient, left-zero-padded to be 32 bytes.
+// 3. The amount to be transferred: amount * 10^decimals
+
+// This is the entire ERC20 ABI
+lazy_static! {
+    static ref ERC20_NAME_METHOD: [u8; 4] = {
+        let method = b"name()";
+        KeccakHasher::hash(method)[0..4].try_into().expect("nope")
+    };
+    static ref ERC20_APPROVE_METHOD: [u8; 4] = {
+        let method = b"approve(address,uint256)";
+        KeccakHasher::hash(method)[0..4].try_into().expect("nope")
+    };
+    static ref ERC20_TOTALSUPPLY_METHOD: [u8; 4] = {
+        let method = b"totalSupply()";
+        KeccakHasher::hash(method)[0..4].try_into().expect("nope")
+    };
+    static ref ERC20_TRANSFERFROM_METHOD: [u8; 4] = {
+        let method = b"transferFrom(address,address,uint256)";
+        KeccakHasher::hash(method)[0..4].try_into().expect("nope")
+    };
+    static ref ERC20_DECIMALS_METHOD: [u8; 4] = {
+        let method = b"decimals()";
+        KeccakHasher::hash(method)[0..4].try_into().expect("nope")
+    };
+    static ref ERC20_VERSION_METHOD: [u8; 4] = {
+        let method = b"version()";
+        KeccakHasher::hash(method)[0..4].try_into().expect("nope")
+    };
+    static ref ERC20_BALANCEOF_METHOD: [u8; 4] = {
+        let method = b"balanceOf(address)";
+        KeccakHasher::hash(method)[0..4].try_into().expect("nope")
+    };
+    static ref ERC20_SYMBOL_METHOD: [u8; 4] = {
+        let method = b"symbol()";
+        KeccakHasher::hash(method)[0..4].try_into().expect("nope")
+    };
+    static ref ERC20_TRANSFER_METHOD: [u8; 4] = {
+        let method = b"transfer(address,uint256)";
+        KeccakHasher::hash(method)[0..4].try_into().expect("nope")
+    };
+    static ref ERC20_APPROVEANDCALL_METHOD: [u8; 4] = {
+        let method = b"approveAndCall(address,uint256,bytes)";
+        KeccakHasher::hash(method)[0..4].try_into().expect("nope")
+    };
+    static ref ERC20_ALLOWANCE_METHOD: [u8; 4] = {
+        let method = b"allowance(address,address)";
+        KeccakHasher::hash(method)[0..4].try_into().expect("nope")
+    };
+}
+
+pub fn erc20_transfer_data(recipient: &str, amount: BigUint) -> String {
+    let rec = recipient.trim_start_matches("0x");
+    let rec_padded = format!("{:0>64}", rec);
+
+    let amnt_bytes = amount.to_bytes_be();
+    let amnt_hex = hex::encode(amnt_bytes);
+    let amnt_hex_padded = format!("{:0>64}", amnt_hex);
+
+    format!(
+        "0x{}{}{}",
+        hex::encode(*ERC20_TRANSFER_METHOD),
+        rec_padded,
+        amnt_hex_padded
+    )
+}
+
+pub fn erc20_balanceof_data(account: &str) -> String {
+    let acc = account.trim_start_matches("0x");
+    let acc_padded = format!("{:0>64}", acc);
+
+    format!("0x{}{}", hex::encode(*ERC20_BALANCEOF_METHOD), acc_padded)
+}
+
+fn to_eth_hex(val: BigUint) -> String {
+    let bytes = val.to_bytes_be();
+    let h = hex::encode(bytes);
+    format!("0x{}", h.trim_start_matches('0'))
+}
+
+/// Generate a 256-bit ETH private key.
+pub fn generate_privkey() -> String {
+    let mut rng = rand::thread_rng();
+    let token = rng.gen_bigint(256);
+    let token_bytes = token.to_bytes_le().1;
+    let key = KeccakHasher::hash(&token_bytes);
+    hex::encode(key)
+}
+
+#[allow(non_snake_case)]
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct EthTx {
+    pub from: String,
+
+    pub to: String,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub gas: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub gasPrice: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub value: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub data: Option<String>,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub nonce: Option<String>,
+}
+
+impl EthTx {
+    pub fn new(
+        from: &str,
+        to: &str,
+        gas: Option<BigUint>,
+        gas_price: Option<BigUint>,
+        value: Option<BigUint>,
+        data: Option<String>,
+        nonce: Option<String>,
+    ) -> Self {
+        let gas_hex = match gas {
+            Some(v) => Some(to_eth_hex(v)),
+            None => None,
+        };
+
+        let gasprice_hex = match gas_price {
+            Some(v) => Some(to_eth_hex(v)),
+            None => None,
+        };
+
+        let value_hex = match value {
+            Some(v) => Some(to_eth_hex(v)),
+            None => None,
+        };
+
+        EthTx {
+            from: from.to_string(),
+            to: to.to_string(),
+            gas: gas_hex,
+            gasPrice: gasprice_hex,
+            value: value_hex,
+            data,
+            nonce,
+        }
+    }
+}
+
+// JSON-RPC interface to Geth.
+// https://eth.wiki/json-rpc/API
+// https://geth.ethereum.org/docs/rpc/
+//
+// geth can be started with: $ geth --ropsten --syncmode light
+// It should then show an Unix socket endpoint like so:
+// INFO [10-25|19:47:32.845] IPC endpoint opened: url=/home/x/.ethereum/ropsten/geth.ipc
+//
+pub struct EthClient {
+    socket_path: String,
+}
+
+impl EthClient {
+    pub fn new(socket_path: String) -> Self {
+        Self { socket_path }
+    }
+
+    async fn request(&self, r: jsonrpc::JsonRequest) -> Result<Value> {
+        debug!(target: "ETH RPC", "--> {}", serde_json::to_string(&r)?);
+        let reply: JsonResult = match jsonrpc::send_unix_request(&self.socket_path, json!(r)).await
+        {
+            Ok(v) => v,
+            Err(e) => return Err(e),
+        };
+
+        match reply {
+            JsonResult::Resp(r) => {
+                debug!(target: "ETH RPC", "<-- {}", serde_json::to_string(&r)?);
+                return Ok(r.result);
+            }
+
+            JsonResult::Err(e) => {
+                debug!(target: "ETH RPC", "<-- {}", serde_json::to_string(&e)?);
+                return Err(Error::JsonRpcError(e.error.message.to_string()));
+            }
+
+            JsonResult::Notif(n) => {
+                debug!(target: "ETH RPC", "<-- {}", serde_json::to_string(&n)?);
+                return Err(Error::JsonRpcError("Unexpected reply".to_string()));
+            }
+        }
+    }
+
+    pub async fn import_privkey(&self, key: &str, passphrase: &str) -> Result<Value> {
+        let req = jsonrpc::request(json!("personal_importRawKey"), json!([key, passphrase]));
+        Ok(self.request(req).await?)
+    }
+
+    /*
+    pub async fn estimate_gas(&self, tx: &EthTx) -> Result<Value> {
+        let req = jsonrpc::request(json!("eth_estimateGas"), json!([tx]));
+        Ok(self.request(req).await?)
+    }
+    */
+
+    pub async fn block_number(&self) -> Result<Value> {
+        let req = jsonrpc::request(json!("eth_blockNumber"), json!([]));
+        Ok(self.request(req).await?)
+    }
+
+    pub async fn get_eth_balance(&self, acc: &str, block: &str) -> Result<Value> {
+        let req = jsonrpc::request(json!("eth_getBalance"), json!([acc, block]));
+        Ok(self.request(req).await?)
+    }
+
+    pub async fn get_erc20_balance(&self, acc: &str, mint: &str) -> Result<Value> {
+        let tx = EthTx::new(
+            acc,
+            mint,
+            None,
+            None,
+            None,
+            Some(erc20_balanceof_data(acc)),
+            None,
+        );
+        let req = jsonrpc::request(json!("eth_call"), json!([tx, "latest"]));
+        Ok(self.request(req).await?)
+    }
+
+    pub async fn send_transaction(&self, tx: &EthTx, passphrase: &str) -> Result<Value> {
+        let req = jsonrpc::request(json!("personal_sendTransaction"), json!([tx, passphrase]));
+        Ok(self.request(req).await?)
+    }
+}
+
+#[allow(unused_imports)]
+mod tests {
+    use super::*;
+    use num_bigint::ToBigUint;
+    use std::str::FromStr;
+
+    #[test]
+    fn test_erc20_transfer_data() {
+        let recipient = "0x5b7b3b499fb69c40c365343cb0dc842fe8c23887";
+        let amnt = BigUint::from_str("34765403556934000640").unwrap();
+
+        assert_eq!(erc20_transfer_data(recipient, amnt), "0xa9059cbb0000000000000000000000005b7b3b499fb69c40c365343cb0dc842fe8c23887000000000000000000000000000000000000000000000001e27786570c272000");
+    }
+}

+ 3 - 0
src/service/mod.rs

@@ -13,4 +13,7 @@ pub mod sol;
 #[cfg(feature = "sol")]
 pub use sol::{SolClient, SolFailed, SolResult};
 
+#[cfg(feature = "eth")]
+pub mod eth;
+
 pub use gateway::{GatewayClient, GatewayService, GatewaySlabsSubscriber};