diff --git a/.gitignore b/.gitignore index e82c96cc..2124cc61 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,21 @@ demo/cronos.h demo/sdk install + +wallet-connect/qrcode.png + +wallet-connect/session.bin + +defi-wallet-core-rs + +demo/third_party/easywsclient + +demo/third_party/json + +wallet-connect/yarn.lock + +wallet-connect/node_modules/ + +.DS_Store + +sessioninfo2.json diff --git a/Cargo.lock b/Cargo.lock index 0251f36b..626fa42d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "aead" version = "0.5.1" @@ -578,6 +584,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + [[package]] name = "byteorder" version = "1.4.3" @@ -694,6 +706,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "checked_int_cast" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" + [[package]] name = "chrono" version = "0.4.23" @@ -833,6 +851,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "concurrent-queue" version = "2.0.0" @@ -1178,7 +1202,9 @@ dependencies = [ "hex", "hkdf", "hmac", + "image", "open", + "qrcode", "qrcodegen", "quickcheck", "quickcheck_macros", @@ -1278,6 +1304,16 @@ dependencies = [ "tonic", ] +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + [[package]] name = "der" version = "0.6.1" @@ -2016,7 +2052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.6.2", ] [[package]] @@ -2232,6 +2268,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "glob" version = "0.3.1" @@ -2541,6 +2587,25 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational 0.3.2", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -2680,6 +2745,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.60" @@ -2872,6 +2946,25 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.6.2" @@ -2923,7 +3016,7 @@ dependencies = [ "num-complex", "num-integer", "num-iter", - "num-rational", + "num-rational 0.4.1", "num-traits", ] @@ -2979,6 +3072,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -3460,6 +3564,18 @@ dependencies = [ "extra-cpp-bindings", ] +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + [[package]] name = "polling" version = "2.5.2" @@ -3607,6 +3723,16 @@ dependencies = [ "prost", ] +[[package]] +name = "qrcode" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" +dependencies = [ + "checked_int_cast", + "image", +] + [[package]] name = "qrcodegen" version = "1.8.0" @@ -4095,6 +4221,12 @@ dependencies = [ "windows-sys 0.36.1", ] +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.1.0" @@ -4818,6 +4950,17 @@ dependencies = [ "syn 2.0.10", ] +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + [[package]] name = "time" version = "0.3.16" @@ -5450,6 +5593,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "wepoll-ffi" version = "0.1.2" diff --git a/demo/extra.cc b/demo/extra.cc index b3570938..d4f8426e 100644 --- a/demo/extra.cc +++ b/demo/extra.cc @@ -5,6 +5,7 @@ #include "sdk/include/rust/cxx.h" #include "third_party/easywsclient/easywsclient.hpp" #include "third_party/json/single_include/nlohmann/json.hpp" +#include #include #include #include @@ -363,16 +364,17 @@ void test_wallet_connect() { * @summary test wallet connect 2.0 * @description basic test for wallet connect 2.0 */ - void test_wallet_connect2() { std::string mycronosrpc = getEnv("CRONOSRPC").c_str(); - bool test_personal = true; - bool test_basic = false; + bool test_personal = false; + bool test_basic = true; bool test_nft = false; std::string filename = "sessioninfo2.json"; bool exit_program = false; try { Box client = make_new_client2(filename); + String uri = client->print_uri(); + std::cout << "uri= " << uri.c_str() << std::endl; WalletConnect2EnsureSessionResult result = client->ensure_session_blocking(60000); std::cout << "session result=" << result.eip155.accounts.size() @@ -405,7 +407,6 @@ void test_wallet_connect2() { } assert(result.eip155.accounts.size() > 0); - bool test_personal = true; if (test_personal) { Vec sig1 = client->sign_personal_blocking( @@ -413,6 +414,30 @@ void test_wallet_connect2() { std::cout << "signature length=" << sig1.size() << endl; } + if (test_basic) { + std::string fromaddress = getenv("MYFROMADDRESS"); + std::cout << "mycronosrpc=" << mycronosrpc << endl; + std::cout << "fromaddress=" << fromaddress << endl; + std::string toaddress = getenv("MYTOADDRESS"); + std::cout << "toaddress=" << toaddress << endl; + std::string mynonce = org::defi_wallet_core::get_eth_nonce( + fromaddress.c_str(), mycronosrpc) + .c_str(); + std::cout << "nonce=" << mynonce << endl; + WalletConnectTxEip155 info; + info.to = toaddress; + info.common.gas_limit = "21000"; // gas limit + info.common.gas_price = "10000"; // gas price + info.value = "100000000000000"; // 0.0001 eth + info.data = Vec(); + info.common.nonce = mynonce; + info.common.chainid = 1; + + assert(result.eip155.accounts.size() > 0); + Vec rawtx = client->sign_eip155_transaction_blocking( + info, result.eip155.accounts[0].address.address); + } + std::cout << "enter q to exit" << std::endl; while (true) { // read input, if q is pressed, quit diff --git a/demo/main.cc b/demo/main.cc index e84e4dbb..9da6d7fb 100644 --- a/demo/main.cc +++ b/demo/main.cc @@ -26,6 +26,7 @@ int main(int argc, char *argv[]) { test_interval(); test_blackscout_cronoscan(); test_wallet_connect(); + I } catch (const std::exception &e) { // Use `Assertion failed`, the same as `assert` function diff --git a/extra-cpp-bindings/src/lib.rs b/extra-cpp-bindings/src/lib.rs index e0c6b85a..88a251e3 100644 --- a/extra-cpp-bindings/src/lib.rs +++ b/extra-cpp-bindings/src/lib.rs @@ -334,6 +334,7 @@ mod ffi { pub fn save_client(self: &mut Walletconnect2Client) -> Result; /// print qrcode in termal, for debugging pub fn print_uri(self: &mut WalletconnectClient) -> Result; + pub fn print_uri(self: &mut Walletconnect2Client) -> Result; /// sign message pub fn sign_personal_blocking( self: &mut WalletconnectClient, @@ -354,6 +355,11 @@ mod ffi { info: &WalletConnectTxEip155, address: [u8; 20], ) -> Result>; + pub fn sign_eip155_transaction_blocking( + self: &mut Walletconnect2Client, + info: &WalletConnectTxEip155, + address: [u8; 20], + ) -> Result>; /// send cronos(eth) eip155 transaction /// Supported Wallets: Trust Wallet, MetaMask and Crypto.com Mobile Defi Wallet @@ -362,6 +368,11 @@ mod ffi { info: &WalletConnectTxEip155, address: [u8; 20], ) -> Result>; + pub fn send_eip155_transaction_blocking( + self: &mut Walletconnect2Client, + info: &WalletConnectTxEip155, + address: [u8; 20], + ) -> Result>; /// eip1559_transaction_request: json string of Eip1559TransactionRequest /// return signed transaction bytes @@ -370,6 +381,11 @@ mod ffi { eip1559_transaction_request: String, address: [u8; 20], ) -> Result>; + pub fn sign_transaction( + self: &mut Walletconnect2Client, + eip1559_transaction_request: String, + address: [u8; 20], + ) -> Result>; /// eip1559_transaction_request: json string of Eip1559TransactionRequest /// return transaction hash bytes @@ -378,6 +394,11 @@ mod ffi { eip1559_transaction_request: String, address: [u8; 20], ) -> Result>; + pub fn send_transaction( + self: &mut Walletconnect2Client, + eip1559_transaction_request: String, + address: [u8; 20], + ) -> Result>; /// sign a contract transaction /// contract_action is a json string of `ContractAction` type, for example: @@ -398,6 +419,12 @@ mod ffi { common: &WalletConnectTxCommon, address: [u8; 20], ) -> Result>; + pub fn sign_contract_transaction( + self: &mut Walletconnect2Client, + contract_action: String, + common: &WalletConnectTxCommon, + address: [u8; 20], + ) -> Result>; // send a contract transaction /// contract_action is a json string of `ContractAction` type @@ -418,6 +445,12 @@ mod ffi { common: &WalletConnectTxCommon, address: [u8; 20], ) -> Result>; + pub fn send_contract_transaction( + self: &mut Walletconnect2Client, + contract_action: String, + common: &WalletConnectTxCommon, + address: [u8; 20], + ) -> Result>; /// returns the transactions of a given address. /// The API key can be obtained from https://cronoscan.com @@ -1017,14 +1050,12 @@ pub fn walletconnect2_client_new( return Err(anyhow!("project_id is empty")); } // print all arguments - println!("relay_server_string: {:?}", relay_server_string); - println!("project_id: {:?}", project_id); - println!("required_namespaces_json: {:?}", required_namespaces_json); - println!("client_meta_json: {:?}", client_meta_json); + println!("relay_server_string: {relay_server_string:?}"); + println!("project_id: {project_id:?}"); + println!("required_namespaces_json: {required_namespaces_json:?}"); + println!("client_meta_json: {client_meta_json:?}"); let mut opts = defi_wallet_connect::v2::ClientOptions::default(); - // print opts - println!("opts1: {:?}", opts); if !relay_server_string.is_empty() { let relay_server = url::Url::parse(&relay_server_string)?; @@ -1047,11 +1078,11 @@ pub fn walletconnect2_client_new( opts.client_meta = client_meta; } - println!("opts: {:?}", opts); + println!("opts: {opts:?}"); let required_namespaces = serde_json::to_string(&opts.required_namespaces)?; - println!("required_namespaces_json: {}", required_namespaces); + println!("required_namespaces_json: {required_namespaces}",); let client_meta = serde_json::to_string(&opts.client_meta)?; - println!("client_meta_json: {}", client_meta); + println!("client_meta_json: {client_meta}"); let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); diff --git a/extra-cpp-bindings/src/walletconnect.rs b/extra-cpp-bindings/src/walletconnect.rs index b50223fb..05ea1ee9 100644 --- a/extra-cpp-bindings/src/walletconnect.rs +++ b/extra-cpp-bindings/src/walletconnect.rs @@ -384,7 +384,7 @@ impl WalletconnectClient { if !userinfo.common.nonce.is_empty() { tx = tx.nonce(U256::from_dec_str(&userinfo.common.nonce)?); } - if !userinfo.common.chainid == 0 { + if userinfo.common.chainid != 0 { tx = tx.chain_id(userinfo.common.chainid); } if !userinfo.value.is_empty() { @@ -437,7 +437,7 @@ impl WalletconnectClient { if !userinfo.common.nonce.is_empty() { tx = tx.nonce(U256::from_dec_str(&userinfo.common.nonce)?); } - if !userinfo.common.chainid == 0 { + if userinfo.common.chainid != 0 { tx = tx.chain_id(userinfo.common.chainid); } if !userinfo.value.is_empty() { diff --git a/extra-cpp-bindings/src/walletconnect2.rs b/extra-cpp-bindings/src/walletconnect2.rs index 58b5f4ac..013f0c7e 100644 --- a/extra-cpp-bindings/src/walletconnect2.rs +++ b/extra-cpp-bindings/src/walletconnect2.rs @@ -1,9 +1,22 @@ use crate::ffi::WalletConnect2Eip155Accounts; use crate::ffi::WalletConnect2EnsureSessionResult; use crate::ffi::WalletConnectAddress; +use crate::ffi::WalletConnectTxCommon; use anyhow::{anyhow, Result}; use defi_wallet_connect::v2::Namespaces; use defi_wallet_connect::v2::{Client, ClientOptions, SessionInfo}; +use qrcodegen::{QrCode, QrCodeEcc}; + +use defi_wallet_connect::v2::WCMiddleware; + +use ethers::core::types::transaction::eip2718::TypedTransaction; + +use ethers::prelude::{Address, Eip1559TransactionRequest, NameOrAddress, U256}; +use ethers::prelude::{Middleware, Signature, TxHash}; +use ethers::types::H160; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + pub struct Walletconnect2Client { pub client: Option, pub rt: tokio::runtime::Runtime, // need to use the same runtime, otherwise c++ side crash @@ -11,6 +24,28 @@ pub struct Walletconnect2Client { pub rx: tokio::sync::mpsc::UnboundedReceiver, // receiver } +#[derive(Serialize, Deserialize)] +enum ContractAction { + ContractApproval(defi_wallet_core_common::ContractApproval), + ContractTransfer(defi_wallet_core_common::ContractTransfer), +} + +async fn sign_typed_tx( + client: Client, + tx: &TypedTransaction, + address: Address, +) -> Result { + let middleware = WCMiddleware::new(client); + let signature = middleware.sign_transaction(tx, address).await?; + Ok(signature) +} + +async fn send_typed_tx(client: Client, tx: TypedTransaction, address: Address) -> Result { + let middleware = WCMiddleware::new(client).with_sender(address); + let receipt = middleware.send_transaction(tx, None).await?.tx_hash(); + Ok(receipt) +} + pub async fn restore_client( contents: String, callback_sender: Option>, @@ -65,6 +100,37 @@ impl Walletconnect2Client { }, ) } + + fn print_qr(qr: &QrCode) { + let border: i32 = 1; + for y in -border..qr.size() + border { + for x in -border..qr.size() + border { + let c = if qr.get_module(x, y) { + "\x1b[40m \x1b[0m" + } else { + "\x1b[47m \x1b[0m" + }; + print!("{c}"); + } + println!(); + } + println!(); + } + + /// print uri(qrcode) for debugging + pub fn print_uri(&mut self) -> Result { + if let Some(client) = self.client.as_ref() { + let result = self.rt.block_on(client.get_session_info()); + let uristring = result.uri(); + if let Ok(qr) = QrCode::encode_text(&uristring, QrCodeEcc::Medium) { + Self::print_qr(&qr); + } + Ok(uristring) + } else { + anyhow::bail!("no client"); + } + } + pub fn sign_personal_blocking( &mut self, message: String, @@ -152,4 +218,328 @@ impl Walletconnect2Client { }, ) } + + /// build cronos(eth) eip155 transaction + pub fn sign_eip155_transaction_blocking( + &mut self, + userinfo: &crate::ffi::WalletConnectTxEip155, + address: [u8; 20], + ) -> Result> { + if self.client.is_none() { + anyhow::bail!("no client"); + } + + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow!("get walllet-connect client error"))?; + let signeraddress = Address::from_slice(&address); + + let mut tx = Eip1559TransactionRequest::new(); + + if !userinfo.to.is_empty() { + tx = tx.to(NameOrAddress::Address(Address::from_str(&userinfo.to)?)); + } + if !userinfo.data.is_empty() { + tx = tx.data(userinfo.data.as_slice().to_vec()); + } + if !userinfo.common.gas_limit.is_empty() { + tx = tx.gas(U256::from_dec_str(&userinfo.common.gas_limit)?); + } + if !userinfo.common.gas_price.is_empty() { + tx = tx + .max_priority_fee_per_gas(U256::from_dec_str(&userinfo.common.gas_price)?) + .max_fee_per_gas(U256::from_dec_str(&userinfo.common.gas_price)?); + } + if !userinfo.common.nonce.is_empty() { + tx = tx.nonce(U256::from_dec_str(&userinfo.common.nonce)?); + } + if userinfo.common.chainid != 0 { + tx = tx.chain_id(userinfo.common.chainid); + } + if !userinfo.value.is_empty() { + tx = tx.value(U256::from_dec_str(&userinfo.value)?); + } + let newclient = client.clone(); + let typedtx = TypedTransaction::Eip1559(tx); + + let sig = self + .rt + .block_on(sign_typed_tx(newclient, &typedtx, signeraddress)) + .map_err(|e| anyhow!("sign_typed_transaction error {}", e.to_string()))?; + + let signed_tx = &typedtx.rlp_signed(&sig); + Ok(signed_tx.to_vec()) + } + + /// send cronos(eth) eip155 transaction + pub fn send_eip155_transaction_blocking( + &mut self, + userinfo: &crate::ffi::WalletConnectTxEip155, + address: [u8; 20], + ) -> Result> { + if self.client.is_none() { + anyhow::bail!("no client"); + } + + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow!("get walllet-connect client error"))?; + let signeraddress = Address::from_slice(&address); + + let mut tx = Eip1559TransactionRequest::new(); + + if !userinfo.to.is_empty() { + tx = tx.to(NameOrAddress::Address(Address::from_str(&userinfo.to)?)); + } + if !userinfo.data.is_empty() { + tx = tx.data(userinfo.data.as_slice().to_vec()); + } + if !userinfo.common.gas_limit.is_empty() { + tx = tx.gas(U256::from_dec_str(&userinfo.common.gas_limit)?); + } + if !userinfo.common.gas_price.is_empty() { + tx = tx + .max_priority_fee_per_gas(U256::from_dec_str(&userinfo.common.gas_price)?) + .max_fee_per_gas(U256::from_dec_str(&userinfo.common.gas_price)?); + } + if !userinfo.common.nonce.is_empty() { + tx = tx.nonce(U256::from_dec_str(&userinfo.common.nonce)?); + } + if userinfo.common.chainid != 0 { + tx = tx.chain_id(userinfo.common.chainid); + } + if !userinfo.value.is_empty() { + tx = tx.value(U256::from_dec_str(&userinfo.value)?); + } + + let newclient = client.clone(); + let typedtx = TypedTransaction::Eip1559(tx); + + let tx_bytes = self + .rt + .block_on(send_typed_tx(newclient, typedtx, signeraddress)) + .map_err(|e| anyhow!("send_typed_transaction error {}", e.to_string()))?; + + Ok(tx_bytes.0.to_vec()) + } + + fn get_signed_tx_raw_bytes( + &self, + newclient: Client, + signeraddress: H160, + typedtx: &mut TypedTransaction, + common: &WalletConnectTxCommon, + ) -> Result> { + let mynonce = U256::from_dec_str(&common.nonce)?; + if !mynonce.is_zero() { + typedtx.set_nonce(mynonce); + } + typedtx.set_from(signeraddress); + if !common.chainid == 0 { + typedtx.set_chain_id(common.chainid); + } + if !common.gas_limit.is_empty() { + typedtx.set_gas(U256::from_dec_str(&common.gas_limit)?); + } + if !common.gas_price.is_empty() { + typedtx.set_gas_price(U256::from_dec_str(&common.gas_price)?); + } + + let sig = self + .rt + .block_on(sign_typed_tx(newclient, typedtx, signeraddress)) + .map_err(|e| anyhow!("sign_typed_transaction error {}", e.to_string()))?; + + let signed_tx = &typedtx.rlp_signed(&sig); + Ok(signed_tx.to_vec()) + } + + fn get_sent_tx_raw_bytes( + &self, + newclient: Client, + signeraddress: H160, + typedtx: &mut TypedTransaction, + common: &WalletConnectTxCommon, + ) -> Result> { + let mynonce = U256::from_dec_str(&common.nonce)?; + if !mynonce.is_zero() { + typedtx.set_nonce(mynonce); + } + typedtx.set_from(signeraddress); + if !common.chainid == 0 { + typedtx.set_chain_id(common.chainid); + } + if !common.gas_limit.is_empty() { + typedtx.set_gas(U256::from_dec_str(&common.gas_limit)?); + } + if !common.gas_price.is_empty() { + typedtx.set_gas_price(U256::from_dec_str(&common.gas_price)?); + } + + let tx_bytes = self + .rt + .block_on(send_typed_tx(newclient, typedtx.clone(), signeraddress)) + .map_err(|e| anyhow!("send_typed_transaction error {}", e.to_string()))?; + + Ok(tx_bytes.0.to_vec()) + } + + pub fn sign_transaction( + &mut self, + eip1559_transaction_request: String, + address: [u8; 20], + ) -> Result> { + if self.client.is_none() { + anyhow::bail!("no client"); + } + + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow!("get walllet-connect client error"))?; + let signeraddress = Address::from_slice(&address); + + // parse json string transaction_info to TransactionRequest + let tx: Eip1559TransactionRequest = serde_json::from_str(&eip1559_transaction_request)?; + let typedtx = TypedTransaction::Eip1559(tx); + + let newclient = client.clone(); + let sig = self + .rt + .block_on(sign_typed_tx(newclient, &typedtx, signeraddress)) + .map_err(|e| anyhow!("sign_typed_transaction error {}", e.to_string()))?; + + let signed_tx = &typedtx.rlp_signed(&sig); + Ok(signed_tx.to_vec()) + } + + pub fn send_transaction( + &mut self, + eip1559_transaction_request: String, + address: [u8; 20], + ) -> Result> { + if self.client.is_none() { + anyhow::bail!("no client"); + } + + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow!("get walllet-connect client error"))?; + let signeraddress = Address::from_slice(&address); + + // parse json string transaction_info to TransactionRequest + let tx: Eip1559TransactionRequest = serde_json::from_str(&eip1559_transaction_request)?; + let typedtx = TypedTransaction::Eip1559(tx); + + let newclient = client.clone(); + let tx_bytes = self + .rt + .block_on(send_typed_tx(newclient, typedtx, signeraddress)) + .map_err(|e| anyhow!("send_typed_transaction error {}", e.to_string()))?; + + Ok(tx_bytes.0.to_vec()) + } + + pub fn sign_contract_transaction( + &mut self, + contract_action: String, + common: &WalletConnectTxCommon, + address: [u8; 20], + ) -> Result> { + if self.client.is_none() { + anyhow::bail!("no client"); + } + let signeraddress = Address::from_slice(&address); + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow!("get walllet-connect client error"))?; + let newclient = client.clone(); + + let action: ContractAction = serde_json::from_str(&contract_action)?; + // parse json string transaction_info to TransactionRequest + // let tx: ContractTransfer = serde_json::from_str(&contract_transaction_info)?; + + let mut typedtx = match action { + ContractAction::ContractApproval(approval) => { + self.rt + .block_on(defi_wallet_core_common::construct_contract_approval_tx( + approval, + defi_wallet_core_common::EthNetwork::Custom { + chain_id: common.chainid, + legacy: false, + }, + common.web3api_url.as_str(), + ))? + } + ContractAction::ContractTransfer(transfer) => { + self.rt + .block_on(defi_wallet_core_common::construct_contract_transfer_tx( + transfer, + defi_wallet_core_common::EthNetwork::Custom { + chain_id: common.chainid, + legacy: false, + }, + // TODO unnessary for walletconnect + common.web3api_url.as_str(), + ))? + } + }; + + let tx = self.get_signed_tx_raw_bytes(newclient, signeraddress, &mut typedtx, common)?; + Ok(tx.to_vec()) + } + + pub fn send_contract_transaction( + &mut self, + contract_action: String, + common: &WalletConnectTxCommon, + address: [u8; 20], + ) -> Result> { + if self.client.is_none() { + anyhow::bail!("no client"); + } + let signeraddress = Address::from_slice(&address); + let client = self + .client + .as_ref() + .ok_or_else(|| anyhow!("get walllet-connect client error"))?; + let newclient = client.clone(); + + let action: ContractAction = serde_json::from_str(&contract_action)?; + // parse json string transaction_info to TransactionRequest + // let tx: ContractTransfer = serde_json::from_str(&contract_transaction_info)?; + + let mut typedtx = match action { + ContractAction::ContractApproval(approval) => { + self.rt + .block_on(defi_wallet_core_common::construct_contract_approval_tx( + approval, + defi_wallet_core_common::EthNetwork::Custom { + chain_id: common.chainid, + legacy: false, + }, + common.web3api_url.as_str(), + ))? + } + ContractAction::ContractTransfer(transfer) => { + self.rt + .block_on(defi_wallet_core_common::construct_contract_transfer_tx( + transfer, + defi_wallet_core_common::EthNetwork::Custom { + chain_id: common.chainid, + legacy: false, + }, + // TODO unnessary for walletconnect + common.web3api_url.as_str(), + ))? + } + }; + + let tx = self.get_sent_tx_raw_bytes(newclient, signeraddress, &mut typedtx, common)?; + Ok(tx.to_vec()) + } } diff --git a/wallet-connect/Cargo.toml b/wallet-connect/Cargo.toml index c34b4e19..bc32797c 100644 --- a/wallet-connect/Cargo.toml +++ b/wallet-connect/Cargo.toml @@ -34,6 +34,8 @@ url = { version = "2", features = ["serde"] } x25519-dalek = "1" zeroize = "1" hex = "0.4" +qrcode = "0.12" +image = "0.23" [dev-dependencies] quickcheck = "1" diff --git a/wallet-connect/examples/web3_v2.rs b/wallet-connect/examples/web3_v2.rs index 4010ba2f..f8e952f8 100644 --- a/wallet-connect/examples/web3_v2.rs +++ b/wallet-connect/examples/web3_v2.rs @@ -1,6 +1,38 @@ +use eyre::Result; +use image::Luma; +use qrcode::QrCode; + +use ethers::abi::Address; +use ethers::core::types::transaction::eip2718::TypedTransaction; +//use ethers::ethers_providers::Middleware; +use defi_wallet_connect::v2::WCMiddleware; +use ethers::prelude::*; + +use std::str::FromStr; + use defi_wallet_connect::v2::{Client, ClientOptions, Metadata, RequiredNamespaces, SessionInfo}; use std::error::Error; use std::io::BufRead; + +#[derive(Debug, Default)] +pub struct WalletConnectTxCommon { + pub gas_limit: String, // decimal string, "1" + pub gas_price: String, // decimal string + pub nonce: String, // decimal string + pub chainid: u64, // integer u64 + pub web3api_url: String, // string +} + +/// wallet connect cronos(eth) eip155-tx signing info +#[derive(Debug, Default)] +pub struct WalletConnectTxEip155 { + pub to: String, // hexstring, "0x..." + pub value: String, // decimal string, in wei units + pub data: Vec, // data, as bytes + + pub common: WalletConnectTxCommon, +} + async fn make_client( callback_sender: Option>, ) -> Result { @@ -42,8 +74,132 @@ async fn save(info: &SessionInfo) -> eyre::Result<()> { Ok(()) } +async fn sign_typed_tx( + client: Client, + tx: &TypedTransaction, + address: Address, +) -> Result { + let middleware = WCMiddleware::new(client); + let signature = middleware.sign_transaction(tx, address).await?; + Ok(signature) +} + +async fn send_typed_tx(client: Client, tx: TypedTransaction, address: Address) -> Result { + let middleware = WCMiddleware::new(client).with_sender(address); + let receipt = middleware.send_transaction(tx, None).await?.tx_hash(); + Ok(receipt) +} + +pub async fn sign_eip155_transaction_blocking( + client: &mut Client, + userinfo: &WalletConnectTxEip155, + address: [u8; 20], +) -> Result> { + let signeraddress = Address::from_slice(&address); + + let mut tx = Eip1559TransactionRequest::new(); + + if !userinfo.to.is_empty() { + tx = tx.to(NameOrAddress::Address(Address::from_str(&userinfo.to)?)); + } + if !userinfo.data.is_empty() { + tx = tx.data(userinfo.data.as_slice().to_vec()); + } + if !userinfo.common.gas_limit.is_empty() { + tx = tx.gas(U256::from_dec_str(&userinfo.common.gas_limit)?); + } + if !userinfo.common.gas_price.is_empty() { + tx = tx + .max_priority_fee_per_gas(U256::from_dec_str(&userinfo.common.gas_price)?) + .max_fee_per_gas(U256::from_dec_str(&userinfo.common.gas_price)?); + } + if !userinfo.common.nonce.is_empty() { + tx = tx.nonce(U256::from_dec_str(&userinfo.common.nonce)?); + } + if userinfo.common.chainid != 0 { + // tx = tx.chain_id(userinfo.common.chainid); + } + if !userinfo.value.is_empty() { + tx = tx.value(U256::from_dec_str(&userinfo.value)?); + } + let newclient = client.clone(); + let typedtx = TypedTransaction::Eip1559(tx); + + let mut sig = sign_typed_tx(newclient, &typedtx, signeraddress) + .await + .map_err(|e| eyre::eyre!("sign_typed_transaction error {}", e.to_string()))?; + + // eip155 v == chainid*2 + 35 + recovery (0 or 1), for mainnet 37 or 38 + // non eip155 v == 27 + recovery (0 or 1) + if sig.v == 27 || sig.v == 28 { + let recovery = sig.v - 27; + sig.v = recovery + 35 + userinfo.common.chainid * 2; + } + + let signed_tx = &typedtx.rlp_signed(&sig); + Ok(signed_tx.to_vec()) +} + +pub async fn send_eip155_transaction_blocking( + client: &mut Client, + userinfo: &WalletConnectTxEip155, + address: [u8; 20], +) -> Result> { + let signeraddress = Address::from_slice(&address); + + let mut tx = Eip1559TransactionRequest::new(); + + if !userinfo.to.is_empty() { + tx = tx.to(NameOrAddress::Address(Address::from_str(&userinfo.to)?)); + } + if !userinfo.data.is_empty() { + tx = tx.data(userinfo.data.as_slice().to_vec()); + } + if !userinfo.common.gas_limit.is_empty() { + tx = tx.gas(U256::from_dec_str(&userinfo.common.gas_limit)?); + } + if !userinfo.common.gas_price.is_empty() { + tx = tx + .max_priority_fee_per_gas(U256::from_dec_str(&userinfo.common.gas_price)?) + .max_fee_per_gas(U256::from_dec_str(&userinfo.common.gas_price)?); + } + if !userinfo.common.nonce.is_empty() { + tx = tx.nonce(U256::from_dec_str(&userinfo.common.nonce)?); + } + if userinfo.common.chainid != 0 { + // tx = tx.chain_id(userinfo.common.chainid); + } + if !userinfo.value.is_empty() { + tx = tx.value(U256::from_dec_str(&userinfo.value)?); + } + + let newclient = client.clone(); + let typedtx = TypedTransaction::Eip1559(tx); + + // print typedtx + println!("typedtx: {:?}", typedtx); + println!("send tx: {:?}", typedtx); + let tx_bytes = send_typed_tx(newclient, typedtx, signeraddress) + .await + .map_err(|e| eyre::eyre!("send_typed_transaction error {}", e.to_string()))?; + + //Ok(tx_bytes.0.to_vec()) + Ok(tx_bytes.0.to_vec()) +} + +async fn make_qrcode(uri: &str) -> Result<()> { + // Generate the QR code for the data you want + let code = QrCode::new(uri)?; + + // Create an empty image buffer + let image = code.render::>().build(); + image.save("qrcode.png")?; + + Ok(()) +} #[tokio::main] async fn main() -> Result<(), Box> { + println!("walletconnect v2.0"); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); tokio::spawn(async move { while let Some(msg) = rx.recv().await { @@ -60,11 +216,15 @@ async fn main() -> Result<(), Box> { make_client(callback_sender).await? }; - let test_ping = true; - let test_signing = true; + let test_ping = false; + let test_signing = false; + let test_tx = true; let test_event_listening = false; let uri = client.get_connection_string().await; + // make qrimage with uri + make_qrcode(&uri).await?; + println!("uri= {}", uri); let namespaces = client.ensure_session().await?; println!( @@ -84,6 +244,35 @@ async fn main() -> Result<(), Box> { println!("sig1: {:?}", sig1); } + if test_tx { + // read env MYTOADDRESS + let to = std::env::var("MYTOADDRESS").expect("MYTOADDRESS not set"); + let fromaddress = namespaces.get_ethereum_addresses()[0].address.clone(); + // print fromaddress + println!("fromaddress= {:?}", fromaddress); + let txinfo = WalletConnectTxEip155 { + common: WalletConnectTxCommon { + chainid: 5, + gas_limit: "21000".into(), + gas_price: "1000000000".into(), + nonce: "0".into(), + web3api_url: "".into(), + }, + to: to.into(), + data: vec![], + value: "1000".into(), + }; + let sig = sign_eip155_transaction_blocking( + &mut client, + &txinfo, + (*fromaddress.as_fixed_bytes()).into(), + ) + .await?; + let sig_hex = hex::encode(sig.as_slice()); + let sig_hex_length = sig_hex.len(); + println!("signature length {sig_hex_length} 0x{sig_hex}"); + } + if test_event_listening { println!("press anykey to exit"); loop { diff --git a/wallet-connect/src/v2/client.rs b/wallet-connect/src/v2/client.rs index 75ade543..061bf856 100644 --- a/wallet-connect/src/v2/client.rs +++ b/wallet-connect/src/v2/client.rs @@ -195,6 +195,9 @@ impl WCMiddleware> { pub fn new(client: Client) -> Self { WCMiddleware(Provider::new(client)) } + pub fn with_sender(self, address: impl Into
) -> Self { + WCMiddleware(self.0.with_sender(address)) + } } /// The wrapper error type for `ethers` middleware-related issues @@ -219,6 +222,22 @@ impl MiddlewareError for WCError { } } +fn pad_zero(s: String) -> String { + if s.len() % 2 != 0 { + format!("0{s}") + } else { + s + } +} + +fn append_hex(s: String) -> String { + if s.starts_with("0x") { + s + } else { + format!("0x{s}") + } +} + #[async_trait] impl Middleware for WCMiddleware> { type Error = WCError>; @@ -245,23 +264,23 @@ impl Middleware for WCMiddleware> { } if let Some(data) = tx.data() { tx_obj.insert("data", format!("0x{}", hex::encode(data))); - } else { - tx_obj.insert("data", "".to_string()); } if let Some(gas) = tx.gas() { - tx_obj.insert("gas", format!("0x{gas:x}")); + // gas not working for webwallet + tx_obj.insert("gasLimit", append_hex(pad_zero(format!("{gas:x}")))); } + if let Some(gas_price) = tx.gas_price() { - tx_obj.insert("gasPrice", format!("0x{gas_price:x}")); + tx_obj.insert("gasPrice", append_hex(pad_zero(format!("{gas_price:x}")))); } if let Some(value) = tx.value() { - tx_obj.insert("value", format!("0x{value:x}")); + tx_obj.insert("value", append_hex(pad_zero(format!("{value:x}")))); } if let Some(nonce) = tx.nonce() { - tx_obj.insert("nonce", format!("0x{nonce:x}")); + tx_obj.insert("nonce", append_hex(pad_zero(format!("{nonce:x}")))); } if let Some(c) = tx.chain_id() { - tx_obj.insert("chainId", format!("0x{c:x}")); + tx_obj.insert("chainId", append_hex(pad_zero(format!("{c:x}")))); } // TODO: put those error cases to WCError instead of wrapping in eyre let tx_bytes: Bytes = self