mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-10 15:17:57 -05:00
service/eth: Add proof of concept Geth interface.
This commit is contained in:
29
Cargo.lock
generated
29
Cargo.lock
generated
@@ -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]]
|
||||
|
||||
11
Cargo.toml
11
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
src/bin/eth.rs
Normal file
75
src/bin/eth.rs
Normal 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
263
src/service/eth.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user