diff --git a/contracts/IERC165.json b/contracts/IERC165.json new file mode 100644 index 0000000..ff87f91 --- /dev/null +++ b/contracts/IERC165.json @@ -0,0 +1,30 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "IERC165", + "sourceName": "@openzeppelin/contracts/utils/introspection/IERC165.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/contracts/IGate.json b/contracts/IGate.json new file mode 100644 index 0000000..289e2c8 --- /dev/null +++ b/contracts/IGate.json @@ -0,0 +1,30 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "IGate", + "sourceName": "contracts/interfaces/IGate.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "isAllowedUser", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/contracts/IMinter.json b/contracts/IMinter.json new file mode 100644 index 0000000..febd88a --- /dev/null +++ b/contracts/IMinter.json @@ -0,0 +1,57 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "IMinter", + "sourceName": "contracts/interfaces/IMinter.sol", + "abi": [ + { + "inputs": [], + "name": "collectible", + "outputs": [ + { + "internalType": "contract IERC721Metadata", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "string", + "name": "tokenURI", + "type": "string" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/contracts/ISubscription.json b/contracts/ISubscription.json index ae55cce..57472f4 100644 --- a/contracts/ISubscription.json +++ b/contracts/ISubscription.json @@ -71,6 +71,13 @@ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [], + "name": "withdrawReceivedAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ], "bytecode": "0x", diff --git a/contracts/IAdapter.json b/contracts/ISubscriptionAdapter.json similarity index 66% rename from contracts/IAdapter.json rename to contracts/ISubscriptionAdapter.json index 5109f14..047a966 100644 --- a/contracts/IAdapter.json +++ b/contracts/ISubscriptionAdapter.json @@ -1,21 +1,8 @@ { "_format": "hh-sol-artifact-1", - "contractName": "IAdapter", - "sourceName": "contracts/interfaces/IAdapter.sol", + "contractName": "ISubscriptionAdapter", + "sourceName": "contracts/interfaces/ISubscriptionAdapter.sol", "abi": [ - { - "inputs": [], - "name": "collectible", - "outputs": [ - { - "internalType": "contract IERC721Metadata", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -97,25 +84,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "user", - "type": "address" - } - ], - "name": "isAllowedUser", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -135,39 +103,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "internalType": "string", - "name": "tokenURI", - "type": "string" - }, - { - "internalType": "uint8", - "name": "v", - "type": "uint8" - }, - { - "internalType": "bytes32", - "name": "r", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "s", - "type": "bytes32" - } - ], - "name": "mint", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "subscription", diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 7d56301..eb2c890 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -782,6 +782,19 @@ components: description: Blockchain contract address. type: string nullable: true + blockchain_features: + description: Blockchain contract features. + type: object + nullable: true + properties: + minter: + description: Minter feature flag. + type: boolean + example: true + subscription: + description: Subscription feature flag. + type: boolean + example: true blockchain_info: description: Additional information about blockchain type: object diff --git a/src/ethereum/contracts.rs b/src/ethereum/contracts.rs index 86743d2..54c8864 100644 --- a/src/ethereum/contracts.rs +++ b/src/ethereum/contracts.rs @@ -3,7 +3,7 @@ use std::path::Path; use web3::{ api::Web3, - contract::{Contract, Options}, + contract::{Contract, Error as ContractError, Options}, ethabi, transports::Http, }; @@ -17,7 +17,10 @@ use super::sync::{ }; use super::utils::parse_address; -const ADAPTER: &str = "IAdapter"; +const ERC165: &str = "IERC165"; +const GATE: &str = "IGate"; +const MINTER: &str = "IMinter"; +const SUBSCRIPTION_ADAPTER: &str = "ISubscriptionAdapter"; const SUBSCRIPTION: &str = "ISubscription"; const ERC721: &str = "IERC721Metadata"; @@ -51,13 +54,38 @@ fn load_abi( Ok(abi) } +// https://eips.ethereum.org/EIPS/eip-165 +// Interface identifier is the XOR of all function selectors in the interface +fn interface_signature(interface: ðabi::Contract) -> [u8; 4] { + interface.functions() + .map(|func| func.short_signature()) + .fold([0; 4], |mut acc, item| { + for i in 0..4 { + acc[i] ^= item[i]; + }; + acc + }) +} + +/// Returns true if contract supports interface (per ERC-165) +async fn is_interface_supported( + contract: &Contract, + interface: ðabi::Contract, +) -> Result { + let signature = interface_signature(interface); + contract.query( + "supportsInterface", + (signature,), None, Options::default(), None, + ).await +} + #[derive(Clone)] pub struct ContractSet { pub web3: Web3, - pub adapter: Contract, - pub collectible: Contract, - pub subscription: Contract, + pub gate: Option>, + pub collectible: Option>, + pub subscription: Option>, } #[derive(Clone)] @@ -75,51 +103,90 @@ pub async fn get_contracts( if chain_id != config.ethereum_chain_id().into() { return Err(EthereumError::ImproperlyConfigured("incorrect chain ID")); }; - let adapter_abi = load_abi(&config.contract_dir, ADAPTER)?; + let adapter_address = parse_address(&config.contract_address)?; - let adapter = Contract::new( + let erc165_abi = load_abi(&config.contract_dir, ERC165)?; + let erc165 = Contract::new( web3.eth(), adapter_address, - adapter_abi, + erc165_abi, ); - let collectible_address = adapter.query( - "collectible", - (), None, Options::default(), None, - ).await?; - let collectible_abi = load_abi(&config.contract_dir, ERC721)?; - let collectible = Contract::new( - web3.eth(), - collectible_address, - collectible_abi, - ); - log::info!("collectible item contract address is {:?}", collectible.address()); + let mut maybe_gate = None; + let mut maybe_collectible = None; + let mut maybe_subscription = None; + let mut sync_targets = vec![]; - let subscription_address = adapter.query( - "subscription", - (), None, Options::default(), None, - ).await?; - let subscription_abi = load_abi(&config.contract_dir, SUBSCRIPTION)?; - let subscription = Contract::new( - web3.eth(), - subscription_address, - subscription_abi, - ); - log::info!("subscription contract address is {:?}", subscription.address()); + let gate_abi = load_abi(&config.contract_dir, GATE)?; + if is_interface_supported(&erc165, &gate_abi).await? { + let gate = Contract::new( + web3.eth(), + adapter_address, + gate_abi, + ); + maybe_gate = Some(gate); + log::info!("found gate interface"); + }; + + let minter_abi = load_abi(&config.contract_dir, MINTER)?; + if is_interface_supported(&erc165, &minter_abi).await? { + let minter = Contract::new( + web3.eth(), + adapter_address, + minter_abi, + ); + log::info!("found minter interface"); + let collectible_address = minter.query( + "collectible", + (), None, Options::default(), None, + ).await?; + let collectible_abi = load_abi(&config.contract_dir, ERC721)?; + let collectible = Contract::new( + web3.eth(), + collectible_address, + collectible_abi, + ); + log::info!("collectible item contract address is {:?}", collectible.address()); + sync_targets.push(collectible.address()); + maybe_collectible = Some(collectible); + }; + + let subscription_adapter_abi = load_abi(&config.contract_dir, SUBSCRIPTION_ADAPTER)?; + if is_interface_supported(&erc165, &subscription_adapter_abi).await? { + let subscription_adapter = Contract::new( + web3.eth(), + adapter_address, + subscription_adapter_abi, + ); + log::info!("found subscription interface"); + let subscription_address = subscription_adapter.query( + "subscription", + (), None, Options::default(), None, + ).await?; + let subscription_abi = load_abi(&config.contract_dir, SUBSCRIPTION)?; + let subscription = Contract::new( + web3.eth(), + subscription_address, + subscription_abi, + ); + log::info!("subscription contract address is {:?}", subscription.address()); + sync_targets.push(subscription.address()); + maybe_subscription = Some(subscription); + }; let current_block = get_current_block_number(&web3, storage_dir).await?; log::info!("current block is {}", current_block); let sync_state = SyncState::new( current_block, - vec![collectible.address(), subscription.address()], + sync_targets, storage_dir, ); let contract_set = ContractSet { web3, - adapter, - collectible, - subscription, + gate: maybe_gate, + collectible: maybe_collectible, + subscription: maybe_subscription, }; Ok(Blockchain { contract_set, sync_state }) } diff --git a/src/ethereum/gate.rs b/src/ethereum/gate.rs index e5cfd9e..323c9a2 100644 --- a/src/ethereum/gate.rs +++ b/src/ethereum/gate.rs @@ -8,8 +8,12 @@ pub async fn is_allowed_user( contract_set: &ContractSet, user_address: &str, ) -> Result { + let gate = match &contract_set.gate { + Some(contract) => contract, + None => return Ok(true), // no gate + }; let user_address = parse_address(user_address)?; - let result: bool = contract_set.adapter.query( + let result: bool = gate.query( "isAllowedUser", (user_address,), None, Options::default(), None, ).await?; diff --git a/src/mastodon_api/instance/types.rs b/src/mastodon_api/instance/types.rs index ed88033..b4ea373 100644 --- a/src/mastodon_api/instance/types.rs +++ b/src/mastodon_api/instance/types.rs @@ -3,8 +3,15 @@ use std::collections::HashMap; use serde::Serialize; use crate::config::Config; +use crate::ethereum::contracts::ContractSet; use crate::mastodon_api::MASTODON_API_VERSION; +#[derive(Serialize)] +struct BlockchainFeatures { + minter: bool, + subscription: bool, +} + #[derive(Serialize)] pub struct InstanceInfo { uri: String, @@ -19,6 +26,7 @@ pub struct InstanceInfo { blockchain_id: Option, blockchain_explorer_url: Option, blockchain_contract_address: Option, + blockchain_features: Option, blockchain_info: Option>, ipfs_gateway_url: Option, } @@ -31,8 +39,14 @@ fn get_full_api_version(version: &str) -> String { ) } -impl From<&Config> for InstanceInfo { - fn from(config: &Config) -> Self { +impl InstanceInfo { + pub fn create(config: &Config, maybe_blockchain: Option<&ContractSet>) -> Self { + let blockchain_features = maybe_blockchain.map(|contract_set| { + BlockchainFeatures { + minter: contract_set.collectible.is_some(), + subscription: contract_set.subscription.is_some(), + } + }); Self { uri: config.instance().host(), title: config.instance_title.clone(), @@ -48,6 +62,7 @@ impl From<&Config> for InstanceInfo { .and_then(|val| val.explorer_url.clone()), blockchain_contract_address: config.blockchain.as_ref() .map(|val| val.contract_address.clone()), + blockchain_features: blockchain_features, blockchain_info: config.blockchain.as_ref() .and_then(|val| val.chain_info.clone()), ipfs_gateway_url: config.ipfs_gateway_url.clone(), diff --git a/src/mastodon_api/instance/views.rs b/src/mastodon_api/instance/views.rs index ecfd215..22c597f 100644 --- a/src/mastodon_api/instance/views.rs +++ b/src/mastodon_api/instance/views.rs @@ -2,13 +2,18 @@ use actix_web::{get, web, HttpResponse, Scope}; use crate::config::Config; use crate::errors::HttpError; +use crate::ethereum::contracts::ContractSet; use super::types::InstanceInfo; #[get("")] async fn instance_view( config: web::Data, + maybe_blockchain: web::Data>, ) -> Result { - let instance = InstanceInfo::from(config.as_ref()); + let instance = InstanceInfo::create( + config.as_ref(), + maybe_blockchain.as_ref().as_ref(), + ); Ok(HttpResponse::Ok().json(instance)) } diff --git a/src/scheduler.rs b/src/scheduler.rs index 1f3b763..2eabb9d 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -46,9 +46,13 @@ async fn nft_monitor_task( Some(blockchain) => blockchain, None => return Ok(()), }; + let collectible = match &blockchain.contract_set.collectible { + Some(contract) => contract, + None => return Ok(()), // feature not enabled + }; process_nft_events( &blockchain.contract_set.web3, - &blockchain.contract_set.collectible, + &collectible, &mut blockchain.sync_state, db_pool, token_waitlist_map, @@ -64,9 +68,13 @@ async fn subscription_monitor_task( Some(blockchain) => blockchain, None => return Ok(()), }; + let subscription = match &blockchain.contract_set.subscription { + Some(contract) => contract, + None => return Ok(()), // feature not enabled + }; check_subscriptions( &blockchain.contract_set.web3, - &blockchain.contract_set.subscription, + &subscription, &mut blockchain.sync_state, db_pool, ).await.map_err(Error::from)