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

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