use std::convert::TryInto; use std::fs; use std::path::PathBuf; use web3::{ api::Web3, contract::{Contract, Options}, ethabi::{Event, EventParam, ParamType, RawLog, token::Token, encode}, transports::Http, types::{BlockNumber, FilterBuilder, H256, U256}, }; use crate::config::{Config, EthereumContract}; use crate::database::{Pool, get_database_client}; use crate::errors::DatabaseError; use crate::ipfs::utils::parse_ipfs_url; use crate::models::posts::queries::{ get_post_by_ipfs_cid, update_post, is_waiting_for_token, }; use super::api::connect; use super::utils::{ parse_address, sign_message, AddressError, SignatureData, SignatureError, }; pub const COLLECTIBLE: &str = "Collectible"; pub const MINTER: &str = "Minter"; #[derive(thiserror::Error, Debug)] pub enum EthereumError { #[error("io error")] IoError(#[from] std::io::Error), #[error("json error")] JsonError(#[from] serde_json::Error), #[error("invalid address")] InvalidAddress(#[from] AddressError), #[error(transparent)] Web3Error(#[from] web3::Error), #[error("artifact error")] ArtifactError, #[error("abi error")] AbiError(#[from] web3::ethabi::Error), #[error("contract error")] ContractError(#[from] web3::contract::Error), #[error("improprely configured")] ImproperlyConfigured, #[error("data conversion error")] ConversionError, #[error("token uri parsing error")] TokenUriParsingError, #[error(transparent)] DatabaseError(#[from] DatabaseError), #[error("signature error")] SigError(#[from] SignatureError), } fn load_abi( contract_dir: &PathBuf, contract_name: &str, ) -> Result, EthereumError> { let contract_artifact_path = contract_dir.join(format!("{}.json", contract_name)); let contract_artifact = fs::read_to_string(contract_artifact_path)?; let contract_artifact_value: serde_json::Value = serde_json::from_str(&contract_artifact)?; let contract_abi = contract_artifact_value.get("abi") .ok_or(EthereumError::ArtifactError)? .to_string().as_bytes().to_vec(); Ok(contract_abi) } pub async fn get_nft_contract( config: &Config, ) -> Result<(Web3, Contract), EthereumError> { 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 minter_abi = load_abi(&config.contract_dir, MINTER)?; let minter_address = parse_address(ðereum_config.address)?; let minter = Contract::from_json( web3.eth(), minter_address, &minter_abi, )?; let token_address = minter.query("token", (), None, Options::default(), None).await?; let token_abi = load_abi(&config.contract_dir, COLLECTIBLE)?; let token = Contract::from_json( web3.eth(), token_address, &token_abi, )?; log::info!("NFT contract address is {:?}", token.address()); Ok((web3, token)) } #[derive(Debug)] struct TokenTransfer { tx_id: Option, from: Token, to: Token, token_id: Token, } pub async fn process_events( web3: &Web3, contract: &Contract, db_pool: &Pool, ) -> Result<(), EthereumError> { let db_client = &**get_database_client(&db_pool).await?; if !is_waiting_for_token(db_client).await? { return Ok(()); } // Search for Transfer events let event_abi_params = vec![ EventParam { name: "from".to_string(), kind: ParamType::Address, indexed: true, }, EventParam { name: "to".to_string(), kind: ParamType::Address, indexed: true, }, EventParam { name: "tokenId".to_string(), kind: ParamType::Uint(256), indexed: true, }, ]; let event_abi = Event { name: "Transfer".to_string(), inputs: event_abi_params, anonymous: false, }; let filter = FilterBuilder::default() .address(vec![contract.address()]) .topics(Some(vec![event_abi.signature()]), None, None, None) .from_block(BlockNumber::Earliest) .build(); let logs = web3.eth().logs(filter).await?; // Convert web3 logs into ethabi logs let transfers: Vec = logs.iter().map(|log| { let raw_log = RawLog { topics: log.topics.clone(), data: log.data.clone().0, }; match event_abi.parse_log(raw_log) { Ok(event) => { let params = event.params; let transfer = TokenTransfer { tx_id: log.transaction_hash, from: params[0].value.clone(), to: params[1].value.clone(), token_id: params[2].value.clone(), }; Ok(transfer) }, Err(err) => Err(err), } }).collect::>()?; for transfer in transfers { let from_address = transfer.from.into_address() .ok_or(EthereumError::ConversionError)?; if from_address.is_zero() { // Mint event found let token_id_u256 = transfer.token_id.into_uint() .ok_or(EthereumError::ConversionError)?; let token_uri_result = contract.query("tokenURI", (token_id_u256,), None, Options::default(), None); let token_uri: String = token_uri_result.await?; let tx_id_h256 = transfer.tx_id.ok_or(EthereumError::ConversionError)?; let tx_id = hex::encode(tx_id_h256.as_bytes()); let ipfs_cid = parse_ipfs_url(&token_uri) .map_err(|_| EthereumError::TokenUriParsingError)?; let mut post = match get_post_by_ipfs_cid(db_client, &ipfs_cid).await { Ok(post) => post, Err(err) => { // Post not found or some other error log::error!("{}", err); continue; }, }; if post.token_id.is_none() { log::info!("post {} was tokenized via {}", post.id, tx_id); let token_id: i32 = token_id_u256.try_into() .map_err(|_| EthereumError::ConversionError)?; post.token_id = Some(token_id); post.token_tx_id = Some(tx_id); update_post(db_client, &post).await?; }; }; }; Ok(()) } pub fn create_mint_signature( contract_config: &EthereumContract, user_address: &str, token_uri: &str, ) -> Result { let contract_address = parse_address(&contract_config.address)?; let user_address = parse_address(user_address)?; let chain_id: U256 = contract_config.chain_id.into(); let chain_id_token = Token::Uint(chain_id.into()); let chain_id_bin = encode(&[chain_id_token]); let message = [ &chain_id_bin, contract_address.as_bytes(), "mint".as_bytes(), user_address.as_bytes(), token_uri.as_bytes(), ].concat(); let signature = sign_message(&contract_config.signing_key, &message)?; Ok(signature) }