diff --git a/.gitignore b/.gitignore index fedaa2b..5c6f94c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .env +.idea/ diff --git a/Cargo.lock b/Cargo.lock index e82bad0..402eee4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + [[package]] name = "base64" version = "0.13.1" @@ -211,6 +217,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.10.0-beta" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" + [[package]] name = "bit-set" version = "0.5.3" @@ -250,6 +262,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -379,6 +400,33 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "ciborium" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" + +[[package]] +name = "ciborium-ll" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -428,7 +476,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5286a0843c21f8367f7be734f89df9b822e0321d8bcce8d6e735aadff7d74979" dependencies = [ "base64 0.21.5", - "bech32", + "bech32 0.9.1", "bs58", "digest", "generic-array", @@ -491,6 +539,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crc16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" + [[package]] name = "crc32fast" version = "1.3.2" @@ -1378,6 +1432,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "hashbrown" version = "0.14.2" @@ -1411,6 +1471,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "hmac" version = "0.12.1" @@ -1787,6 +1853,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.7.3" @@ -1936,17 +2011,27 @@ name = "offchain-gateway-rs" version = "0.0.1" dependencies = [ "axum", + "base32", + "bech32 0.10.0-beta", + "blake2", + "bs58", "chrono", + "ciborium", + "crc16", + "crc32fast", "dotenvy", "ethers", "ethers-contract", "ethers-contract-derive", "ethers-core", "hex", + "hex-literal", + "lazy_static", "postgres", "postgres-types", "serde", "serde_json", + "sha2", "thiserror", "tokio", "tokio-postgres", @@ -2490,10 +2575,19 @@ checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata", + "regex-automata 0.4.3", "regex-syntax 0.8.2", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + [[package]] name = "regex-automata" version = "0.4.3" @@ -2505,6 +2599,12 @@ dependencies = [ "regex-syntax 0.8.2", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.7.5" @@ -3517,10 +3617,14 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] diff --git a/Cargo.toml b/Cargo.toml index 8430d7e..a62bb2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,27 @@ serde_json = "1.0.108" thiserror = "1.0.50" tokio = {version = "1", features = ["full"]} tokio-postgres = "0.7.10" -tower-http = { version = "0.4.3", features = ["cors"] } +tower-http = { version = "0.4.4", features = ["cors"] } tracing = "0.1.40" -tracing-subscriber = "0.3.18" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"]} +lazy_static = { version = "1.4.0", features = [] } + +# Multicoin encoding +bs58 = "0.5.0" +base32 = "0.4.0" +bech32 = "0.10.0-beta" +blake2 = "0.10.6" +sha2 = "0.10.8" +crc16 = "0.4.0" +ciborium = "0.2.1" +crc32fast = "1.3.2" + +[dev-dependencies] +hex-literal = "0.4.1" [features] postgres = [] selfservice = [] eoa-auth = [] -default = ["postgres", "selfservice", "eoa-auth"] +admin-auth = [] +default = ["postgres", "selfservice", "eoa-auth", "admin-auth"] diff --git a/src/gateway/endpoint.rs b/src/gateway/endpoint.rs index 0a68937..88f69c6 100644 --- a/src/gateway/endpoint.rs +++ b/src/gateway/endpoint.rs @@ -24,11 +24,11 @@ pub async fn route( #[derive(Debug, Error)] pub enum CCIPEndpointError { - #[error("Invalid prefix {0}")] + #[error("Invalid prefix: {0}")] DecodeError(#[from] super::payload::ResolverDecodeError), - #[error("Resolve error {0}")] + #[error("Resolve error: {0}")] ResolveError(#[from] super::resolution::ResolveError), - #[error("Sign error {0}")] + #[error("Sign error: {0}")] SignError(#[from] super::signing::SignError), } diff --git a/src/gateway/resolution.rs b/src/gateway/resolution.rs index 7b3f606..7f51033 100644 --- a/src/gateway/resolution.rs +++ b/src/gateway/resolution.rs @@ -1,9 +1,11 @@ use std::sync::Arc; -use ethers::{abi::Token, providers::namehash, types::H160, utils::keccak256}; +use ethers::{abi::Token, providers::namehash, utils::keccak256}; use thiserror::Error; -use tracing::info; +use tracing::{debug, info}; +use crate::multicoin::cointype::coins::CoinType; +use crate::multicoin::encoding::MulticoinEncoder; use crate::{ccip::lookup::ResolverFunctionCall, state::GlobalState}; use super::{payload::ResolveCCIPPostPayload, signing::UnsignedPayload}; @@ -56,20 +58,26 @@ impl UnresolvedQuery<'_> { vec![Token::String(value)] } ResolverFunctionCall::AddrMultichain(_bf, chain) => { - info!(name = self.name, chain = chain, "Resolution Address Multichain"); + info!( + name = self.name, + chain = chain, + "Resolution Address Multichain" + ); let hash = namehash(&self.name).to_fixed_bytes().to_vec(); - let x = state.db.get_addresses(&hash, &[&chain.to_string()]).await; + let addresses = state.db.get_addresses(&hash, &[&chain.to_string()]).await; - let value = x + let value: &str = addresses .get(&chain.to_string()) - .to_owned() .ok_or(ResolveError::NotFound)? - .clone() + .as_ref() .ok_or(ResolveError::NotFound)?; - let bytes = value.as_bytes().to_vec(); + let bytes = CoinType::from(*chain as u32).encode(value).map_err(|err| { + debug!("error while trying to encode {}: {}", chain, err); + ResolveError::Unparsable + })?; vec![Token::Bytes(bytes)] } diff --git a/src/http.rs b/src/http.rs index 52712dc..6e7111b 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,6 +1,5 @@ -use std::{net::SocketAddr, sync::Arc}; +use std::{env, net::SocketAddr, sync::Arc}; -use crate::state::GlobalState; use axum::{ routing::{get, post}, Router, Server, @@ -8,6 +7,8 @@ use axum::{ use tower_http::cors::CorsLayer; use tracing::{debug, info}; +use crate::state::GlobalState; + /// Starts the HTTP Server pub async fn serve(state: GlobalState) { info!("Starting webserver"); @@ -24,7 +25,13 @@ pub async fn serve(state: GlobalState) { .with_state(Arc::new(state)) .layer(CorsLayer::very_permissive()); - let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); + let addr = SocketAddr::from(( + [0, 0, 0, 0], + env::var("PORT") + .unwrap_or("3000".to_string()) + .parse::() + .expect("port should fit in u16"), + )); debug!("Listening on {}", addr); Server::bind(&addr) diff --git a/src/main.rs b/src/main.rs index 9564415..83f50aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,21 +2,33 @@ use std::{env, str::FromStr}; use dotenvy::dotenv; use ethers::signers::{LocalWallet, Signer}; -use tracing::info; +use tracing::{info, Level}; +use tracing_subscriber::{EnvFilter, FmtSubscriber}; -mod http; pub mod ccip; -pub mod state; -pub mod utils; -pub mod gateway; pub mod database; +pub mod gateway; +mod http; +pub mod multicoin; pub mod selfservice; +pub mod state; +pub mod utils; #[tokio::main] async fn main() { dotenv().ok(); - tracing_subscriber::fmt().init(); + let filter = EnvFilter::new(format!("offchain_gateway={}", Level::DEBUG)); + + let subscriber = FmtSubscriber::builder() + // all spans/events with a level higher than TRACE (e.g, debug, info, warn, etc.) + // will be written to stdout. + .with_max_level(Level::DEBUG) + .with_env_filter(filter) + // completes the builder. + .finish(); + + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); let db = database::bootstrap().await; diff --git a/src/multicoin/cointype/coins.rs b/src/multicoin/cointype/coins.rs new file mode 100644 index 0000000..fdcb5c9 --- /dev/null +++ b/src/multicoin/cointype/coins.rs @@ -0,0 +1,38 @@ +use super::slip44::SLIP44; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CoinType { + Slip44(SLIP44), + Evm, +} + +impl From for CoinType { + fn from(value: u32) -> CoinType { + if value >= 0x8000_0000 { + return CoinType::Evm; + } + + SLIP44::from(value).into() + } +} + +#[cfg(test)] +mod tests { + use super::super::slip44::SLIP44; + use super::*; + + #[test] + fn test_coin_type() { + assert_eq!(CoinType::from(0), SLIP44::Bitcoin.into()); + } + + #[test] + fn test_coin_type_evm() { + assert_eq!(CoinType::from(2147483649), CoinType::Evm); + } + + #[test] + fn test_coin_type_evm_gnosis() { + assert_eq!(CoinType::from(2147483748), CoinType::Evm); + } +} diff --git a/src/multicoin/cointype/mod.rs b/src/multicoin/cointype/mod.rs new file mode 100644 index 0000000..193e2d9 --- /dev/null +++ b/src/multicoin/cointype/mod.rs @@ -0,0 +1,4 @@ +use self::coins::CoinType; + +pub mod coins; +pub mod slip44; diff --git a/src/multicoin/cointype/slip44.rs b/src/multicoin/cointype/slip44.rs new file mode 100644 index 0000000..97a2f7e --- /dev/null +++ b/src/multicoin/cointype/slip44.rs @@ -0,0 +1,48 @@ +use ethers_core::types::U256; + +use super::CoinType; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SLIP44 { + Hedera, + Ripple, + Solana, + Cardano, + Stellar, + Bitcoin, + Binance, + Litecoin, + Dogecoin, + Ethereum, + Polkadot, + Rootstock, + EthereumClassic, + Other(U256), +} + +impl From for SLIP44 { + fn from(val: u32) -> SLIP44 { + match val { + 0 => SLIP44::Bitcoin, + 2 => SLIP44::Litecoin, + 3 => SLIP44::Dogecoin, + 60 => SLIP44::Ethereum, + 61 => SLIP44::EthereumClassic, + 144 => SLIP44::Ripple, + 148 => SLIP44::Stellar, + 3030 => SLIP44::Hedera, + 1815 => SLIP44::Cardano, + 137 => SLIP44::Rootstock, + 714 => SLIP44::Binance, + 501 => SLIP44::Solana, + 354 => SLIP44::Polkadot, + val => SLIP44::Other(val.into()), + } + } +} + +impl From for CoinType { + fn from(val: SLIP44) -> Self { + CoinType::Slip44(val) + } +} diff --git a/src/multicoin/encoding/binance.rs b/src/multicoin/encoding/binance.rs new file mode 100644 index 0000000..5f0f784 --- /dev/null +++ b/src/multicoin/encoding/binance.rs @@ -0,0 +1,26 @@ +use bech32::primitives::hrp::Hrp; +use lazy_static::lazy_static; + +use super::{MulticoinEncoder, MulticoinEncoderError}; + +lazy_static! { + static ref BNB_HRP: Hrp = Hrp::parse_unchecked("bnb"); +} + +pub struct BinanceEncoder {} + +impl MulticoinEncoder for BinanceEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + let (hrp, data) = bech32::decode(data).map_err(|_| { + MulticoinEncoderError::InvalidStructure("failed to decode bech32".to_string()) + })?; + + if hrp != *BNB_HRP { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid binance hrp".to_string(), + )); + } + + Ok(data) + } +} diff --git a/src/multicoin/encoding/bitcoin.rs b/src/multicoin/encoding/bitcoin.rs new file mode 100644 index 0000000..fab3dca --- /dev/null +++ b/src/multicoin/encoding/bitcoin.rs @@ -0,0 +1,85 @@ +// use lazy_static::lazy_static; + +use crate::multicoin::encoding::segwit::SegWitEncoder; + +use super::{p2pkh::P2PKHEncoder, p2sh::P2SHEncoder, MulticoinEncoder, MulticoinEncoderError}; + +pub struct BitcoinEncoder { + segwit_encoder: Option, + p2pkh_encoder: P2PKHEncoder, + p2sh_encoder: P2SHEncoder, +} + +impl BitcoinEncoder { + pub fn new( + segwit_hrp: Option<&str>, + p2pkh_versions: &'static [u8], + p2sh_versions: &'static [u8], + ) -> Self { + Self { + segwit_encoder: segwit_hrp.map(|hrp| SegWitEncoder::new(hrp)), + p2pkh_encoder: P2PKHEncoder { + accepted_versions: p2pkh_versions, + }, + p2sh_encoder: P2SHEncoder { + accepted_versions: p2sh_versions, + }, + } + } +} + +impl MulticoinEncoder for BitcoinEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + if let Some(segwit_encoder) = &self.segwit_encoder { + if let Ok(address) = segwit_encoder.encode(data) { + return Ok(address); + } + } + + self.p2pkh_encoder + .encode(data) + .or_else(|_| self.p2sh_encoder.encode(data)) + .map_err(|_| MulticoinEncoderError::InvalidStructure(String::new())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_btc_p2pkh() { + let decoded = BitcoinEncoder::new(Some("bc"), &[0x00], &[0x05]) + .encode("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa") + .unwrap(); + + assert_eq!( + decoded, + &hex_literal::hex!("76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac") + ); + } + + #[tokio::test] + async fn test_btc_p2sh() { + let decoded = BitcoinEncoder::new(Some("bc"), &[0x00], &[0x05]) + .encode("3Ai1JZ8pdJb2ksieUV8FsxSNVJCpoPi8W6") + .unwrap(); + + assert_eq!( + decoded, + &hex_literal::hex!("a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1887") + ); + } + + #[tokio::test] + async fn test_btc_segwit() { + let decoded = BitcoinEncoder::new(Some("bc"), &[0x00], &[0x05]) + .encode("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4") + .unwrap(); + + assert_eq!( + decoded, + &hex_literal::hex!("0014751e76e8199196d454941c45d1b3a323f1433bd6") + ); + } +} diff --git a/src/multicoin/encoding/cardano.rs b/src/multicoin/encoding/cardano.rs new file mode 100644 index 0000000..7ac18f3 --- /dev/null +++ b/src/multicoin/encoding/cardano.rs @@ -0,0 +1,75 @@ +use bech32::primitives::hrp::Hrp; +use bs58::Alphabet; +use ciborium::value::Integer; +use ciborium::Value; +use lazy_static::lazy_static; + +use super::{MulticoinEncoder, MulticoinEncoderError}; + +lazy_static! { + static ref ADA_HRP: Hrp = Hrp::parse_unchecked("addr"); +} + +pub struct CardanoEncoder {} + +// None if invalid bryon address +fn encode_cardano_bryon(data: &str) -> Result, MulticoinEncoderError> { + if !data.starts_with("Ae2") && !data.starts_with("Ddz") { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid bryon address prefix".to_string(), + )); + } + + let decoded = bs58::decode(data) + .with_alphabet(Alphabet::BITCOIN) + .into_vec() + .map_err(|_| { + MulticoinEncoderError::InvalidStructure("failed to decode bs58".to_string()) + })?; + + let (Value::Tag(tag, data_raw), Value::Integer(checksum)) = + ciborium::from_reader(decoded.as_slice()).map_err(|_| { + MulticoinEncoderError::InvalidStructure("failed to cbor decode".to_string()) + })? + else { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid cbor structure".to_string(), + )); + }; + + let Some(data) = data_raw.as_bytes() else { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid cbor structure".to_string(), + )); + }; + + let checksum_check = crc32fast::hash(data); + + if tag != 24 || checksum != Integer::from(checksum_check) { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid cbor structure".to_string(), + )); + }; + + Ok(data.clone()) +} + +fn encode_cardano_shelley(data: &str) -> Result, MulticoinEncoderError> { + let (hrp, data) = bech32::decode(data).map_err(|_| { + MulticoinEncoderError::InvalidStructure("failed to bech32 encode".to_string()) + })?; + + if hrp != *ADA_HRP { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid bech32 address prefix".to_string(), + )); + } + + Ok(data) +} + +impl MulticoinEncoder for CardanoEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + encode_cardano_bryon(data).or_else(|_| encode_cardano_shelley(data)) + } +} diff --git a/src/multicoin/encoding/checksum_address.rs b/src/multicoin/encoding/checksum_address.rs new file mode 100644 index 0000000..7ef4319 --- /dev/null +++ b/src/multicoin/encoding/checksum_address.rs @@ -0,0 +1,10 @@ +use super::{MulticoinEncoder, MulticoinEncoderError}; + +pub struct EvmEncoder {} + +impl MulticoinEncoder for EvmEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + ethers::utils::hex::decode(data) + .map_err(|err| MulticoinEncoderError::InvalidStructure(err.to_string())) + } +} diff --git a/src/multicoin/encoding/hedera.rs b/src/multicoin/encoding/hedera.rs new file mode 100644 index 0000000..2ad7f0d --- /dev/null +++ b/src/multicoin/encoding/hedera.rs @@ -0,0 +1,29 @@ +use super::{MulticoinEncoder, MulticoinEncoderError}; + +pub struct HederaEncoder {} + +impl MulticoinEncoder for HederaEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + let parts: Vec<&str> = data.split('.').collect(); + if parts.len() != 3 { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid length".to_string(), + )); + } + + let (Ok(shard), Ok(realm), Ok(account)) = ( + parts[0].parse::(), + parts[1].parse::(), + parts[2].parse::(), + ) else { + return Err(MulticoinEncoderError::InvalidStructure("".to_string())); + }; + + let mut result = Vec::new(); + result.extend_from_slice(&shard.to_be_bytes()); + result.extend_from_slice(&realm.to_be_bytes()); + result.extend_from_slice(&account.to_be_bytes()); + + Ok(result) + } +} diff --git a/src/multicoin/encoding/mod.rs b/src/multicoin/encoding/mod.rs new file mode 100644 index 0000000..e86c63e --- /dev/null +++ b/src/multicoin/encoding/mod.rs @@ -0,0 +1,68 @@ +use thiserror::Error; + +use crate::multicoin::encoding::binance::BinanceEncoder; +use crate::multicoin::encoding::bitcoin::BitcoinEncoder; +use crate::multicoin::encoding::cardano::CardanoEncoder; +use crate::multicoin::encoding::hedera::HederaEncoder; +use crate::multicoin::encoding::polkadot::PolkadotEncoder; +use crate::multicoin::encoding::ripple::RippleEncoder; +use crate::multicoin::encoding::solana::SolanaEncoder; +use crate::multicoin::encoding::stellar::StellarEncoder; + +use super::cointype::{coins::CoinType, slip44::SLIP44}; + +use self::checksum_address::EvmEncoder; + +pub mod binance; +pub mod bitcoin; +pub mod cardano; +pub mod checksum_address; +pub mod hedera; +pub mod p2pkh; +pub mod p2sh; +pub mod polkadot; +pub mod ripple; +pub mod segwit; +pub mod solana; +pub mod stellar; + +#[derive(Debug, Error)] +pub enum MulticoinEncoderError { + #[error("Invalid structure: {0}")] + InvalidStructure(String), + + #[error("Not supported")] + NotSupported, +} + +pub trait MulticoinEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError>; +} + +impl MulticoinEncoder for CoinType { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + let encoder: Box = match self { + Self::Slip44(slip44) => match slip44 { + SLIP44::Ethereum | SLIP44::EthereumClassic | SLIP44::Rootstock => { + Box::new(EvmEncoder {}) + } + SLIP44::Bitcoin => Box::new(BitcoinEncoder::new(Some("bc"), &[0x00], &[0x05])), + SLIP44::Litecoin => { + Box::new(BitcoinEncoder::new(Some("ltc"), &[0x30], &[0x32, 0x05])) + } + SLIP44::Dogecoin => Box::new(BitcoinEncoder::new(None, &[0x1e], &[0x16])), + SLIP44::Solana => Box::new(SolanaEncoder {}), + SLIP44::Hedera => Box::new(HederaEncoder {}), + SLIP44::Stellar => Box::new(StellarEncoder {}), + SLIP44::Ripple => Box::new(RippleEncoder {}), + SLIP44::Cardano => Box::new(CardanoEncoder {}), + SLIP44::Binance => Box::new(BinanceEncoder {}), + SLIP44::Polkadot => Box::new(PolkadotEncoder {}), + _ => return Err(MulticoinEncoderError::NotSupported), + }, + Self::Evm => Box::new(EvmEncoder {}), + }; + + encoder.encode(data) + } +} diff --git a/src/multicoin/encoding/p2pkh.rs b/src/multicoin/encoding/p2pkh.rs new file mode 100644 index 0000000..9d72bcc --- /dev/null +++ b/src/multicoin/encoding/p2pkh.rs @@ -0,0 +1,57 @@ +use bs58::Alphabet; + +use crate::utils; + +use super::{MulticoinEncoder, MulticoinEncoderError}; + +pub struct P2PKHEncoder { + pub(crate) accepted_versions: &'static [u8], +} + +impl MulticoinEncoder for P2PKHEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + let decoded = bs58::decode(data) + .with_alphabet(Alphabet::BITCOIN) + .into_vec() + .map_err(|_| { + MulticoinEncoderError::InvalidStructure("failed to decode bs58".to_string()) + })?; + + // version byte, at least one data byte, 4 bytes of checksum + if decoded.len() < 6 { + return Err(MulticoinEncoderError::InvalidStructure("".to_string())); + } + + if !self + .accepted_versions + .iter() + .any(|version| decoded[0] == *version) + { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid version".to_string(), + )); + } + + let checksum_begin = decoded.len() - 4; + let checksum = &decoded[checksum_begin..]; + let data = &decoded[..checksum_begin]; + + let checksum_check = &utils::sha256::hash(utils::sha256::hash(data))[..4]; + + if checksum != checksum_check { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid checksum".to_string(), + )); + } + + let pub_key_hash = &data[1..]; + + Ok([ + &[0x76, 0xa9, pub_key_hash.len() as u8] as &[u8], + pub_key_hash, + &[0x88, 0xac], + ] + .concat() + .to_vec()) + } +} diff --git a/src/multicoin/encoding/p2sh.rs b/src/multicoin/encoding/p2sh.rs new file mode 100644 index 0000000..0d3edc5 --- /dev/null +++ b/src/multicoin/encoding/p2sh.rs @@ -0,0 +1,55 @@ +use super::{MulticoinEncoder, MulticoinEncoderError}; +use crate::utils; +use bs58::Alphabet; + +pub struct P2SHEncoder { + pub(crate) accepted_versions: &'static [u8], +} + +impl MulticoinEncoder for P2SHEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + let decoded = bs58::decode(data) + .with_alphabet(Alphabet::BITCOIN) + .into_vec() + .map_err(|_| { + MulticoinEncoderError::InvalidStructure("failed to decode bs58".to_string()) + })?; + + // version byte, at least one data byte, 4 bytes of checksum + if decoded.len() < 6 { + return Err(MulticoinEncoderError::InvalidStructure("".to_string())); + } + + if !self + .accepted_versions + .iter() + .any(|version| decoded[0] == *version) + { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid version".to_string(), + )); + } + + let checksum_begin = decoded.len() - 4; + let checksum = &decoded[checksum_begin..]; + let data = &decoded[..checksum_begin]; + + let checksum_check = &utils::sha256::hash(utils::sha256::hash(data))[..4]; + + if checksum != checksum_check { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid checksum".to_string(), + )); + } + + let pub_key_hash = &data[1..]; + + Ok([ + &[0xa9, pub_key_hash.len() as u8] as &[u8], + pub_key_hash, + &[0x87], + ] + .concat() + .to_vec()) + } +} diff --git a/src/multicoin/encoding/polkadot.rs b/src/multicoin/encoding/polkadot.rs new file mode 100644 index 0000000..066aada --- /dev/null +++ b/src/multicoin/encoding/polkadot.rs @@ -0,0 +1,41 @@ +use blake2::{Blake2b512, Digest}; +use bs58::Alphabet; + +use super::{MulticoinEncoder, MulticoinEncoderError}; + +pub struct PolkadotEncoder {} + +static HASH_PREFIX: &[u8] = b"SS58PRE"; + +impl MulticoinEncoder for PolkadotEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + let decoded = bs58::decode(data) + .with_alphabet(Alphabet::BITCOIN) + .into_vec() + .map_err(|_| { + MulticoinEncoderError::InvalidStructure("failed to decode bs58".to_string()) + })?; + + // null byte, at least 1 byte of data and 2 bytes of a hash + if decoded.len() < 4 { + return Err(MulticoinEncoderError::InvalidStructure("".to_string())); + } + + let hash_begin = decoded.len() - 2; + let hash = &decoded[hash_begin..]; + let data = &decoded[1..hash_begin]; + + let mut hasher = Blake2b512::new(); + hasher.update([HASH_PREFIX, &[0], data].concat()); + + let check_hash = hasher.finalize(); + + if hash != &check_hash.as_slice()[..2] { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid checksum".to_string(), + )); + } + + Ok(data.to_vec()) + } +} diff --git a/src/multicoin/encoding/ripple.rs b/src/multicoin/encoding/ripple.rs new file mode 100644 index 0000000..ab66f50 --- /dev/null +++ b/src/multicoin/encoding/ripple.rs @@ -0,0 +1,37 @@ +use bs58::Alphabet; + +use crate::utils; + +use super::{MulticoinEncoder, MulticoinEncoderError}; + +pub struct RippleEncoder {} + +impl MulticoinEncoder for RippleEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + let decoded = bs58::decode(data) + .with_alphabet(Alphabet::RIPPLE) + .into_vec() + .map_err(|_| { + MulticoinEncoderError::InvalidStructure("failed to decode bs58".to_string()) + })?; + + // at least 1 byte of data and 4 bytes of checksum + if decoded.len() < 5 { + return Err(MulticoinEncoderError::InvalidStructure("".to_string())); + } + + let checksum_begin = decoded.len() - 4; + let checksum = &decoded[checksum_begin..]; + let data = &decoded[..checksum_begin]; + + let checksum_check = &utils::sha256::hash(utils::sha256::hash(data))[..4]; + + if checksum != checksum_check { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid checksum".to_string(), + )); + } + + Ok(data.to_vec()) + } +} diff --git a/src/multicoin/encoding/segwit.rs b/src/multicoin/encoding/segwit.rs new file mode 100644 index 0000000..2219033 --- /dev/null +++ b/src/multicoin/encoding/segwit.rs @@ -0,0 +1,42 @@ +use bech32::Hrp; + +use crate::multicoin::encoding::{MulticoinEncoder, MulticoinEncoderError}; + +pub struct SegWitEncoder { + pub human_readable_part: Hrp, +} + +impl SegWitEncoder { + pub fn new(human_readable_part: &str) -> SegWitEncoder { + SegWitEncoder { + human_readable_part: Hrp::parse_unchecked(human_readable_part), + } + } +} + +impl MulticoinEncoder for SegWitEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + let (hrp, version, data) = bech32::segwit::decode(data).map_err(|_| { + MulticoinEncoderError::InvalidStructure("failed to bech32 decode".to_string()) + })?; + + if hrp != self.human_readable_part { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid segwit prefix".to_string(), + )); + } + + let version_u8 = version.to_u8(); + let version = match version_u8 { + 0x00 => Ok(0x00), + 0x01..=0x10 => Ok(version_u8 + 0x50), + _ => Err(MulticoinEncoderError::InvalidStructure( + "invalid segwit version".to_string(), + )), + }?; + + Ok([&[version, data.len() as u8] as &[u8], &data] + .concat() + .to_vec()) + } +} diff --git a/src/multicoin/encoding/solana.rs b/src/multicoin/encoding/solana.rs new file mode 100644 index 0000000..cf7f843 --- /dev/null +++ b/src/multicoin/encoding/solana.rs @@ -0,0 +1,11 @@ +use super::{MulticoinEncoder, MulticoinEncoderError}; + +pub struct SolanaEncoder {} + +impl MulticoinEncoder for SolanaEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + bs58::decode(data).into_vec().map_err(|_| { + MulticoinEncoderError::InvalidStructure("failed to decode bs58".to_string()) + }) + } +} diff --git a/src/multicoin/encoding/stellar.rs b/src/multicoin/encoding/stellar.rs new file mode 100644 index 0000000..e6142c9 --- /dev/null +++ b/src/multicoin/encoding/stellar.rs @@ -0,0 +1,34 @@ +use base32::Alphabet; +use crc16::{State, XMODEM}; + +use super::{MulticoinEncoder, MulticoinEncoderError}; + +pub struct StellarEncoder {} + +impl MulticoinEncoder for StellarEncoder { + fn encode(&self, data: &str) -> Result, MulticoinEncoderError> { + let decoded = base32::decode(Alphabet::RFC4648 { padding: false }, data).ok_or( + MulticoinEncoderError::InvalidStructure("failed to decode base32".to_string()), + )?; + + // ed25519 version byte, at least 1 byte of data and 2 bytes of a hash + if decoded.len() < 4 { + return Err(MulticoinEncoderError::InvalidStructure("".to_string())); + } + + let hash_begin = decoded.len() - 2; + let checksum_bytes = &decoded[hash_begin..]; + let checksum = u16::from_le_bytes([checksum_bytes[0], checksum_bytes[1]]); + let data = &decoded[1..hash_begin]; + + let checksum_check = State::::calculate(&decoded[..hash_begin]); + + if checksum != checksum_check { + return Err(MulticoinEncoderError::InvalidStructure( + "invalid checksum".to_string(), + )); + } + + Ok(data.to_vec()) + } +} diff --git a/src/multicoin/mod.rs b/src/multicoin/mod.rs new file mode 100644 index 0000000..bfdb2cc --- /dev/null +++ b/src/multicoin/mod.rs @@ -0,0 +1,2 @@ +pub mod cointype; +pub mod encoding; diff --git a/src/selfservice/endpoint.rs b/src/selfservice/endpoint.rs index c16ac65..b8dd86f 100644 --- a/src/selfservice/endpoint.rs +++ b/src/selfservice/endpoint.rs @@ -1,18 +1,12 @@ -use axum::{ - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, - Json, -}; -use ethers::core::utils::rlp::Decodable; -use ethers::{core::k256::ecdsa, providers::namehash, types::Signature}; -use ethers_contract_derive::{Eip712, EthAbiType}; -use ethers_core::types::{transaction::eip712::Eip712, H160}; use std::{collections::HashMap, str::FromStr, sync::Arc}; + +use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; +use ethers::providers::namehash; +use ethers_core::types::H160; +use serde::{Deserialize, Serialize}; use tracing::info; use crate::state::GlobalState; -use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] pub struct UpdateNamePayload { diff --git a/src/selfservice/view.rs b/src/selfservice/view.rs index fba2571..52671cf 100644 --- a/src/selfservice/view.rs +++ b/src/selfservice/view.rs @@ -1,6 +1,5 @@ use std::{collections::HashMap, sync::Arc}; -use crate::state::GlobalState; use axum::{ extract::{Path, State}, response::IntoResponse, @@ -8,7 +7,8 @@ use axum::{ }; use ethers::providers::namehash; use serde::{Deserialize, Serialize}; -use tracing::info; + +use crate::state::GlobalState; #[derive(Debug, Serialize, Deserialize)] pub struct ViewPayload { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 4670a6a..4dbc4a3 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1 +1,2 @@ pub mod dns; +pub mod sha256; diff --git a/src/utils/sha256.rs b/src/utils/sha256.rs new file mode 100644 index 0000000..3addcd1 --- /dev/null +++ b/src/utils/sha256.rs @@ -0,0 +1,8 @@ +use sha2::{Digest, Sha256}; + +pub fn hash>(data: T) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(data); + + hasher.finalize().as_slice().into() +}