diff --git a/config.yaml.example b/config.yaml.example index 0ffdb86..9943ac0 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -16,13 +16,16 @@ registrations_open: false # Login message must contain instance URL login_message: 'Sign this message to log in to https://myserver.net. Do not sign this message on other sites!' -ethereum_contract_dir: contracts -ethereum_json_rpc_url: 'http://127.0.0.1:8545' -# Block explorer base URL (must be compatible with https://eips.ethereum.org/EIPS/eip-3091) -ethereum_explorer_url: null -ethereum_contract: - address: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' - chain_id: 31337 +# To disable blockchain integration, set `blockchain` to `null` +blockchain: + # CAIP-2 chain ID (https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md) + chain_id: eip155:31337 + contract_address: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' + contract_dir: contracts + api_url: 'http://127.0.0.1:8545' + # Block explorer base URL (should be compatible with https://eips.ethereum.org/EIPS/eip-3091) + explorer_url: null + # Instance private key signing_key: null ipfs_api_url: 'http://127.0.0.1:5001' diff --git a/src/config.rs b/src/config.rs index 0f7c91c..cdd3e79 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,6 +8,7 @@ use url::Url; use crate::activitypub::views::get_instance_actor_url; use crate::errors::ConversionError; +use crate::ethereum::utils::{parse_caip2_chain_id, ChainIdError}; use crate::utils::crypto::deserialize_private_key; #[derive(Clone, Debug)] @@ -65,12 +66,25 @@ fn default_environment() -> Environment { Environment::Development } fn default_log_level() -> LogLevel { LogLevel::Info } #[derive(Clone, Deserialize)] -pub struct EthereumContract { - pub address: String, - pub chain_id: u32, +pub struct BlockchainConfig { + pub chain_id: String, + pub contract_address: String, + pub contract_dir: PathBuf, + pub api_url: String, + pub explorer_url: Option, pub signing_key: String, } +impl BlockchainConfig { + fn try_ethereum_chain_id(&self) -> Result { + parse_caip2_chain_id(&self.chain_id) + } + + pub fn ethereum_chain_id(&self) -> u32 { + self.try_ethereum_chain_id().unwrap() + } +} + #[derive(Clone, Deserialize)] pub struct Config { #[serde(default = "default_environment")] @@ -102,11 +116,10 @@ pub struct Config { pub login_message: String, - // Ethereum & IPFS - pub ethereum_contract_dir: Option, - pub ethereum_json_rpc_url: Option, - pub ethereum_explorer_url: Option, - pub ethereum_contract: Option, + // Blockchain integration + pub blockchain: Option, + + // IPFS pub ipfs_api_url: Option, pub ipfs_gateway_url: Option, } @@ -186,8 +199,9 @@ pub fn parse_config() -> Config { if !config.storage_dir.exists() { panic!("storage directory does not exist"); }; - if let Some(contract_dir) = &config.ethereum_contract_dir { - if !contract_dir.exists() { + if let Some(blockchain_config) = config.blockchain.as_ref() { + blockchain_config.try_ethereum_chain_id().unwrap(); + if !blockchain_config.contract_dir.exists() { panic!("contract directory does not exist"); }; }; diff --git a/src/ethereum/gate.rs b/src/ethereum/gate.rs index 59a898d..abddebe 100644 --- a/src/ethereum/gate.rs +++ b/src/ethereum/gate.rs @@ -1,25 +1,18 @@ use web3::contract::{Contract, Options}; -use crate::config::Config; +use crate::config::BlockchainConfig; use super::api::connect; use super::contracts::{MANAGER, load_abi}; use super::errors::EthereumError; use super::utils::parse_address; pub async fn is_allowed_user( - config: &Config, + config: &BlockchainConfig, user_address: &str, ) -> Result { - let contract_dir = config.ethereum_contract_dir.as_ref() - .ok_or(EthereumError::ImproperlyConfigured)?; - let json_rpc_url = config.ethereum_json_rpc_url.as_ref() - .ok_or(EthereumError::ImproperlyConfigured)?; - let web3 = connect(json_rpc_url)?; - let ethereum_config = config.ethereum_contract.as_ref() - .ok_or(EthereumError::ImproperlyConfigured)?; - - let manager_abi = load_abi(contract_dir, MANAGER)?; - let manager_address = parse_address(ðereum_config.address)?; + let web3 = connect(&config.api_url)?; + let manager_abi = load_abi(&config.contract_dir, MANAGER)?; + let manager_address = parse_address(&config.contract_address)?; let manager = Contract::from_json( web3.eth(), manager_address, diff --git a/src/ethereum/nft.rs b/src/ethereum/nft.rs index 27296e4..c0589ef 100644 --- a/src/ethereum/nft.rs +++ b/src/ethereum/nft.rs @@ -11,7 +11,7 @@ use web3::{ types::{BlockNumber, FilterBuilder, H256, U256}, }; -use crate::config::{Config, EthereumContract}; +use crate::config::BlockchainConfig; use crate::database::{Pool, get_database_client}; use crate::errors::DatabaseError; use crate::ipfs::utils::parse_ipfs_url; @@ -28,18 +28,11 @@ use super::utils::{parse_address, sign_message, SignatureData}; const TOKEN_WAIT_TIME: i64 = 10; // in minutes pub async fn get_nft_contract( - config: &Config, + config: &BlockchainConfig, ) -> Result<(Web3, Contract), EthereumError> { - let contract_dir = config.ethereum_contract_dir.as_ref() - .ok_or(EthereumError::ImproperlyConfigured)?; - let json_rpc_url = config.ethereum_json_rpc_url.as_ref() - .ok_or(EthereumError::ImproperlyConfigured)?; - let web3 = connect(json_rpc_url)?; - let ethereum_config = config.ethereum_contract.as_ref() - .ok_or(EthereumError::ImproperlyConfigured)?; - - let manager_abi = load_abi(contract_dir, MANAGER)?; - let manager_address = parse_address(ðereum_config.address)?; + let web3 = connect(&config.api_url)?; + let manager_abi = load_abi(&config.contract_dir, MANAGER)?; + let manager_address = parse_address(&config.contract_address)?; let manager = Contract::from_json( web3.eth(), manager_address, @@ -50,7 +43,7 @@ pub async fn get_nft_contract( "collectible", (), None, Options::default(), None, ).await?; - let token_abi = load_abi(contract_dir, COLLECTIBLE)?; + let token_abi = load_abi(&config.contract_dir, COLLECTIBLE)?; let token = Contract::from_json( web3.eth(), token_address, @@ -192,13 +185,13 @@ pub async fn process_events( } pub fn create_mint_signature( - contract_config: &EthereumContract, + blockchain_config: &BlockchainConfig, user_address: &str, token_uri: &str, ) -> Result { - let contract_address = parse_address(&contract_config.address)?; + let contract_address = parse_address(&blockchain_config.contract_address)?; let user_address = parse_address(user_address)?; - let chain_id: U256 = contract_config.chain_id.into(); + let chain_id: U256 = blockchain_config.ethereum_chain_id().into(); let chain_id_token = Token::Uint(chain_id); let chain_id_bin = encode(&[chain_id_token]); let message = [ @@ -208,6 +201,6 @@ pub fn create_mint_signature( user_address.as_bytes(), token_uri.as_bytes(), ].concat(); - let signature = sign_message(&contract_config.signing_key, &message)?; + let signature = sign_message(&blockchain_config.signing_key, &message)?; Ok(signature) } diff --git a/src/ethereum/utils.rs b/src/ethereum/utils.rs index 1a2de64..62b525c 100644 --- a/src/ethereum/utils.rs +++ b/src/ethereum/utils.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use regex::Regex; use secp256k1::{Error as KeyError, SecretKey, rand::rngs::OsRng}; use serde::Serialize; use web3::{ @@ -7,6 +8,32 @@ use web3::{ types::Address, }; +#[derive(thiserror::Error, Debug)] +pub enum ChainIdError { + #[error("invalid chain ID")] + InvalidChainId, + + #[error("unsupported chain")] + UnsupportedChain, + + #[error("invalid EIP155 chain ID")] + InvalidEip155ChainId(#[from] std::num::ParseIntError), +} + +/// Parses CAIP-2 chain ID +/// https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md +pub fn parse_caip2_chain_id(chain_id: &str) -> Result { + // eip155 namespace: ethereum chain + let caip2_re = Regex::new(r"(?P\w+):(?P\w+)").unwrap(); + let caip2_caps = caip2_re.captures(chain_id) + .ok_or(ChainIdError::InvalidChainId)?; + if &caip2_caps["namespace"] != "eip155" { + return Err(ChainIdError::UnsupportedChain); + }; + let eth_chain_id: u32 = caip2_caps["chain_id"].parse()?; + Ok(eth_chain_id) +} + pub fn generate_ethereum_address() -> (SecretKey, Address) { let mut rng = OsRng::new().expect("failed to initialize RNG"); let secret_key = SecretKey::new(&mut rng); @@ -57,3 +84,23 @@ pub fn sign_message( }; Ok(signature_data) } + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_caip2_chain_id() { + let chain_id = "eip155:1"; + let result = parse_caip2_chain_id(chain_id).unwrap(); + assert_eq!(result, 1); + } + + #[test] + fn test_parse_caip2_chain_id_unsupported() { + let chain_id = "bip122:000000000019d6689c085ae165831e93"; + let error = parse_caip2_chain_id(chain_id).err().unwrap(); + assert!(matches!(error, ChainIdError::UnsupportedChain)); + } +} diff --git a/src/main.rs b/src/main.rs index c8fe516..3b8db75 100644 --- a/src/main.rs +++ b/src/main.rs @@ -93,11 +93,11 @@ async fn main() -> std::io::Result<()> { .service(activitypub::object_view) .service(nodeinfo::get_nodeinfo) .service(nodeinfo::get_nodeinfo_2_0); - if let Some(contract_dir) = &config.ethereum_contract_dir { + if let Some(blockchain_config) = &config.blockchain { // Serve artifacts if available app = app.service(actix_files::Files::new( "/contracts", - contract_dir, + &blockchain_config.contract_dir, )); } app diff --git a/src/mastodon_api/accounts/views.rs b/src/mastodon_api/accounts/views.rs index d34cc77..7fe579a 100644 --- a/src/mastodon_api/accounts/views.rs +++ b/src/mastodon_api/accounts/views.rs @@ -70,11 +70,11 @@ pub async fn create_account( return Err(ValidationError("invalid invite code").into()); } } - if config.ethereum_contract.is_some() { - // Wallet address is required only if ethereum integration is enabled + if let Some(blockchain_config) = config.blockchain.as_ref() { + // Wallet address is required only if blockchain integration is enabled let wallet_address = account_data.wallet_address.as_ref() .ok_or(ValidationError("wallet address is required"))?; - let is_allowed = is_allowed_user(&config, wallet_address).await + let is_allowed = is_allowed_user(blockchain_config, wallet_address).await .map_err(|_| HttpError::InternalError)?; if !is_allowed { return Err(ValidationError("not allowed to sign up").into()); diff --git a/src/mastodon_api/instance/types.rs b/src/mastodon_api/instance/types.rs index f82789d..c76f049 100644 --- a/src/mastodon_api/instance/types.rs +++ b/src/mastodon_api/instance/types.rs @@ -29,11 +29,12 @@ impl From<&Config> for InstanceInfo { version: config.version.clone(), registrations: config.registrations_open, login_message: config.login_message.clone(), - ethereum_explorer_url: config.ethereum_explorer_url.clone(), - nft_contract_name: config.ethereum_contract.as_ref() + ethereum_explorer_url: config.blockchain.as_ref() + .and_then(|val| val.explorer_url.clone()), + nft_contract_name: config.blockchain.as_ref() .and(Some(MANAGER.into())), - nft_contract_address: config.ethereum_contract.as_ref() - .map(|val| val.address.clone()), + nft_contract_address: config.blockchain.as_ref() + .map(|val| val.contract_address.clone()), ipfs_gateway_url: config.ipfs_gateway_url.clone(), } } diff --git a/src/mastodon_api/statuses/views.rs b/src/mastodon_api/statuses/views.rs index eb62aaf..730a15b 100644 --- a/src/mastodon_api/statuses/views.rs +++ b/src/mastodon_api/statuses/views.rs @@ -432,7 +432,7 @@ async fn get_signature( ) -> Result { let db_client = &**get_database_client(&db_pool).await?; let current_user = get_current_user(db_client, auth.token()).await?; - let contract_config = config.ethereum_contract.as_ref() + let blockchain_config = config.blockchain.as_ref() .ok_or(HttpError::NotSupported)?; let wallet_address = current_user.wallet_address .ok_or(HttpError::PermissionError)?; @@ -446,7 +446,7 @@ async fn get_signature( .ok_or(HttpError::PermissionError)?; let token_uri = get_ipfs_url(&ipfs_cid); let signature = create_mint_signature( - contract_config, + blockchain_config, &wallet_address, &token_uri, ).map_err(|_| HttpError::InternalError)?; diff --git a/src/scheduler.rs b/src/scheduler.rs index 9402de4..d1ff0bf 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -11,9 +11,9 @@ use crate::ethereum::nft::{get_nft_contract, process_events}; pub fn run(config: Config, db_pool: Pool) -> () { actix_rt::spawn(async move { let mut interval = actix_rt::time::interval(Duration::from_secs(30)); - let web3_contract = if config.ethereum_contract.is_some() { + let web3_contract = if let Some(blockchain_config) = &config.blockchain { // Verify config and create contract interface - get_nft_contract(&config).await + get_nft_contract(blockchain_config).await .map_err(|err| log::error!("{}", err)) .ok() } else {