2021-01-04 22:08:36 -05:00
|
|
|
pub mod wallet;
|
2021-02-24 02:08:25 -05:00
|
|
|
mod wallet_rpc;
|
2021-01-04 22:08:36 -05:00
|
|
|
|
2021-05-11 08:22:59 -04:00
|
|
|
pub use ::monero::network::Network;
|
2021-05-11 04:03:33 -04:00
|
|
|
pub use ::monero::{Address, PrivateKey, PublicKey};
|
2021-01-21 01:09:53 -05:00
|
|
|
pub use curve25519_dalek::scalar::Scalar;
|
|
|
|
pub use wallet::Wallet;
|
2021-02-24 02:08:25 -05:00
|
|
|
pub use wallet_rpc::{WalletRpc, WalletRpcProcess};
|
2021-01-21 01:09:53 -05:00
|
|
|
|
2021-01-20 19:20:57 -05:00
|
|
|
use crate::bitcoin;
|
2020-10-26 21:11:03 -04:00
|
|
|
use anyhow::Result;
|
2021-01-04 22:08:36 -05:00
|
|
|
use rand::{CryptoRng, RngCore};
|
2021-03-03 19:40:28 -05:00
|
|
|
use rust_decimal::prelude::*;
|
2021-03-03 19:28:58 -05:00
|
|
|
use rust_decimal::Decimal;
|
2021-01-04 22:08:36 -05:00
|
|
|
use serde::{Deserialize, Serialize};
|
2021-03-03 19:28:58 -05:00
|
|
|
use std::convert::TryFrom;
|
2021-03-03 19:40:28 -05:00
|
|
|
use std::fmt;
|
2021-03-03 19:28:58 -05:00
|
|
|
use std::ops::{Add, Mul, Sub};
|
|
|
|
use std::str::FromStr;
|
2020-11-06 14:57:35 -05:00
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
pub const PICONERO_OFFSET: u64 = 1_000_000_000_000;
|
2020-11-30 15:41:22 -05:00
|
|
|
|
2021-05-11 08:22:59 -04:00
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
#[serde(remote = "Network")]
|
|
|
|
#[allow(non_camel_case_types)]
|
|
|
|
pub enum network {
|
2021-05-11 04:03:33 -04:00
|
|
|
Mainnet,
|
|
|
|
Stagenet,
|
|
|
|
Testnet,
|
|
|
|
}
|
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> PrivateKey {
|
|
|
|
let mut bytes = scalar.to_bytes();
|
|
|
|
|
|
|
|
// we must reverse the bytes because a secp256k1 scalar is big endian, whereas a
|
|
|
|
// ed25519 scalar is little endian
|
|
|
|
bytes.reverse();
|
|
|
|
|
|
|
|
PrivateKey::from_scalar(Scalar::from_bytes_mod_order(bytes))
|
|
|
|
}
|
|
|
|
|
2023-01-10 07:43:07 -05:00
|
|
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
2021-01-04 22:08:36 -05:00
|
|
|
pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey);
|
|
|
|
|
RPC server for API Interface (#1276)
* saving: implementing internal api shared by cli and rpc server
* writing async rpc methods and using arc for shared struct references
* cleaning up, renamed Init to Context
* saving: cleaning up and initial work for tests
* Respond with bitcoin withdraw txid
* Print RPC server address
* Cleanup, formatting, add `get_seller`, `get_swap_start_date` RPC endpoints
* fixing tests in cli module
* uncommenting and fixing more tests
* split api module and propagate errors with rpc server
* moving methods to api and validating addresses for rpc
* add broadcast channel to handle shutdowns gracefully and prepare for RPC server test
* added files
* Update rpc.rs
* adding new unfinished RPC tests
* updating rpc-server tests
* fixing warnings
* fixing formatting and cargo clippy warnings
* fix missing import in test
* fix: add data_dir to config to make config command work
* set server listen address manually and return file locations in JSON on Config
* Add called api method and swap_id to tracing for context, reduced boilerplate
* Pass server_address properly to RpcServer
* Update Cargo.lock
* dprint fmt
* Add cancel_refund RPC endpoint
* Combine Cmd and Params
* Disallow concurrent swaps
* Use RwLock instead of Mutex to allow for parallel reads and add get_current_swap endpoint
* Return wallet descriptor to RPC API caller
* Append all cli logs to single log file
After careful consideration, I've concluded that it's not practical/possible to ensure that the previous behaviour (one log file per swap) is preserved due to limitations of the tracing-subscriber crate and a big in the built in JSON formatter
* Add get_swap_expired_timelock timelock, other small refactoring
- Add get_swap_expired_timelock endpoint to return expired timelock if one exists. Fails if bitcoin lock tx has not yet published or if swap is already finished.
- Rename current_epoch to expired_timelock to enforce consistent method names
- Add blocks left until current expired timelock expires (next timelock expires) to ExpiredTimelock struct
- Change .expect() to .unwrap() in rpc server method register because those will only fail if we register the same method twice which will never happen
* initiating swaps in a separate task and handling shutdown signals with broadcast queues
* Replace get_swap_start_date, get_seller, get_expired_timelock with one get_swap_info rpc method
* WIP: Struct for concurrent swaps manager
* Ensure correct tracing spans
* Add note regarding Request, Method structs
* Update request.rs
* Add tracing span attribute log_reference_id to logs caused by rpc call
* Sync bitcoin wallet before initial max_giveable call
* use Span::current() to pass down to tracing span to spawned tasks
* Remove unused shutdown channel
* Add `get_monero_recovery_info` RPC endpoint
- Add `get_monero_recovery_info` RPC endpoint
- format PrivateViewKey using Display
* Rename `Method::RawHistory` to `Method::GetRawStates`
* Wait for swap to be suspended after sending signal
* Remove notes
* Add tracing span attribute log_reference_id to logs caused by rpc call
* Sync bitcoin wallet before initial max_giveable call
* use Span::current() to pass down to tracing span to spawned tasks
* Remove unused shutdown channel
* Add `get_monero_recovery_info` RPC endpoint
- Add `get_monero_recovery_info` RPC endpoint
- format PrivateViewKey using Display
* Rename `Method::RawHistory` to `Method::GetRawStates`
* Wait for swap to be suspended after sending signal
* Return additonal info on GetSwapInfo
* Update wallet.rs
* fix compile issues for tests and use serial_test crate
* fix rpc tests, only check for RPC errors and not returned values
* Rename `get_raw_history` tp `get_raw_states`
* Fix typo in rpc server stopped tracing log
* Remove unnecessary success property on suspend_current_swap response
* fixing test_cli_arguments and other tests
* WIP: RPC server integration tests
* WIP: Integration tests for RPC server
* Update rpc tests
* fix compile and warnings in tests/rpc.rs
* test: fix assert
* clippy --fix
* remove otp file
* cargo clippy fixes
* move resume swap initialization code out of spawned task
* Use `in_current_span` to pass down tracing span to spawned tasks
* moving buy_xmr initialization code out of spawned tasks
* cargo fmt
* Moving swap initialization code inside tokio select block to handle swap lock release logic
* Remove unnecessary swap suspension listener from determine_btc_to_swap call in BuyXmr
* Spawn event loop before requesting quote
* Release swap lock after receiving shutdown signal
* Remove inner tokio::select in BuyXmr and Resume
* Improve debug text for swap resume
* Return error to API caller if bid quote request fails
* Print error if one occurs during process invoked by API call
* Return bid quote to API caller
* Use type safe query! macro for database retrieval of states
* Return tx_lock_fee to API caller on GetSwapInfo call
Update request.rs
* Allow API caller to retrieve last synced bitcoin balane and avoid costly sync
* Return restore height on MoneroRecovery command to API Caller
* Include entire error cause-chain in API response
* Add span to bitcoin wallet logs
* Log event loop connection properties as tracing fields
* Wait for background tasks to complete before exiting CLI
* clippy
* specify sqlx patch version explicitly
* remove mem::forget and replace with _guard
* ci: add rpc test job
* test: wrap rpc test in #[cfg(test)]
* add missing tokio::test attribute
* fix and merge rpc tests, parse uuuid and multiaddr from serde_json value
* default Tor socks port to 9050, Cargo fmt
* Update swap/sqlite_dev_setup.sh: add version
Co-authored-by: Byron Hambly <byron@hambly.dev>
* ci: free up space on ubuntu test job
* Update swap/src/bitcoin/wallet.rs
Co-authored-by: Byron Hambly <byron@hambly.dev>
* Update swap/src/bitcoin/wallet.rs
Co-authored-by: Byron Hambly <byron@hambly.dev>
* fmt
---------
Co-authored-by: binarybaron <86064887+binarybaron@users.noreply.github.com>
Co-authored-by: Byron Hambly <byron@hambly.dev>
2024-05-22 09:12:58 -04:00
|
|
|
impl fmt::Display for PrivateViewKey {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
// Delegate to the Display implementation of PrivateKey
|
|
|
|
write!(f, "{}", self.0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
impl PrivateViewKey {
|
|
|
|
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
|
|
|
let scalar = Scalar::random(rng);
|
|
|
|
let private_key = PrivateKey::from_scalar(scalar);
|
|
|
|
|
|
|
|
Self(private_key)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn public(&self) -> PublicViewKey {
|
|
|
|
PublicViewKey(PublicKey::from_private_key(&self.0))
|
2020-10-26 21:11:03 -04:00
|
|
|
}
|
2021-01-04 22:08:36 -05:00
|
|
|
}
|
2020-10-26 21:11:03 -04:00
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
impl Add for PrivateViewKey {
|
|
|
|
type Output = Self;
|
2020-10-26 21:11:03 -04:00
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
fn add(self, rhs: Self) -> Self::Output {
|
|
|
|
Self(self.0 + rhs.0)
|
2020-10-26 21:11:03 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
impl From<PrivateViewKey> for PrivateKey {
|
|
|
|
fn from(from: PrivateViewKey) -> Self {
|
|
|
|
from.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<PublicViewKey> for PublicKey {
|
|
|
|
fn from(from: PublicViewKey) -> Self {
|
|
|
|
from.0
|
|
|
|
}
|
|
|
|
}
|
2020-10-26 21:11:03 -04:00
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
pub struct PublicViewKey(PublicKey);
|
2020-10-26 21:11:03 -04:00
|
|
|
|
2023-01-10 07:43:07 -05:00
|
|
|
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd)]
|
2021-01-04 22:08:36 -05:00
|
|
|
pub struct Amount(u64);
|
2020-11-30 15:41:22 -05:00
|
|
|
|
2022-07-28 08:37:07 -04:00
|
|
|
// Median tx fees on Monero as found here: https://www.monero.how/monero-transaction-fees, XMR 0.000_008 * 2 (to be on the safe side)
|
|
|
|
pub const MONERO_FEE: Amount = Amount::from_piconero(16_000_000);
|
2021-06-23 23:54:26 -04:00
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
impl Amount {
|
|
|
|
pub const ZERO: Self = Self(0);
|
2021-02-16 00:37:44 -05:00
|
|
|
pub const ONE_XMR: Self = Self(PICONERO_OFFSET);
|
2021-01-04 22:08:36 -05:00
|
|
|
/// Create an [Amount] with piconero precision and the given number of
|
|
|
|
/// piconeros.
|
|
|
|
///
|
|
|
|
/// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR.
|
2021-06-23 23:54:26 -04:00
|
|
|
pub const fn from_piconero(amount: u64) -> Self {
|
2021-01-04 22:08:36 -05:00
|
|
|
Amount(amount)
|
|
|
|
}
|
2020-11-30 15:41:22 -05:00
|
|
|
|
2022-08-03 06:30:51 -04:00
|
|
|
/// Return Monero Amount as Piconero.
|
2021-01-04 22:08:36 -05:00
|
|
|
pub fn as_piconero(&self) -> u64 {
|
|
|
|
self.0
|
|
|
|
}
|
2020-12-04 00:27:17 -05:00
|
|
|
|
2022-08-03 06:30:51 -04:00
|
|
|
/// Calculate the maximum amount of Bitcoin that can be bought at a given
|
|
|
|
/// asking price for this amount of Monero including the median fee.
|
2022-07-28 08:37:07 -04:00
|
|
|
pub fn max_bitcoin_for_price(&self, ask_price: bitcoin::Amount) -> Option<bitcoin::Amount> {
|
2022-08-03 06:30:51 -04:00
|
|
|
let pico_minus_fee = self.as_piconero().saturating_sub(MONERO_FEE.as_piconero());
|
2022-02-23 20:12:45 -05:00
|
|
|
|
2022-08-03 06:30:51 -04:00
|
|
|
if pico_minus_fee == 0 {
|
2022-07-28 08:37:07 -04:00
|
|
|
return Some(bitcoin::Amount::ZERO);
|
2022-02-23 20:12:45 -05:00
|
|
|
}
|
|
|
|
|
2022-08-03 06:30:51 -04:00
|
|
|
// safely convert the BTC/XMR rate to sat/pico
|
2022-11-22 08:39:42 -05:00
|
|
|
let ask_sats = Decimal::from(ask_price.to_sat());
|
2022-08-03 06:30:51 -04:00
|
|
|
let pico_per_xmr = Decimal::from(PICONERO_OFFSET);
|
|
|
|
let ask_sats_per_pico = ask_sats / pico_per_xmr;
|
|
|
|
|
|
|
|
let pico = Decimal::from(pico_minus_fee);
|
|
|
|
let max_sats = pico.checked_mul(ask_sats_per_pico)?;
|
|
|
|
let satoshi = max_sats.to_u64()?;
|
2022-02-23 20:12:45 -05:00
|
|
|
|
2022-07-28 08:37:07 -04:00
|
|
|
Some(bitcoin::Amount::from_sat(satoshi))
|
2022-02-23 20:12:45 -05:00
|
|
|
}
|
|
|
|
|
2021-02-07 23:24:32 -05:00
|
|
|
pub fn from_monero(amount: f64) -> Result<Self> {
|
|
|
|
let decimal = Decimal::try_from(amount)?;
|
|
|
|
Self::from_decimal(decimal)
|
|
|
|
}
|
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
pub fn parse_monero(amount: &str) -> Result<Self> {
|
|
|
|
let decimal = Decimal::from_str(amount)?;
|
2021-02-07 23:24:32 -05:00
|
|
|
Self::from_decimal(decimal)
|
|
|
|
}
|
|
|
|
|
2021-03-31 22:00:15 -04:00
|
|
|
pub fn as_piconero_decimal(&self) -> Decimal {
|
|
|
|
Decimal::from(self.as_piconero())
|
|
|
|
}
|
|
|
|
|
2024-08-01 12:35:03 -04:00
|
|
|
pub fn as_xmr(&self) -> Decimal {
|
|
|
|
let mut decimal = Decimal::from(self.0);
|
|
|
|
decimal
|
|
|
|
.set_scale(12)
|
|
|
|
.expect("12 is smaller than max precision of 28");
|
|
|
|
decimal
|
|
|
|
}
|
|
|
|
|
2021-02-07 23:24:32 -05:00
|
|
|
fn from_decimal(amount: Decimal) -> Result<Self> {
|
2021-01-04 22:08:36 -05:00
|
|
|
let piconeros_dec =
|
2021-02-07 23:24:32 -05:00
|
|
|
amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
|
2021-01-04 22:08:36 -05:00
|
|
|
let piconeros = piconeros_dec
|
|
|
|
.to_u64()
|
2021-02-07 23:24:32 -05:00
|
|
|
.ok_or_else(|| OverflowError(amount.to_string()))?;
|
2021-01-04 22:08:36 -05:00
|
|
|
Ok(Amount(piconeros))
|
2020-10-26 21:11:03 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
impl Add for Amount {
|
|
|
|
type Output = Amount;
|
|
|
|
|
|
|
|
fn add(self, rhs: Self) -> Self::Output {
|
|
|
|
Self(self.0 + rhs.0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Sub for Amount {
|
|
|
|
type Output = Amount;
|
2020-10-26 21:11:03 -04:00
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
fn sub(self, rhs: Self) -> Self::Output {
|
|
|
|
Self(self.0 - rhs.0)
|
|
|
|
}
|
|
|
|
}
|
2020-10-26 21:11:03 -04:00
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
impl Mul<u64> for Amount {
|
|
|
|
type Output = Amount;
|
2020-10-26 21:11:03 -04:00
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
fn mul(self, rhs: u64) -> Self::Output {
|
|
|
|
Self(self.0 * rhs)
|
2020-10-26 21:11:03 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
impl From<Amount> for u64 {
|
|
|
|
fn from(from: Amount) -> u64 {
|
|
|
|
from.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-03 19:40:28 -05:00
|
|
|
impl fmt::Display for Amount {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
2024-08-01 12:35:03 -04:00
|
|
|
let xmr_value = self.as_xmr();
|
|
|
|
write!(f, "{} XMR", xmr_value)
|
2021-01-04 22:08:36 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-10 07:43:07 -05:00
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
2021-01-04 22:08:36 -05:00
|
|
|
pub struct TransferProof {
|
|
|
|
tx_hash: TxHash,
|
|
|
|
#[serde(with = "monero_private_key")]
|
|
|
|
tx_key: PrivateKey,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TransferProof {
|
|
|
|
pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self {
|
|
|
|
Self { tx_hash, tx_key }
|
|
|
|
}
|
|
|
|
pub fn tx_hash(&self) -> TxHash {
|
|
|
|
self.tx_hash.clone()
|
|
|
|
}
|
|
|
|
pub fn tx_key(&self) -> PrivateKey {
|
|
|
|
self.tx_key
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: add constructor/ change String to fixed length byte array
|
2023-01-10 07:43:07 -05:00
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
2021-01-04 22:08:36 -05:00
|
|
|
pub struct TxHash(pub String);
|
|
|
|
|
|
|
|
impl From<TxHash> for String {
|
|
|
|
fn from(from: TxHash) -> Self {
|
|
|
|
from.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-05 00:24:02 -05:00
|
|
|
impl fmt::Display for TxHash {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
write!(f, "{}", self.0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
2021-03-17 22:42:19 -04:00
|
|
|
#[error("expected {expected}, got {actual}")]
|
2021-01-04 22:08:36 -05:00
|
|
|
pub struct InsufficientFunds {
|
|
|
|
pub expected: Amount,
|
|
|
|
pub actual: Amount,
|
|
|
|
}
|
|
|
|
|
2023-01-10 07:43:07 -05:00
|
|
|
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
2021-01-04 22:08:36 -05:00
|
|
|
#[error("Overflow, cannot convert {0} to u64")]
|
|
|
|
pub struct OverflowError(pub String);
|
|
|
|
|
2021-01-07 18:44:31 -05:00
|
|
|
pub mod monero_private_key {
|
2021-03-03 19:28:58 -05:00
|
|
|
use monero::consensus::{Decodable, Encodable};
|
|
|
|
use monero::PrivateKey;
|
|
|
|
use serde::de::Visitor;
|
|
|
|
use serde::ser::Error;
|
|
|
|
use serde::{de, Deserializer, Serializer};
|
|
|
|
use std::fmt;
|
|
|
|
use std::io::Cursor;
|
2021-01-07 18:44:31 -05:00
|
|
|
|
|
|
|
struct BytesVisitor;
|
|
|
|
|
|
|
|
impl<'de> Visitor<'de> for BytesVisitor {
|
|
|
|
type Value = PrivateKey;
|
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
write!(formatter, "a byte array representing a Monero private key")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_bytes<E>(self, s: &[u8]) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: de::Error,
|
|
|
|
{
|
|
|
|
let mut s = s;
|
|
|
|
PrivateKey::consensus_decode(&mut s).map_err(|err| E::custom(format!("{:?}", err)))
|
|
|
|
}
|
2021-10-06 03:58:13 -04:00
|
|
|
|
|
|
|
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: de::Error,
|
|
|
|
{
|
|
|
|
let bytes = hex::decode(s).map_err(|err| E::custom(format!("{:?}", err)))?;
|
|
|
|
PrivateKey::consensus_decode(&mut bytes.as_slice())
|
|
|
|
.map_err(|err| E::custom(format!("{:?}", err)))
|
|
|
|
}
|
2021-01-07 18:44:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn serialize<S>(x: &PrivateKey, s: S) -> Result<S::Ok, S::Error>
|
|
|
|
where
|
|
|
|
S: Serializer,
|
|
|
|
{
|
|
|
|
let mut bytes = Cursor::new(vec![]);
|
|
|
|
x.consensus_encode(&mut bytes)
|
|
|
|
.map_err(|err| S::Error::custom(format!("{:?}", err)))?;
|
2021-10-06 03:58:13 -04:00
|
|
|
if s.is_human_readable() {
|
|
|
|
s.serialize_str(&hex::encode(bytes.into_inner()))
|
|
|
|
} else {
|
|
|
|
s.serialize_bytes(bytes.into_inner().as_ref())
|
|
|
|
}
|
2021-01-07 18:44:31 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn deserialize<'de, D>(
|
|
|
|
deserializer: D,
|
|
|
|
) -> Result<PrivateKey, <D as Deserializer<'de>>::Error>
|
|
|
|
where
|
|
|
|
D: Deserializer<'de>,
|
|
|
|
{
|
2021-10-06 03:58:13 -04:00
|
|
|
let key = {
|
|
|
|
if deserializer.is_human_readable() {
|
|
|
|
deserializer.deserialize_string(BytesVisitor)?
|
|
|
|
} else {
|
|
|
|
deserializer.deserialize_bytes(BytesVisitor)?
|
|
|
|
}
|
|
|
|
};
|
2021-01-07 18:44:31 -05:00
|
|
|
Ok(key)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub mod monero_amount {
|
|
|
|
use crate::monero::Amount;
|
|
|
|
use serde::{Deserialize, Deserializer, Serializer};
|
|
|
|
|
|
|
|
pub fn serialize<S>(x: &Amount, s: S) -> Result<S::Ok, S::Error>
|
|
|
|
where
|
|
|
|
S: Serializer,
|
|
|
|
{
|
|
|
|
s.serialize_u64(x.as_piconero())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Amount, <D as Deserializer<'de>>::Error>
|
|
|
|
where
|
|
|
|
D: Deserializer<'de>,
|
|
|
|
{
|
|
|
|
let picos = u64::deserialize(deserializer)?;
|
|
|
|
let amount = Amount::from_piconero(picos);
|
|
|
|
|
|
|
|
Ok(amount)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
RPC server for API Interface (#1276)
* saving: implementing internal api shared by cli and rpc server
* writing async rpc methods and using arc for shared struct references
* cleaning up, renamed Init to Context
* saving: cleaning up and initial work for tests
* Respond with bitcoin withdraw txid
* Print RPC server address
* Cleanup, formatting, add `get_seller`, `get_swap_start_date` RPC endpoints
* fixing tests in cli module
* uncommenting and fixing more tests
* split api module and propagate errors with rpc server
* moving methods to api and validating addresses for rpc
* add broadcast channel to handle shutdowns gracefully and prepare for RPC server test
* added files
* Update rpc.rs
* adding new unfinished RPC tests
* updating rpc-server tests
* fixing warnings
* fixing formatting and cargo clippy warnings
* fix missing import in test
* fix: add data_dir to config to make config command work
* set server listen address manually and return file locations in JSON on Config
* Add called api method and swap_id to tracing for context, reduced boilerplate
* Pass server_address properly to RpcServer
* Update Cargo.lock
* dprint fmt
* Add cancel_refund RPC endpoint
* Combine Cmd and Params
* Disallow concurrent swaps
* Use RwLock instead of Mutex to allow for parallel reads and add get_current_swap endpoint
* Return wallet descriptor to RPC API caller
* Append all cli logs to single log file
After careful consideration, I've concluded that it's not practical/possible to ensure that the previous behaviour (one log file per swap) is preserved due to limitations of the tracing-subscriber crate and a big in the built in JSON formatter
* Add get_swap_expired_timelock timelock, other small refactoring
- Add get_swap_expired_timelock endpoint to return expired timelock if one exists. Fails if bitcoin lock tx has not yet published or if swap is already finished.
- Rename current_epoch to expired_timelock to enforce consistent method names
- Add blocks left until current expired timelock expires (next timelock expires) to ExpiredTimelock struct
- Change .expect() to .unwrap() in rpc server method register because those will only fail if we register the same method twice which will never happen
* initiating swaps in a separate task and handling shutdown signals with broadcast queues
* Replace get_swap_start_date, get_seller, get_expired_timelock with one get_swap_info rpc method
* WIP: Struct for concurrent swaps manager
* Ensure correct tracing spans
* Add note regarding Request, Method structs
* Update request.rs
* Add tracing span attribute log_reference_id to logs caused by rpc call
* Sync bitcoin wallet before initial max_giveable call
* use Span::current() to pass down to tracing span to spawned tasks
* Remove unused shutdown channel
* Add `get_monero_recovery_info` RPC endpoint
- Add `get_monero_recovery_info` RPC endpoint
- format PrivateViewKey using Display
* Rename `Method::RawHistory` to `Method::GetRawStates`
* Wait for swap to be suspended after sending signal
* Remove notes
* Add tracing span attribute log_reference_id to logs caused by rpc call
* Sync bitcoin wallet before initial max_giveable call
* use Span::current() to pass down to tracing span to spawned tasks
* Remove unused shutdown channel
* Add `get_monero_recovery_info` RPC endpoint
- Add `get_monero_recovery_info` RPC endpoint
- format PrivateViewKey using Display
* Rename `Method::RawHistory` to `Method::GetRawStates`
* Wait for swap to be suspended after sending signal
* Return additonal info on GetSwapInfo
* Update wallet.rs
* fix compile issues for tests and use serial_test crate
* fix rpc tests, only check for RPC errors and not returned values
* Rename `get_raw_history` tp `get_raw_states`
* Fix typo in rpc server stopped tracing log
* Remove unnecessary success property on suspend_current_swap response
* fixing test_cli_arguments and other tests
* WIP: RPC server integration tests
* WIP: Integration tests for RPC server
* Update rpc tests
* fix compile and warnings in tests/rpc.rs
* test: fix assert
* clippy --fix
* remove otp file
* cargo clippy fixes
* move resume swap initialization code out of spawned task
* Use `in_current_span` to pass down tracing span to spawned tasks
* moving buy_xmr initialization code out of spawned tasks
* cargo fmt
* Moving swap initialization code inside tokio select block to handle swap lock release logic
* Remove unnecessary swap suspension listener from determine_btc_to_swap call in BuyXmr
* Spawn event loop before requesting quote
* Release swap lock after receiving shutdown signal
* Remove inner tokio::select in BuyXmr and Resume
* Improve debug text for swap resume
* Return error to API caller if bid quote request fails
* Print error if one occurs during process invoked by API call
* Return bid quote to API caller
* Use type safe query! macro for database retrieval of states
* Return tx_lock_fee to API caller on GetSwapInfo call
Update request.rs
* Allow API caller to retrieve last synced bitcoin balane and avoid costly sync
* Return restore height on MoneroRecovery command to API Caller
* Include entire error cause-chain in API response
* Add span to bitcoin wallet logs
* Log event loop connection properties as tracing fields
* Wait for background tasks to complete before exiting CLI
* clippy
* specify sqlx patch version explicitly
* remove mem::forget and replace with _guard
* ci: add rpc test job
* test: wrap rpc test in #[cfg(test)]
* add missing tokio::test attribute
* fix and merge rpc tests, parse uuuid and multiaddr from serde_json value
* default Tor socks port to 9050, Cargo fmt
* Update swap/sqlite_dev_setup.sh: add version
Co-authored-by: Byron Hambly <byron@hambly.dev>
* ci: free up space on ubuntu test job
* Update swap/src/bitcoin/wallet.rs
Co-authored-by: Byron Hambly <byron@hambly.dev>
* Update swap/src/bitcoin/wallet.rs
Co-authored-by: Byron Hambly <byron@hambly.dev>
* fmt
---------
Co-authored-by: binarybaron <86064887+binarybaron@users.noreply.github.com>
Co-authored-by: Byron Hambly <byron@hambly.dev>
2024-05-22 09:12:58 -04:00
|
|
|
pub mod monero_address {
|
|
|
|
use anyhow::{bail, Context, Result};
|
|
|
|
use std::str::FromStr;
|
|
|
|
|
|
|
|
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)]
|
|
|
|
#[error("Invalid monero address provided, expected address on network {expected:?} but address provided is on {actual:?}")]
|
|
|
|
pub struct MoneroAddressNetworkMismatch {
|
|
|
|
pub expected: monero::Network,
|
|
|
|
pub actual: monero::Network,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn parse(s: &str) -> Result<monero::Address> {
|
|
|
|
monero::Address::from_str(s).with_context(|| {
|
|
|
|
format!(
|
|
|
|
"Failed to parse {} as a monero address, please make sure it is a valid address",
|
|
|
|
s
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn validate(
|
|
|
|
address: monero::Address,
|
|
|
|
expected_network: monero::Network,
|
|
|
|
) -> Result<monero::Address> {
|
|
|
|
if address.network != expected_network {
|
|
|
|
bail!(MoneroAddressNetworkMismatch {
|
|
|
|
expected: expected_network,
|
|
|
|
actual: address.network,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
Ok(address)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn validate_is_testnet(
|
|
|
|
address: monero::Address,
|
|
|
|
is_testnet: bool,
|
|
|
|
) -> Result<monero::Address> {
|
|
|
|
let expected_network = if is_testnet {
|
|
|
|
monero::Network::Stagenet
|
|
|
|
} else {
|
|
|
|
monero::Network::Mainnet
|
|
|
|
};
|
|
|
|
validate(address, expected_network)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-04 22:08:36 -05:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn display_monero_min() {
|
|
|
|
let min_pics = 1;
|
|
|
|
let amount = Amount::from_piconero(min_pics);
|
|
|
|
let monero = amount.to_string();
|
|
|
|
assert_eq!("0.000000000001 XMR", monero);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn display_monero_one() {
|
|
|
|
let min_pics = 1000000000000;
|
|
|
|
let amount = Amount::from_piconero(min_pics);
|
|
|
|
let monero = amount.to_string();
|
|
|
|
assert_eq!("1.000000000000 XMR", monero);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn display_monero_max() {
|
|
|
|
let max_pics = 18_446_744_073_709_551_615;
|
|
|
|
let amount = Amount::from_piconero(max_pics);
|
|
|
|
let monero = amount.to_string();
|
|
|
|
assert_eq!("18446744.073709551615 XMR", monero);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn parse_monero_min() {
|
|
|
|
let monero_min = "0.000000000001";
|
|
|
|
let amount = Amount::parse_monero(monero_min).unwrap();
|
|
|
|
let pics = amount.0;
|
|
|
|
assert_eq!(1, pics);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn parse_monero() {
|
|
|
|
let monero = "123";
|
|
|
|
let amount = Amount::parse_monero(monero).unwrap();
|
|
|
|
let pics = amount.0;
|
|
|
|
assert_eq!(123000000000000, pics);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn parse_monero_max() {
|
|
|
|
let monero = "18446744.073709551615";
|
|
|
|
let amount = Amount::parse_monero(monero).unwrap();
|
|
|
|
let pics = amount.0;
|
|
|
|
assert_eq!(18446744073709551615, pics);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn parse_monero_overflows() {
|
|
|
|
let overflow_pics = "18446744.073709551616";
|
|
|
|
let error = Amount::parse_monero(overflow_pics).unwrap_err();
|
|
|
|
assert_eq!(
|
|
|
|
error.downcast_ref::<OverflowError>().unwrap(),
|
|
|
|
&OverflowError(overflow_pics.to_owned())
|
|
|
|
);
|
2020-10-26 21:11:03 -04:00
|
|
|
}
|
2021-01-07 18:44:31 -05:00
|
|
|
|
2022-02-23 20:12:45 -05:00
|
|
|
#[test]
|
2022-07-28 08:37:07 -04:00
|
|
|
fn max_bitcoin_to_trade() {
|
|
|
|
// sanity check: if the asking price is 1 BTC / 1 XMR
|
2022-08-03 06:30:51 -04:00
|
|
|
// and we have μ XMR + fee
|
|
|
|
// then max BTC we can buy is μ
|
2022-07-28 08:37:07 -04:00
|
|
|
let ask = bitcoin::Amount::from_btc(1.0).unwrap();
|
2022-08-03 06:30:51 -04:00
|
|
|
|
|
|
|
let xmr = Amount::parse_monero("1.0").unwrap() + MONERO_FEE;
|
2022-07-28 08:37:07 -04:00
|
|
|
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
2022-02-23 20:12:45 -05:00
|
|
|
|
2022-08-03 06:30:51 -04:00
|
|
|
assert_eq!(btc, bitcoin::Amount::from_btc(1.0).unwrap());
|
|
|
|
|
|
|
|
let xmr = Amount::parse_monero("0.5").unwrap() + MONERO_FEE;
|
|
|
|
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(btc, bitcoin::Amount::from_btc(0.5).unwrap());
|
|
|
|
|
|
|
|
let xmr = Amount::parse_monero("2.5").unwrap() + MONERO_FEE;
|
|
|
|
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(btc, bitcoin::Amount::from_btc(2.5).unwrap());
|
|
|
|
|
|
|
|
let xmr = Amount::parse_monero("420").unwrap() + MONERO_FEE;
|
|
|
|
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(btc, bitcoin::Amount::from_btc(420.0).unwrap());
|
|
|
|
|
|
|
|
let xmr = Amount::parse_monero("0.00001").unwrap() + MONERO_FEE;
|
|
|
|
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(btc, bitcoin::Amount::from_btc(0.00001).unwrap());
|
|
|
|
|
|
|
|
// other ask prices
|
|
|
|
|
|
|
|
let ask = bitcoin::Amount::from_btc(0.5).unwrap();
|
|
|
|
let xmr = Amount::parse_monero("2").unwrap() + MONERO_FEE;
|
|
|
|
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(btc, bitcoin::Amount::from_btc(1.0).unwrap());
|
|
|
|
|
|
|
|
let ask = bitcoin::Amount::from_btc(2.0).unwrap();
|
|
|
|
let xmr = Amount::parse_monero("1").unwrap() + MONERO_FEE;
|
|
|
|
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(btc, bitcoin::Amount::from_btc(2.0).unwrap());
|
2022-02-23 20:12:45 -05:00
|
|
|
|
2022-07-28 08:37:07 -04:00
|
|
|
let ask = bitcoin::Amount::from_sat(382_900);
|
2022-08-03 06:30:51 -04:00
|
|
|
let xmr = Amount::parse_monero("10").unwrap();
|
|
|
|
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(btc, bitcoin::Amount::from_sat(3_828_993));
|
|
|
|
|
|
|
|
// example from https://github.com/comit-network/xmr-btc-swap/issues/1084
|
|
|
|
// with rate from kraken at that time
|
|
|
|
let ask = bitcoin::Amount::from_sat(685_800);
|
|
|
|
let xmr = Amount::parse_monero("0.826286435921").unwrap();
|
2022-07-28 08:37:07 -04:00
|
|
|
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
|
|
|
|
2022-08-03 06:30:51 -04:00
|
|
|
assert_eq!(btc, bitcoin::Amount::from_sat(566_656));
|
2022-07-28 08:37:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn max_bitcoin_to_trade_overflow() {
|
|
|
|
let xmr = Amount::from_monero(30.0).unwrap();
|
|
|
|
let ask = bitcoin::Amount::from_sat(728_688);
|
|
|
|
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
|
|
|
|
2022-08-03 06:30:51 -04:00
|
|
|
assert_eq!(bitcoin::Amount::from_sat(21_860_628), btc);
|
2022-07-28 08:37:07 -04:00
|
|
|
|
|
|
|
let xmr = Amount::from_piconero(u64::MAX);
|
|
|
|
let ask = bitcoin::Amount::from_sat(u64::MAX);
|
|
|
|
let btc = xmr.max_bitcoin_for_price(ask);
|
|
|
|
|
|
|
|
assert!(btc.is_none());
|
2022-02-23 20:12:45 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn geting_max_bitcoin_to_trade_with_balance_smaller_than_locking_fee() {
|
2022-08-03 06:30:51 -04:00
|
|
|
let ask = bitcoin::Amount::from_sat(382_900);
|
|
|
|
let xmr = Amount::parse_monero("0.00001").unwrap();
|
|
|
|
let btc = xmr.max_bitcoin_for_price(ask).unwrap();
|
2022-02-23 20:12:45 -05:00
|
|
|
|
2022-08-03 06:30:51 -04:00
|
|
|
assert_eq!(bitcoin::Amount::ZERO, btc);
|
2022-02-23 20:12:45 -05:00
|
|
|
}
|
|
|
|
|
2021-01-07 18:44:31 -05:00
|
|
|
use rand::rngs::OsRng;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
2023-01-10 07:43:07 -05:00
|
|
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
2021-01-07 18:44:31 -05:00
|
|
|
pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey);
|
|
|
|
|
2023-01-10 07:43:07 -05:00
|
|
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
2021-01-07 18:44:31 -05:00
|
|
|
pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount);
|
|
|
|
|
|
|
|
#[test]
|
2021-10-06 03:58:13 -04:00
|
|
|
fn serde_monero_private_key_json() {
|
|
|
|
let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(
|
|
|
|
crate::monero::Scalar::random(&mut OsRng),
|
|
|
|
));
|
|
|
|
let encoded = serde_json::to_vec(&key).unwrap();
|
|
|
|
let decoded: MoneroPrivateKey = serde_json::from_slice(&encoded).unwrap();
|
|
|
|
assert_eq!(key, decoded);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn serde_monero_private_key_cbor() {
|
2021-01-07 18:44:31 -05:00
|
|
|
let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(
|
|
|
|
crate::monero::Scalar::random(&mut OsRng),
|
|
|
|
));
|
|
|
|
let encoded = serde_cbor::to_vec(&key).unwrap();
|
|
|
|
let decoded: MoneroPrivateKey = serde_cbor::from_slice(&encoded).unwrap();
|
|
|
|
assert_eq!(key, decoded);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn serde_monero_amount() {
|
|
|
|
let amount = MoneroAmount(crate::monero::Amount::from_piconero(1000));
|
|
|
|
let encoded = serde_cbor::to_vec(&amount).unwrap();
|
|
|
|
let decoded: MoneroAmount = serde_cbor::from_slice(&encoded).unwrap();
|
|
|
|
assert_eq!(amount, decoded);
|
|
|
|
}
|
2020-10-26 21:11:03 -04:00
|
|
|
}
|