Change configuration options related to blockchain integration

This commit is contained in:
silverpill 2022-01-25 22:17:28 +00:00
parent 5730ae0072
commit f2fb44bb63
10 changed files with 110 additions and 59 deletions

View file

@ -16,13 +16,16 @@ registrations_open: false
# Login message must contain instance URL # 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!' login_message: 'Sign this message to log in to https://myserver.net. Do not sign this message on other sites!'
ethereum_contract_dir: contracts # To disable blockchain integration, set `blockchain` to `null`
ethereum_json_rpc_url: 'http://127.0.0.1:8545' blockchain:
# Block explorer base URL (must be compatible with https://eips.ethereum.org/EIPS/eip-3091) # CAIP-2 chain ID (https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md)
ethereum_explorer_url: null chain_id: eip155:31337
ethereum_contract: contract_address: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0'
address: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' contract_dir: contracts
chain_id: 31337 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 signing_key: null
ipfs_api_url: 'http://127.0.0.1:5001' ipfs_api_url: 'http://127.0.0.1:5001'

View file

@ -8,6 +8,7 @@ use url::Url;
use crate::activitypub::views::get_instance_actor_url; use crate::activitypub::views::get_instance_actor_url;
use crate::errors::ConversionError; use crate::errors::ConversionError;
use crate::ethereum::utils::{parse_caip2_chain_id, ChainIdError};
use crate::utils::crypto::deserialize_private_key; use crate::utils::crypto::deserialize_private_key;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -65,12 +66,25 @@ fn default_environment() -> Environment { Environment::Development }
fn default_log_level() -> LogLevel { LogLevel::Info } fn default_log_level() -> LogLevel { LogLevel::Info }
#[derive(Clone, Deserialize)] #[derive(Clone, Deserialize)]
pub struct EthereumContract { pub struct BlockchainConfig {
pub address: String, pub chain_id: String,
pub chain_id: u32, pub contract_address: String,
pub contract_dir: PathBuf,
pub api_url: String,
pub explorer_url: Option<String>,
pub signing_key: String, pub signing_key: String,
} }
impl BlockchainConfig {
fn try_ethereum_chain_id(&self) -> Result<u32, ChainIdError> {
parse_caip2_chain_id(&self.chain_id)
}
pub fn ethereum_chain_id(&self) -> u32 {
self.try_ethereum_chain_id().unwrap()
}
}
#[derive(Clone, Deserialize)] #[derive(Clone, Deserialize)]
pub struct Config { pub struct Config {
#[serde(default = "default_environment")] #[serde(default = "default_environment")]
@ -102,11 +116,10 @@ pub struct Config {
pub login_message: String, pub login_message: String,
// Ethereum & IPFS // Blockchain integration
pub ethereum_contract_dir: Option<PathBuf>, pub blockchain: Option<BlockchainConfig>,
pub ethereum_json_rpc_url: Option<String>,
pub ethereum_explorer_url: Option<String>, // IPFS
pub ethereum_contract: Option<EthereumContract>,
pub ipfs_api_url: Option<String>, pub ipfs_api_url: Option<String>,
pub ipfs_gateway_url: Option<String>, pub ipfs_gateway_url: Option<String>,
} }
@ -186,8 +199,9 @@ pub fn parse_config() -> Config {
if !config.storage_dir.exists() { if !config.storage_dir.exists() {
panic!("storage directory does not exist"); panic!("storage directory does not exist");
}; };
if let Some(contract_dir) = &config.ethereum_contract_dir { if let Some(blockchain_config) = config.blockchain.as_ref() {
if !contract_dir.exists() { blockchain_config.try_ethereum_chain_id().unwrap();
if !blockchain_config.contract_dir.exists() {
panic!("contract directory does not exist"); panic!("contract directory does not exist");
}; };
}; };

View file

@ -1,25 +1,18 @@
use web3::contract::{Contract, Options}; use web3::contract::{Contract, Options};
use crate::config::Config; use crate::config::BlockchainConfig;
use super::api::connect; use super::api::connect;
use super::contracts::{MANAGER, load_abi}; use super::contracts::{MANAGER, load_abi};
use super::errors::EthereumError; use super::errors::EthereumError;
use super::utils::parse_address; use super::utils::parse_address;
pub async fn is_allowed_user( pub async fn is_allowed_user(
config: &Config, config: &BlockchainConfig,
user_address: &str, user_address: &str,
) -> Result<bool, EthereumError> { ) -> Result<bool, EthereumError> {
let contract_dir = config.ethereum_contract_dir.as_ref() let web3 = connect(&config.api_url)?;
.ok_or(EthereumError::ImproperlyConfigured)?; let manager_abi = load_abi(&config.contract_dir, MANAGER)?;
let json_rpc_url = config.ethereum_json_rpc_url.as_ref() let manager_address = parse_address(&config.contract_address)?;
.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(&ethereum_config.address)?;
let manager = Contract::from_json( let manager = Contract::from_json(
web3.eth(), web3.eth(),
manager_address, manager_address,

View file

@ -11,7 +11,7 @@ use web3::{
types::{BlockNumber, FilterBuilder, H256, U256}, types::{BlockNumber, FilterBuilder, H256, U256},
}; };
use crate::config::{Config, EthereumContract}; use crate::config::BlockchainConfig;
use crate::database::{Pool, get_database_client}; use crate::database::{Pool, get_database_client};
use crate::errors::DatabaseError; use crate::errors::DatabaseError;
use crate::ipfs::utils::parse_ipfs_url; 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 const TOKEN_WAIT_TIME: i64 = 10; // in minutes
pub async fn get_nft_contract( pub async fn get_nft_contract(
config: &Config, config: &BlockchainConfig,
) -> Result<(Web3<Http>, Contract<Http>), EthereumError> { ) -> Result<(Web3<Http>, Contract<Http>), EthereumError> {
let contract_dir = config.ethereum_contract_dir.as_ref() let web3 = connect(&config.api_url)?;
.ok_or(EthereumError::ImproperlyConfigured)?; let manager_abi = load_abi(&config.contract_dir, MANAGER)?;
let json_rpc_url = config.ethereum_json_rpc_url.as_ref() let manager_address = parse_address(&config.contract_address)?;
.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(&ethereum_config.address)?;
let manager = Contract::from_json( let manager = Contract::from_json(
web3.eth(), web3.eth(),
manager_address, manager_address,
@ -50,7 +43,7 @@ pub async fn get_nft_contract(
"collectible", "collectible",
(), None, Options::default(), None, (), None, Options::default(), None,
).await?; ).await?;
let token_abi = load_abi(contract_dir, COLLECTIBLE)?; let token_abi = load_abi(&config.contract_dir, COLLECTIBLE)?;
let token = Contract::from_json( let token = Contract::from_json(
web3.eth(), web3.eth(),
token_address, token_address,
@ -192,13 +185,13 @@ pub async fn process_events(
} }
pub fn create_mint_signature( pub fn create_mint_signature(
contract_config: &EthereumContract, blockchain_config: &BlockchainConfig,
user_address: &str, user_address: &str,
token_uri: &str, token_uri: &str,
) -> Result<SignatureData, EthereumError> { ) -> Result<SignatureData, EthereumError> {
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 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_token = Token::Uint(chain_id);
let chain_id_bin = encode(&[chain_id_token]); let chain_id_bin = encode(&[chain_id_token]);
let message = [ let message = [
@ -208,6 +201,6 @@ pub fn create_mint_signature(
user_address.as_bytes(), user_address.as_bytes(),
token_uri.as_bytes(), token_uri.as_bytes(),
].concat(); ].concat();
let signature = sign_message(&contract_config.signing_key, &message)?; let signature = sign_message(&blockchain_config.signing_key, &message)?;
Ok(signature) Ok(signature)
} }

View file

@ -1,5 +1,6 @@
use std::str::FromStr; use std::str::FromStr;
use regex::Regex;
use secp256k1::{Error as KeyError, SecretKey, rand::rngs::OsRng}; use secp256k1::{Error as KeyError, SecretKey, rand::rngs::OsRng};
use serde::Serialize; use serde::Serialize;
use web3::{ use web3::{
@ -7,6 +8,32 @@ use web3::{
types::Address, 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<u32, ChainIdError> {
// eip155 namespace: ethereum chain
let caip2_re = Regex::new(r"(?P<namespace>\w+):(?P<chain_id>\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) { pub fn generate_ethereum_address() -> (SecretKey, Address) {
let mut rng = OsRng::new().expect("failed to initialize RNG"); let mut rng = OsRng::new().expect("failed to initialize RNG");
let secret_key = SecretKey::new(&mut rng); let secret_key = SecretKey::new(&mut rng);
@ -57,3 +84,23 @@ pub fn sign_message(
}; };
Ok(signature_data) 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));
}
}

View file

@ -93,11 +93,11 @@ async fn main() -> std::io::Result<()> {
.service(activitypub::object_view) .service(activitypub::object_view)
.service(nodeinfo::get_nodeinfo) .service(nodeinfo::get_nodeinfo)
.service(nodeinfo::get_nodeinfo_2_0); .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 // Serve artifacts if available
app = app.service(actix_files::Files::new( app = app.service(actix_files::Files::new(
"/contracts", "/contracts",
contract_dir, &blockchain_config.contract_dir,
)); ));
} }
app app

View file

@ -70,11 +70,11 @@ pub async fn create_account(
return Err(ValidationError("invalid invite code").into()); return Err(ValidationError("invalid invite code").into());
} }
} }
if config.ethereum_contract.is_some() { if let Some(blockchain_config) = config.blockchain.as_ref() {
// Wallet address is required only if ethereum integration is enabled // Wallet address is required only if blockchain integration is enabled
let wallet_address = account_data.wallet_address.as_ref() let wallet_address = account_data.wallet_address.as_ref()
.ok_or(ValidationError("wallet address is required"))?; .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)?; .map_err(|_| HttpError::InternalError)?;
if !is_allowed { if !is_allowed {
return Err(ValidationError("not allowed to sign up").into()); return Err(ValidationError("not allowed to sign up").into());

View file

@ -29,11 +29,12 @@ impl From<&Config> for InstanceInfo {
version: config.version.clone(), version: config.version.clone(),
registrations: config.registrations_open, registrations: config.registrations_open,
login_message: config.login_message.clone(), login_message: config.login_message.clone(),
ethereum_explorer_url: config.ethereum_explorer_url.clone(), ethereum_explorer_url: config.blockchain.as_ref()
nft_contract_name: config.ethereum_contract.as_ref() .and_then(|val| val.explorer_url.clone()),
nft_contract_name: config.blockchain.as_ref()
.and(Some(MANAGER.into())), .and(Some(MANAGER.into())),
nft_contract_address: config.ethereum_contract.as_ref() nft_contract_address: config.blockchain.as_ref()
.map(|val| val.address.clone()), .map(|val| val.contract_address.clone()),
ipfs_gateway_url: config.ipfs_gateway_url.clone(), ipfs_gateway_url: config.ipfs_gateway_url.clone(),
} }
} }

View file

@ -432,7 +432,7 @@ async fn get_signature(
) -> Result<HttpResponse, HttpError> { ) -> Result<HttpResponse, HttpError> {
let db_client = &**get_database_client(&db_pool).await?; let db_client = &**get_database_client(&db_pool).await?;
let current_user = get_current_user(db_client, auth.token()).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)?; .ok_or(HttpError::NotSupported)?;
let wallet_address = current_user.wallet_address let wallet_address = current_user.wallet_address
.ok_or(HttpError::PermissionError)?; .ok_or(HttpError::PermissionError)?;
@ -446,7 +446,7 @@ async fn get_signature(
.ok_or(HttpError::PermissionError)?; .ok_or(HttpError::PermissionError)?;
let token_uri = get_ipfs_url(&ipfs_cid); let token_uri = get_ipfs_url(&ipfs_cid);
let signature = create_mint_signature( let signature = create_mint_signature(
contract_config, blockchain_config,
&wallet_address, &wallet_address,
&token_uri, &token_uri,
).map_err(|_| HttpError::InternalError)?; ).map_err(|_| HttpError::InternalError)?;

View file

@ -11,9 +11,9 @@ use crate::ethereum::nft::{get_nft_contract, process_events};
pub fn run(config: Config, db_pool: Pool) -> () { pub fn run(config: Config, db_pool: Pool) -> () {
actix_rt::spawn(async move { actix_rt::spawn(async move {
let mut interval = actix_rt::time::interval(Duration::from_secs(30)); 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 // Verify config and create contract interface
get_nft_contract(&config).await get_nft_contract(blockchain_config).await
.map_err(|err| log::error!("{}", err)) .map_err(|err| log::error!("{}", err))
.ok() .ok()
} else { } else {