service/eth: Add proof of concept Geth interface.

This commit is contained in:
parazyd
2021-10-25 21:02:47 +02:00
parent df5eb0ca05
commit 34cbcfb87b
5 changed files with 378 additions and 3 deletions

29
Cargo.lock generated
View File

@@ -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]]

View File

@@ -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
src/bin/eth.rs Normal file
View File

@@ -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
src/service/eth.rs Normal file
View File

@@ -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");
}
}

View File

@@ -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};