Change configuration options related to blockchain integration
This commit is contained in:
parent
5730ae0072
commit
f2fb44bb63
10 changed files with 110 additions and 59 deletions
|
@ -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'
|
||||
|
|
|
@ -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");
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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(ð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,
|
||||
|
|
|
@ -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(ð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<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)
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue