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: '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'

View file

@ -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<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)]
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<PathBuf>,
pub ethereum_json_rpc_url: Option<String>,
pub ethereum_explorer_url: Option<String>,
pub ethereum_contract: Option<EthereumContract>,
// Blockchain integration
pub blockchain: Option<BlockchainConfig>,
// IPFS
pub ipfs_api_url: Option<String>,
pub ipfs_gateway_url: Option<String>,
}
@ -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");
};
};

View file

@ -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<bool, 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(&ethereum_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,

View file

@ -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<Http>, Contract<Http>), 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(&ethereum_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<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 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)
}

View file

@ -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<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) {
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));
}
}

View file

@ -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

View file

@ -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());

View file

@ -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(),
}
}

View file

@ -432,7 +432,7 @@ async fn get_signature(
) -> Result<HttpResponse, HttpError> {
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)?;

View file

@ -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 {