fedimovies/src/ethereum/nft.rs
2021-09-14 12:24:05 +00:00

233 lines
7.2 KiB
Rust

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<Vec<u8>, 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<Http>, Contract<Http>), 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(&ethereum_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<H256>,
from: Token,
to: Token,
token_id: Token,
}
pub async fn process_events(
web3: &Web3<Http>,
contract: &Contract<Http>,
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<TokenTransfer> = 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::<Result<_, web3::ethabi::Error>>()?;
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<SignatureData, EthereumError> {
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)
}