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 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'
|
||||||
|
|
|
@ -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");
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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(ðereum_config.address)?;
|
|
||||||
let manager = Contract::from_json(
|
let manager = Contract::from_json(
|
||||||
web3.eth(),
|
web3.eth(),
|
||||||
manager_address,
|
manager_address,
|
||||||
|
|
|
@ -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(ðereum_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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue