diff --git a/Cargo.lock b/Cargo.lock index a00f441..d679c48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1969,6 +1969,19 @@ dependencies = [ "fxhash", ] +[[package]] +name = "hdwallet" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a03ba7d4c9ea41552cd4351965ff96883e629693ae85005c501bb4b9e1c48a7" +dependencies = [ + "lazy_static", + "rand_core", + "ring 0.16.20", + "secp256k1 0.26.0", + "thiserror", +] + [[package]] name = "heck" version = "0.4.1" @@ -3740,13 +3753,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4124a35fe33ae14259c490fd70fa199a32b9ce9502f2ee6bc4f81ec06fa65894" +dependencies = [ + "secp256k1-sys 0.8.1", +] + [[package]] name = "secp256k1" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acea373acb8c21ecb5a23741452acd2593ed44ee3d343e72baaa143bc89d0d5" dependencies = [ - "secp256k1-sys", + "secp256k1-sys 0.9.1", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", ] [[package]] @@ -4143,6 +4174,7 @@ dependencies = [ "ethers", "ethers-core", "futures 0.3.29", + "hdwallet", "hex", "http", "hyper", @@ -4154,7 +4186,7 @@ dependencies = [ "reqwest", "rustls", "rustls-pemfile", - "secp256k1", + "secp256k1 0.28.0", "serde", "serde_json", "serde_yaml", @@ -4199,7 +4231,7 @@ dependencies = [ "reqwest", "rustls", "rustls-pemfile", - "secp256k1", + "secp256k1 0.28.0", "serde", "serde_json", "serde_yaml", diff --git a/subfile-exchange/Cargo.toml b/subfile-exchange/Cargo.toml index 5499144..e2cefe2 100644 --- a/subfile-exchange/Cargo.toml +++ b/subfile-exchange/Cargo.toml @@ -28,6 +28,7 @@ ethers = "2.0.11" # ethers = {version = "2.0.11", features = [ "abigen-online" ]} ethers-core = "2.0.11" futures = { version = "0.3", features = ["compat"] } +hdwallet = "0.4.1" hex = "0.4.3" http = "0.2" hyper = { version = "0.14.27", features = [ "server" ]} @@ -43,7 +44,7 @@ secp256k1 = "0.28.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" -sha2 = "0.10.8" +sha2 = "0.10" tap_core = { version = "0.7.0", git = "https://github.com/semiotic-ai/timeline-aggregation-protocol" } tempfile = "3.2.0" tokio = { version = "1.28", features = ["time", "sync", "macros", "test-util", "rt-multi-thread"] } diff --git a/subfile-exchange/src/config.rs b/subfile-exchange/src/config.rs index c06a309..92ce572 100644 --- a/subfile-exchange/src/config.rs +++ b/subfile-exchange/src/config.rs @@ -7,6 +7,8 @@ use tracing::subscriber::SetGlobalDefaultError; use tracing_subscriber::EnvFilter; use tracing_subscriber::FmtSubscriber; +use crate::util::parse_key; + #[derive(Clone, Debug, Parser, Serialize, Deserialize)] #[command( name = "subfile-exchange", @@ -51,12 +53,12 @@ impl Cli { pub enum Role { Downloader(DownloaderArgs), Publisher(PublisherArgs), - Server(ServerArgs), + Wallet(WalletArgs), } #[derive(Clone, Debug, Args, Serialize, Deserialize, Default)] #[group(required = false, multiple = true)] -pub struct ServerArgs { +pub struct WalletArgs { #[arg( long, value_name = "HOST", @@ -64,7 +66,7 @@ pub struct ServerArgs { env = "HOST", help = "Subfile server host" )] - pub host: String, + pub host: Option, #[arg( long, value_name = "PORT", @@ -72,46 +74,47 @@ pub struct ServerArgs { env = "PORT", help = "Subfile server port" )] - pub port: usize, - // Taking from config right now, later can read from DB table for managing server states - #[arg( - long, - value_name = "SUBFILES", - env = "SUBFILES", - value_delimiter = ',', - help = "Comma separated list of IPFS hashes and local location of the subfiles to serve upon start-up; format: [ipfs_hash:local_path]" - )] - pub subfiles: Vec, + pub port: Option, #[clap( long, - value_name = "free-query-auth-token", - env = "FREE_QUERY_AUTH_TOKEN", - help = "Auth token that clients can use to query for free" + value_name = "KEY", + value_parser = parse_key, + env = "PRIVATE_KEY", + hide_env_values = true, + help = "Private key to the Graphcast ID wallet (Precendence over mnemonics)", )] - pub free_query_auth_token: Option, + pub private_key: Option, #[clap( long, - value_name = "admin-auth-token", - env = "ADMIN_AUTH_TOKEN", - help = "Admin Auth token for server management" + value_name = "KEY", + value_parser = parse_key, + env = "MNEMONIC", + hide_env_values = true, + help = "Mnemonic to the Graphcast ID wallet (first address of the wallet is used; Only one of private key or mnemonic is needed)", )] - pub admin_auth_token: Option, + pub mnemonic: Option, #[clap( long, - value_name = "mnemonic", - env = "MNEMONIC", - help = "Mnemonic for the operator wallet" + value_name = "provider_url", + env = "PROVIDER", + help = "Blockchain provider endpoint" )] - pub mnemonic: String, - //TODO: More complex price management - #[arg( + pub provider: String, + //TODO: chain id should be resolvable through provider + // #[clap( + // long, + // value_name = "chain_id", + // env = "CHAIN_ID", + // help = "Protocol network's Chain ID" + // )] + // pub chain_id: u64, + #[clap( long, - value_name = "PRICE_PER_BYTE", - default_value = "1", - env = "PRICE_PER_BYTE", - help = "Price per byte; price do not currently have a unit, perhaps use DAI or GRT, refer to TAP" + value_name = "verifier", + env = "VERIFIER", + help = "TAP verifier contract address" )] - pub price_per_byte: f32, + pub verifier: Option, } #[derive(Clone, Debug, Args, Serialize, Deserialize, Default)] diff --git a/subfile-exchange/src/errors.rs b/subfile-exchange/src/errors.rs index f4a4929..4c177b9 100644 --- a/subfile-exchange/src/errors.rs +++ b/subfile-exchange/src/errors.rs @@ -16,6 +16,7 @@ pub enum Error { InvalidPriceFormat(String), ContractError(String), ObjectStoreError(object_store::Error), + WalletError(ethers::signers::WalletError), } impl fmt::Display for Error { @@ -35,6 +36,7 @@ impl fmt::Display for Error { Error::InvalidPriceFormat(ref msg) => write!(f, "Price format error: {}", msg), Error::ContractError(ref msg) => write!(f, "Contract call error: {}", msg), Error::ObjectStoreError(ref err) => write!(f, "Object store error: {}", err), + Error::WalletError(ref err) => write!(f, "Wallet error: {}", err), } } } diff --git a/subfile-exchange/src/main.rs b/subfile-exchange/src/main.rs index 0900fa2..4d20457 100644 --- a/subfile-exchange/src/main.rs +++ b/subfile-exchange/src/main.rs @@ -53,11 +53,23 @@ async fn main() { } } } - Role::Server(server_args) => { + Role::Wallet(wallet_args) => { tracing::info!( - server = tracing::field::debug(&server_args), - "Use subfile-service crate" + server = tracing::field::debug(&wallet_args), + "Use the provided wallet to send transactions" ); + + // Server enable payments through the staking contract, + // assume indexer is already registered on the staking registry contract + //1. `allocate` - indexer address, Qm hash in bytes32, token amount, allocation_id, metadata: utils.hexlify(Array(32).fill(0)), allocation_id_proof + //2. `close_allocate` -allocationID: String, poi: BytesLike (0x0 32bytes) + //3. `close_allocate` and then `allocate` + // receipt validation and storage is handled by the indexer-service framework + // receipt redemption is handled by indexer-agent + + // Client payments - assume client signer is valid (should work without gateways) + //1. `deposit` - to a sender address and an amount + //2. `depositMany` - to Vec } } } diff --git a/subfile-exchange/src/util.rs b/subfile-exchange/src/util.rs index 1f695fe..36ca567 100644 --- a/subfile-exchange/src/util.rs +++ b/subfile-exchange/src/util.rs @@ -1,16 +1,313 @@ -use ethers::signers::{ - coins_bip39::English, LocalWallet, MnemonicBuilder, Signer, Wallet, WalletError, -}; +use alloy_primitives::U256; +use ethers::signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer, Wallet}; use ethers_core::k256::ecdsa::SigningKey; +use ethers_core::types::H160; +use hdwallet::{ChainPath, DefaultKeyChain, KeyChain}; +use std::{fmt, iter, str}; + +use crate::errors::Error; /// Build Wallet from Private key or Mnemonic -pub fn build_wallet(value: &str) -> Result, WalletError> { +pub fn build_wallet(value: &str) -> Result, Error> { value .parse::() - .or(MnemonicBuilder::::default().phrase(value).build()) + .or(MnemonicBuilder::::default() + .phrase(value) + .build() + .map_err(Error::WalletError)) } /// Get wallet public address to String pub fn wallet_address(wallet: &Wallet) -> String { format!("{:?}", wallet.address()) } + +/// Validate that private key as an Eth wallet +pub fn parse_key(value: &str) -> Result { + let wallet = build_wallet(value)?; + let address = wallet_address(&wallet); + tracing::trace!(address, "Resolved wallet address"); + Ok(String::from(value)) +} + +// Given a HD wallet, utilize the additional paths to generate child wallet, +// return child wallet public key, private key, wallet address +pub fn derive_key_pair( + key_chain: &DefaultKeyChain, + epoch: u64, + qm_hash: &str, + index: u64, +) -> Result<(String, H160), Error> { + let path = format!( + "m/{}", + std::iter::once(epoch.to_string()) + .chain(qm_hash.as_bytes().iter().map(|b| b.to_string())) + .chain(std::iter::once(index.to_string())) + .collect::>() + .join("/") + ); + let chain_path = ChainPath::from(path); + + let (derived_key, _) = key_chain + .derive_private_key(chain_path) + .map_err(|e| Error::ContractError(e.to_string()))?; + let private_key = derived_key.private_key.display_secret().to_string(); + let wallet = build_wallet(&private_key)?; + + Ok((private_key, wallet.address())) +} + +/* Token unit and formatting */ +const ONE_18: u128 = 1_000_000_000_000_000_000; + +/// GRT with 18 fractional digits +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct GRT(pub UDecimal18); + +/// Represents a positive decimal value with 18 fractional digits precision. Using U256 as storage. +#[derive(Copy, Clone, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct UDecimal18(U256); + +impl From for UDecimal18 { + fn from(value: U256) -> Self { + Self(U256::from(value) * U256::from(ONE_18)) + } +} + +impl From for UDecimal18 { + fn from(value: u128) -> Self { + Self::from(U256::from(value)) + } +} + +impl TryFrom for UDecimal18 { + type Error = >::Error; + fn try_from(value: f64) -> Result { + U256::try_from(value * 1e18).map(Self) + } +} + +impl From for f64 { + fn from(value: UDecimal18) -> Self { + f64::from(value.0) * 1e-18 + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct InvalidDecimalString; + +impl fmt::Display for InvalidDecimalString { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "invalid decimal string") + } +} + +impl str::FromStr for UDecimal18 { + type Err = InvalidDecimalString; + fn from_str(s: &str) -> Result { + // We require at least one ASCII digit. Otherwise `U256::from_str_radix` will return 0 for + // some inputs we consider invalid. + if !s.chars().any(|c: char| -> bool { c.is_ascii_digit() }) { + return Err(InvalidDecimalString); + } + let (int, frac) = s.split_at(s.chars().position(|c| c == '.').unwrap_or(s.len())); + let p = 18; + let digits = int + .chars() + // append fractional digits (after decimal point) + .chain(frac.chars().skip(1).chain(iter::repeat('0')).take(p)) + .collect::(); + Ok(UDecimal18( + U256::from_str_radix(&digits, 10).map_err(|_| InvalidDecimalString)?, + )) + } +} + +impl fmt::Display for UDecimal18 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.0 == U256::from(0) { + return write!(f, "0"); + } + let p = 18; + let digits = self.0.to_string().into_bytes(); + let ctz = digits + .iter() + .rev() + .take_while(|&&b| b == b'0') + .count() + .min(p); + if digits.len() < p { + let fill = "0".repeat(p - digits.len()); + let frac = &digits[0..digits.len() - ctz]; + write!(f, "0.{}{}", fill, unsafe { str::from_utf8_unchecked(frac) }) + } else { + let (mut int, mut frac) = digits.split_at(digits.len() - p); + frac = &frac[0..frac.len() - ctz]; + if int.is_empty() { + int = &[b'0']; + } + if ctz == p { + write!(f, "{}", unsafe { str::from_utf8_unchecked(int) }) + } else { + write!( + f, + "{}.{}", + unsafe { str::from_utf8_unchecked(int) }, + unsafe { str::from_utf8_unchecked(frac) } + ) + } + } + } +} + +impl fmt::Debug for UDecimal18 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{self}") + } +} + +impl UDecimal18 { + /// This will use the value of the given U256 directly, without scaling by 1e18. + pub fn from_raw_u256(value: U256) -> Self { + Self(value) + } + + pub fn raw_u256(&self) -> &U256 { + &self.0 + } + + pub fn as_u128(&self) -> Option { + if self.0 % U256::from(ONE_18) > U256::ZERO { + return None; + } + let inner = self.0 / U256::from(ONE_18); + inner.try_into().ok() + } + + pub fn saturating_add(self, rhs: Self) -> Self { + Self(self.0.saturating_add(rhs.0)) + } +} + +impl std::ops::Add for UDecimal18 { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + Self(self.0.add(rhs.0)) + } +} + +impl std::ops::Mul for UDecimal18 { + type Output = Self; + fn mul(self, rhs: Self) -> Self::Output { + Self((self.0 * rhs.0) / U256::from(ONE_18)) + } +} + +impl std::ops::Div for UDecimal18 { + type Output = Self; + fn div(self, rhs: Self) -> Self::Output { + Self((self.0 * U256::from(ONE_18)) / rhs.0) + } +} + +impl std::iter::Sum for UDecimal18 { + fn sum>(iter: I) -> Self { + Self(iter.map(|u| u.0).sum()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn u256_from_str() { + assert_eq!("100".parse::().unwrap(), U256::from(100)); + assert_eq!("0x100".parse::().unwrap(), U256::from(256)); + } + + #[test] + fn udecimal18_from_str() { + let tests: &[(&str, Option<(&str, u128)>)] = &[ + ("", None), + ("?", None), + (".", None), + ("1.1.1", None), + ("10.10?1", None), + ("1?0.01", None), + ("0", Some(("0", 0))), + ("0.0", Some(("0", 0))), + (".0", Some(("0", 0))), + ("0.", Some(("0", 0))), + ("00.00", Some(("0", 0))), + ("1", Some(("1", ONE_18))), + ("1.0", Some(("1", ONE_18))), + ("1.", Some(("1", ONE_18))), + ("0.1", Some(("0.1", ONE_18 / 10))), + (".1", Some(("0.1", ONE_18 / 10))), + ("0.0000000000000000012", Some(("0.000000000000000001", 1))), + ("0.001001", Some(("0.001001", 1_001_000_000_000_000))), + ("0.001", Some(("0.001", ONE_18 / 1_000))), + ("100.001", Some(("100.001", 100_001_000_000_000_000_000))), + ("100.000", Some(("100", 100 * ONE_18))), + ("123.0", Some(("123", 123 * ONE_18))), + ("123", Some(("123", 123 * ONE_18))), + ( + "123456789123456789.123456789123456789123456789", + Some(( + "123456789123456789.123456789123456789", + 123_456_789_123_456_789_123_456_789_123_456_789, + )), + ), + ]; + for (input, expected) in tests { + let output = input.parse::(); + println!("\"{input}\" => {output:?}"); + match expected { + &Some((repr, internal)) => { + assert_eq!(output.as_ref().map(|d| d.0), Ok(U256::from(internal))); + assert_eq!(output.as_ref().map(ToString::to_string), Ok(repr.into())); + } + None => assert_eq!(output, Err(InvalidDecimalString)), + } + } + } + + #[test] + fn udecimal_from_f64() { + let tests = [ + 0.0, + 0.5, + 0.01, + 0.0042, + 1.0, + 123.456, + 1e14, + 1e17, + 1e18, + 1e19, + 2.0f64.powi(128) - 1.0, + 2.0f64.powi(128), + 1e26, + 1e27, + 1e28, + 1e29, + 1e30, + 1e31, + 1e32, + ]; + for test in tests { + let expected = (test * 1e18_f64).floor(); + let decimal = UDecimal18::try_from(test).unwrap(); + let output = decimal.0.to_string().parse::().unwrap(); + let error = (expected - output).abs() / expected.max(1e-30); + println!( + "expected: {}\n decimal: {}\n error: {:.3}%\n---", + expected / 1e18, + decimal, + error * 100.0 + ); + assert!(error < 0.005); + } + } +} diff --git a/subfile-service/src/subfile_server/util.rs b/subfile-service/src/subfile_server/util.rs index 0f9e029..410f04a 100644 --- a/subfile-service/src/subfile_server/util.rs +++ b/subfile-service/src/subfile_server/util.rs @@ -1,5 +1,4 @@ use build_info::BuildInfo; -use ethers::signers::WalletError; use serde::{Deserialize, Serialize}; use std::fs; @@ -92,10 +91,9 @@ fn load_private_key(filename: &str) -> Result { } /// Validate that private key as an Eth wallet -pub fn public_key(value: &str) -> Result { - // The wallet can be stored instead of the original private key +pub fn public_key(value: &str) -> Result { let wallet = build_wallet(value)?; let addr = wallet_address(&wallet); - tracing::info!(address = addr, "Resolved Graphcast id"); + tracing::trace!(address = addr, "Resolved wallet address"); Ok(addr) }