diff --git a/.env.example b/.env.example index 72248c8..b5a026d 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,11 @@ LISTEN_URL='0.0.0.0:8000' INFURA_PROJECT_ID= -PRIVATE_KEY= -CHAIN_ID=3 +SIGNER_PRIVATE_KEY= +CHAIN_ID=4 MORALIS_BASE_URL=https://deep-index.moralis.io/api/v2/ MORALIS_API_KEY= SWAGGER_JSON=/swagger.json + +# Testing envs +SIGNER_ADDRESS= +TESTING_ADDRESS= \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0e8e029..dea880c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.0.2" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc14de609ea2ae2467f5a626d403082ce3c5ca248ba26c9ee6f617a5fd096517" +checksum = "a5885cb81a0d4d0d322864bea1bb6c2a8144626b4fdc625d4c51eba197e7797a" dependencies = [ "actix-codec 0.5.0", "actix-rt", @@ -543,11 +543,12 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ "generic-array", + "typenum", ] [[package]] @@ -604,13 +605,12 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer 0.10.0", "crypto-common", - "generic-array", ] [[package]] @@ -1594,14 +1594,13 @@ checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" [[package]] name = "rand" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", - "rand_hc", ] [[package]] @@ -1623,15 +1622,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core", -] - [[package]] name = "redox_syscall" version = "0.2.10" @@ -1861,7 +1851,7 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.1", + "digest 0.10.3", ] [[package]] @@ -1943,6 +1933,7 @@ name = "sugarfunge-integration" version = "0.0.0" dependencies = [ "actix-cors", + "actix-http", "actix-web", "actix-web-prom", "awc", @@ -1952,6 +1943,7 @@ dependencies = [ "ethcontract", "ethcontract-generate", "log", + "rand", "serde", "serde_json", "snailquote", diff --git a/Cargo.toml b/Cargo.toml index b9b18ee..3c17448 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,5 +22,9 @@ env_logger = "0.8" log = "0.4" ethcontract = { version = "0.15.4" } +[dev-dependencies] +rand = "0.8.5" +actix-http = "3.0.4" + [build-dependencies] ethcontract-generate = { version = "0.15.4" } diff --git a/README.md b/README.md index 0bb55ce..9c240e3 100644 --- a/README.md +++ b/README.md @@ -6,29 +6,34 @@ - Install [Rust](https://www.rust-lang.org/). Using [Rustup](https://rustup.rs/) Run the following in your terminal, then follow the onscreen instructions: + ``` -$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` ### Usage - Clone the repository. + ```bash -$ git clone git@github.com:SugarFunge/sugarfunge-integration.git +git clone git@github.com:SugarFunge/sugarfunge-integration.git ``` - Compile the truffle contracts and copy the `build/contracts` folder to the root folder of this repository + ```bash -$ cd sugarfunge-integration -$ cp $TRUFFLE_ROOT/build/contracts . +cd sugarfunge-integration +cp $TRUFFLE_ROOT/build/contracts . ``` - Copy the environment file as **.env** and make the changes based on your needs + ```bash -$ cp .env.example .env +cp .env.example .env ``` - Start the API + ```bash # Normal run $ cargo run @@ -36,6 +41,15 @@ $ cargo run $ cargo watch -x 'run --bin sugarfunge-integration' ``` +### Testing + +```bash +# Normal run +$ cargo test -- --test-threads=1 +# Debugging +$ RUST_BACKTRACE=full cargo test -- --test-threads=1 --nocapture +``` + ## Swagger API Documentation & Prometheus Server ### Software requirements @@ -47,18 +61,21 @@ $ cargo watch -x 'run --bin sugarfunge-integration' ### Usage - Copy the environment file as **.env** and make the changes based on your needs (Optional if you already configured the API above) + ```bash -$ cp .env.example .env +cp .env.example .env ``` - Start the docker-compose file ([Access Swagger UI](http://localhost:7000)) ([Access Prometheus Server](http://localhost:9090)) + ```bash -$ docker-compose up -d +docker-compose up -d ``` - Stop the docker-compose file + ```bash -$ docker-compose down +docker-compose down ``` ## Environment configuration @@ -66,14 +83,16 @@ $ docker-compose down - Default environment file: **.env** - Example environment file: **.env.example** -| Variable Name | Description | -| --------------------------- | ------------------------------------------- | -| RUST_LOG | Rust log level | -| RUST_BACKTRACE | Show Rust backtrace (0 or 1) | -| LISTEN_URL | API Listen URL | -| INFURA_PROJECT_ID | Infura Project ID | -| PRIVATE_KEY | Private Key used to interact with contracts | -| CHAIN_ID | Chain ID (Default: 3 / Ropsten testnet) | -| MORALIS_BASE_URL | Moralis API base URL | -| MORALIS_API_KEY | Moralis API Key | -| SWAGGER_JSON | Swagger json file path inside the container | +| Variable Name | Description | +| --------------------------- | --------------------------------------------------------------| +| RUST_LOG | Rust log level | +| RUST_BACKTRACE | Show Rust backtrace (0 or 1) | +| LISTEN_URL | API Listen URL | +| INFURA_PROJECT_ID | Infura Project ID | +| SIGNER_PRIVATE_KEY | Private Key used to interact with contracts | +| SIGNER_ADDRESS | Address used to interact with contracts during tests | +| TESTING_ADDRESS | Receiver Address used to interact with contracts during tests | +| CHAIN_ID | Chain ID (Default: 4 / Rinkeby testnet) | +| MORALIS_BASE_URL | Moralis API base URL | +| MORALIS_API_KEY | Moralis API Key | +| SWAGGER_JSON | Swagger json file path inside the container | diff --git a/build.rs b/build.rs index 1ea80d1..cc1a0b6 100644 --- a/build.rs +++ b/build.rs @@ -8,7 +8,7 @@ fn main() { let dest = std::path::Path::new(&out_dir).join("SugarFungeAsset.rs"); let artifact = TruffleLoader::new() - .load_from_file("./contracts/SugarFunge/SugarFungeAsset.json") + .load_from_file("./contracts/SugarFungeAsset.json") .unwrap(); for contract in artifact.iter() { @@ -24,7 +24,7 @@ fn main() { let dest = std::path::Path::new(&out_dir).join("Wrapped1155Factory.rs"); let artifact = TruffleLoader::new() - .load_from_file("./contracts/ErcWrapper/Wrapped1155Factory.json") + .load_from_file("./contracts/Wrapped1155Factory.json") .unwrap(); for contract in artifact.iter() { diff --git a/src/asset.rs b/src/asset.rs index d4a2d39..f483b03 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1,9 +1,16 @@ -use crate::{error::ApiError, config::Config}; -use std::{env, str::FromStr}; -use ethcontract::{prelude::*, web3::ethabi::{Token, encode}}; -use actix_web::{post, web::{Data, Json}, Responder}; -use serde::{Serialize, Deserialize}; +use crate::{config::Config, error::ApiError}; +use actix_web::{ + post, + web::{Data, Json}, + Responder, +}; +use ethcontract::{ + prelude::*, + web3::ethabi::{encode, Token}, +}; +use serde::{Deserialize, Serialize}; use serde_json::json; +use std::{env, str::FromStr}; include!(concat!(env!("OUT_DIR"), "/SugarFungeAsset.rs")); @@ -11,15 +18,15 @@ include!(concat!(env!("OUT_DIR"), "/SugarFungeAsset.rs")); pub struct AssetData { pub name: String, pub symbol: String, - pub decimals: u64 + pub decimals: u64, } #[derive(Serialize, Deserialize, Debug)] pub struct AssetMint { - account: String, - amount: u64, - id: u64, - data: AssetData, + pub account: String, + pub amount: u64, + pub id: u64, + pub data: AssetData, } #[derive(Serialize, Deserialize, Debug)] @@ -41,10 +48,7 @@ pub struct AssetBatchTransfer { } pub fn get_web3(config: &Config) -> Result, ApiError> { - - let infura_url = { - format!("https://ropsten.infura.io/v3/{}", config.project_id) - }; + let infura_url = { format!("https://rinkeby.infura.io/v3/{}", config.project_id) }; match Http::new(&infura_url) { Ok(http) => Ok(Web3::new(http)), @@ -53,32 +57,28 @@ pub fn get_web3(config: &Config) -> Result, ApiError> { } pub fn get_asset_data(name: String, symbol: String, decimals: u64) -> Bytes> { - - ethcontract::Bytes(encode( - &[ - Token::Tuple( - [ - Token::String(name), - Token::String(symbol), - Token::Uint(decimals.into()) - ] - .to_vec()) - ]) - ) + ethcontract::Bytes(encode(&[Token::Tuple( + [ + Token::String(name), + Token::String(symbol), + Token::Uint(decimals.into()), + ] + .to_vec(), + )])) } pub fn get_batch_asset_data(data: Vec) -> Bytes> { - let mut tokens: Vec = [].to_vec(); for asset in data { let token = Token::Tuple( [ - Token::String(asset.name.to_string()), - Token::String(asset.symbol.to_string()), - Token::Uint(asset.decimals.into()) + Token::String(asset.name.to_string()), + Token::String(asset.symbol.to_string()), + Token::Uint(asset.decimals.into()), ] - .to_vec()); + .to_vec(), + ); tokens.push(token); } @@ -87,35 +87,43 @@ pub fn get_batch_asset_data(data: Vec) -> Bytes> { } pub async fn asset_mint_nft(config: &Config, mint: &AssetMint) -> Result { - let account = { - let key: PrivateKey = config.private_key.to_owned(); + let key: PrivateKey = config.signer_private_key.to_owned(); Account::Offline(key, Some(config.chain_id)) }; let web3 = get_web3(config)?; - let mut contract = SugarFungeAsset::deployed(&web3).await?; + let mut contract = SugarFungeAsset::deployed(&web3).await?; contract.defaults_mut().from = Some(account); - - let result = contract.mint( - H160::from_str(&mint.account).unwrap(), - mint.id.into(), - mint.amount.into(), - get_asset_data(mint.data.name.to_owned(), mint.data.symbol.to_owned(), mint.data.decimals)) + let result = contract + .mint( + H160::from_str(&mint.account).unwrap(), + mint.id.into(), + mint.amount.into(), + get_asset_data( + mint.data.name.to_owned(), + mint.data.symbol.to_owned(), + mint.data.decimals, + ), + ) .send() .await?; - Ok(Json(json!({ - "tx": format!("0x{:x}", result.hash()) + "tx": format!("0x{:x}", result.hash()), + "to": &result.as_receipt().unwrap().to, + "from": &result.as_receipt().unwrap().from, + "hash": &result.hash() }))) } -pub async fn asset_transfer_nft(config: &Config, transfer: &AssetTransfer) -> Result { - +pub async fn asset_transfer_nft( + config: &Config, + transfer: &AssetTransfer, +) -> Result { let account = { - let key: PrivateKey = config.private_key.to_owned(); + let key: PrivateKey = config.signer_private_key.to_owned(); Account::Offline(key, Some(config.chain_id)) }; @@ -125,24 +133,36 @@ pub async fn asset_transfer_nft(config: &Config, transfer: &AssetTransfer) -> Re contract.defaults_mut().from = Some(account); - let result = contract.safe_transfer_from ( - H160::from_str(&transfer.from).unwrap(), - H160::from_str(&transfer.to).unwrap(), - transfer.id.into(), - transfer.amount.into(), - get_asset_data(transfer.data.name.to_owned(), transfer.data.symbol.to_owned(), transfer.data.decimals)) + let result = contract + .safe_transfer_from( + H160::from_str(&transfer.from).unwrap(), + H160::from_str(&transfer.to).unwrap(), + transfer.id.into(), + transfer.amount.into(), + get_asset_data( + transfer.data.name.to_owned(), + transfer.data.symbol.to_owned(), + transfer.data.decimals, + ), + ) .send() .await?; Ok(Json(json!({ - "tx": format!("0x{:x}", result.hash()) + "tx": format!("0x{:x}", result.hash()), + "to": &result.as_receipt().unwrap().to, + "from": &result.as_receipt().unwrap().from, + "hash": &result.hash() + }))) } -pub async fn asset_batch_transfer_nft(config: &Config, transfer: &AssetBatchTransfer) -> Result { - +pub async fn asset_batch_transfer_nft( + config: &Config, + transfer: &AssetBatchTransfer, +) -> Result { let account = { - let key: PrivateKey = config.private_key.to_owned(); + let key: PrivateKey = config.signer_private_key.to_owned(); Account::Offline(key, Some(config.chain_id)) }; @@ -152,18 +172,22 @@ pub async fn asset_batch_transfer_nft(config: &Config, transfer: &AssetBatchTran contract.defaults_mut().from = Some(account); - let result = contract.safe_batch_transfer_from ( - H160::from_str(&transfer.from).unwrap(), - H160::from_str(&transfer.to).unwrap(), - transfer.ids.iter().map(|x| x.to_owned().into()).collect(), - transfer.amounts.iter().map(|x| x.to_owned().into()).collect(), - get_batch_asset_data(transfer.data.to_vec())) + let result = contract + .safe_batch_transfer_from( + H160::from_str(&transfer.from).unwrap(), + H160::from_str(&transfer.to).unwrap(), + transfer.ids.iter().map(|x| x.to_owned().into()).collect(), + transfer + .amounts + .iter() + .map(|x| x.to_owned().into()) + .collect(), + get_batch_asset_data(transfer.data.to_vec()), + ) .send() .await?; - Ok(Json(json!({ - "tx": format!("0x{:x}", result.hash()) - }))) + Ok(Json(json!({ "tx": format!("0x{:x}", result.hash()) }))) } #[post("mint_nft")] @@ -181,7 +205,10 @@ async fn transfer_nft(req_body: String, config: Data) -> Result) -> Result { +async fn batch_transfer_nft( + req_body: String, + config: Data, +) -> Result { let req_data: AssetBatchTransfer = serde_json::from_str(&req_body)?; asset_batch_transfer_nft(&config, &req_data).await diff --git a/src/config.rs b/src/config.rs index ab4d5c3..62e04e8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,43 +1,53 @@ -use std::env; use ethcontract::PrivateKey; +use std::env; #[derive(Clone, Debug)] pub struct Config { - pub listen_url: String, + pub listen_url: String, pub project_id: String, - pub private_key: PrivateKey, + pub signer_private_key: PrivateKey, + pub signer_address: String, pub chain_id: u64, pub moralis_base_url: String, pub moralis_api_key: String, + pub testing_address: String, } -pub fn init() -> Config { +pub fn init() -> Config { let panic_message: String = "enviroment variable is not set".to_string(); Config { listen_url: match env::var("LISTEN_URL") { Ok(var) => var, - Err(_) => panic!("LISTEN_URL {}", panic_message) + Err(_) => panic!("LISTEN_URL {}", panic_message), }, project_id: match env::var("INFURA_PROJECT_ID") { Ok(var) => var, - Err(_) => panic!("INFURA_PROJECT_ID {}", panic_message) + Err(_) => panic!("INFURA_PROJECT_ID {}", panic_message), }, - private_key: match env::var("PRIVATE_KEY") { + signer_private_key: match env::var("SIGNER_PRIVATE_KEY") { Ok(var) => var.parse().expect("invalid PK"), - Err(_) => panic!("PRIVATE_KEY {}", panic_message) + Err(_) => panic!("PRIVATE_KEY {}", panic_message), + }, + signer_address: match env::var("SIGNER_ADDRESS") { + Ok(var) => var, + Err(_) => "".to_string(), }, chain_id: match env::var("CHAIN_ID") { Ok(var) => var.parse::().unwrap(), - Err(_) => panic!("CHAIN_ID {}", panic_message) + Err(_) => panic!("CHAIN_ID {}", panic_message), }, moralis_base_url: match env::var("MORALIS_BASE_URL") { Ok(var) => var, - Err(_) => panic!("MORALIS_BASE_URL {}", panic_message) + Err(_) => panic!("MORALIS_BASE_URL {}", panic_message), }, moralis_api_key: match env::var("MORALIS_API_KEY") { Ok(var) => var, - Err(_) => panic!("MORALIS_API_KEY {}", panic_message) + Err(_) => panic!("MORALIS_API_KEY {}", panic_message), + }, + testing_address: match env::var("TESTING_ADDRESS") { + Ok(var) => var, + Err(_) => "".to_string(), }, } } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ae9b1e6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod asset; +pub mod config; +pub mod error; +pub mod moralis; +pub mod wrapper; diff --git a/src/main.rs b/src/main.rs index fcab1f4..379357b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,34 +1,28 @@ -mod config; -mod error; -mod asset; -mod moralis; -mod wrapper; - use actix_cors::Cors; -use asset::*; -use moralis::*; -use wrapper::*; -use actix_web::{HttpServer, App, web::Data, http}; +use actix_web::{http, web::Data, App, HttpServer}; use actix_web_prom::PrometheusMetricsBuilder; -use serde::{Serialize, Deserialize}; use dotenv::dotenv; - +use serde::{Deserialize, Serialize}; +use sugarfunge_integration::asset::*; +use sugarfunge_integration::config; +use sugarfunge_integration::moralis::*; +use sugarfunge_integration::wrapper::*; #[derive(Serialize, Deserialize, Debug)] enum ContentType { - JSON + JSON, } impl ContentType { + #[warn(dead_code)] pub fn as_str(&self) -> &'static str { match *self { - ContentType::JSON => "application/json" + ContentType::JSON => "application/json", } } } #[actix_web::main] async fn main() -> std::io::Result<()> { - dotenv().ok(); env_logger::init(); @@ -41,7 +35,7 @@ async fn main() -> std::io::Result<()> { let url = env.listen_url.to_owned(); - HttpServer::new( move || { + HttpServer::new(move || { let cors = Cors::default() .allowed_origin_fn(|origin, _req_head| { origin.as_bytes().starts_with(b"http://localhost") @@ -59,6 +53,7 @@ async fn main() -> std::io::Result<()> { .service(get_nfts) .service(get_contract_nfts) .service(get_nft_transfers) + .service(get_transaction) .service(get_nft_transfers_by_block) .service(get_all_token_ids) .service(get_contract_nft_transfers) diff --git a/src/moralis.rs b/src/moralis.rs index a35b6d9..21876ee 100644 --- a/src/moralis.rs +++ b/src/moralis.rs @@ -6,15 +6,15 @@ use log::error; use snailquote::unescape; #[derive(Serialize, Deserialize, Debug)] -struct Address { - address: String, - options: QueryParams +pub struct Address { + pub address: String, + pub options: QueryParams } #[derive(Serialize, Deserialize, Debug)] -struct Token { - token_address: String, - options: QueryParams +pub struct Token { + pub token_address: String, + pub options: QueryParams } #[derive(Serialize, Deserialize, Debug)] @@ -23,6 +23,11 @@ struct AccountToken { token_address: String, options: QueryParams } +#[derive(Serialize, Deserialize, Debug)] +pub struct Transaction { + pub tx: String, + pub options: QueryParams +} #[derive(Serialize, Deserialize, Debug)] struct TokenId { @@ -39,10 +44,10 @@ struct BlockNumber { #[derive(Debug, PartialEq, Deserialize, Serialize)] pub struct QueryParams { - chain: Option, - format: Option, - offset: Option, - limit: Option + pub chain: Option, + pub format: Option, + pub offset: Option, + pub limit: Option } pub fn check_query_params(params: &QueryParams) -> QueryParams { @@ -50,7 +55,7 @@ pub fn check_query_params(params: &QueryParams) -> QueryParams { QueryParams { chain: match ¶ms.chain { Some(chain) => Some(chain.to_string()), - None => Some("ropsten".to_string()), + None => Some("rinkeby".to_string()), }, format: match ¶ms.format { Some(format) => Some(format.to_string()), @@ -184,3 +189,12 @@ async fn get_token_id_owners(req_body: String, config: Data) -> Result) -> Result { + let req_data: Transaction = serde_json::from_str(&req_body)?; + + let url: String = config.moralis_base_url.to_owned() + "transaction/"+ &unescape(&req_data.tx).unwrap(); + + moralis_call(&config, &url, check_query_params(&req_data.options)).await +} \ No newline at end of file diff --git a/src/wrapper.rs b/src/wrapper.rs index 4288ffa..b90c9ea 100644 --- a/src/wrapper.rs +++ b/src/wrapper.rs @@ -1,19 +1,30 @@ -use crate::{error::ApiError, config::Config, asset::{AssetData, AssetTransfer, get_web3, get_asset_data, asset_transfer_nft, AssetBatchTransfer, asset_batch_transfer_nft}}; -use std::{env, str::FromStr, fmt::Debug}; +use crate::{ + asset::{ + asset_batch_transfer_nft, asset_transfer_nft, get_asset_data, get_web3, AssetBatchTransfer, + AssetData, AssetTransfer, + }, + config::Config, + error::ApiError, +}; +use actix_web::{ + post, + web::{Data, Json}, + Responder, +}; use ethcontract::prelude::*; -use actix_web::{post, web::{Data, Json}, Responder}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use serde_json::json; +use std::{env, fmt::Debug, str::FromStr}; include!(concat!(env!("OUT_DIR"), "/SugarFungeAsset.rs")); include!(concat!(env!("OUT_DIR"), "/Wrapped1155Factory.rs")); #[derive(Serialize, Deserialize, Debug)] pub struct Wrap1155 { - from: String, - amount: u64, - id: u64, - data: AssetData, + pub from: String, + pub amount: u64, + pub id: u64, + pub data: AssetData, } #[derive(Serialize, Deserialize, Debug)] @@ -32,14 +43,13 @@ pub struct GetWrapped1155 { #[derive(Serialize, Deserialize, Debug)] pub struct Unwrap1155 { - id: u64, - amount: u64, - recipient_address: String, - data: AssetData, + pub id: u64, + pub amount: u64, + pub recipient_address: String, + pub data: AssetData, } pub async fn wrapper_wrap(config: &Config, token: Wrap1155) -> Result { - let web3 = get_web3(config)?; let factory_contract = Wrapped1155Factory::deployed(&web3).await?; @@ -49,14 +59,16 @@ pub async fn wrapper_wrap(config: &Config, token: Wrap1155) -> Result Result { - +pub async fn wrapper_batch_wrap( + config: &Config, + token: BatchWrap1155, +) -> Result { let web3 = get_web3(config)?; let factory_contract = Wrapped1155Factory::deployed(&web3).await?; @@ -66,16 +78,18 @@ pub async fn wrapper_batch_wrap(config: &Config, token: BatchWrap1155) -> Result to: format!("0x{:x}", Wrapped1155Factory::address(&factory_contract)), amounts: token.amounts, ids: token.ids, - data: token.data + data: token.data, }; asset_batch_transfer_nft(config, &transfer).await } -pub async fn wrapper_unwrap(config: &Config, unwrap: &Unwrap1155) -> Result { - +pub async fn wrapper_unwrap( + config: &Config, + unwrap: &Unwrap1155, +) -> Result { let account = { - let key: PrivateKey = config.private_key.to_owned(); + let key: PrivateKey = config.signer_private_key.to_owned(); Account::Offline(key, Some(config.chain_id)) }; @@ -87,24 +101,39 @@ pub async fn wrapper_unwrap(config: &Config, unwrap: &Unwrap1155) -> Result Result { - +pub async fn wrapper_get_wrapped( + config: &Config, + wrapped: &GetWrapped1155, +) -> Result { let account = { - let key: PrivateKey = config.private_key.to_owned(); + let key: PrivateKey = config.signer_private_key.to_owned(); Account::Offline(key, Some(config.chain_id)) }; @@ -116,16 +145,24 @@ pub async fn wrapper_get_wrapped(config: &Config, wrapped: &GetWrapped1155) -> R let sugarfunge_contract = SugarFungeAsset::deployed(&web3).await?; - let result = contract.get_wrapped_1155( - H160::from_str(&format!("0x{:x}", SugarFungeAsset::address(&sugarfunge_contract))).unwrap(), - wrapped.id.into(), - get_asset_data(wrapped.data.name.to_owned(), wrapped.data.symbol.to_owned(), wrapped.data.decimals)) + let result = contract + .get_wrapped_1155( + H160::from_str(&format!( + "0x{:x}", + SugarFungeAsset::address(&sugarfunge_contract) + )) + .unwrap(), + wrapped.id.into(), + get_asset_data( + wrapped.data.name.to_owned(), + wrapped.data.symbol.to_owned(), + wrapped.data.decimals, + ), + ) .call() .await?; - Ok(Json(json!({ - "tx": format!("0x{:x}", result) - }))) + Ok(Json(json!({ "tx": format!("0x{:x}", result) }))) } #[post("wrap_1155")] @@ -136,7 +173,10 @@ async fn wrap_1155(req_body: String, config: Data) -> Result) -> Result { +async fn batch_wrap_1155( + req_body: String, + config: Data, +) -> Result { let req_data: BatchWrap1155 = serde_json::from_str(&req_body)?; wrapper_batch_wrap(&config, req_data).await @@ -150,7 +190,10 @@ async fn unwrap_1155(req_body: String, config: Data) -> Result) -> Result { +async fn get_wrapped_1155( + req_body: String, + config: Data, +) -> Result { let req_data: GetWrapped1155 = serde_json::from_str(&req_body)?; wrapper_get_wrapped(&config, &req_data).await diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..440dae7 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,379 @@ +mod utils; +use crate::utils::{ + get_address_nfts, get_app, get_nft, print_success, setup_env_var, TransactionInfo, +}; +use actix_web::{ + self, + test::{self, TestRequest}, +}; +use ethcontract::{ + transaction::confirm::{wait_for_confirmation, ConfirmParams}, + H256, +}; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use std::println as info; +use sugarfunge_integration::{ + asset::{get_web3, AssetData, AssetMint, AssetTransfer}, + wrapper::{Unwrap1155, Wrap1155}, +}; + +//Structs +#[derive(Serialize, Deserialize, Debug)] +pub struct ApiResponse { + tx: String, + to: String, + from: String, + hash: H256, +} + +//ASSET + +//Testing Mint process +#[actix_web::test] +async fn test_1_asset_mint() { + //Setup Env variables + let env = utils::setup_env_var(); + + //Initialize rng to get random mock data + let mut rng = rand::thread_rng(); + + info!("\n\n Testing Mint process"); + + //Mock Data + let mock_address: String = env.signer_address.clone(); + let mock_asset_id: u64 = rng.gen_range(0..10); + let mock_amount: u64 = rng.gen_range(50..100); + + //Constructing the mint request + let req_data = AssetMint { + account: mock_address.clone(), + amount: mock_amount, + id: mock_asset_id, + data: AssetData { + name: String::from("name"), + symbol: String::from("symbol"), + decimals: 8, + }, + }; + + //Initializing testing app + let mut app = get_app(&env).await; + + //Fetching moralis to get address's tokens + let moralis_response: TransactionInfo = get_address_nfts(&app, &mock_address).await; + + //Saving amount into a variable + let amount_before_mint: u64 = get_nft(moralis_response.clone(), Some(mock_asset_id)) + .amount + .clone() + .parse::() + .unwrap(); + + info!( + "Amount of address asset before minting -> {:?}", + amount_before_mint + ); + + info!("Attempting to mint {:?} assets", mock_amount); + + //Fetching mint nft (INFURA) + let res = TestRequest::post() + .uri("/mint_nft") + .set_json(&req_data) + .send_request(&mut app) + .await; + assert!(res.status().is_success(), "Failed to mint NFT"); + //Parsing the response + let response: ApiResponse = test::read_body_json(res).await; + + //Initializing web3 + let web3 = get_web3(&env).unwrap(); + + //Waiting at least 10 confirmations to fetch the data again + wait_for_confirmation(&web3, response.hash, ConfirmParams::with_confirmations(10)) + .await + .expect("Failed while waiting for blockchain confirmation"); + + //RE-Fetching address's tokens + let moralis_response: TransactionInfo = get_address_nfts(&app, &mock_address).await; + + //Extracting amount from a specific token id + let amount_after_mint: u64 = get_nft(moralis_response.clone(), Some(mock_asset_id)) + .amount + .clone() + .parse::() + .unwrap(); + info!( + "Amount of address's asset after minting -> {:?}", + amount_after_mint + ); + + //Checking if the from address is the same as the transaction from address + assert_eq!(&response.from.to_lowercase(), &mock_address.to_lowercase()); + //Checking if the amount that user's hold is what it has plus the minted amount + let expected_token_amount = amount_before_mint.checked_add(mock_amount).unwrap(); + assert_eq!(expected_token_amount, amount_after_mint); + + //Print success message on terminal + print_success("Mint NFT Test PASSED".to_string()); +} +//Testing Transfer process +#[actix_web::test] +async fn test_2_asset_transfer() { + //Setup Env variables + let env = utils::setup_env_var(); + + //Initialize rng to get random mock data + let mut rng = rand::thread_rng(); + + //Initializing testing app + let mut app = get_app(&env).await; + + info!("\n\n Testing Transfer process"); + + //Mock Data + let mock_address = &env.signer_address; //From address + let mock_second_address: String = String::from(&env.testing_address); //To address + //Fetching moralis to get address's tokens + let moralis_response: TransactionInfo = get_address_nfts(&app, &mock_address).await; + let mock_asset_id: u64 = moralis_response + .result + .first() + .unwrap() + .token_id + .parse::() + .unwrap(); + + //Saving amount into a variable + let amount_before_transfer: u64 = get_nft(moralis_response.clone(), Some(mock_asset_id)) + .amount + .clone() + .parse::() + .unwrap(); + + //Random amount to transfer (from 1 to 20) + let mock_amount: u64 = rng.gen_range(1..20); + + //Constructing the transfer request + let req_data = AssetTransfer { + from: mock_address.clone(), + to: mock_second_address.clone(), + amount: mock_amount.clone(), + id: mock_asset_id, + data: AssetData { + name: String::from("name"), + symbol: String::from("symbol"), + decimals: 8, + }, + }; + + info!( + "Amount of address asset before transferring -> {:?}", + &amount_before_transfer + ); + + info!("Attempting to transfer {:?} assets", mock_amount); + + //Fetching transfer nft (INFURA) + let res = TestRequest::post() + .uri("/transfer_nft") + .set_json(&req_data) + .send_request(&mut app) + .await; + assert!(res.status().is_success(), "Failed to transfer NFT"); + //Parsing the response + let response: ApiResponse = test::read_body_json(res).await; + + //Initializing web3 + let web3 = get_web3(&env).unwrap(); + //Waiting at least 10 confirmations to fetch the data again + wait_for_confirmation(&web3, response.hash, ConfirmParams::with_confirmations(10)) + .await + .expect("Failed while waiting for blockchain confirmation"); + + //Fetching moralis to get address's tokens + let moralis_response: TransactionInfo = get_address_nfts(&app, &mock_address).await; + + let amount_after_transfer: u64 = get_nft(moralis_response.clone(), Some(mock_asset_id)) + .amount + .clone() + .parse::() + .unwrap(); + info!( + "Amount of address's asset after transfer -> {:?}", + amount_after_transfer + ); + + //Checking if the from address is the same as the transaction from address + assert_eq!(&response.from.to_lowercase(), &mock_address.to_lowercase()); + + //Checking if the amount that user's hold is what it has minus the transferred amount + let expected_token_amount = amount_before_transfer.checked_sub(mock_amount).unwrap(); + assert_eq!(expected_token_amount, amount_after_transfer); + + //Print success message on terminal + print_success("Transfer NFT Test PASSED".to_string()); +} + +//WRAPPER + +//Testing Wrapper process +#[actix_web::test] +async fn test_3_asset_wrap() { + //Setup Env variables + let env = setup_env_var(); + + //Initialize rng to get random mock data + let mut rng = rand::thread_rng(); + + //Initializing testing app + let mut app = get_app(&env).await; + + info!("\n\n Testing Wrapper process"); + + //Mock Data + let mock_address = &env.signer_address; + + //Fetching moralis to get address's tokens + let moralis_response: TransactionInfo = get_address_nfts(&app, &mock_address).await; + let returned_nft = get_nft(moralis_response.clone(), None); + let mock_asset_id: u64 = returned_nft.token_id.parse::().unwrap(); + + //Saving amount into a variable + let amount_before_wrap: u64 = returned_nft.amount.clone().parse::().unwrap(); + if amount_before_wrap == 0 { + panic!("Address doesn't have a nft"); + } + let mock_amount: u64 = rng.gen_range(5..20); + + //Mocking request data (INFURA) + //Constructing the transfer request + let req_data = Wrap1155 { + from: mock_address.clone(), + amount: mock_amount, + id: mock_asset_id, + data: AssetData { + name: String::from("name"), + symbol: String::from("symbol"), + decimals: 8, + }, + }; + info!( + "Amount of address asset before wrapping -> {:?}", + amount_before_wrap + ); + + info!("Attempting to wrap {:?} assets", mock_amount); + let res = TestRequest::post() + .uri("/wrap_1155") + .set_json(&req_data) + .send_request(&mut app) + .await; + assert!(res.status().is_success(), "Failed to wrap NFT"); + //Parsing the response + let response: ApiResponse = test::read_body_json(res).await; + + //Initializing web3 + let web3 = get_web3(&env).unwrap(); + //Waiting at least 10 confirmations to fetch the data again + wait_for_confirmation(&web3, response.hash, ConfirmParams::with_confirmations(10)) + .await + .expect("Failed while waiting for blockchain confirmation"); + + //Fetching moralis to get address's tokens + let moralis_response: TransactionInfo = get_address_nfts(&app, &mock_address).await; + let address_nft = get_nft(moralis_response.clone(), Some(mock_asset_id)); + let amount_after_wrap: u64 = address_nft.amount.clone().parse::().unwrap(); + info!( + "Amount of address's asset after wrapping -> {:?}", + amount_after_wrap + ); + //Checking if the from address is the same as the transaction from address + assert_eq!(&response.from.to_lowercase(), &mock_address.to_lowercase()); + //Checking if the amount that user's hold is what it has plus the minted amount + let expected_token_amount = amount_before_wrap.checked_sub(mock_amount).unwrap(); + assert_eq!(expected_token_amount, amount_after_wrap); + + //Print success message on terminal + print_success("Wrap Test PASSED".to_string()); +} +//Testing Unwrapper process +#[actix_web::test] +async fn test_4_asset_unwrap() { + //Setup Env variables + let env = setup_env_var(); + + //Initialize rng to get random mock data + let mut rng = rand::thread_rng(); + + //Initializing testing app + let mut app = get_app(&env).await; + + info!("\n\n Testing Unwrapper process"); + + //Mock Data + let mock_address = &env.signer_address; + //Fetching moralis to get address's tokens + let moralis_response: TransactionInfo = get_address_nfts(&app, &mock_address).await; + let returned_nft = get_nft(moralis_response.clone(), None); + let mock_asset_id: u64 = returned_nft.token_id.parse::().unwrap(); + //Saving amount into a variable + let amount_before_unwrap: u64 = returned_nft.amount.clone().parse::().unwrap(); + if amount_before_unwrap == 0 { + panic!("Address doesn't have a nft"); + } + let mock_amount: u64 = rng.gen_range(1..5); + + //Mocking request data (INFURA) + //Constructing the transfer request + let req_data = Unwrap1155 { + recipient_address: mock_address.clone(), + amount: mock_amount, + id: mock_asset_id, + data: AssetData { + name: String::from("name"), + symbol: String::from("symbol"), + decimals: 8, + }, + }; + info!( + "Amount of address asset before unwrapping -> {:?}", + amount_before_unwrap + ); + + info!("Attempting to unwrap {:?} assets", mock_amount); + let res = TestRequest::post() + .uri("/unwrap_1155") + .set_json(&req_data) + .send_request(&mut app) + .await; + assert!(res.status().is_success(), "Failed to unwrap NFT"); + //Parsing the response + let response: ApiResponse = test::read_body_json(res).await; + + //Initializing web3 + let web3 = get_web3(&env).unwrap(); + //Waiting at least 10 confirmations to fetch the data again + wait_for_confirmation(&web3, response.hash, ConfirmParams::with_confirmations(10)) + .await + .expect("Failed while waiting for blockchain confirmation"); + + //Fetching moralis to get address's tokens + let moralis_response: TransactionInfo = get_address_nfts(&app, &mock_address).await; + let address_nft = get_nft(moralis_response.clone(), Some(mock_asset_id)); + let amount_after_unwrap: u64 = address_nft.amount.clone().parse::().unwrap(); + info!( + "Amount of address's asset after unwrapping -> {:?}", + amount_after_unwrap + ); + //Checking if the from address is the same as the transaction from address + assert_eq!(&response.from.to_lowercase(), &mock_address.to_lowercase()); + //Checking if the amount that user's hold is what it has plus the unwrapped amount + let expected_token_amount = amount_before_unwrap.checked_add(mock_amount).unwrap(); + assert_eq!(expected_token_amount, amount_after_unwrap); + + //Print success message on terminal + print_success("Unwrap Test PASSED".to_string()); +} diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs new file mode 100644 index 0000000..2982f32 --- /dev/null +++ b/tests/utils/mod.rs @@ -0,0 +1,135 @@ +use actix_http::Request; +use actix_web::{ + self, + dev::{Service, ServiceResponse}, + test::{self, TestRequest}, + web::Data, + App, Error, +}; +use dotenv::dotenv; +use sugarfunge_integration::{ + asset::{batch_transfer_nft, mint_nft, transfer_nft}, + config::{self, Config}, + moralis::{ + get_all_token_ids, get_contract_nft_transfers, get_contract_nfts, get_nft_metadata, + get_nft_owners, get_nft_transfers, get_nft_transfers_by_block, get_nfts, + get_token_id_metadata, get_token_id_owners, get_transaction, Address, QueryParams, + }, + wrapper::{batch_wrap_1155, get_wrapped_1155, unwrap_1155, wrap_1155}, +}; + +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ResultVec { + pub amount: String, + pub token_address: String, + pub token_hash: String, + pub token_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TransactionInfo { + pub cursor: String, + pub page: u128, + pub page_size: u128, + pub result: Vec, + pub status: String, + pub total: u128, +} + +pub fn setup_env_var() -> Config { + //Setup Env variables + dotenv().ok(); + config::init() +} + +//This function asks moralis for the list of address's nfts and return it parsed +pub async fn get_address_nfts( + mut app: impl Service, + mock_address: &String, +) -> TransactionInfo { + //Mocking request data (MORALIS "get_nft") + let options = QueryParams { + chain: Some(String::from("rinkeby")), + format: Some(String::from("decimal")), + limit: Some(0), + offset: Some(0), + }; + let req_moralis = Address { + address: mock_address.to_string(), + options, + }; + //Fetching NFT amount before minting (MORALIS) + let moralis_res = TestRequest::post() + .uri("/get_nfts") + .set_json(&req_moralis) + .send_request(&mut app) + .await; + assert!( + moralis_res.status().is_success(), + "Failed to fetch transaction info" + ); + let moralis_response: TransactionInfo = test::read_body_json(moralis_res).await; + moralis_response +} + +//This function creates and returns an instance of App +pub async fn get_app( + env: &Config, +) -> impl Service { + //Initializing testing app + test::init_service( + App::new() + .service(mint_nft) + .service(transfer_nft) + .service(batch_transfer_nft) + .service(get_nfts) + .service(get_contract_nfts) + .service(get_nft_transfers) + .service(get_transaction) + .service(get_nft_transfers_by_block) + .service(get_all_token_ids) + .service(get_contract_nft_transfers) + .service(get_nft_metadata) + .service(get_nft_owners) + .service(get_token_id_metadata) + .service(get_token_id_owners) + .service(wrap_1155) + .service(batch_wrap_1155) + .service(unwrap_1155) + .service(get_wrapped_1155) + .app_data(Data::new(env.clone())), + ) + .await +} +//This function return the amount of a nft id from a moralis result vector +pub fn get_nft(moralis_response: TransactionInfo, mock_asset_id: Option) -> ResultVec { + //Default value + let default_nft_vec = ResultVec { + amount: String::from("0"), + token_address: String::from(""), + token_hash: String::from(""), + token_id: String::from(""), + }; + + if let Some(asset_id) = mock_asset_id { + moralis_response + .result + .iter() + .find(|element| element.token_id == asset_id.to_string()) + .unwrap_or(&default_nft_vec) + .clone() + } else { + moralis_response + .result + .first() + .unwrap_or(&default_nft_vec) + .clone() + } +} + +pub fn print_success(text: String) { + println!("\x1b[0;32m{:#?}\x1b[0m", text); +}