Fuck blockchain
This commit is contained in:
parent
cdb728a70a
commit
5ef024d923
35 changed files with 730 additions and 3574 deletions
1880
Cargo.lock
generated
1880
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -50,8 +50,6 @@ env_logger = { version = "0.9.0", default-features = false }
|
||||||
ed25519-dalek = "1.0.1"
|
ed25519-dalek = "1.0.1"
|
||||||
ed25519 = "1.5.3"
|
ed25519 = "1.5.3"
|
||||||
blake2 = "0.10.5"
|
blake2 = "0.10.5"
|
||||||
# Used to query Monero node
|
|
||||||
monero-rpc = "0.3.2"
|
|
||||||
# Used to determine the number of CPUs on the system
|
# Used to determine the number of CPUs on the system
|
||||||
num_cpus = "1.13.0"
|
num_cpus = "1.13.0"
|
||||||
# Used for working with regular expressions
|
# Used for working with regular expressions
|
||||||
|
@ -60,8 +58,6 @@ regex = "1.6.0"
|
||||||
reqwest = { version = "0.11.13", features = ["json", "multipart", "socks"] }
|
reqwest = { version = "0.11.13", features = ["json", "multipart", "socks"] }
|
||||||
# Used for working with RSA keys
|
# Used for working with RSA keys
|
||||||
rsa = "0.5.0"
|
rsa = "0.5.0"
|
||||||
# Used for working with ethereum keys
|
|
||||||
secp256k1 = { version = "0.21.3", features = ["rand", "rand-std"] }
|
|
||||||
# Used for serialization/deserialization
|
# Used for serialization/deserialization
|
||||||
serde = { version = "1.0.136", features = ["derive"] }
|
serde = { version = "1.0.136", features = ["derive"] }
|
||||||
serde_json = "1.0.89"
|
serde_json = "1.0.89"
|
||||||
|
@ -77,8 +73,6 @@ tokio = { version = "1.20.4", features = ["macros"] }
|
||||||
url = "2.2.2"
|
url = "2.2.2"
|
||||||
# Used to work with UUIDs
|
# Used to work with UUIDs
|
||||||
uuid = { version = "1.1.2", features = ["serde", "v4"] }
|
uuid = { version = "1.1.2", features = ["serde", "v4"] }
|
||||||
# Used to query ethereum node
|
|
||||||
web3 = { version = "0.18.0", default-features = false, features = ["http", "http-tls", "signing"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mitra-config = { path = "mitra-config", features = ["test-utils"] }
|
mitra-config = { path = "mitra-config", features = ["test-utils"] }
|
||||||
|
@ -88,7 +82,4 @@ mitra-utils = { path = "mitra-utils", features = ["test-utils"] }
|
||||||
serial_test = "0.7.0"
|
serial_test = "0.7.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
ethereum-extras = []
|
|
||||||
fep-e232 = []
|
|
||||||
|
|
||||||
production = ["mitra-config/production"]
|
production = ["mitra-config/production"]
|
||||||
|
|
16
README.md
16
README.md
|
@ -8,27 +8,19 @@ Features:
|
||||||
|
|
||||||
- Micro-blogging service (includes support for quote posts, custom emojis and more).
|
- Micro-blogging service (includes support for quote posts, custom emojis and more).
|
||||||
- Mastodon API.
|
- Mastodon API.
|
||||||
- Content subscription service. Subscriptions provide a way to receive monthly payments from subscribers and to publish private content made exclusively for them.
|
|
||||||
- Supported payment methods: [Monero](https://www.getmonero.org/get-started/what-is-monero/) and [ERC-20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/) tokens (on Ethereum and other EVM-compatible blockchains).
|
|
||||||
- [Sign-in with a wallet](https://eips.ethereum.org/EIPS/eip-4361).
|
|
||||||
- Donation buttons.
|
|
||||||
- Account migrations (from one server to another). Identity can be detached from the server.
|
- Account migrations (from one server to another). Identity can be detached from the server.
|
||||||
- Federation over Tor.
|
- Federation over Tor.
|
||||||
|
|
||||||
Follow: [@mitra@mitra.social](https://mitra.social/@mitra)
|
|
||||||
|
|
||||||
Matrix chat: [#mitra:halogen.city](https://matrix.to/#/#mitra:halogen.city)
|
|
||||||
|
|
||||||
## Instances
|
## Instances
|
||||||
|
|
||||||
- [FediList](http://demo.fedilist.com/instance?software=mitra)
|
- [FediList](http://demo.fedilist.com/instance?software=reef)
|
||||||
- [Fediverse Observer](https://mitra.fediverse.observer/list)
|
- [Fediverse Observer](https://reef.fediverse.observer/list)
|
||||||
|
|
||||||
Demo instance: https://public.mitra.social/ ([invite-only](https://public.mitra.social/about))
|
Demo instance: https://nullpointer.social/ ([invite-only](https://nullpointer.social/about))
|
||||||
|
|
||||||
## Code
|
## Code
|
||||||
|
|
||||||
Server: https://codeberg.org/silverpill/mitra (this repo)
|
Server: (this repo)
|
||||||
|
|
||||||
Web client: https://codeberg.org/silverpill/mitra-web
|
Web client: https://codeberg.org/silverpill/mitra-web
|
||||||
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# https://monerodocs.org/interacting/monero-wallet-rpc-reference/
|
|
||||||
|
|
||||||
daemon-address=http://example.tld:18081
|
|
||||||
|
|
||||||
untrusted-daemon=1
|
|
||||||
non-interactive=1
|
|
||||||
|
|
||||||
rpc-bind-port=18082
|
|
||||||
disable-rpc-login=1
|
|
||||||
|
|
||||||
wallet-dir=/var/lib/monero-wallet/wallet
|
|
||||||
|
|
||||||
log-file=/var/lib/monero-wallet/wallet-rpc.log
|
|
||||||
log-level=0
|
|
||||||
max-log-file-size=10000000
|
|
||||||
max-log-files=50
|
|
|
@ -8,20 +8,11 @@ use mitra::activitypub::{
|
||||||
builders::delete_person::prepare_delete_person,
|
builders::delete_person::prepare_delete_person,
|
||||||
fetcher::fetchers::fetch_actor,
|
fetcher::fetchers::fetch_actor,
|
||||||
};
|
};
|
||||||
use mitra::ethereum::{
|
|
||||||
signatures::generate_ecdsa_key,
|
|
||||||
sync::save_current_block_number,
|
|
||||||
utils::key_to_ethereum_address,
|
|
||||||
};
|
|
||||||
use mitra::media::{
|
use mitra::media::{
|
||||||
remove_files,
|
remove_files,
|
||||||
remove_media,
|
remove_media,
|
||||||
MediaStorage,
|
MediaStorage,
|
||||||
};
|
};
|
||||||
use mitra::monero::{
|
|
||||||
helpers::check_expired_invoice,
|
|
||||||
wallet::create_monero_wallet,
|
|
||||||
};
|
|
||||||
use mitra::validators::emojis::EMOJI_LOCAL_MAX_SIZE;
|
use mitra::validators::emojis::EMOJI_LOCAL_MAX_SIZE;
|
||||||
use mitra_config::Config;
|
use mitra_config::Config;
|
||||||
use mitra_models::{
|
use mitra_models::{
|
||||||
|
@ -114,12 +105,7 @@ pub struct GenerateEthereumAddress;
|
||||||
|
|
||||||
impl GenerateEthereumAddress {
|
impl GenerateEthereumAddress {
|
||||||
pub fn execute(&self) -> () {
|
pub fn execute(&self) -> () {
|
||||||
let private_key = generate_ecdsa_key();
|
println!("dummy");
|
||||||
let address = key_to_ethereum_address(&private_key);
|
|
||||||
println!(
|
|
||||||
"address {:?}; private key {}",
|
|
||||||
address, private_key.display_secret(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -526,9 +512,8 @@ impl UpdateCurrentBlock {
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
&self,
|
&self,
|
||||||
_config: &Config,
|
_config: &Config,
|
||||||
db_client: &impl DatabaseClient,
|
_db_client: &impl DatabaseClient,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
save_current_block_number(db_client, self.number).await?;
|
|
||||||
println!("current block updated");
|
println!("current block updated");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -565,16 +550,8 @@ pub struct CreateMoneroWallet {
|
||||||
impl CreateMoneroWallet {
|
impl CreateMoneroWallet {
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
&self,
|
&self,
|
||||||
config: &Config,
|
_config: &Config,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let monero_config = config.blockchain()
|
|
||||||
.and_then(|conf| conf.monero_config())
|
|
||||||
.ok_or(anyhow!("monero configuration not found"))?;
|
|
||||||
create_monero_wallet(
|
|
||||||
monero_config,
|
|
||||||
self.name.clone(),
|
|
||||||
self.password.clone(),
|
|
||||||
).await?;
|
|
||||||
println!("wallet created");
|
println!("wallet created");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -589,17 +566,9 @@ pub struct CheckExpiredInvoice {
|
||||||
impl CheckExpiredInvoice {
|
impl CheckExpiredInvoice {
|
||||||
pub async fn execute(
|
pub async fn execute(
|
||||||
&self,
|
&self,
|
||||||
config: &Config,
|
_config: &Config,
|
||||||
db_client: &impl DatabaseClient,
|
_db_client: &impl DatabaseClient,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let monero_config = config.blockchain()
|
|
||||||
.and_then(|conf| conf.monero_config())
|
|
||||||
.ok_or(anyhow!("monero configuration not found"))?;
|
|
||||||
check_expired_invoice(
|
|
||||||
monero_config,
|
|
||||||
db_client,
|
|
||||||
&self.id,
|
|
||||||
).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ use crate::activitypub::vocabulary::{
|
||||||
PROPERTY_VALUE,
|
PROPERTY_VALUE,
|
||||||
};
|
};
|
||||||
use crate::errors::ValidationError;
|
use crate::errors::ValidationError;
|
||||||
use crate::ethereum::identity::verify_eip191_signature;
|
|
||||||
use crate::identity::{
|
use crate::identity::{
|
||||||
claims::create_identity_claim,
|
claims::create_identity_claim,
|
||||||
minisign::{
|
minisign::{
|
||||||
|
@ -79,15 +78,8 @@ pub fn parse_identity_proof(
|
||||||
&signature_bin,
|
&signature_bin,
|
||||||
).map_err(|_| ValidationError("invalid identity proof"))?;
|
).map_err(|_| ValidationError("invalid identity proof"))?;
|
||||||
},
|
},
|
||||||
Did::Pkh(ref did_pkh) => {
|
Did::Pkh(ref _did_pkh) => {
|
||||||
if !matches!(proof_type, IdentityProofType::LegacyEip191IdentityProof) {
|
return Err(ValidationError("incorrect proof type"));
|
||||||
return Err(ValidationError("incorrect proof type"));
|
|
||||||
};
|
|
||||||
verify_eip191_signature(
|
|
||||||
did_pkh,
|
|
||||||
&message,
|
|
||||||
signature,
|
|
||||||
).map_err(|_| ValidationError("invalid identity proof"))?;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let proof = IdentityProof {
|
let proof = IdentityProof {
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
use web3::{
|
|
||||||
api::Web3,
|
|
||||||
transports::Http,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn connect(json_rpc_url: &str) -> Result<Web3<Http>, web3::Error> {
|
|
||||||
let transport = Http::new(json_rpc_url)?;
|
|
||||||
let connection = Web3::new(transport);
|
|
||||||
Ok(connection)
|
|
||||||
}
|
|
|
@ -1,206 +0,0 @@
|
||||||
use std::fs;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use web3::{
|
|
||||||
api::Web3,
|
|
||||||
contract::{Contract, Error as ContractError, Options},
|
|
||||||
ethabi,
|
|
||||||
transports::Http,
|
|
||||||
};
|
|
||||||
|
|
||||||
use mitra_config::EthereumConfig;
|
|
||||||
use mitra_models::database::DatabaseClient;
|
|
||||||
|
|
||||||
use super::api::connect;
|
|
||||||
use super::errors::EthereumError;
|
|
||||||
use super::sync::{
|
|
||||||
get_current_block_number,
|
|
||||||
SyncState,
|
|
||||||
};
|
|
||||||
use super::utils::parse_address;
|
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum ArtifactError {
|
|
||||||
#[error("io error")]
|
|
||||||
IoError(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("json error")]
|
|
||||||
JsonError(#[from] serde_json::Error),
|
|
||||||
|
|
||||||
#[error("key error")]
|
|
||||||
KeyError,
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
AbiError(#[from] ethabi::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_abi(
|
|
||||||
contract_dir: &Path,
|
|
||||||
contract_name: &str,
|
|
||||||
) -> Result<ethabi::Contract, ArtifactError> {
|
|
||||||
let artifact_path = contract_dir.join(format!("{}.json", contract_name));
|
|
||||||
let artifact = fs::read_to_string(artifact_path)?;
|
|
||||||
let artifact_value: serde_json::Value =
|
|
||||||
serde_json::from_str(&artifact)?;
|
|
||||||
let abi_json = artifact_value.get("abi")
|
|
||||||
.ok_or(ArtifactError::KeyError)?
|
|
||||||
.to_string();
|
|
||||||
let abi = ethabi::Contract::load(abi_json.as_bytes())?;
|
|
||||||
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<Http>,
|
|
||||||
interface: ðabi::Contract,
|
|
||||||
) -> Result<bool, ContractError> {
|
|
||||||
let signature = interface_signature(interface);
|
|
||||||
contract.query(
|
|
||||||
"supportsInterface",
|
|
||||||
(signature,), None, Options::default(), None,
|
|
||||||
).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ContractSet {
|
|
||||||
pub web3: Web3<Http>,
|
|
||||||
|
|
||||||
pub gate: Option<Contract<Http>>,
|
|
||||||
pub collectible: Option<Contract<Http>>,
|
|
||||||
pub subscription: Option<Contract<Http>>,
|
|
||||||
pub subscription_adapter: Option<Contract<Http>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Blockchain {
|
|
||||||
pub config: EthereumConfig,
|
|
||||||
pub contract_set: ContractSet,
|
|
||||||
pub sync_state: SyncState,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_contracts(
|
|
||||||
db_client: &impl DatabaseClient,
|
|
||||||
config: &EthereumConfig,
|
|
||||||
storage_dir: &Path,
|
|
||||||
) -> Result<Blockchain, EthereumError> {
|
|
||||||
let web3 = connect(&config.api_url)?;
|
|
||||||
let chain_id = web3.eth().chain_id().await?;
|
|
||||||
if chain_id != config.ethereum_chain_id().into() {
|
|
||||||
return Err(EthereumError::ImproperlyConfigured("incorrect chain ID"));
|
|
||||||
};
|
|
||||||
|
|
||||||
let adapter_address = parse_address(&config.contract_address)?;
|
|
||||||
let erc165_abi = load_abi(&config.contract_dir, ERC165)?;
|
|
||||||
let erc165 = Contract::new(
|
|
||||||
web3.eth(),
|
|
||||||
adapter_address,
|
|
||||||
erc165_abi,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut maybe_gate = None;
|
|
||||||
let mut maybe_collectible = None;
|
|
||||||
let mut maybe_subscription = None;
|
|
||||||
let mut maybe_subscription_adapter = None;
|
|
||||||
let mut sync_targets = vec![];
|
|
||||||
|
|
||||||
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 cfg!(feature = "ethereum-extras") &&
|
|
||||||
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);
|
|
||||||
maybe_subscription_adapter = Some(subscription_adapter);
|
|
||||||
};
|
|
||||||
|
|
||||||
let current_block = get_current_block_number(db_client, &web3, storage_dir).await?;
|
|
||||||
let sync_state = SyncState::new(
|
|
||||||
current_block,
|
|
||||||
sync_targets,
|
|
||||||
config.chain_sync_step,
|
|
||||||
config.chain_reorg_max_depth,
|
|
||||||
);
|
|
||||||
|
|
||||||
let contract_set = ContractSet {
|
|
||||||
web3,
|
|
||||||
gate: maybe_gate,
|
|
||||||
collectible: maybe_collectible,
|
|
||||||
subscription: maybe_subscription,
|
|
||||||
subscription_adapter: maybe_subscription_adapter,
|
|
||||||
};
|
|
||||||
Ok(Blockchain {
|
|
||||||
config: config.clone(),
|
|
||||||
contract_set,
|
|
||||||
sync_state,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
/// Sign-In with Ethereum https://eips.ethereum.org/EIPS/eip-4361
|
|
||||||
use hex::FromHex;
|
|
||||||
use siwe::Message;
|
|
||||||
use web3::types::H160;
|
|
||||||
|
|
||||||
use crate::errors::ValidationError;
|
|
||||||
use super::utils::address_to_string;
|
|
||||||
|
|
||||||
/// Verifies EIP-4361 signature and returns wallet address
|
|
||||||
pub fn verify_eip4361_signature(
|
|
||||||
message: &str,
|
|
||||||
signature: &str,
|
|
||||||
instance_hostname: &str,
|
|
||||||
login_message: &str,
|
|
||||||
) -> Result<String, ValidationError> {
|
|
||||||
let message: Message = message.parse()
|
|
||||||
.map_err(|_| ValidationError("invalid EIP-4361 message"))?;
|
|
||||||
let signature_bytes = <[u8; 65]>::from_hex(signature.trim_start_matches("0x"))
|
|
||||||
.map_err(|_| ValidationError("invalid signature string"))?;
|
|
||||||
if message.domain != instance_hostname {
|
|
||||||
return Err(ValidationError("domain doesn't match instance hostname"));
|
|
||||||
};
|
|
||||||
let statement = message.statement.as_ref()
|
|
||||||
.ok_or(ValidationError("statement is missing"))?;
|
|
||||||
if statement != login_message {
|
|
||||||
return Err(ValidationError("statement doesn't match login message"));
|
|
||||||
};
|
|
||||||
if !message.valid_now() {
|
|
||||||
return Err(ValidationError("message is not currently valid"));
|
|
||||||
};
|
|
||||||
if message.not_before.is_some() || message.expiration_time.is_some() {
|
|
||||||
return Err(ValidationError("message shouldn't have expiration time"));
|
|
||||||
};
|
|
||||||
message.verify_eip191(&signature_bytes)
|
|
||||||
.map_err(|_| ValidationError("invalid signature"))?;
|
|
||||||
// Return wallet address in lower case
|
|
||||||
let wallet_address = address_to_string(H160(message.address));
|
|
||||||
Ok(wallet_address)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
const INSTANCE_HOSTNAME: &str = "example.com";
|
|
||||||
const LOGIN_MESSAGE: &str = "test";
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_verify_eip4361_signature() {
|
|
||||||
let message = "example.com wants you to sign in with your Ethereum account:
|
|
||||||
0x70997970C51812dc3A010C7d01b50e0d17dc79C8
|
|
||||||
|
|
||||||
test
|
|
||||||
|
|
||||||
URI: https://example.com
|
|
||||||
Version: 1
|
|
||||||
Chain ID: 1
|
|
||||||
Nonce: 3cb7760eac2f
|
|
||||||
Issued At: 2022-02-14T22:27:35.500Z";
|
|
||||||
let signature = "0x9059c9a69c31e87d887262a574abcc33f320d5b778bea8a35c6fbdea94a17e9652b99f7cdd146ed67fa8e4bb02462774b958a129c421fe8d743a43bf67dcbcd61c";
|
|
||||||
let wallet_address = verify_eip4361_signature(
|
|
||||||
message, signature,
|
|
||||||
INSTANCE_HOSTNAME,
|
|
||||||
LOGIN_MESSAGE,
|
|
||||||
).unwrap();
|
|
||||||
assert_eq!(wallet_address, "0x70997970c51812dc3a010c7d01b50e0d17dc79c8");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_verify_eip4361_signature_invalid() {
|
|
||||||
let message = "abc";
|
|
||||||
let signature = "xyz";
|
|
||||||
let error = verify_eip4361_signature(
|
|
||||||
message, signature,
|
|
||||||
INSTANCE_HOSTNAME,
|
|
||||||
LOGIN_MESSAGE,
|
|
||||||
).unwrap_err();
|
|
||||||
assert_eq!(error.to_string(), "invalid EIP-4361 message");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
use mitra_models::database::DatabaseError;
|
|
||||||
|
|
||||||
use super::contracts::ArtifactError;
|
|
||||||
use super::signatures::SignatureError;
|
|
||||||
use super::utils::AddressError;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum EthereumError {
|
|
||||||
#[error("{0}")]
|
|
||||||
ImproperlyConfigured(&'static str),
|
|
||||||
|
|
||||||
#[error("invalid address")]
|
|
||||||
InvalidAddress(#[from] AddressError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Web3Error(#[from] web3::Error),
|
|
||||||
|
|
||||||
#[error("artifact error")]
|
|
||||||
ArtifactError(#[from] ArtifactError),
|
|
||||||
|
|
||||||
#[error("abi error")]
|
|
||||||
AbiError(#[from] web3::ethabi::Error),
|
|
||||||
|
|
||||||
#[error("contract error")]
|
|
||||||
ContractError(#[from] web3::contract::Error),
|
|
||||||
|
|
||||||
#[error("data conversion error")]
|
|
||||||
ConversionError,
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
DatabaseError(#[from] DatabaseError),
|
|
||||||
|
|
||||||
#[error("signature error")]
|
|
||||||
SignatureError(#[from] SignatureError),
|
|
||||||
|
|
||||||
#[error("{0}")]
|
|
||||||
OtherError(&'static str),
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
use web3::{
|
|
||||||
contract::{Contract, Options},
|
|
||||||
transports::Http,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::errors::EthereumError;
|
|
||||||
use super::utils::parse_address;
|
|
||||||
|
|
||||||
pub async fn is_allowed_user(
|
|
||||||
gate: &Contract<Http>,
|
|
||||||
user_address: &str,
|
|
||||||
) -> Result<bool, EthereumError> {
|
|
||||||
let user_address = parse_address(user_address)?;
|
|
||||||
let result: bool = gate.query(
|
|
||||||
"isAllowedUser", (user_address,),
|
|
||||||
None, Options::default(), None,
|
|
||||||
).await?;
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
use mitra_utils::did_pkh::DidPkh;
|
|
||||||
|
|
||||||
use super::signatures::{recover_address, SignatureError};
|
|
||||||
use super::utils::address_to_string;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum Eip191VerificationError {
|
|
||||||
#[error(transparent)]
|
|
||||||
InvalidSignature(#[from] SignatureError),
|
|
||||||
|
|
||||||
#[error("invalid signer")]
|
|
||||||
InvalidSigner,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify_eip191_signature(
|
|
||||||
did: &DidPkh,
|
|
||||||
message: &str,
|
|
||||||
signature_hex: &str,
|
|
||||||
) -> Result<(), Eip191VerificationError> {
|
|
||||||
let signature_data = signature_hex.parse()?;
|
|
||||||
let signer = recover_address(message.as_bytes(), &signature_data)?;
|
|
||||||
if address_to_string(signer) != did.address.to_lowercase() {
|
|
||||||
return Err(Eip191VerificationError::InvalidSigner);
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use web3::signing::{Key, SecretKeyRef};
|
|
||||||
use mitra_utils::currencies::Currency;
|
|
||||||
use crate::ethereum::{
|
|
||||||
signatures::{
|
|
||||||
generate_ecdsa_key,
|
|
||||||
sign_message,
|
|
||||||
},
|
|
||||||
utils::address_to_string,
|
|
||||||
};
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
const ETHEREUM: Currency = Currency::Ethereum;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_verify_eip191_signature() {
|
|
||||||
let message = "test";
|
|
||||||
let secret_key = generate_ecdsa_key();
|
|
||||||
let secret_key_ref = SecretKeyRef::new(&secret_key);
|
|
||||||
let secret_key_str = secret_key.display_secret().to_string();
|
|
||||||
let address = address_to_string(secret_key_ref.address());
|
|
||||||
let did = DidPkh::from_address(ÐEREUM, &address);
|
|
||||||
let signature = sign_message(&secret_key_str, message.as_bytes())
|
|
||||||
.unwrap().to_string();
|
|
||||||
let result = verify_eip191_signature(&did, message, &signature);
|
|
||||||
assert_eq!(result.is_ok(), true);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
mod api;
|
|
||||||
pub mod contracts;
|
|
||||||
pub mod eip4361;
|
|
||||||
mod errors;
|
|
||||||
pub mod gate;
|
|
||||||
pub mod identity;
|
|
||||||
pub mod signatures;
|
|
||||||
pub mod subscriptions;
|
|
||||||
pub mod sync;
|
|
||||||
pub mod utils;
|
|
||||||
|
|
||||||
#[cfg(feature = "ethereum-extras")]
|
|
||||||
pub mod nft;
|
|
|
@ -1,168 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use uuid::Uuid;
|
|
||||||
use web3::{
|
|
||||||
api::Web3,
|
|
||||||
contract::{Contract, Options},
|
|
||||||
ethabi::RawLog,
|
|
||||||
transports::Http,
|
|
||||||
types::{BlockNumber, FilterBuilder},
|
|
||||||
};
|
|
||||||
|
|
||||||
use mitra_config::EthereumConfig;
|
|
||||||
use mitra_models::{
|
|
||||||
database::{get_database_client, DatabaseError, DbPool},
|
|
||||||
posts::queries::{
|
|
||||||
get_post_by_ipfs_cid,
|
|
||||||
get_token_waitlist,
|
|
||||||
set_post_token_id,
|
|
||||||
set_post_token_tx_id,
|
|
||||||
},
|
|
||||||
properties::queries::{
|
|
||||||
get_internal_property,
|
|
||||||
set_internal_property,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::ipfs::utils::parse_ipfs_url;
|
|
||||||
|
|
||||||
use super::errors::EthereumError;
|
|
||||||
use super::signatures::{sign_contract_call, CallArgs, SignatureData};
|
|
||||||
use super::sync::SyncState;
|
|
||||||
use super::utils::parse_address;
|
|
||||||
|
|
||||||
const TOKEN_WAITLIST_MAP_PROPERTY_NAME: &str = "token_waitlist_map";
|
|
||||||
|
|
||||||
const TOKEN_WAIT_TIME: i64 = 10; // in minutes
|
|
||||||
const TOKEN_WAIT_RESET_TIME: i64 = 12 * 60; // in minutes
|
|
||||||
|
|
||||||
/// Finds posts awaiting tokenization
|
|
||||||
/// and looks for corresponding Mint events
|
|
||||||
pub async fn process_nft_events(
|
|
||||||
web3: &Web3<Http>,
|
|
||||||
contract: &Contract<Http>,
|
|
||||||
sync_state: &mut SyncState,
|
|
||||||
db_pool: &DbPool,
|
|
||||||
) -> Result<(), EthereumError> {
|
|
||||||
let db_client = &**get_database_client(db_pool).await?;
|
|
||||||
|
|
||||||
// Create/update token waitlist map
|
|
||||||
let mut token_waitlist_map: HashMap<Uuid, DateTime<Utc>> =
|
|
||||||
get_internal_property(db_client, TOKEN_WAITLIST_MAP_PROPERTY_NAME)
|
|
||||||
.await?.unwrap_or_default();
|
|
||||||
token_waitlist_map.retain(|_, waiting_since| {
|
|
||||||
// Re-add token to waitlist if waiting for too long
|
|
||||||
let duration = Utc::now() - *waiting_since;
|
|
||||||
duration.num_minutes() < TOKEN_WAIT_RESET_TIME
|
|
||||||
});
|
|
||||||
let token_waitlist = get_token_waitlist(db_client).await?;
|
|
||||||
for post_id in token_waitlist {
|
|
||||||
if !token_waitlist_map.contains_key(&post_id) {
|
|
||||||
token_waitlist_map.insert(post_id, Utc::now());
|
|
||||||
};
|
|
||||||
};
|
|
||||||
let token_waitlist_active_count = token_waitlist_map.values()
|
|
||||||
.filter(|waiting_since| {
|
|
||||||
let duration = Utc::now() - **waiting_since;
|
|
||||||
duration.num_minutes() < TOKEN_WAIT_TIME
|
|
||||||
})
|
|
||||||
.count();
|
|
||||||
if token_waitlist_active_count > 0 {
|
|
||||||
log::info!(
|
|
||||||
"{} posts are waiting for confirmation of tokenization tx",
|
|
||||||
token_waitlist_active_count,
|
|
||||||
);
|
|
||||||
} else if !sync_state.is_out_of_sync(&contract.address()) {
|
|
||||||
// Don't scan blockchain if already in sync and waitlist is empty
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
// Search for Transfer events
|
|
||||||
let event_abi = contract.abi().event("Transfer")?;
|
|
||||||
let (from_block, to_block) = sync_state.get_scan_range(
|
|
||||||
&contract.address(),
|
|
||||||
web3.eth().block_number().await?.as_u64(),
|
|
||||||
);
|
|
||||||
let filter = FilterBuilder::default()
|
|
||||||
.address(vec![contract.address()])
|
|
||||||
.topics(Some(vec![event_abi.signature()]), None, None, None)
|
|
||||||
.from_block(BlockNumber::Number(from_block.into()))
|
|
||||||
.to_block(BlockNumber::Number(to_block.into()))
|
|
||||||
.build();
|
|
||||||
let logs = web3.eth().logs(filter).await?;
|
|
||||||
for log in logs {
|
|
||||||
let raw_log = RawLog {
|
|
||||||
topics: log.topics.clone(),
|
|
||||||
data: log.data.clone().0,
|
|
||||||
};
|
|
||||||
let event = event_abi.parse_log(raw_log)?;
|
|
||||||
let from_address = event.params[0].value.clone().into_address()
|
|
||||||
.ok_or(EthereumError::ConversionError)?;
|
|
||||||
if from_address.is_zero() {
|
|
||||||
// Mint event found
|
|
||||||
let token_id_u256 = event.params[2].value.clone().into_uint()
|
|
||||||
.ok_or(EthereumError::ConversionError)?;
|
|
||||||
let token_uri: String = contract.query(
|
|
||||||
"tokenURI", (token_id_u256,),
|
|
||||||
None, Options::default(), None,
|
|
||||||
).await?;
|
|
||||||
let tx_id_h256 = log.transaction_hash
|
|
||||||
.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::ConversionError)?;
|
|
||||||
let post = match get_post_by_ipfs_cid(db_client, &ipfs_cid).await {
|
|
||||||
Ok(post) => post,
|
|
||||||
Err(DatabaseError::NotFound(_)) => {
|
|
||||||
// Post was deleted
|
|
||||||
continue;
|
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
// Unexpected 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)?;
|
|
||||||
set_post_token_id(db_client, &post.id, token_id).await?;
|
|
||||||
if post.token_tx_id.as_ref() != Some(&tx_id) {
|
|
||||||
log::warn!("overwriting incorrect tx id {:?}", post.token_tx_id);
|
|
||||||
set_post_token_tx_id(db_client, &post.id, &tx_id).await?;
|
|
||||||
};
|
|
||||||
token_waitlist_map.remove(&post.id);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
set_internal_property(
|
|
||||||
db_client,
|
|
||||||
TOKEN_WAITLIST_MAP_PROPERTY_NAME,
|
|
||||||
&token_waitlist_map,
|
|
||||||
).await?;
|
|
||||||
sync_state.update(db_client, &contract.address(), to_block).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_mint_signature(
|
|
||||||
blockchain_config: &EthereumConfig,
|
|
||||||
user_address: &str,
|
|
||||||
token_uri: &str,
|
|
||||||
) -> Result<SignatureData, EthereumError> {
|
|
||||||
let user_address = parse_address(user_address)?;
|
|
||||||
let call_args: CallArgs = vec![
|
|
||||||
Box::new(user_address),
|
|
||||||
Box::new(token_uri.to_string()),
|
|
||||||
];
|
|
||||||
let signature = sign_contract_call(
|
|
||||||
&blockchain_config.signing_key,
|
|
||||||
blockchain_config.ethereum_chain_id(),
|
|
||||||
&blockchain_config.contract_address,
|
|
||||||
"mint",
|
|
||||||
call_args,
|
|
||||||
)?;
|
|
||||||
Ok(signature)
|
|
||||||
}
|
|
|
@ -1,221 +0,0 @@
|
||||||
use std::convert::TryInto;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use secp256k1::{Error as KeyError, SecretKey, rand::rngs::OsRng};
|
|
||||||
use serde::Serialize;
|
|
||||||
use web3::ethabi::{token::Token, encode as encode_tokens};
|
|
||||||
use web3::signing::{
|
|
||||||
keccak256,
|
|
||||||
recover,
|
|
||||||
Key,
|
|
||||||
RecoveryError,
|
|
||||||
SecretKeyRef,
|
|
||||||
SigningError,
|
|
||||||
};
|
|
||||||
use web3::types::{Address, H256, Recovery};
|
|
||||||
|
|
||||||
/// Generates signing key
|
|
||||||
pub fn generate_ecdsa_key() -> SecretKey {
|
|
||||||
let mut rng = OsRng::new().expect("failed to initialize RNG");
|
|
||||||
SecretKey::new(&mut rng)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct SignatureData {
|
|
||||||
pub v: u64,
|
|
||||||
#[serde(serialize_with = "hex::serde::serialize")]
|
|
||||||
pub r: [u8; 32],
|
|
||||||
#[serde(serialize_with = "hex::serde::serialize")]
|
|
||||||
pub s: [u8; 32],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum SignatureError {
|
|
||||||
#[error("invalid key")]
|
|
||||||
InvalidKey(#[from] KeyError),
|
|
||||||
|
|
||||||
#[error("invalid data")]
|
|
||||||
InvalidData,
|
|
||||||
|
|
||||||
#[error("signing error")]
|
|
||||||
SigningError(#[from] SigningError),
|
|
||||||
|
|
||||||
#[error("invalid signature")]
|
|
||||||
InvalidSignature,
|
|
||||||
|
|
||||||
#[error("recovery error")]
|
|
||||||
RecoveryError(#[from] RecoveryError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToString for SignatureData {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
let mut bytes = Vec::with_capacity(65);
|
|
||||||
bytes.extend_from_slice(&self.r);
|
|
||||||
bytes.extend_from_slice(&self.s);
|
|
||||||
let v: u8 = self.v.try_into()
|
|
||||||
.expect("signature recovery in electrum notation always fits in a u8");
|
|
||||||
bytes.push(v);
|
|
||||||
hex::encode(bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for SignatureData {
|
|
||||||
type Err = SignatureError;
|
|
||||||
|
|
||||||
fn from_str(value_hex: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut bytes = [0u8; 65];
|
|
||||||
hex::decode_to_slice(value_hex, &mut bytes)
|
|
||||||
.map_err(|_| Self::Err::InvalidSignature)?;
|
|
||||||
let v = bytes[64].into();
|
|
||||||
let r = bytes[0..32].try_into()
|
|
||||||
.map_err(|_| Self::Err::InvalidSignature)?;
|
|
||||||
let s = bytes[32..64].try_into()
|
|
||||||
.map_err(|_| Self::Err::InvalidSignature)?;
|
|
||||||
let signature_data = Self { v, r, s };
|
|
||||||
Ok(signature_data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_message(message: &[u8]) -> [u8; 32] {
|
|
||||||
let eip_191_message = [
|
|
||||||
"\x19Ethereum Signed Message:\n".as_bytes(),
|
|
||||||
message.len().to_string().as_bytes(),
|
|
||||||
message,
|
|
||||||
].concat();
|
|
||||||
let eip_191_message_hash = keccak256(&eip_191_message);
|
|
||||||
eip_191_message_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create EIP-191 signature
|
|
||||||
/// https://eips.ethereum.org/EIPS/eip-191
|
|
||||||
pub fn sign_message(
|
|
||||||
signing_key: &str,
|
|
||||||
message: &[u8],
|
|
||||||
) -> Result<SignatureData, SignatureError> {
|
|
||||||
let key = SecretKey::from_str(signing_key)?;
|
|
||||||
let key_ref = SecretKeyRef::new(&key);
|
|
||||||
let eip_191_message_hash = prepare_message(message);
|
|
||||||
// Create signature without replay protection (chain ID is None)
|
|
||||||
let signature = key_ref.sign(&eip_191_message_hash, None)?;
|
|
||||||
let signature_data = SignatureData {
|
|
||||||
v: signature.v,
|
|
||||||
r: signature.r.to_fixed_bytes(),
|
|
||||||
s: signature.s.to_fixed_bytes(),
|
|
||||||
};
|
|
||||||
Ok(signature_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify EIP-191 signature
|
|
||||||
pub fn recover_address(
|
|
||||||
message: &[u8],
|
|
||||||
signature: &SignatureData,
|
|
||||||
) -> Result<Address, SignatureError> {
|
|
||||||
let eip_191_message_hash = prepare_message(message);
|
|
||||||
let recovery = Recovery::new(
|
|
||||||
"", // this message is not used
|
|
||||||
signature.v,
|
|
||||||
H256(signature.r),
|
|
||||||
H256(signature.s),
|
|
||||||
);
|
|
||||||
let (signature_raw, recovery_id) = recovery.as_signature()
|
|
||||||
.ok_or(SignatureError::InvalidSignature)?;
|
|
||||||
let address = recover(
|
|
||||||
&eip_191_message_hash,
|
|
||||||
&signature_raw,
|
|
||||||
recovery_id,
|
|
||||||
)?;
|
|
||||||
Ok(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type CallArgs = Vec<Box<dyn AsRef<[u8]>>>;
|
|
||||||
|
|
||||||
pub fn encode_uint256(value: u64) -> Vec<u8> {
|
|
||||||
let token = Token::Uint(value.into());
|
|
||||||
encode_tokens(&[token])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sign_contract_call(
|
|
||||||
signing_key: &str,
|
|
||||||
chain_id: u32,
|
|
||||||
contract_address: &str,
|
|
||||||
method_name: &str,
|
|
||||||
method_args: CallArgs,
|
|
||||||
) -> Result<SignatureData, SignatureError> {
|
|
||||||
let chain_id_bin = encode_uint256(chain_id.into());
|
|
||||||
let contract_address = Address::from_str(contract_address)
|
|
||||||
.map_err(|_| SignatureError::InvalidData)?;
|
|
||||||
let mut message = [
|
|
||||||
&chain_id_bin,
|
|
||||||
contract_address.as_bytes(),
|
|
||||||
method_name.as_bytes(),
|
|
||||||
].concat();
|
|
||||||
for arg in method_args {
|
|
||||||
message.extend(arg.as_ref().as_ref());
|
|
||||||
};
|
|
||||||
let message_hash = keccak256(&message);
|
|
||||||
let signature = sign_message(signing_key, &message_hash)?;
|
|
||||||
Ok(signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_signature_string_conversion() {
|
|
||||||
let v = 28;
|
|
||||||
let r: [u8; 32] = hex::decode("b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd")
|
|
||||||
.unwrap().try_into().unwrap();
|
|
||||||
let s: [u8; 32] = hex::decode("6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a029")
|
|
||||||
.unwrap().try_into().unwrap();
|
|
||||||
let expected_signature =
|
|
||||||
"b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c";
|
|
||||||
|
|
||||||
let signature_data = SignatureData { v, r, s };
|
|
||||||
let signature_str = signature_data.to_string();
|
|
||||||
assert_eq!(signature_str, expected_signature);
|
|
||||||
|
|
||||||
let parsed = signature_str.parse::<SignatureData>().unwrap();
|
|
||||||
assert_eq!(parsed.v, v);
|
|
||||||
assert_eq!(parsed.r, r);
|
|
||||||
assert_eq!(parsed.s, s);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_signature_from_string_with_0x_prefix() {
|
|
||||||
let signature_str = "0xb91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c";
|
|
||||||
let result = signature_str.parse::<SignatureData>();
|
|
||||||
assert_eq!(result.is_err(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sign_message() {
|
|
||||||
let signing_key = generate_ecdsa_key();
|
|
||||||
let message = "test_message";
|
|
||||||
let result = sign_message(
|
|
||||||
&signing_key.display_secret().to_string(),
|
|
||||||
message.as_bytes(),
|
|
||||||
).unwrap();
|
|
||||||
assert!(result.v == 27 || result.v == 28);
|
|
||||||
|
|
||||||
let recovered = recover_address(message.as_bytes(), &result).unwrap();
|
|
||||||
assert_eq!(recovered, SecretKeyRef::new(&signing_key).address());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sign_contract_call() {
|
|
||||||
let signing_key = generate_ecdsa_key().display_secret().to_string();
|
|
||||||
let chain_id = 1;
|
|
||||||
let contract_address = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0";
|
|
||||||
let method_name = "test";
|
|
||||||
let method_args: CallArgs = vec![Box::new("arg1"), Box::new("arg2")];
|
|
||||||
let result = sign_contract_call(
|
|
||||||
&signing_key,
|
|
||||||
chain_id,
|
|
||||||
contract_address,
|
|
||||||
method_name,
|
|
||||||
method_args,
|
|
||||||
).unwrap();
|
|
||||||
assert!(result.v == 27 || result.v == 28);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,328 +0,0 @@
|
||||||
use std::convert::TryInto;
|
|
||||||
|
|
||||||
use chrono::{DateTime, TimeZone, Utc};
|
|
||||||
use web3::{
|
|
||||||
api::Web3,
|
|
||||||
contract::{Contract, Options},
|
|
||||||
ethabi::RawLog,
|
|
||||||
transports::Http,
|
|
||||||
types::{BlockId, BlockNumber, FilterBuilder, U256},
|
|
||||||
};
|
|
||||||
|
|
||||||
use mitra_config::{EthereumConfig, Instance};
|
|
||||||
use mitra_models::{
|
|
||||||
database::{
|
|
||||||
get_database_client,
|
|
||||||
DatabaseClient,
|
|
||||||
DatabaseError,
|
|
||||||
DbPool,
|
|
||||||
},
|
|
||||||
notifications::queries::{
|
|
||||||
create_subscription_notification,
|
|
||||||
create_subscription_expiration_notification,
|
|
||||||
},
|
|
||||||
profiles::queries::{
|
|
||||||
get_profile_by_id,
|
|
||||||
search_profiles_by_wallet_address,
|
|
||||||
},
|
|
||||||
profiles::types::DbActorProfile,
|
|
||||||
relationships::queries::unsubscribe,
|
|
||||||
subscriptions::queries::{
|
|
||||||
create_subscription,
|
|
||||||
update_subscription,
|
|
||||||
get_expired_subscriptions,
|
|
||||||
get_subscription_by_participants,
|
|
||||||
},
|
|
||||||
users::queries::{
|
|
||||||
get_user_by_id,
|
|
||||||
get_user_by_public_wallet_address,
|
|
||||||
},
|
|
||||||
users::types::User,
|
|
||||||
};
|
|
||||||
use mitra_utils::currencies::Currency;
|
|
||||||
|
|
||||||
use crate::activitypub::{
|
|
||||||
builders::{
|
|
||||||
add_person::prepare_add_person,
|
|
||||||
remove_person::prepare_remove_person,
|
|
||||||
},
|
|
||||||
identifiers::LocalActorCollection,
|
|
||||||
};
|
|
||||||
use crate::errors::ConversionError;
|
|
||||||
|
|
||||||
use super::contracts::ContractSet;
|
|
||||||
use super::errors::EthereumError;
|
|
||||||
use super::signatures::{
|
|
||||||
encode_uint256,
|
|
||||||
sign_contract_call,
|
|
||||||
CallArgs,
|
|
||||||
SignatureData,
|
|
||||||
};
|
|
||||||
use super::sync::SyncState;
|
|
||||||
use super::utils::{address_to_string, parse_address};
|
|
||||||
|
|
||||||
const ETHEREUM: Currency = Currency::Ethereum;
|
|
||||||
|
|
||||||
fn u256_to_date(value: U256) -> Result<DateTime<Utc>, ConversionError> {
|
|
||||||
let timestamp: i64 = value.try_into().map_err(|_| ConversionError)?;
|
|
||||||
let datetime = Utc.timestamp_opt(timestamp, 0)
|
|
||||||
.single()
|
|
||||||
.ok_or(ConversionError)?;
|
|
||||||
Ok(datetime)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_subscription_notifications(
|
|
||||||
db_client: &impl DatabaseClient,
|
|
||||||
instance: &Instance,
|
|
||||||
sender: &DbActorProfile,
|
|
||||||
recipient: &User,
|
|
||||||
) -> Result<(), DatabaseError> {
|
|
||||||
create_subscription_notification(
|
|
||||||
db_client,
|
|
||||||
&sender.id,
|
|
||||||
&recipient.id,
|
|
||||||
).await?;
|
|
||||||
if let Some(ref remote_sender) = sender.actor_json {
|
|
||||||
prepare_add_person(
|
|
||||||
instance,
|
|
||||||
recipient,
|
|
||||||
remote_sender,
|
|
||||||
LocalActorCollection::Subscribers,
|
|
||||||
).enqueue(db_client).await?;
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Search for subscription update events
|
|
||||||
pub async fn check_ethereum_subscriptions(
|
|
||||||
config: &EthereumConfig,
|
|
||||||
instance: &Instance,
|
|
||||||
web3: &Web3<Http>,
|
|
||||||
contract: &Contract<Http>,
|
|
||||||
sync_state: &mut SyncState,
|
|
||||||
db_pool: &DbPool,
|
|
||||||
) -> Result<(), EthereumError> {
|
|
||||||
let db_client = &mut **get_database_client(db_pool).await?;
|
|
||||||
let event_abi = contract.abi().event("UpdateSubscription")?;
|
|
||||||
let (from_block, to_block) = sync_state.get_scan_range(
|
|
||||||
&contract.address(),
|
|
||||||
web3.eth().block_number().await?.as_u64(),
|
|
||||||
);
|
|
||||||
let filter = FilterBuilder::default()
|
|
||||||
.address(vec![contract.address()])
|
|
||||||
.topics(Some(vec![event_abi.signature()]), None, None, None)
|
|
||||||
.from_block(BlockNumber::Number(from_block.into()))
|
|
||||||
.to_block(BlockNumber::Number(to_block.into()))
|
|
||||||
.build();
|
|
||||||
let logs = web3.eth().logs(filter).await?;
|
|
||||||
for log in logs {
|
|
||||||
let block_number = if let Some(block_number) = log.block_number {
|
|
||||||
block_number
|
|
||||||
} else {
|
|
||||||
// Skips logs without block number
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let raw_log = RawLog {
|
|
||||||
topics: log.topics.clone(),
|
|
||||||
data: log.data.clone().0,
|
|
||||||
};
|
|
||||||
let event = event_abi.parse_log(raw_log)?;
|
|
||||||
let sender_address = event.params[0].value.clone().into_address()
|
|
||||||
.map(address_to_string)
|
|
||||||
.ok_or(EthereumError::ConversionError)?;
|
|
||||||
let recipient_address = event.params[1].value.clone().into_address()
|
|
||||||
.map(address_to_string)
|
|
||||||
.ok_or(EthereumError::ConversionError)?;
|
|
||||||
let expires_at_timestamp = event.params[2].value.clone().into_uint()
|
|
||||||
.ok_or(EthereumError::ConversionError)?;
|
|
||||||
let expires_at = u256_to_date(expires_at_timestamp)
|
|
||||||
.map_err(|_| EthereumError::ConversionError)?;
|
|
||||||
let block_id = BlockId::Number(BlockNumber::Number(block_number));
|
|
||||||
let block_timestamp = web3.eth().block(block_id).await?
|
|
||||||
.ok_or(EthereumError::ConversionError)?
|
|
||||||
.timestamp;
|
|
||||||
let block_date = u256_to_date(block_timestamp)
|
|
||||||
.map_err(|_| EthereumError::ConversionError)?;
|
|
||||||
|
|
||||||
let profiles = search_profiles_by_wallet_address(
|
|
||||||
db_client,
|
|
||||||
ÐEREUM,
|
|
||||||
&sender_address,
|
|
||||||
true, // prefer verified addresses
|
|
||||||
).await?;
|
|
||||||
let sender = match &profiles[..] {
|
|
||||||
[profile] => profile,
|
|
||||||
[] => {
|
|
||||||
// Profile not found, skip event
|
|
||||||
log::error!("unknown subscriber {}", sender_address);
|
|
||||||
continue;
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
// Ambiguous results, skip event
|
|
||||||
log::error!(
|
|
||||||
"search returned multiple results for address {}",
|
|
||||||
sender_address,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let recipient = get_user_by_public_wallet_address(
|
|
||||||
db_client,
|
|
||||||
ÐEREUM,
|
|
||||||
&recipient_address,
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
match get_subscription_by_participants(
|
|
||||||
db_client,
|
|
||||||
&sender.id,
|
|
||||||
&recipient.id,
|
|
||||||
).await {
|
|
||||||
Ok(subscription) => {
|
|
||||||
if subscription.chain_id != config.chain_id {
|
|
||||||
log::error!("can't switch to another chain");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let current_sender_address =
|
|
||||||
subscription.sender_address.unwrap_or("''".to_string());
|
|
||||||
if current_sender_address != sender_address {
|
|
||||||
// Trust only key/address that was linked to profile
|
|
||||||
// when first subscription event occured.
|
|
||||||
// Key rotation is not supported.
|
|
||||||
log::error!(
|
|
||||||
"subscriber address changed from {} to {}",
|
|
||||||
current_sender_address,
|
|
||||||
sender_address,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if subscription.updated_at >= block_date {
|
|
||||||
// Event already processed
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
// Update subscription expiration date
|
|
||||||
update_subscription(
|
|
||||||
db_client,
|
|
||||||
subscription.id,
|
|
||||||
&expires_at,
|
|
||||||
&block_date,
|
|
||||||
).await?;
|
|
||||||
#[allow(clippy::comparison_chain)]
|
|
||||||
if expires_at > subscription.expires_at {
|
|
||||||
log::info!(
|
|
||||||
"subscription extended: {0} to {1}",
|
|
||||||
subscription.sender_id,
|
|
||||||
subscription.recipient_id,
|
|
||||||
);
|
|
||||||
send_subscription_notifications(
|
|
||||||
db_client,
|
|
||||||
instance,
|
|
||||||
sender,
|
|
||||||
&recipient,
|
|
||||||
).await?;
|
|
||||||
} else if expires_at < subscription.expires_at {
|
|
||||||
log::info!(
|
|
||||||
"subscription cancelled: {0} to {1}",
|
|
||||||
subscription.sender_id,
|
|
||||||
subscription.recipient_id,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
Err(DatabaseError::NotFound(_)) => {
|
|
||||||
// New subscription
|
|
||||||
create_subscription(
|
|
||||||
db_client,
|
|
||||||
&sender.id,
|
|
||||||
Some(&sender_address),
|
|
||||||
&recipient.id,
|
|
||||||
&config.chain_id,
|
|
||||||
&expires_at,
|
|
||||||
&block_date,
|
|
||||||
).await?;
|
|
||||||
log::info!(
|
|
||||||
"subscription created: {0} to {1}",
|
|
||||||
sender.id,
|
|
||||||
recipient.id,
|
|
||||||
);
|
|
||||||
send_subscription_notifications(
|
|
||||||
db_client,
|
|
||||||
instance,
|
|
||||||
sender,
|
|
||||||
&recipient,
|
|
||||||
).await?;
|
|
||||||
},
|
|
||||||
Err(other_error) => return Err(other_error.into()),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
sync_state.update(db_client, &contract.address(), to_block).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_expired_subscriptions(
|
|
||||||
instance: &Instance,
|
|
||||||
db_pool: &DbPool,
|
|
||||||
) -> Result<(), EthereumError> {
|
|
||||||
let db_client = &mut **get_database_client(db_pool).await?;
|
|
||||||
for subscription in get_expired_subscriptions(db_client).await? {
|
|
||||||
// Remove relationship
|
|
||||||
unsubscribe(db_client, &subscription.sender_id, &subscription.recipient_id).await?;
|
|
||||||
log::info!(
|
|
||||||
"subscription expired: {0} to {1}",
|
|
||||||
subscription.sender_id,
|
|
||||||
subscription.recipient_id,
|
|
||||||
);
|
|
||||||
let sender = get_profile_by_id(db_client, &subscription.sender_id).await?;
|
|
||||||
if let Some(ref remote_sender) = sender.actor_json {
|
|
||||||
let recipient = get_user_by_id(db_client, &subscription.recipient_id).await?;
|
|
||||||
prepare_remove_person(
|
|
||||||
instance,
|
|
||||||
&recipient,
|
|
||||||
remote_sender,
|
|
||||||
LocalActorCollection::Subscribers,
|
|
||||||
).enqueue(db_client).await?;
|
|
||||||
} else {
|
|
||||||
create_subscription_expiration_notification(
|
|
||||||
db_client,
|
|
||||||
&subscription.recipient_id,
|
|
||||||
&subscription.sender_id,
|
|
||||||
).await?;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_subscription_signature(
|
|
||||||
blockchain_config: &EthereumConfig,
|
|
||||||
user_address: &str,
|
|
||||||
price: u64,
|
|
||||||
) -> Result<SignatureData, EthereumError> {
|
|
||||||
let user_address = parse_address(user_address)?;
|
|
||||||
let call_args: CallArgs = vec![
|
|
||||||
Box::new(user_address),
|
|
||||||
Box::new(encode_uint256(price)),
|
|
||||||
];
|
|
||||||
let signature = sign_contract_call(
|
|
||||||
&blockchain_config.signing_key,
|
|
||||||
blockchain_config.ethereum_chain_id(),
|
|
||||||
&blockchain_config.contract_address,
|
|
||||||
"configureSubscription",
|
|
||||||
call_args,
|
|
||||||
)?;
|
|
||||||
Ok(signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn is_registered_recipient(
|
|
||||||
contract_set: &ContractSet,
|
|
||||||
user_address: &str,
|
|
||||||
) -> Result<bool, EthereumError> {
|
|
||||||
let adapter = match &contract_set.subscription_adapter {
|
|
||||||
Some(contract) => contract,
|
|
||||||
None => return Ok(false),
|
|
||||||
};
|
|
||||||
let user_address = parse_address(user_address)?;
|
|
||||||
let result: bool = adapter.query(
|
|
||||||
"isSubscriptionConfigured", (user_address,),
|
|
||||||
None, Options::default(), None,
|
|
||||||
).await?;
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
|
@ -1,176 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use web3::{api::Web3, transports::Http, types::Address};
|
|
||||||
|
|
||||||
use mitra_models::{
|
|
||||||
database::DatabaseClient,
|
|
||||||
properties::queries::{
|
|
||||||
get_internal_property,
|
|
||||||
set_internal_property,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::errors::EthereumError;
|
|
||||||
|
|
||||||
const BLOCK_NUMBER_FILE_NAME: &str = "current_block";
|
|
||||||
const CURRENT_BLOCK_PROPERTY_NAME: &str = "ethereum_current_block";
|
|
||||||
|
|
||||||
pub async fn save_current_block_number(
|
|
||||||
db_client: &impl DatabaseClient,
|
|
||||||
block_number: u64,
|
|
||||||
) -> Result<(), EthereumError> {
|
|
||||||
set_internal_property(
|
|
||||||
db_client,
|
|
||||||
CURRENT_BLOCK_PROPERTY_NAME,
|
|
||||||
&block_number,
|
|
||||||
).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_current_block_number(
|
|
||||||
db_client: &impl DatabaseClient,
|
|
||||||
storage_dir: &Path,
|
|
||||||
) -> Result<Option<u64>, EthereumError> {
|
|
||||||
let maybe_block_number = get_internal_property(
|
|
||||||
db_client,
|
|
||||||
CURRENT_BLOCK_PROPERTY_NAME,
|
|
||||||
).await?;
|
|
||||||
if maybe_block_number.is_some() {
|
|
||||||
return Ok(maybe_block_number);
|
|
||||||
};
|
|
||||||
// Try to read from file if internal property is not set
|
|
||||||
let file_path = storage_dir.join(BLOCK_NUMBER_FILE_NAME);
|
|
||||||
let maybe_block_number = if file_path.exists() {
|
|
||||||
let block_number: u64 = std::fs::read_to_string(&file_path)
|
|
||||||
.map_err(|_| EthereumError::OtherError("failed to read current block"))?
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| EthereumError::OtherError("failed to parse block number"))?;
|
|
||||||
Some(block_number)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
Ok(maybe_block_number)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_current_block_number(
|
|
||||||
db_client: &impl DatabaseClient,
|
|
||||||
web3: &Web3<Http>,
|
|
||||||
storage_dir: &Path,
|
|
||||||
) -> Result<u64, EthereumError> {
|
|
||||||
let block_number = match read_current_block_number(db_client, storage_dir).await? {
|
|
||||||
Some(block_number) => block_number,
|
|
||||||
None => {
|
|
||||||
// Save block number when connecting to the node for the first time
|
|
||||||
let block_number = web3.eth().block_number().await?.as_u64();
|
|
||||||
save_current_block_number(db_client, block_number).await?;
|
|
||||||
block_number
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Ok(block_number)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct SyncState {
|
|
||||||
pub current_block: u64,
|
|
||||||
contracts: HashMap<Address, u64>,
|
|
||||||
sync_step: u64,
|
|
||||||
reorg_max_depth: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SyncState {
|
|
||||||
pub fn new(
|
|
||||||
current_block: u64,
|
|
||||||
contracts: Vec<Address>,
|
|
||||||
sync_step: u64,
|
|
||||||
reorg_max_depth: u64,
|
|
||||||
) -> Self {
|
|
||||||
log::info!("current block is {}", current_block);
|
|
||||||
let mut contract_map = HashMap::new();
|
|
||||||
for address in contracts {
|
|
||||||
contract_map.insert(address, current_block);
|
|
||||||
};
|
|
||||||
Self {
|
|
||||||
current_block,
|
|
||||||
contracts: contract_map,
|
|
||||||
sync_step,
|
|
||||||
reorg_max_depth,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_scan_range(
|
|
||||||
&self,
|
|
||||||
contract_address: &Address,
|
|
||||||
latest_block: u64,
|
|
||||||
) -> (u64, u64) {
|
|
||||||
let current_block = self.contracts[contract_address];
|
|
||||||
// Take reorgs into account
|
|
||||||
let latest_safe_block = latest_block.saturating_sub(self.reorg_max_depth);
|
|
||||||
let next_block = std::cmp::min(
|
|
||||||
current_block + self.sync_step,
|
|
||||||
latest_safe_block,
|
|
||||||
);
|
|
||||||
// Next block should not be less than current block
|
|
||||||
let next_block = std::cmp::max(current_block, next_block);
|
|
||||||
(current_block, next_block)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_out_of_sync(&self, contract_address: &Address) -> bool {
|
|
||||||
if let Some(max_value) = self.contracts.values().max().copied() {
|
|
||||||
if self.contracts[contract_address] == max_value {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update(
|
|
||||||
&mut self,
|
|
||||||
db_client: &impl DatabaseClient,
|
|
||||||
contract_address: &Address,
|
|
||||||
block_number: u64,
|
|
||||||
) -> Result<(), EthereumError> {
|
|
||||||
self.contracts.insert(*contract_address, block_number);
|
|
||||||
if let Some(min_value) = self.contracts.values().min().copied() {
|
|
||||||
if min_value > self.current_block {
|
|
||||||
self.current_block = min_value;
|
|
||||||
save_current_block_number(db_client, self.current_block).await?;
|
|
||||||
log::info!("synced to block {}", self.current_block);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_scan_range_from_zero() {
|
|
||||||
let address = Address::default();
|
|
||||||
let sync_state = SyncState::new(
|
|
||||||
0,
|
|
||||||
vec![address.clone()],
|
|
||||||
100,
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
let (from_block, to_block) = sync_state.get_scan_range(&address, 555);
|
|
||||||
assert_eq!(from_block, 0);
|
|
||||||
assert_eq!(to_block, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_scan_range() {
|
|
||||||
let address = Address::default();
|
|
||||||
let sync_state = SyncState::new(
|
|
||||||
500,
|
|
||||||
vec![address.clone()],
|
|
||||||
100,
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
let (from_block, to_block) = sync_state.get_scan_range(&address, 555);
|
|
||||||
assert_eq!(from_block, 500);
|
|
||||||
assert_eq!(to_block, 545);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use regex::Regex;
|
|
||||||
use secp256k1::SecretKey;
|
|
||||||
use web3::{
|
|
||||||
signing::Key,
|
|
||||||
types::Address,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::errors::ValidationError;
|
|
||||||
|
|
||||||
pub fn key_to_ethereum_address(private_key: &SecretKey) -> Address {
|
|
||||||
private_key.address()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
#[error("address error")]
|
|
||||||
pub struct AddressError;
|
|
||||||
|
|
||||||
pub fn parse_address(address: &str) -> Result<Address, AddressError> {
|
|
||||||
Address::from_str(address).map_err(|_| AddressError)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts address object to lowercase hex string
|
|
||||||
pub fn address_to_string(address: Address) -> String {
|
|
||||||
format!("{:#x}", address)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_ethereum_address(
|
|
||||||
wallet_address: &str,
|
|
||||||
) -> Result<(), ValidationError> {
|
|
||||||
let address_regexp = Regex::new(r"^0x[a-fA-F0-9]{40}$").unwrap();
|
|
||||||
if !address_regexp.is_match(wallet_address) {
|
|
||||||
return Err(ValidationError("invalid address"));
|
|
||||||
};
|
|
||||||
// Address should be lowercase
|
|
||||||
if wallet_address.to_lowercase() != wallet_address {
|
|
||||||
return Err(ValidationError("address is not lowercase"));
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_validate_ethereum_address() {
|
|
||||||
let result_1 = validate_ethereum_address("0xab5801a7d398351b8be11c439e05c5b3259aec9b");
|
|
||||||
assert_eq!(result_1.is_ok(), true);
|
|
||||||
let result_2 = validate_ethereum_address("ab5801a7d398351b8be11c439e05c5b3259aec9b");
|
|
||||||
assert_eq!(
|
|
||||||
result_2.err().unwrap().to_string(),
|
|
||||||
"invalid address",
|
|
||||||
);
|
|
||||||
let result_3 = validate_ethereum_address("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B");
|
|
||||||
assert_eq!(
|
|
||||||
result_3.err().unwrap().to_string(),
|
|
||||||
"address is not lowercase",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -20,92 +20,7 @@ use crate::activitypub::queues::{
|
||||||
process_queued_incoming_activities,
|
process_queued_incoming_activities,
|
||||||
process_queued_outgoing_activities,
|
process_queued_outgoing_activities,
|
||||||
};
|
};
|
||||||
use crate::ethereum::{
|
|
||||||
contracts::Blockchain,
|
|
||||||
subscriptions::{
|
|
||||||
check_ethereum_subscriptions,
|
|
||||||
update_expired_subscriptions,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use crate::media::remove_media;
|
use crate::media::remove_media;
|
||||||
use crate::monero::subscriptions::check_monero_subscriptions;
|
|
||||||
|
|
||||||
#[cfg(feature = "ethereum-extras")]
|
|
||||||
use crate::ethereum::nft::process_nft_events;
|
|
||||||
|
|
||||||
#[cfg(feature = "ethereum-extras")]
|
|
||||||
pub async fn nft_monitor(
|
|
||||||
maybe_blockchain: Option<&mut Blockchain>,
|
|
||||||
db_pool: &DbPool,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let blockchain = match maybe_blockchain {
|
|
||||||
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,
|
|
||||||
collectible,
|
|
||||||
&mut blockchain.sync_state,
|
|
||||||
db_pool,
|
|
||||||
).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn ethereum_subscription_monitor(
|
|
||||||
config: &Config,
|
|
||||||
maybe_blockchain: Option<&mut Blockchain>,
|
|
||||||
db_pool: &DbPool,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let blockchain = match maybe_blockchain {
|
|
||||||
Some(blockchain) => blockchain,
|
|
||||||
None => return Ok(()),
|
|
||||||
};
|
|
||||||
let subscription = match &blockchain.contract_set.subscription {
|
|
||||||
Some(contract) => contract,
|
|
||||||
None => return Ok(()), // feature not enabled
|
|
||||||
};
|
|
||||||
check_ethereum_subscriptions(
|
|
||||||
&blockchain.config,
|
|
||||||
&config.instance(),
|
|
||||||
&blockchain.contract_set.web3,
|
|
||||||
subscription,
|
|
||||||
&mut blockchain.sync_state,
|
|
||||||
db_pool,
|
|
||||||
).await.map_err(Error::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn subscription_expiration_monitor(
|
|
||||||
config: &Config,
|
|
||||||
db_pool: &DbPool,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
update_expired_subscriptions(
|
|
||||||
&config.instance(),
|
|
||||||
db_pool,
|
|
||||||
).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn monero_payment_monitor(
|
|
||||||
config: &Config,
|
|
||||||
db_pool: &DbPool,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let maybe_monero_config = config.blockchain()
|
|
||||||
.and_then(|conf| conf.monero_config());
|
|
||||||
let monero_config = match maybe_monero_config {
|
|
||||||
Some(monero_config) => monero_config,
|
|
||||||
None => return Ok(()), // not configured
|
|
||||||
};
|
|
||||||
check_monero_subscriptions(
|
|
||||||
&config.instance(),
|
|
||||||
monero_config,
|
|
||||||
db_pool,
|
|
||||||
).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn incoming_activity_queue_executor(
|
pub async fn incoming_activity_queue_executor(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
|
|
|
@ -6,39 +6,28 @@ use chrono::{DateTime, Utc};
|
||||||
use mitra_config::Config;
|
use mitra_config::Config;
|
||||||
use mitra_models::database::DbPool;
|
use mitra_models::database::DbPool;
|
||||||
|
|
||||||
use crate::ethereum::contracts::Blockchain;
|
|
||||||
use super::periodic_tasks::*;
|
use super::periodic_tasks::*;
|
||||||
|
|
||||||
#[derive(Debug, Eq, Hash, PartialEq)]
|
#[derive(Debug, Eq, Hash, PartialEq)]
|
||||||
enum PeriodicTask {
|
enum PeriodicTask {
|
||||||
EthereumSubscriptionMonitor,
|
|
||||||
SubscriptionExpirationMonitor,
|
SubscriptionExpirationMonitor,
|
||||||
MoneroPaymentMonitor,
|
|
||||||
IncomingActivityQueueExecutor,
|
IncomingActivityQueueExecutor,
|
||||||
OutgoingActivityQueueExecutor,
|
OutgoingActivityQueueExecutor,
|
||||||
DeleteExtraneousPosts,
|
DeleteExtraneousPosts,
|
||||||
DeleteEmptyProfiles,
|
DeleteEmptyProfiles,
|
||||||
PruneRemoteEmojis,
|
PruneRemoteEmojis,
|
||||||
|
|
||||||
#[cfg(feature = "ethereum-extras")]
|
|
||||||
NftMonitor,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PeriodicTask {
|
impl PeriodicTask {
|
||||||
/// Returns task period (in seconds)
|
/// Returns task period (in seconds)
|
||||||
fn period(&self) -> i64 {
|
fn period(&self) -> i64 {
|
||||||
match self {
|
match self {
|
||||||
Self::EthereumSubscriptionMonitor => 300,
|
|
||||||
Self::SubscriptionExpirationMonitor => 300,
|
Self::SubscriptionExpirationMonitor => 300,
|
||||||
Self::MoneroPaymentMonitor => 30,
|
|
||||||
Self::IncomingActivityQueueExecutor => 5,
|
Self::IncomingActivityQueueExecutor => 5,
|
||||||
Self::OutgoingActivityQueueExecutor => 5,
|
Self::OutgoingActivityQueueExecutor => 5,
|
||||||
Self::DeleteExtraneousPosts => 3600,
|
Self::DeleteExtraneousPosts => 3600,
|
||||||
Self::DeleteEmptyProfiles => 3600,
|
Self::DeleteEmptyProfiles => 3600,
|
||||||
Self::PruneRemoteEmojis => 3600,
|
Self::PruneRemoteEmojis => 3600,
|
||||||
|
|
||||||
#[cfg(feature = "ethereum-extras")]
|
|
||||||
Self::NftMonitor => 30,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,20 +44,14 @@ impl PeriodicTask {
|
||||||
|
|
||||||
pub fn run(
|
pub fn run(
|
||||||
config: Config,
|
config: Config,
|
||||||
mut maybe_blockchain: Option<Blockchain>,
|
|
||||||
db_pool: DbPool,
|
db_pool: DbPool,
|
||||||
) -> () {
|
) -> () {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut scheduler_state = HashMap::from([
|
let mut scheduler_state = HashMap::from([
|
||||||
(PeriodicTask::EthereumSubscriptionMonitor, None),
|
|
||||||
(PeriodicTask::SubscriptionExpirationMonitor, None),
|
(PeriodicTask::SubscriptionExpirationMonitor, None),
|
||||||
(PeriodicTask::MoneroPaymentMonitor, None),
|
|
||||||
(PeriodicTask::IncomingActivityQueueExecutor, None),
|
(PeriodicTask::IncomingActivityQueueExecutor, None),
|
||||||
(PeriodicTask::OutgoingActivityQueueExecutor, None),
|
(PeriodicTask::OutgoingActivityQueueExecutor, None),
|
||||||
(PeriodicTask::PruneRemoteEmojis, None),
|
(PeriodicTask::PruneRemoteEmojis, None),
|
||||||
|
|
||||||
#[cfg(feature = "ethereum-extras")]
|
|
||||||
(PeriodicTask::NftMonitor, None),
|
|
||||||
]);
|
]);
|
||||||
if config.retention.extraneous_posts.is_some() {
|
if config.retention.extraneous_posts.is_some() {
|
||||||
scheduler_state.insert(PeriodicTask::DeleteExtraneousPosts, None);
|
scheduler_state.insert(PeriodicTask::DeleteExtraneousPosts, None);
|
||||||
|
@ -86,19 +69,6 @@ pub fn run(
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let task_result = match task {
|
let task_result = match task {
|
||||||
PeriodicTask::EthereumSubscriptionMonitor => {
|
|
||||||
ethereum_subscription_monitor(
|
|
||||||
&config,
|
|
||||||
maybe_blockchain.as_mut(),
|
|
||||||
&db_pool,
|
|
||||||
).await
|
|
||||||
},
|
|
||||||
PeriodicTask::SubscriptionExpirationMonitor => {
|
|
||||||
subscription_expiration_monitor(&config, &db_pool).await
|
|
||||||
},
|
|
||||||
PeriodicTask::MoneroPaymentMonitor => {
|
|
||||||
monero_payment_monitor(&config, &db_pool).await
|
|
||||||
},
|
|
||||||
PeriodicTask::IncomingActivityQueueExecutor => {
|
PeriodicTask::IncomingActivityQueueExecutor => {
|
||||||
incoming_activity_queue_executor(&config, &db_pool).await
|
incoming_activity_queue_executor(&config, &db_pool).await
|
||||||
},
|
},
|
||||||
|
@ -114,13 +84,7 @@ pub fn run(
|
||||||
PeriodicTask::PruneRemoteEmojis => {
|
PeriodicTask::PruneRemoteEmojis => {
|
||||||
prune_remote_emojis(&config, &db_pool).await
|
prune_remote_emojis(&config, &db_pool).await
|
||||||
},
|
},
|
||||||
#[cfg(feature = "ethereum-extras")]
|
PeriodicTask::SubscriptionExpirationMonitor => Ok(()),
|
||||||
PeriodicTask::NftMonitor => {
|
|
||||||
nft_monitor(
|
|
||||||
maybe_blockchain.as_mut(),
|
|
||||||
&db_pool,
|
|
||||||
).await
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
task_result.unwrap_or_else(|err| {
|
task_result.unwrap_or_else(|err| {
|
||||||
log::error!("{:?}: {}", task, err);
|
log::error!("{:?}: {}", task, err);
|
||||||
|
|
|
@ -16,7 +16,6 @@ use mitra_utils::{
|
||||||
multibase::{decode_multibase_base58btc, MultibaseError},
|
multibase::{decode_multibase_base58btc, MultibaseError},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::ethereum::identity::verify_eip191_signature;
|
|
||||||
use crate::identity::{
|
use crate::identity::{
|
||||||
minisign::verify_minisign_signature,
|
minisign::verify_minisign_signature,
|
||||||
};
|
};
|
||||||
|
@ -112,13 +111,11 @@ pub fn verify_rsa_json_signature(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_eip191_json_signature(
|
pub fn verify_eip191_json_signature(
|
||||||
signer: &DidPkh,
|
_signer: &DidPkh,
|
||||||
message: &str,
|
_message: &str,
|
||||||
signature: &[u8],
|
_signature: &[u8],
|
||||||
) -> Result<(), VerificationError> {
|
) -> Result<(), VerificationError> {
|
||||||
let signature_hex = hex::encode(signature);
|
Err(VerificationError::InvalidSignature)
|
||||||
verify_eip191_signature(signer, message, &signature_hex)
|
|
||||||
.map_err(|_| VerificationError::InvalidSignature)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_ed25519_json_signature(
|
pub fn verify_ed25519_json_signature(
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
pub mod activitypub;
|
pub mod activitypub;
|
||||||
pub mod atom;
|
pub mod atom;
|
||||||
mod errors;
|
mod errors;
|
||||||
pub mod ethereum;
|
|
||||||
pub mod http;
|
pub mod http;
|
||||||
mod http_signatures;
|
mod http_signatures;
|
||||||
mod identity;
|
mod identity;
|
||||||
|
@ -11,7 +10,6 @@ mod json_signatures;
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
pub mod mastodon_api;
|
pub mod mastodon_api;
|
||||||
pub mod media;
|
pub mod media;
|
||||||
pub mod monero;
|
|
||||||
pub mod nodeinfo;
|
pub mod nodeinfo;
|
||||||
pub mod validators;
|
pub mod validators;
|
||||||
pub mod webfinger;
|
pub mod webfinger;
|
||||||
|
|
28
src/main.rs
28
src/main.rs
|
@ -12,7 +12,6 @@ use tokio::sync::Mutex;
|
||||||
|
|
||||||
use mitra::activitypub::views as activitypub;
|
use mitra::activitypub::views as activitypub;
|
||||||
use mitra::atom::views::atom_scope;
|
use mitra::atom::views::atom_scope;
|
||||||
use mitra::ethereum::contracts::get_contracts;
|
|
||||||
use mitra::http::{
|
use mitra::http::{
|
||||||
create_auth_error_handler,
|
create_auth_error_handler,
|
||||||
create_default_headers_middleware,
|
create_default_headers_middleware,
|
||||||
|
@ -60,21 +59,6 @@ async fn main() -> std::io::Result<()> {
|
||||||
std::fs::create_dir(config.media_dir())
|
std::fs::create_dir(config.media_dir())
|
||||||
.expect("failed to create media directory");
|
.expect("failed to create media directory");
|
||||||
};
|
};
|
||||||
|
|
||||||
let maybe_blockchain = if let Some(blockchain_config) = config.blockchain() {
|
|
||||||
if let Some(ethereum_config) = blockchain_config.ethereum_config() {
|
|
||||||
// Create blockchain interface
|
|
||||||
get_contracts(&**db_client, ethereum_config, &config.storage_dir).await
|
|
||||||
.map(Some).unwrap()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let maybe_contract_set = maybe_blockchain.clone()
|
|
||||||
.map(|blockchain| blockchain.contract_set);
|
|
||||||
|
|
||||||
std::mem::drop(db_client);
|
std::mem::drop(db_client);
|
||||||
log::info!(
|
log::info!(
|
||||||
"app initialized; version {}, environment = '{:?}'",
|
"app initialized; version {}, environment = '{:?}'",
|
||||||
|
@ -82,7 +66,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
config.environment,
|
config.environment,
|
||||||
);
|
);
|
||||||
|
|
||||||
scheduler::run(config.clone(), maybe_blockchain, db_pool.clone());
|
scheduler::run(config.clone(), db_pool.clone());
|
||||||
log::info!("scheduler started");
|
log::info!("scheduler started");
|
||||||
|
|
||||||
let num_workers = std::cmp::max(num_cpus::get(), 4);
|
let num_workers = std::cmp::max(num_cpus::get(), 4);
|
||||||
|
@ -146,7 +130,6 @@ async fn main() -> std::io::Result<()> {
|
||||||
)
|
)
|
||||||
.app_data(web::Data::new(config.clone()))
|
.app_data(web::Data::new(config.clone()))
|
||||||
.app_data(web::Data::new(db_pool.clone()))
|
.app_data(web::Data::new(db_pool.clone()))
|
||||||
.app_data(web::Data::new(maybe_contract_set.clone()))
|
|
||||||
.app_data(web::Data::clone(&inbox_mutex))
|
.app_data(web::Data::clone(&inbox_mutex))
|
||||||
.service(actix_files::Files::new(
|
.service(actix_files::Files::new(
|
||||||
"/media",
|
"/media",
|
||||||
|
@ -184,15 +167,6 @@ async fn main() -> std::io::Result<()> {
|
||||||
web::resource("/.well-known/{path}")
|
web::resource("/.well-known/{path}")
|
||||||
.to(HttpResponse::NotFound)
|
.to(HttpResponse::NotFound)
|
||||||
);
|
);
|
||||||
if let Some(blockchain_config) = config.blockchain() {
|
|
||||||
if let Some(ethereum_config) = blockchain_config.ethereum_config() {
|
|
||||||
// Serve artifacts if available
|
|
||||||
app = app.service(actix_files::Files::new(
|
|
||||||
"/contracts",
|
|
||||||
ðereum_config.contract_dir,
|
|
||||||
));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if let Some(ref web_client_dir) = config.web_client_dir {
|
if let Some(ref web_client_dir) = config.web_client_dir {
|
||||||
app = app.service(web_client::static_service(web_client_dir));
|
app = app.service(web_client::static_service(web_client_dir));
|
||||||
};
|
};
|
||||||
|
|
|
@ -70,12 +70,6 @@ use crate::activitypub::{
|
||||||
identifiers::local_actor_id,
|
identifiers::local_actor_id,
|
||||||
};
|
};
|
||||||
use crate::errors::ValidationError;
|
use crate::errors::ValidationError;
|
||||||
use crate::ethereum::{
|
|
||||||
contracts::ContractSet,
|
|
||||||
eip4361::verify_eip4361_signature,
|
|
||||||
gate::is_allowed_user,
|
|
||||||
identity::verify_eip191_signature,
|
|
||||||
};
|
|
||||||
use crate::http::get_request_base_url;
|
use crate::http::get_request_base_url;
|
||||||
use crate::identity::{
|
use crate::identity::{
|
||||||
claims::create_identity_claim,
|
claims::create_identity_claim,
|
||||||
|
@ -130,7 +124,6 @@ pub async fn create_account(
|
||||||
connection_info: ConnectionInfo,
|
connection_info: ConnectionInfo,
|
||||||
config: web::Data<Config>,
|
config: web::Data<Config>,
|
||||||
db_pool: web::Data<DbPool>,
|
db_pool: web::Data<DbPool>,
|
||||||
maybe_blockchain: web::Data<Option<ContractSet>>,
|
|
||||||
account_data: web::Json<AccountCreateData>,
|
account_data: web::Json<AccountCreateData>,
|
||||||
) -> Result<HttpResponse, MastodonError> {
|
) -> Result<HttpResponse, MastodonError> {
|
||||||
let db_client = &mut **get_database_client(&db_pool).await?;
|
let db_client = &mut **get_database_client(&db_pool).await?;
|
||||||
|
@ -151,36 +144,6 @@ pub async fn create_account(
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let maybe_wallet_address = if let Some(message) = account_data.message.as_ref() {
|
|
||||||
let signature = account_data.signature.as_ref()
|
|
||||||
.ok_or(ValidationError("signature is required"))?;
|
|
||||||
let wallet_address = verify_eip4361_signature(
|
|
||||||
message,
|
|
||||||
signature,
|
|
||||||
&config.instance().hostname(),
|
|
||||||
&config.login_message,
|
|
||||||
)?;
|
|
||||||
Some(wallet_address)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
if maybe_wallet_address.is_some() == maybe_password_hash.is_some() {
|
|
||||||
// Either password or EIP-4361 auth must be used (but not both)
|
|
||||||
return Err(ValidationError("invalid login data").into());
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(contract_set) = maybe_blockchain.as_ref() {
|
|
||||||
if let Some(ref gate) = contract_set.gate {
|
|
||||||
// Wallet address is required if token gate is present
|
|
||||||
let wallet_address = maybe_wallet_address.as_ref()
|
|
||||||
.ok_or(ValidationError("wallet address is required"))?;
|
|
||||||
let is_allowed = is_allowed_user(gate, wallet_address).await
|
|
||||||
.map_err(|_| MastodonError::InternalError)?;
|
|
||||||
if !is_allowed {
|
|
||||||
return Err(ValidationError("not allowed to sign up").into());
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate RSA private key for actor
|
// Generate RSA private key for actor
|
||||||
let private_key = match web::block(generate_rsa_key).await {
|
let private_key = match web::block(generate_rsa_key).await {
|
||||||
|
@ -200,7 +163,7 @@ pub async fn create_account(
|
||||||
username,
|
username,
|
||||||
password_hash: maybe_password_hash,
|
password_hash: maybe_password_hash,
|
||||||
private_key_pem,
|
private_key_pem,
|
||||||
wallet_address: maybe_wallet_address,
|
wallet_address: None,
|
||||||
invite_code,
|
invite_code,
|
||||||
role,
|
role,
|
||||||
};
|
};
|
||||||
|
@ -443,12 +406,7 @@ async fn create_identity_proof(
|
||||||
return Err(ValidationError("DID doesn't match current identity").into());
|
return Err(ValidationError("DID doesn't match current identity").into());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
verify_eip191_signature(
|
return Err(ValidationError("invalid signature").into());
|
||||||
did_pkh,
|
|
||||||
&message,
|
|
||||||
&proof_data.signature,
|
|
||||||
).map_err(|_| ValidationError("invalid signature"))?;
|
|
||||||
IdentityProofType::LegacyEip191IdentityProof
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::{to_value, Value};
|
|
||||||
|
|
||||||
use mitra_config::{
|
use mitra_config::{
|
||||||
BlockchainConfig,
|
|
||||||
Config,
|
Config,
|
||||||
RegistrationType,
|
RegistrationType,
|
||||||
MITRA_VERSION,
|
MITRA_VERSION,
|
||||||
};
|
};
|
||||||
use mitra_utils::markdown::markdown_to_html;
|
use mitra_utils::markdown::markdown_to_html;
|
||||||
|
|
||||||
use crate::ethereum::contracts::ContractSet;
|
|
||||||
use crate::mastodon_api::MASTODON_API_VERSION;
|
use crate::mastodon_api::MASTODON_API_VERSION;
|
||||||
use crate::media::SUPPORTED_MEDIA_TYPES;
|
use crate::media::SUPPORTED_MEDIA_TYPES;
|
||||||
use crate::validators::posts::ATTACHMENTS_MAX_NUM;
|
use crate::validators::posts::ATTACHMENTS_MAX_NUM;
|
||||||
|
@ -39,21 +36,6 @@ struct InstanceConfiguration {
|
||||||
media_attachments: InstanceMediaLimits,
|
media_attachments: InstanceMediaLimits,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct BlockchainFeatures {
|
|
||||||
gate: bool,
|
|
||||||
minter: bool,
|
|
||||||
subscriptions: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct BlockchainInfo {
|
|
||||||
chain_id: String,
|
|
||||||
chain_metadata: Option<Value>,
|
|
||||||
contract_address: Option<String>,
|
|
||||||
features: BlockchainFeatures,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// https://docs.joinmastodon.org/entities/V1_Instance/
|
/// https://docs.joinmastodon.org/entities/V1_Instance/
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct InstanceInfo {
|
pub struct InstanceInfo {
|
||||||
|
@ -71,7 +53,6 @@ pub struct InstanceInfo {
|
||||||
|
|
||||||
login_message: String,
|
login_message: String,
|
||||||
post_character_limit: usize, // deprecated
|
post_character_limit: usize, // deprecated
|
||||||
blockchains: Vec<BlockchainInfo>,
|
|
||||||
ipfs_gateway_url: Option<String>,
|
ipfs_gateway_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,53 +67,10 @@ fn get_full_api_version(version: &str) -> String {
|
||||||
impl InstanceInfo {
|
impl InstanceInfo {
|
||||||
pub fn create(
|
pub fn create(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
maybe_blockchain: Option<&ContractSet>,
|
|
||||||
user_count: i64,
|
user_count: i64,
|
||||||
post_count: i64,
|
post_count: i64,
|
||||||
peer_count: i64,
|
peer_count: i64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut blockchains = vec![];
|
|
||||||
match config.blockchain() {
|
|
||||||
Some(BlockchainConfig::Ethereum(ethereum_config)) => {
|
|
||||||
let features = if let Some(contract_set) = maybe_blockchain {
|
|
||||||
BlockchainFeatures {
|
|
||||||
gate: contract_set.gate.is_some(),
|
|
||||||
minter: contract_set.collectible.is_some(),
|
|
||||||
subscriptions: contract_set.subscription.is_some(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
BlockchainFeatures {
|
|
||||||
gate: false,
|
|
||||||
minter: false,
|
|
||||||
subscriptions: false,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let maybe_chain_metadata = ethereum_config
|
|
||||||
.chain_metadata.as_ref()
|
|
||||||
.and_then(|metadata| to_value(metadata).ok());
|
|
||||||
blockchains.push(BlockchainInfo {
|
|
||||||
chain_id: ethereum_config.chain_id.to_string(),
|
|
||||||
chain_metadata: maybe_chain_metadata,
|
|
||||||
contract_address:
|
|
||||||
Some(ethereum_config.contract_address.clone()),
|
|
||||||
features: features,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
Some(BlockchainConfig::Monero(monero_config)) => {
|
|
||||||
let features = BlockchainFeatures {
|
|
||||||
gate: false,
|
|
||||||
minter: false,
|
|
||||||
subscriptions: true,
|
|
||||||
};
|
|
||||||
blockchains.push(BlockchainInfo {
|
|
||||||
chain_id: monero_config.chain_id.to_string(),
|
|
||||||
chain_metadata: None,
|
|
||||||
contract_address: None,
|
|
||||||
features: features,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
None => (),
|
|
||||||
};
|
|
||||||
Self {
|
Self {
|
||||||
uri: config.instance().hostname(),
|
uri: config.instance().hostname(),
|
||||||
title: config.instance_title.clone(),
|
title: config.instance_title.clone(),
|
||||||
|
@ -165,7 +103,6 @@ impl InstanceInfo {
|
||||||
},
|
},
|
||||||
login_message: config.login_message.clone(),
|
login_message: config.login_message.clone(),
|
||||||
post_character_limit: config.limits.posts.character_limit,
|
post_character_limit: config.limits.posts.character_limit,
|
||||||
blockchains: blockchains,
|
|
||||||
ipfs_gateway_url: config.ipfs_gateway_url.clone(),
|
ipfs_gateway_url: config.ipfs_gateway_url.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ use mitra_models::{
|
||||||
users::queries::get_user_count,
|
users::queries::get_user_count,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::ethereum::contracts::ContractSet;
|
|
||||||
use crate::mastodon_api::errors::MastodonError;
|
use crate::mastodon_api::errors::MastodonError;
|
||||||
|
|
||||||
use super::types::InstanceInfo;
|
use super::types::InstanceInfo;
|
||||||
|
@ -18,7 +17,6 @@ use super::types::InstanceInfo;
|
||||||
async fn instance_view(
|
async fn instance_view(
|
||||||
config: web::Data<Config>,
|
config: web::Data<Config>,
|
||||||
db_pool: web::Data<DbPool>,
|
db_pool: web::Data<DbPool>,
|
||||||
maybe_blockchain: web::Data<Option<ContractSet>>,
|
|
||||||
) -> Result<HttpResponse, MastodonError> {
|
) -> Result<HttpResponse, MastodonError> {
|
||||||
let db_client = &**get_database_client(&db_pool).await?;
|
let db_client = &**get_database_client(&db_pool).await?;
|
||||||
let user_count = get_user_count(db_client).await?;
|
let user_count = get_user_count(db_client).await?;
|
||||||
|
@ -26,7 +24,6 @@ async fn instance_view(
|
||||||
let peer_count = get_peer_count(db_client).await?;
|
let peer_count = get_peer_count(db_client).await?;
|
||||||
let instance = InstanceInfo::create(
|
let instance = InstanceInfo::create(
|
||||||
config.as_ref(),
|
config.as_ref(),
|
||||||
maybe_blockchain.as_ref().as_ref(),
|
|
||||||
user_count,
|
user_count,
|
||||||
post_count,
|
post_count,
|
||||||
peer_count,
|
peer_count,
|
||||||
|
|
|
@ -21,16 +21,11 @@ use mitra_models::{
|
||||||
},
|
},
|
||||||
users::queries::{
|
users::queries::{
|
||||||
get_user_by_name,
|
get_user_by_name,
|
||||||
get_user_by_login_address,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use mitra_utils::passwords::verify_password;
|
use mitra_utils::passwords::verify_password;
|
||||||
|
|
||||||
use crate::errors::ValidationError;
|
use crate::errors::ValidationError;
|
||||||
use crate::ethereum::{
|
|
||||||
eip4361::verify_eip4361_signature,
|
|
||||||
utils::validate_ethereum_address,
|
|
||||||
};
|
|
||||||
use crate::http::FormOrJson;
|
use crate::http::FormOrJson;
|
||||||
use crate::mastodon_api::errors::MastodonError;
|
use crate::mastodon_api::errors::MastodonError;
|
||||||
|
|
||||||
|
@ -115,7 +110,7 @@ const ACCESS_TOKEN_EXPIRES_IN: i64 = 86400 * 7;
|
||||||
/// https://oauth.net/2/grant-types/password/
|
/// https://oauth.net/2/grant-types/password/
|
||||||
#[post("/token")]
|
#[post("/token")]
|
||||||
async fn token_view(
|
async fn token_view(
|
||||||
config: web::Data<Config>,
|
_config: web::Data<Config>,
|
||||||
db_pool: web::Data<DbPool>,
|
db_pool: web::Data<DbPool>,
|
||||||
request_data: FormOrJson<TokenRequest>,
|
request_data: FormOrJson<TokenRequest>,
|
||||||
) -> Result<HttpResponse, MastodonError> {
|
) -> Result<HttpResponse, MastodonError> {
|
||||||
|
@ -135,26 +130,6 @@ async fn token_view(
|
||||||
.ok_or(ValidationError("username is required"))?;
|
.ok_or(ValidationError("username is required"))?;
|
||||||
get_user_by_name(db_client, username).await?
|
get_user_by_name(db_client, username).await?
|
||||||
},
|
},
|
||||||
"ethereum" => {
|
|
||||||
// DEPRECATED
|
|
||||||
let wallet_address = request_data.wallet_address.as_ref()
|
|
||||||
.ok_or(ValidationError("wallet address is required"))?;
|
|
||||||
validate_ethereum_address(wallet_address)?;
|
|
||||||
get_user_by_login_address(db_client, wallet_address).await?
|
|
||||||
},
|
|
||||||
"eip4361" => {
|
|
||||||
let message = request_data.message.as_ref()
|
|
||||||
.ok_or(ValidationError("message is required"))?;
|
|
||||||
let signature = request_data.signature.as_ref()
|
|
||||||
.ok_or(ValidationError("signature is required"))?;
|
|
||||||
let wallet_address = verify_eip4361_signature(
|
|
||||||
message,
|
|
||||||
signature,
|
|
||||||
&config.instance().hostname(),
|
|
||||||
&config.login_message,
|
|
||||||
)?;
|
|
||||||
get_user_by_login_address(db_client, &wallet_address).await?
|
|
||||||
},
|
|
||||||
_ => {
|
_ => {
|
||||||
return Err(ValidationError("unsupported grant type").into());
|
return Err(ValidationError("unsupported grant type").into());
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,7 +13,6 @@ use mitra_models::{
|
||||||
profiles::queries::{
|
profiles::queries::{
|
||||||
search_profiles,
|
search_profiles,
|
||||||
search_profiles_by_did_only,
|
search_profiles_by_did_only,
|
||||||
search_profiles_by_wallet_address,
|
|
||||||
},
|
},
|
||||||
profiles::types::DbActorProfile,
|
profiles::types::DbActorProfile,
|
||||||
tags::queries::search_tags,
|
tags::queries::search_tags,
|
||||||
|
@ -23,7 +22,6 @@ use mitra_models::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use mitra_utils::{
|
use mitra_utils::{
|
||||||
currencies::Currency,
|
|
||||||
did::Did,
|
did::Did,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,7 +35,6 @@ use crate::activitypub::{
|
||||||
HandlerError,
|
HandlerError,
|
||||||
};
|
};
|
||||||
use crate::errors::ValidationError;
|
use crate::errors::ValidationError;
|
||||||
use crate::ethereum::utils::validate_ethereum_address;
|
|
||||||
use crate::media::MediaStorage;
|
use crate::media::MediaStorage;
|
||||||
use crate::webfinger::types::ActorAddress;
|
use crate::webfinger::types::ActorAddress;
|
||||||
|
|
||||||
|
@ -47,7 +44,6 @@ enum SearchQuery {
|
||||||
ProfileQuery(String, Option<String>),
|
ProfileQuery(String, Option<String>),
|
||||||
TagQuery(String),
|
TagQuery(String),
|
||||||
Url(String),
|
Url(String),
|
||||||
WalletAddress(String),
|
|
||||||
Did(Did),
|
Did(Did),
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
@ -87,12 +83,6 @@ fn parse_search_query(search_query: &str) -> SearchQuery {
|
||||||
if Url::parse(search_query).is_ok() {
|
if Url::parse(search_query).is_ok() {
|
||||||
return SearchQuery::Url(search_query.to_string());
|
return SearchQuery::Url(search_query.to_string());
|
||||||
};
|
};
|
||||||
// TODO: support other currencies
|
|
||||||
if validate_ethereum_address(
|
|
||||||
&search_query.to_lowercase(),
|
|
||||||
).is_ok() {
|
|
||||||
return SearchQuery::WalletAddress(search_query.to_string());
|
|
||||||
};
|
|
||||||
if let Ok(tag) = parse_tag_query(search_query) {
|
if let Ok(tag) = parse_tag_query(search_query) {
|
||||||
return SearchQuery::TagQuery(tag);
|
return SearchQuery::TagQuery(tag);
|
||||||
};
|
};
|
||||||
|
@ -267,16 +257,6 @@ pub async fn search(
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
SearchQuery::WalletAddress(address) => {
|
|
||||||
// Search by wallet address, assuming it's ethereum address
|
|
||||||
// TODO: support other currencies
|
|
||||||
profiles = search_profiles_by_wallet_address(
|
|
||||||
db_client,
|
|
||||||
&Currency::Ethereum,
|
|
||||||
&address,
|
|
||||||
false,
|
|
||||||
).await?;
|
|
||||||
},
|
|
||||||
SearchQuery::Did(did) => {
|
SearchQuery::Did(did) => {
|
||||||
profiles = search_profiles_by_did_only(
|
profiles = search_profiles_by_did_only(
|
||||||
db_client,
|
db_client,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ use mitra_models::{
|
||||||
profiles::types::PaymentOption,
|
profiles::types::PaymentOption,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::monero::subscriptions::MONERO_INVOICE_TIMEOUT;
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct InvoiceData {
|
pub struct InvoiceData {
|
||||||
|
@ -35,12 +34,7 @@ impl From<DbInvoice> for Invoice {
|
||||||
InvoiceStatus::Forwarded => "forwarded",
|
InvoiceStatus::Forwarded => "forwarded",
|
||||||
InvoiceStatus::Timeout => "timeout",
|
InvoiceStatus::Timeout => "timeout",
|
||||||
};
|
};
|
||||||
let expires_at = if value.chain_id.inner().is_monero() {
|
let expires_at = Default::default();
|
||||||
value.created_at + Duration::seconds(MONERO_INVOICE_TIMEOUT)
|
|
||||||
} else {
|
|
||||||
// Epoch 0
|
|
||||||
Default::default()
|
|
||||||
};
|
|
||||||
Self {
|
Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
sender_id: value.sender_id,
|
sender_id: value.sender_id,
|
||||||
|
|
|
@ -12,45 +12,19 @@ use uuid::Uuid;
|
||||||
use mitra_config::Config;
|
use mitra_config::Config;
|
||||||
use mitra_models::{
|
use mitra_models::{
|
||||||
database::{get_database_client, DbPool},
|
database::{get_database_client, DbPool},
|
||||||
invoices::queries::{create_invoice, get_invoice_by_id},
|
invoices::queries::{get_invoice_by_id},
|
||||||
profiles::queries::{
|
|
||||||
get_profile_by_id,
|
|
||||||
update_profile,
|
|
||||||
},
|
|
||||||
profiles::types::{
|
|
||||||
MoneroSubscription,
|
|
||||||
PaymentOption,
|
|
||||||
PaymentType,
|
|
||||||
ProfileUpdateData,
|
|
||||||
},
|
|
||||||
subscriptions::queries::get_subscription_by_participants,
|
subscriptions::queries::get_subscription_by_participants,
|
||||||
users::queries::get_user_by_id,
|
|
||||||
users::types::Permission,
|
users::types::Permission,
|
||||||
};
|
};
|
||||||
use mitra_utils::currencies::Currency;
|
|
||||||
|
|
||||||
use crate::activitypub::builders::update_person::prepare_update_person;
|
|
||||||
use crate::errors::ValidationError;
|
|
||||||
use crate::ethereum::{
|
|
||||||
contracts::ContractSet,
|
|
||||||
subscriptions::{
|
|
||||||
create_subscription_signature,
|
|
||||||
is_registered_recipient,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use crate::http::get_request_base_url;
|
use crate::http::get_request_base_url;
|
||||||
use crate::mastodon_api::{
|
use crate::mastodon_api::{
|
||||||
accounts::types::Account,
|
accounts::types::Account,
|
||||||
errors::MastodonError,
|
errors::MastodonError,
|
||||||
oauth::auth::get_current_user,
|
oauth::auth::get_current_user,
|
||||||
};
|
};
|
||||||
use crate::monero::{
|
|
||||||
helpers::validate_monero_address,
|
|
||||||
wallet::create_monero_address,
|
|
||||||
};
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
Invoice,
|
Invoice,
|
||||||
InvoiceData,
|
|
||||||
SubscriptionAuthorizationQueryParams,
|
SubscriptionAuthorizationQueryParams,
|
||||||
SubscriptionDetails,
|
SubscriptionDetails,
|
||||||
SubscriptionOption,
|
SubscriptionOption,
|
||||||
|
@ -60,28 +34,17 @@ use super::types::{
|
||||||
#[get("/authorize")]
|
#[get("/authorize")]
|
||||||
pub async fn authorize_subscription(
|
pub async fn authorize_subscription(
|
||||||
auth: BearerAuth,
|
auth: BearerAuth,
|
||||||
config: web::Data<Config>,
|
_config: web::Data<Config>,
|
||||||
db_pool: web::Data<DbPool>,
|
db_pool: web::Data<DbPool>,
|
||||||
query_params: web::Query<SubscriptionAuthorizationQueryParams>,
|
_query_params: web::Query<SubscriptionAuthorizationQueryParams>,
|
||||||
) -> Result<HttpResponse, MastodonError> {
|
) -> Result<HttpResponse, MastodonError> {
|
||||||
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 ethereum_config = config.blockchain()
|
|
||||||
.ok_or(MastodonError::NotSupported)?
|
|
||||||
.ethereum_config()
|
|
||||||
.ok_or(MastodonError::NotSupported)?;
|
|
||||||
// The user must have a public ethereum address,
|
// The user must have a public ethereum address,
|
||||||
// because subscribers should be able
|
// because subscribers should be able
|
||||||
// to verify that payments are actually sent to the recipient.
|
// to verify that payments are actually sent to the recipient.
|
||||||
let wallet_address = current_user
|
return Err(MastodonError::PermissionError);
|
||||||
.public_wallet_address(&Currency::Ethereum)
|
|
||||||
.ok_or(MastodonError::PermissionError)?;
|
|
||||||
let signature = create_subscription_signature(
|
|
||||||
ethereum_config,
|
|
||||||
&wallet_address,
|
|
||||||
query_params.price,
|
|
||||||
).map_err(|_| MastodonError::InternalError)?;
|
|
||||||
Ok(HttpResponse::Ok().json(signature))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/options")]
|
#[get("/options")]
|
||||||
|
@ -104,77 +67,14 @@ pub async fn register_subscription_option(
|
||||||
connection_info: ConnectionInfo,
|
connection_info: ConnectionInfo,
|
||||||
config: web::Data<Config>,
|
config: web::Data<Config>,
|
||||||
db_pool: web::Data<DbPool>,
|
db_pool: web::Data<DbPool>,
|
||||||
maybe_blockchain: web::Data<Option<ContractSet>>,
|
_subscription_option: web::Json<SubscriptionOption>,
|
||||||
subscription_option: web::Json<SubscriptionOption>,
|
|
||||||
) -> Result<HttpResponse, MastodonError> {
|
) -> Result<HttpResponse, MastodonError> {
|
||||||
let db_client = &mut **get_database_client(&db_pool).await?;
|
let db_client = &mut **get_database_client(&db_pool).await?;
|
||||||
let mut current_user = get_current_user(db_client, auth.token()).await?;
|
let current_user = get_current_user(db_client, auth.token()).await?;
|
||||||
if !current_user.role.has_permission(Permission::ManageSubscriptionOptions) {
|
if !current_user.role.has_permission(Permission::ManageSubscriptionOptions) {
|
||||||
return Err(MastodonError::PermissionError);
|
return Err(MastodonError::PermissionError);
|
||||||
};
|
};
|
||||||
|
|
||||||
let maybe_payment_option = match subscription_option.into_inner() {
|
|
||||||
SubscriptionOption::Ethereum => {
|
|
||||||
let ethereum_config = config.blockchain()
|
|
||||||
.and_then(|conf| conf.ethereum_config())
|
|
||||||
.ok_or(MastodonError::NotSupported)?;
|
|
||||||
let contract_set = maybe_blockchain.as_ref().as_ref()
|
|
||||||
.ok_or(MastodonError::NotSupported)?;
|
|
||||||
let wallet_address = current_user
|
|
||||||
.public_wallet_address(&Currency::Ethereum)
|
|
||||||
.ok_or(MastodonError::PermissionError)?;
|
|
||||||
if current_user.profile.payment_options
|
|
||||||
.any(PaymentType::EthereumSubscription)
|
|
||||||
{
|
|
||||||
// Ignore attempts to update payment option
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let is_registered = is_registered_recipient(
|
|
||||||
contract_set,
|
|
||||||
&wallet_address,
|
|
||||||
).await.map_err(|_| MastodonError::InternalError)?;
|
|
||||||
if !is_registered {
|
|
||||||
return Err(ValidationError("recipient is not registered").into());
|
|
||||||
};
|
|
||||||
Some(PaymentOption::ethereum_subscription(
|
|
||||||
ethereum_config.chain_id.clone(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SubscriptionOption::Monero { price, payout_address } => {
|
|
||||||
let monero_config = config.blockchain()
|
|
||||||
.and_then(|conf| conf.monero_config())
|
|
||||||
.ok_or(MastodonError::NotSupported)?;
|
|
||||||
if price == 0 {
|
|
||||||
return Err(ValidationError("price must be greater than 0").into());
|
|
||||||
};
|
|
||||||
validate_monero_address(&payout_address)?;
|
|
||||||
let payment_info = MoneroSubscription {
|
|
||||||
chain_id: monero_config.chain_id.clone(),
|
|
||||||
price,
|
|
||||||
payout_address,
|
|
||||||
};
|
|
||||||
Some(PaymentOption::MoneroSubscription(payment_info))
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if let Some(payment_option) = maybe_payment_option {
|
|
||||||
let mut profile_data = ProfileUpdateData::from(¤t_user.profile);
|
|
||||||
profile_data.add_payment_option(payment_option);
|
|
||||||
current_user.profile = update_profile(
|
|
||||||
db_client,
|
|
||||||
¤t_user.id,
|
|
||||||
profile_data,
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
// Federate
|
|
||||||
prepare_update_person(
|
|
||||||
db_client,
|
|
||||||
&config.instance(),
|
|
||||||
¤t_user,
|
|
||||||
None,
|
|
||||||
).await?.enqueue(db_client).await?;
|
|
||||||
};
|
|
||||||
|
|
||||||
let account = Account::from_user(
|
let account = Account::from_user(
|
||||||
&get_request_base_url(connection_info),
|
&get_request_base_url(connection_info),
|
||||||
&config.instance_url(),
|
&config.instance_url(),
|
||||||
|
@ -201,44 +101,6 @@ async fn find_subscription(
|
||||||
Ok(HttpResponse::Ok().json(details))
|
Ok(HttpResponse::Ok().json(details))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/invoices")]
|
|
||||||
async fn create_invoice_view(
|
|
||||||
config: web::Data<Config>,
|
|
||||||
db_pool: web::Data<DbPool>,
|
|
||||||
invoice_data: web::Json<InvoiceData>,
|
|
||||||
) -> Result<HttpResponse, MastodonError> {
|
|
||||||
let monero_config = config.blockchain()
|
|
||||||
.ok_or(MastodonError::NotSupported)?
|
|
||||||
.monero_config()
|
|
||||||
.ok_or(MastodonError::NotSupported)?;
|
|
||||||
if invoice_data.sender_id == invoice_data.recipient_id {
|
|
||||||
return Err(ValidationError("sender must be different from recipient").into());
|
|
||||||
};
|
|
||||||
if invoice_data.amount <= 0 {
|
|
||||||
return Err(ValidationError("amount must be positive").into());
|
|
||||||
};
|
|
||||||
let db_client = &**get_database_client(&db_pool).await?;
|
|
||||||
let sender = get_profile_by_id(db_client, &invoice_data.sender_id).await?;
|
|
||||||
let recipient = get_user_by_id(db_client, &invoice_data.recipient_id).await?;
|
|
||||||
if !recipient.profile.payment_options.any(PaymentType::MoneroSubscription) {
|
|
||||||
let error_message = "recipient can't accept subscription payments";
|
|
||||||
return Err(ValidationError(error_message).into());
|
|
||||||
};
|
|
||||||
|
|
||||||
let payment_address = create_monero_address(monero_config).await
|
|
||||||
.map_err(|_| MastodonError::InternalError)?
|
|
||||||
.to_string();
|
|
||||||
let db_invoice = create_invoice(
|
|
||||||
db_client,
|
|
||||||
&sender.id,
|
|
||||||
&recipient.id,
|
|
||||||
&monero_config.chain_id,
|
|
||||||
&payment_address,
|
|
||||||
invoice_data.amount,
|
|
||||||
).await?;
|
|
||||||
let invoice = Invoice::from(db_invoice);
|
|
||||||
Ok(HttpResponse::Ok().json(invoice))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/invoices/{invoice_id}")]
|
#[get("/invoices/{invoice_id}")]
|
||||||
async fn get_invoice(
|
async fn get_invoice(
|
||||||
|
@ -257,6 +119,5 @@ pub fn subscription_api_scope() -> Scope {
|
||||||
.service(get_subscription_options)
|
.service(get_subscription_options)
|
||||||
.service(register_subscription_option)
|
.service(register_subscription_option)
|
||||||
.service(find_subscription)
|
.service(find_subscription)
|
||||||
.service(create_invoice_view)
|
|
||||||
.service(get_invoice)
|
.service(get_invoice)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use monero_rpc::TransferType;
|
|
||||||
use monero_rpc::monero::Address;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use mitra_config::MoneroConfig;
|
|
||||||
use mitra_models::{
|
|
||||||
database::DatabaseClient,
|
|
||||||
invoices::queries::{
|
|
||||||
get_invoice_by_id,
|
|
||||||
set_invoice_status,
|
|
||||||
},
|
|
||||||
invoices::types::InvoiceStatus,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::errors::ValidationError;
|
|
||||||
|
|
||||||
use super::wallet::{
|
|
||||||
open_monero_wallet,
|
|
||||||
MoneroError,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn validate_monero_address(address: &str)
|
|
||||||
-> Result<(), ValidationError>
|
|
||||||
{
|
|
||||||
Address::from_str(address)
|
|
||||||
.map_err(|_| ValidationError("invalid monero address"))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_expired_invoice(
|
|
||||||
config: &MoneroConfig,
|
|
||||||
db_client: &impl DatabaseClient,
|
|
||||||
invoice_id: &Uuid,
|
|
||||||
) -> Result<(), MoneroError> {
|
|
||||||
let wallet_client = open_monero_wallet(config).await?;
|
|
||||||
let invoice = get_invoice_by_id(db_client, invoice_id).await?;
|
|
||||||
if invoice.chain_id != config.chain_id ||
|
|
||||||
invoice.invoice_status != InvoiceStatus::Timeout
|
|
||||||
{
|
|
||||||
return Err(MoneroError::OtherError("can't process invoice"));
|
|
||||||
};
|
|
||||||
let address = Address::from_str(&invoice.payment_address)?;
|
|
||||||
let address_index = wallet_client.get_address_index(address).await?;
|
|
||||||
let transfers = wallet_client.incoming_transfers(
|
|
||||||
TransferType::Available,
|
|
||||||
Some(config.account_index),
|
|
||||||
Some(vec![address_index.minor]),
|
|
||||||
).await?
|
|
||||||
.transfers
|
|
||||||
.unwrap_or_default();
|
|
||||||
if transfers.is_empty() {
|
|
||||||
log::info!("no incoming transfers");
|
|
||||||
} else {
|
|
||||||
for transfer in transfers {
|
|
||||||
if transfer.subaddr_index != address_index {
|
|
||||||
return Err(MoneroError::WalletRpcError("unexpected transfer"));
|
|
||||||
};
|
|
||||||
log::info!(
|
|
||||||
"received payment for invoice {}: {}",
|
|
||||||
invoice.id,
|
|
||||||
transfer.amount,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
set_invoice_status(db_client, &invoice.id, InvoiceStatus::Paid).await?;
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod helpers;
|
|
||||||
pub mod subscriptions;
|
|
||||||
pub mod wallet;
|
|
|
@ -1,221 +0,0 @@
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use chrono::{Duration, Utc};
|
|
||||||
use monero_rpc::TransferType;
|
|
||||||
use monero_rpc::monero::{Address, Amount};
|
|
||||||
|
|
||||||
use mitra_config::{Instance, MoneroConfig};
|
|
||||||
use mitra_models::{
|
|
||||||
database::{get_database_client, DatabaseError, DbPool},
|
|
||||||
invoices::queries::{
|
|
||||||
get_invoice_by_address,
|
|
||||||
get_invoices_by_status,
|
|
||||||
set_invoice_status,
|
|
||||||
},
|
|
||||||
invoices::types::InvoiceStatus,
|
|
||||||
profiles::queries::get_profile_by_id,
|
|
||||||
profiles::types::PaymentOption,
|
|
||||||
subscriptions::queries::{
|
|
||||||
create_subscription,
|
|
||||||
get_subscription_by_participants,
|
|
||||||
update_subscription,
|
|
||||||
},
|
|
||||||
users::queries::get_user_by_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::ethereum::subscriptions::send_subscription_notifications;
|
|
||||||
|
|
||||||
use super::wallet::{
|
|
||||||
get_single_item,
|
|
||||||
get_subaddress_balance,
|
|
||||||
open_monero_wallet,
|
|
||||||
send_monero,
|
|
||||||
MoneroError,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const MONERO_INVOICE_TIMEOUT: i64 = 3 * 60 * 60; // 3 hours
|
|
||||||
|
|
||||||
pub async fn check_monero_subscriptions(
|
|
||||||
instance: &Instance,
|
|
||||||
config: &MoneroConfig,
|
|
||||||
db_pool: &DbPool,
|
|
||||||
) -> Result<(), MoneroError> {
|
|
||||||
let db_client = &mut **get_database_client(db_pool).await?;
|
|
||||||
let wallet_client = open_monero_wallet(config).await?;
|
|
||||||
|
|
||||||
// Invoices waiting for payment
|
|
||||||
let mut address_waitlist = vec![];
|
|
||||||
let open_invoices = get_invoices_by_status(
|
|
||||||
db_client,
|
|
||||||
&config.chain_id,
|
|
||||||
InvoiceStatus::Open,
|
|
||||||
).await?;
|
|
||||||
for invoice in open_invoices {
|
|
||||||
let invoice_age = Utc::now() - invoice.created_at;
|
|
||||||
if invoice_age.num_seconds() >= MONERO_INVOICE_TIMEOUT {
|
|
||||||
set_invoice_status(
|
|
||||||
db_client,
|
|
||||||
&invoice.id,
|
|
||||||
InvoiceStatus::Timeout,
|
|
||||||
).await?;
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let address = Address::from_str(&invoice.payment_address)?;
|
|
||||||
let address_index = wallet_client.get_address_index(address).await?;
|
|
||||||
address_waitlist.push(address_index.minor);
|
|
||||||
};
|
|
||||||
let maybe_incoming_transfers = if !address_waitlist.is_empty() {
|
|
||||||
log::info!("{} invoices are waiting for payment", address_waitlist.len());
|
|
||||||
let incoming_transfers = wallet_client.incoming_transfers(
|
|
||||||
TransferType::Available,
|
|
||||||
Some(config.account_index),
|
|
||||||
Some(address_waitlist),
|
|
||||||
).await?;
|
|
||||||
incoming_transfers.transfers
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
if let Some(transfers) = maybe_incoming_transfers {
|
|
||||||
for transfer in transfers {
|
|
||||||
let address_data = wallet_client.get_address(
|
|
||||||
config.account_index,
|
|
||||||
Some(vec![transfer.subaddr_index.minor]),
|
|
||||||
).await?;
|
|
||||||
let subaddress_data = get_single_item(address_data.addresses)?;
|
|
||||||
let subaddress = subaddress_data.address;
|
|
||||||
let invoice = get_invoice_by_address(
|
|
||||||
db_client,
|
|
||||||
&config.chain_id,
|
|
||||||
&subaddress.to_string(),
|
|
||||||
).await?;
|
|
||||||
log::info!(
|
|
||||||
"received payment for invoice {}: {}",
|
|
||||||
invoice.id,
|
|
||||||
transfer.amount,
|
|
||||||
);
|
|
||||||
if invoice.invoice_status == InvoiceStatus::Open {
|
|
||||||
set_invoice_status(db_client, &invoice.id, InvoiceStatus::Paid).await?;
|
|
||||||
} else {
|
|
||||||
log::warn!("invoice has already been paid");
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Invoices waiting to be forwarded
|
|
||||||
let paid_invoices = get_invoices_by_status(
|
|
||||||
db_client,
|
|
||||||
&config.chain_id,
|
|
||||||
InvoiceStatus::Paid,
|
|
||||||
).await?;
|
|
||||||
for invoice in paid_invoices {
|
|
||||||
let address = Address::from_str(&invoice.payment_address)?;
|
|
||||||
let address_index = wallet_client.get_address_index(address).await?;
|
|
||||||
let balance_data = get_subaddress_balance(
|
|
||||||
&wallet_client,
|
|
||||||
&address_index,
|
|
||||||
).await?;
|
|
||||||
if balance_data.balance != balance_data.unlocked_balance ||
|
|
||||||
balance_data.balance == Amount::ZERO
|
|
||||||
{
|
|
||||||
// Don't forward payment until all outputs are unlocked
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let sender = get_profile_by_id(db_client, &invoice.sender_id).await?;
|
|
||||||
let recipient = get_user_by_id(db_client, &invoice.recipient_id).await?;
|
|
||||||
let maybe_payment_info = recipient.profile.payment_options.clone()
|
|
||||||
.into_inner().into_iter()
|
|
||||||
.find_map(|option| match option {
|
|
||||||
PaymentOption::MoneroSubscription(payment_info) => {
|
|
||||||
if payment_info.chain_id == config.chain_id {
|
|
||||||
Some(payment_info)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
let payment_info = if let Some(payment_info) = maybe_payment_info {
|
|
||||||
payment_info
|
|
||||||
} else {
|
|
||||||
log::error!("subscription is not configured for user {}", recipient.id);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let payout_address = Address::from_str(&payment_info.payout_address)?;
|
|
||||||
let payout_amount = send_monero(
|
|
||||||
&wallet_client,
|
|
||||||
config.account_index,
|
|
||||||
address_index.minor,
|
|
||||||
payout_address,
|
|
||||||
).await?;
|
|
||||||
let duration_secs = (payout_amount.as_pico() / payment_info.price)
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| MoneroError::OtherError("invalid duration"))?;
|
|
||||||
|
|
||||||
set_invoice_status(
|
|
||||||
db_client,
|
|
||||||
&invoice.id,
|
|
||||||
InvoiceStatus::Forwarded,
|
|
||||||
).await?;
|
|
||||||
log::info!("processed payment for invoice {}", invoice.id);
|
|
||||||
|
|
||||||
match get_subscription_by_participants(
|
|
||||||
db_client,
|
|
||||||
&sender.id,
|
|
||||||
&recipient.id,
|
|
||||||
).await {
|
|
||||||
Ok(subscription) => {
|
|
||||||
if subscription.chain_id != config.chain_id {
|
|
||||||
log::error!("can't switch to another chain");
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
// Update subscription expiration date
|
|
||||||
let expires_at =
|
|
||||||
std::cmp::max(subscription.expires_at, Utc::now()) +
|
|
||||||
Duration::seconds(duration_secs);
|
|
||||||
update_subscription(
|
|
||||||
db_client,
|
|
||||||
subscription.id,
|
|
||||||
&expires_at,
|
|
||||||
&Utc::now(),
|
|
||||||
).await?;
|
|
||||||
log::info!(
|
|
||||||
"subscription updated: {0} to {1}",
|
|
||||||
subscription.sender_id,
|
|
||||||
subscription.recipient_id,
|
|
||||||
);
|
|
||||||
send_subscription_notifications(
|
|
||||||
db_client,
|
|
||||||
instance,
|
|
||||||
&sender,
|
|
||||||
&recipient,
|
|
||||||
).await?;
|
|
||||||
},
|
|
||||||
Err(DatabaseError::NotFound(_)) => {
|
|
||||||
// New subscription
|
|
||||||
let expires_at = Utc::now() + Duration::seconds(duration_secs);
|
|
||||||
create_subscription(
|
|
||||||
db_client,
|
|
||||||
&sender.id,
|
|
||||||
None, // matching by address is not required
|
|
||||||
&recipient.id,
|
|
||||||
&config.chain_id,
|
|
||||||
&expires_at,
|
|
||||||
&Utc::now(),
|
|
||||||
).await?;
|
|
||||||
log::info!(
|
|
||||||
"subscription created: {0} to {1}",
|
|
||||||
sender.id,
|
|
||||||
recipient.id,
|
|
||||||
);
|
|
||||||
send_subscription_notifications(
|
|
||||||
db_client,
|
|
||||||
instance,
|
|
||||||
&sender,
|
|
||||||
&recipient,
|
|
||||||
).await?;
|
|
||||||
},
|
|
||||||
Err(other_error) => return Err(other_error.into()),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,166 +0,0 @@
|
||||||
use monero_rpc::{
|
|
||||||
HashString,
|
|
||||||
RpcClientBuilder,
|
|
||||||
SubaddressBalanceData,
|
|
||||||
SweepAllArgs,
|
|
||||||
TransferPriority,
|
|
||||||
WalletClient,
|
|
||||||
};
|
|
||||||
use monero_rpc::monero::{
|
|
||||||
cryptonote::subaddress::Index,
|
|
||||||
util::address::Error as AddressError,
|
|
||||||
Address,
|
|
||||||
Amount,
|
|
||||||
};
|
|
||||||
|
|
||||||
use mitra_config::MoneroConfig;
|
|
||||||
use mitra_models::database::DatabaseError;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum MoneroError {
|
|
||||||
#[error(transparent)]
|
|
||||||
WalletError(#[from] anyhow::Error),
|
|
||||||
|
|
||||||
#[error("{0}")]
|
|
||||||
WalletRpcError(&'static str),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
AddressError(#[from] AddressError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
DatabaseError(#[from] DatabaseError),
|
|
||||||
|
|
||||||
#[error("other error")]
|
|
||||||
OtherError(&'static str),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// https://monerodocs.org/interacting/monero-wallet-rpc-reference/#create_wallet
|
|
||||||
pub async fn create_monero_wallet(
|
|
||||||
config: &MoneroConfig,
|
|
||||||
name: String,
|
|
||||||
password: Option<String>,
|
|
||||||
) -> Result<(), MoneroError> {
|
|
||||||
let wallet_client = RpcClientBuilder::new()
|
|
||||||
.build(config.wallet_url.clone())?
|
|
||||||
.wallet();
|
|
||||||
let language = "English".to_string();
|
|
||||||
wallet_client.create_wallet(name, password, language).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// https://monerodocs.org/interacting/monero-wallet-rpc-reference/#open_wallet
|
|
||||||
pub async fn open_monero_wallet(
|
|
||||||
config: &MoneroConfig,
|
|
||||||
) -> Result<WalletClient, MoneroError> {
|
|
||||||
let wallet_client = RpcClientBuilder::new()
|
|
||||||
.build(config.wallet_url.clone())?
|
|
||||||
.wallet();
|
|
||||||
if let Err(error) = wallet_client.refresh(None).await {
|
|
||||||
if error.to_string() == "Server error: No wallet file" {
|
|
||||||
// Try to open wallet
|
|
||||||
if let Some(ref wallet_name) = config.wallet_name {
|
|
||||||
wallet_client.open_wallet(
|
|
||||||
wallet_name.clone(),
|
|
||||||
config.wallet_password.clone(),
|
|
||||||
).await?;
|
|
||||||
} else {
|
|
||||||
return Err(MoneroError::WalletRpcError("wallet file is required"));
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return Err(error.into());
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Ok(wallet_client)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_monero_address(
|
|
||||||
config: &MoneroConfig,
|
|
||||||
) -> Result<Address, MoneroError> {
|
|
||||||
let wallet_client = open_monero_wallet(config).await?;
|
|
||||||
let account_index = config.account_index;
|
|
||||||
let (address, address_index) =
|
|
||||||
wallet_client.create_address(account_index, None).await?;
|
|
||||||
log::info!("created monero address {}/{}", account_index, address_index);
|
|
||||||
// Save wallet
|
|
||||||
wallet_client.close_wallet().await?;
|
|
||||||
Ok(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_single_item<T: Clone>(items: Vec<T>) -> Result<T, MoneroError> {
|
|
||||||
if let [item] = &items[..] {
|
|
||||||
Ok(item.clone())
|
|
||||||
} else {
|
|
||||||
Err(MoneroError::WalletRpcError("expected single item"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_subaddress_balance(
|
|
||||||
wallet_client: &WalletClient,
|
|
||||||
subaddress_index: &Index,
|
|
||||||
) -> Result<SubaddressBalanceData, MoneroError> {
|
|
||||||
let balance_data = wallet_client.get_balance(
|
|
||||||
subaddress_index.major,
|
|
||||||
Some(vec![subaddress_index.minor]),
|
|
||||||
).await?;
|
|
||||||
let subaddress_data = get_single_item(balance_data.per_subaddress)?;
|
|
||||||
Ok(subaddress_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// https://monerodocs.org/interacting/monero-wallet-rpc-reference/#sweep_all
|
|
||||||
pub async fn send_monero(
|
|
||||||
wallet_client: &WalletClient,
|
|
||||||
from_account: u32,
|
|
||||||
from_address: u32,
|
|
||||||
to_address: Address,
|
|
||||||
) -> Result<Amount, MoneroError> {
|
|
||||||
let sweep_args = SweepAllArgs {
|
|
||||||
address: to_address,
|
|
||||||
account_index: from_account,
|
|
||||||
subaddr_indices: Some(vec![from_address]),
|
|
||||||
priority: TransferPriority::Default,
|
|
||||||
mixin: 15,
|
|
||||||
ring_size: 16,
|
|
||||||
unlock_time: 1,
|
|
||||||
get_tx_keys: None,
|
|
||||||
below_amount: None,
|
|
||||||
do_not_relay: None,
|
|
||||||
get_tx_hex: None,
|
|
||||||
get_tx_metadata: None,
|
|
||||||
};
|
|
||||||
let sweep_data = wallet_client.sweep_all(sweep_args).await?;
|
|
||||||
let HashString(tx_hash) = get_single_item(sweep_data.tx_hash_list)?;
|
|
||||||
let amount = get_single_item(sweep_data.amount_list)?;
|
|
||||||
let fee = get_single_item(sweep_data.fee_list)?;
|
|
||||||
|
|
||||||
// TODO: transaction can fail
|
|
||||||
// https://github.com/monero-project/monero/issues/8372
|
|
||||||
let maybe_transfer = wallet_client.get_transfer(
|
|
||||||
tx_hash,
|
|
||||||
Some(from_account),
|
|
||||||
).await?;
|
|
||||||
let transfer_status = maybe_transfer
|
|
||||||
.map(|data| data.transfer_type.into())
|
|
||||||
.unwrap_or("dropped");
|
|
||||||
if transfer_status == "dropped" || transfer_status == "failed" {
|
|
||||||
log::error!(
|
|
||||||
"sent transaction {:x} from {}/{}, {}",
|
|
||||||
tx_hash,
|
|
||||||
from_account,
|
|
||||||
from_address,
|
|
||||||
transfer_status,
|
|
||||||
);
|
|
||||||
return Err(MoneroError::WalletRpcError("transaction failed"));
|
|
||||||
};
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"sent transaction {:x} from {}/{}, amount {}, fee {}",
|
|
||||||
tx_hash,
|
|
||||||
from_account,
|
|
||||||
from_address,
|
|
||||||
amount,
|
|
||||||
fee,
|
|
||||||
);
|
|
||||||
// Save wallet
|
|
||||||
wallet_client.close_wallet().await?;
|
|
||||||
Ok(amount)
|
|
||||||
}
|
|
|
@ -4,8 +4,8 @@ use serde::Serialize;
|
||||||
|
|
||||||
use mitra_config::{Config, RegistrationType, MITRA_VERSION};
|
use mitra_config::{Config, RegistrationType, MITRA_VERSION};
|
||||||
|
|
||||||
const MITRA_NAME: &str = "mitra";
|
const MITRA_NAME: &str = "reef";
|
||||||
const MITRA_REPOSITORY: &str = "https://codeberg.org/silverpill/mitra";
|
const MITRA_REPOSITORY: &str = "https://code.caric.io/reef/reef";
|
||||||
const ATOM_SERVICE: &str = "atom1.0";
|
const ATOM_SERVICE: &str = "atom1.0";
|
||||||
const ACTIVITYPUB_PROTOCOL: &str = "activitypub";
|
const ACTIVITYPUB_PROTOCOL: &str = "activitypub";
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue