Merge xmr_btc crate
Created network, storage and protocol modules. Organised files into the modules where the belong. xmr_btc crate moved into isolated modulein swap crate. Remove the xmr_btc module and integrate into swap crate. Consolidate message related code Reorganise imports Remove unused parent Message enum Remove unused parent State enum Remove unused dependencies from Cargo.tomlpull/125/head
parent
a0d859147a
commit
c900d12593
@ -1,2 +1,2 @@
|
||||
[workspace]
|
||||
members = ["monero-harness", "xmr-btc", "swap"]
|
||||
members = ["monero-harness", "swap"]
|
||||
|
@ -1,202 +1,288 @@
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_trait::async_trait;
|
||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||
use bitcoin::util::psbt::PartiallySignedTransaction;
|
||||
use bitcoin_harness::{bitcoind_rpc::PsbtBase64, BitcoindRpcApi};
|
||||
use reqwest::Url;
|
||||
use std::time::Duration;
|
||||
use tokio::time::interval;
|
||||
use xmr_btc::{
|
||||
bitcoin::{
|
||||
BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight, SignTxLock,
|
||||
TransactionBlockHeight, WatchForRawTransaction,
|
||||
},
|
||||
config::Config,
|
||||
};
|
||||
|
||||
pub use ::bitcoin::{Address, Transaction};
|
||||
pub use xmr_btc::bitcoin::*;
|
||||
|
||||
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet {
|
||||
pub inner: bitcoin_harness::Wallet,
|
||||
pub network: bitcoin::Network,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result<Self> {
|
||||
let wallet = bitcoin_harness::Wallet::new(name, url).await?;
|
||||
|
||||
Ok(Self {
|
||||
inner: wallet,
|
||||
network,
|
||||
})
|
||||
use bitcoin::hashes::{hex::ToHex, Hash};
|
||||
use ecdsa_fun::{adaptor::Adaptor, fun::Point, nonce::Deterministic, ECDSA};
|
||||
use miniscript::{Descriptor, Segwitv0};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha256;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{config::Config, ExpiredTimelocks};
|
||||
|
||||
use crate::bitcoin::timelocks::{BlockHeight, Timelock};
|
||||
pub use crate::bitcoin::transactions::{TxCancel, TxLock, TxPunish, TxRedeem, TxRefund};
|
||||
pub use ::bitcoin::{util::psbt::PartiallySignedTransaction, *};
|
||||
pub use ecdsa_fun::{adaptor::EncryptedSignature, fun::Scalar, Signature};
|
||||
pub use wallet::Wallet;
|
||||
|
||||
pub mod timelocks;
|
||||
pub mod transactions;
|
||||
pub mod wallet;
|
||||
|
||||
// TODO: Configurable tx-fee (note: parties have to agree prior to swapping)
|
||||
// Current reasoning:
|
||||
// tx with largest weight (as determined by get_weight() upon broadcast in e2e
|
||||
// test) = 609 assuming segwit and 60 sat/vB:
|
||||
// (609 / 4) * 60 (sat/vB) = 9135 sats
|
||||
// Recommended: Overpay a bit to ensure we don't have to wait too long for test
|
||||
// runs.
|
||||
pub const TX_FEE: u64 = 15_000;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct SecretKey {
|
||||
inner: Scalar,
|
||||
public: Point,
|
||||
}
|
||||
|
||||
impl SecretKey {
|
||||
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
|
||||
let scalar = Scalar::random(rng);
|
||||
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
let public = ecdsa.verification_key_for(&scalar);
|
||||
|
||||
Self {
|
||||
inner: scalar,
|
||||
public,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn balance(&self) -> Result<Amount> {
|
||||
let balance = self.inner.balance().await?;
|
||||
Ok(balance)
|
||||
pub fn public(&self) -> PublicKey {
|
||||
PublicKey(self.public)
|
||||
}
|
||||
|
||||
pub async fn new_address(&self) -> Result<Address> {
|
||||
self.inner.new_address().await.map_err(Into::into)
|
||||
pub fn to_bytes(&self) -> [u8; 32] {
|
||||
self.inner.to_bytes()
|
||||
}
|
||||
|
||||
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
|
||||
let fee = self
|
||||
.inner
|
||||
.get_wallet_transaction(txid)
|
||||
.await
|
||||
.map(|res| {
|
||||
res.fee.map(|signed_amount| {
|
||||
signed_amount
|
||||
.abs()
|
||||
.to_unsigned()
|
||||
.expect("Absolute value is always positive")
|
||||
})
|
||||
})?
|
||||
.context("Rpc response did not contain a fee")?;
|
||||
|
||||
Ok(fee)
|
||||
pub fn sign(&self, digest: SigHash) -> Signature {
|
||||
let ecdsa = ECDSA::<Deterministic<Sha256>>::default();
|
||||
|
||||
ecdsa.sign(&self.inner, &digest.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BuildTxLockPsbt for Wallet {
|
||||
async fn build_tx_lock_psbt(
|
||||
&self,
|
||||
output_address: Address,
|
||||
output_amount: Amount,
|
||||
) -> Result<PartiallySignedTransaction> {
|
||||
let psbt = self.inner.fund_psbt(output_address, output_amount).await?;
|
||||
let as_hex = base64::decode(psbt)?;
|
||||
// TxRefund encsigning explanation:
|
||||
//
|
||||
// A and B, are the Bitcoin Public Keys which go on the joint output for
|
||||
// TxLock_Bitcoin. S_a and S_b, are the Monero Public Keys which go on the
|
||||
// joint output for TxLock_Monero
|
||||
|
||||
// tx_refund: multisig(A, B), published by bob
|
||||
// bob can produce sig on B for tx_refund using b
|
||||
// alice sends over an encrypted signature on A for tx_refund using a encrypted
|
||||
// with S_b we want to leak s_b
|
||||
|
||||
let psbt = bitcoin::consensus::deserialize(&as_hex)?;
|
||||
// produced (by Alice) encsig - published (by Bob) sig = s_b (it's not really
|
||||
// subtraction, it's recover)
|
||||
|
||||
Ok(psbt)
|
||||
// self = a, Y = S_b, digest = tx_refund
|
||||
pub fn encsign(&self, Y: PublicKey, digest: SigHash) -> EncryptedSignature {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
|
||||
adaptor.encrypted_sign(&self.inner, &Y.0, &digest.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SignTxLock for Wallet {
|
||||
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction> {
|
||||
let psbt = PartiallySignedTransaction::from(tx_lock);
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PublicKey(Point);
|
||||
|
||||
let psbt = bitcoin::consensus::serialize(&psbt);
|
||||
let as_base64 = base64::encode(psbt);
|
||||
impl From<PublicKey> for Point {
|
||||
fn from(from: PublicKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
let psbt = self
|
||||
.inner
|
||||
.wallet_process_psbt(PsbtBase64(as_base64))
|
||||
.await?;
|
||||
let PsbtBase64(signed_psbt) = PsbtBase64::from(psbt);
|
||||
impl From<Scalar> for SecretKey {
|
||||
fn from(scalar: Scalar) -> Self {
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
let public = ecdsa.verification_key_for(&scalar);
|
||||
|
||||
let as_hex = base64::decode(signed_psbt)?;
|
||||
let psbt: PartiallySignedTransaction = bitcoin::consensus::deserialize(&as_hex)?;
|
||||
Self {
|
||||
inner: scalar,
|
||||
public,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tx = psbt.extract_tx();
|
||||
impl From<SecretKey> for Scalar {
|
||||
fn from(sk: SecretKey) -> Self {
|
||||
sk.inner
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tx)
|
||||
impl From<Scalar> for PublicKey {
|
||||
fn from(scalar: Scalar) -> Self {
|
||||
let ecdsa = ECDSA::<()>::default();
|
||||
PublicKey(ecdsa.verification_key_for(&scalar))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BroadcastSignedTransaction for Wallet {
|
||||
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid> {
|
||||
let txid = self.inner.send_raw_transaction(transaction).await?;
|
||||
tracing::info!("Bitcoin tx broadcasted! TXID = {}", txid);
|
||||
Ok(txid)
|
||||
pub fn verify_sig(
|
||||
verification_key: &PublicKey,
|
||||
transaction_sighash: &SigHash,
|
||||
sig: &Signature,
|
||||
) -> Result<()> {
|
||||
let ecdsa = ECDSA::verify_only();
|
||||
|
||||
if ecdsa.verify(&verification_key.0, &transaction_sighash.into_inner(), &sig) {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(InvalidSignature)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
|
||||
// to `ConstantBackoff`.
|
||||
#[async_trait]
|
||||
impl WatchForRawTransaction for Wallet {
|
||||
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction {
|
||||
(|| async { Ok(self.inner.get_raw_transaction(txid).await?) })
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried")
|
||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||
#[error("signature is invalid")]
|
||||
pub struct InvalidSignature;
|
||||
|
||||
pub fn verify_encsig(
|
||||
verification_key: PublicKey,
|
||||
encryption_key: PublicKey,
|
||||
digest: &SigHash,
|
||||
encsig: &EncryptedSignature,
|
||||
) -> Result<()> {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
|
||||
if adaptor.verify_encrypted_signature(
|
||||
&verification_key.0,
|
||||
&encryption_key.0,
|
||||
&digest.into_inner(),
|
||||
&encsig,
|
||||
) {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!(InvalidEncryptedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, thiserror::Error)]
|
||||
#[error("encrypted signature is invalid")]
|
||||
pub struct InvalidEncryptedSignature;
|
||||
|
||||
pub fn build_shared_output_descriptor(A: Point, B: Point) -> Descriptor<bitcoin::PublicKey> {
|
||||
const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))";
|
||||
|
||||
// NOTE: This shouldn't be a source of error, but maybe it is
|
||||
let A = ToHex::to_hex(&secp256k1::PublicKey::from(A));
|
||||
let B = ToHex::to_hex(&secp256k1::PublicKey::from(B));
|
||||
|
||||
let miniscript = MINISCRIPT_TEMPLATE.replace("A", &A).replace("B", &B);
|
||||
|
||||
let miniscript = miniscript::Miniscript::<bitcoin::PublicKey, Segwitv0>::from_str(&miniscript)
|
||||
.expect("a valid miniscript");
|
||||
|
||||
Descriptor::Wsh(miniscript)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetRawTransaction for Wallet {
|
||||
// todo: potentially replace with option
|
||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
||||
Ok(self.inner.get_raw_transaction(txid).await?)
|
||||
}
|
||||
pub trait BuildTxLockPsbt {
|
||||
async fn build_tx_lock_psbt(
|
||||
&self,
|
||||
output_address: Address,
|
||||
output_amount: Amount,
|
||||
) -> Result<PartiallySignedTransaction>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetBlockHeight for Wallet {
|
||||
async fn get_block_height(&self) -> BlockHeight {
|
||||
let height = (|| async { Ok(self.inner.client.getblockcount().await?) })
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried");
|
||||
|
||||
BlockHeight::new(height)
|
||||
}
|
||||
pub trait SignTxLock {
|
||||
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TransactionBlockHeight for Wallet {
|
||||
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight {
|
||||
#[derive(Debug)]
|
||||
enum Error {
|
||||
Io,
|
||||
NotYetMined,
|
||||
}
|
||||
pub trait BroadcastSignedTransaction {
|
||||
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid>;
|
||||
}
|
||||
|
||||
let height = (|| async {
|
||||
let block_height = self
|
||||
.inner
|
||||
.transaction_block_height(txid)
|
||||
.await
|
||||
.map_err(|_| backoff::Error::Transient(Error::Io))?;
|
||||
#[async_trait]
|
||||
pub trait WatchForRawTransaction {
|
||||
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction;
|
||||
}
|
||||
|
||||
let block_height =
|
||||
block_height.ok_or_else(|| backoff::Error::Transient(Error::NotYetMined))?;
|
||||
#[async_trait]
|
||||
pub trait WaitForTransactionFinality {
|
||||
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()>;
|
||||
}
|
||||
|
||||
Result::<_, backoff::Error<Error>>::Ok(block_height)
|
||||
})
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried");
|
||||
#[async_trait]
|
||||
pub trait GetBlockHeight {
|
||||
async fn get_block_height(&self) -> BlockHeight;
|
||||
}
|
||||
|
||||
BlockHeight::new(height)
|
||||
}
|
||||
#[async_trait]
|
||||
pub trait TransactionBlockHeight {
|
||||
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WaitForTransactionFinality for Wallet {
|
||||
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()> {
|
||||
// TODO(Franck): This assumes that bitcoind runs with txindex=1
|
||||
|
||||
// Divide by 4 to not check too often yet still be aware of the new block early
|
||||
// on.
|
||||
let mut interval = interval(config.bitcoin_avg_block_time / 4);
|
||||
|
||||
loop {
|
||||
let tx = self.inner.client.get_raw_transaction_verbose(txid).await?;
|
||||
if let Some(confirmations) = tx.confirmations {
|
||||
if confirmations >= config.bitcoin_finality_confirmations {
|
||||
break;
|
||||
}
|
||||
}
|
||||
interval.tick().await;
|
||||
}
|
||||
pub trait WaitForBlockHeight {
|
||||
async fn wait_for_block_height(&self, height: BlockHeight);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
#[async_trait]
|
||||
pub trait GetRawTransaction {
|
||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Network {
|
||||
fn get_network(&self) -> bitcoin::Network;
|
||||
}
|
||||
|
||||
pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result<SecretKey> {
|
||||
let adaptor = Adaptor::<Sha256, Deterministic<Sha256>>::default();
|
||||
|
||||
let s = adaptor
|
||||
.recover_decryption_key(&S.0, &sig, &encsig)
|
||||
.map(SecretKey::from)
|
||||
.ok_or_else(|| anyhow!("secret recovery failure"))?;
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub async fn poll_until_block_height_is_gte<B>(client: &B, target: BlockHeight)
|
||||
where
|
||||
B: GetBlockHeight,
|
||||
{
|
||||
while client.get_block_height().await < target {
|
||||
tokio::time::delay_for(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Network for Wallet {
|
||||
fn get_network(&self) -> bitcoin::Network {
|
||||
self.network
|
||||
pub async fn current_epoch<W>(
|
||||
bitcoin_wallet: &W,
|
||||
cancel_timelock: Timelock,
|
||||
punish_timelock: Timelock,
|
||||
lock_tx_id: ::bitcoin::Txid,
|
||||
) -> anyhow::Result<ExpiredTimelocks>
|
||||
where
|
||||
W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight,
|
||||
{
|
||||
let current_block_height = bitcoin_wallet.get_block_height().await;
|
||||
let lock_tx_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await;
|
||||
let cancel_timelock_height = lock_tx_height + cancel_timelock;
|
||||
let punish_timelock_height = cancel_timelock_height + punish_timelock;
|
||||
|
||||
match (
|
||||
current_block_height < cancel_timelock_height,
|
||||
current_block_height < punish_timelock_height,
|
||||
) {
|
||||
(true, _) => Ok(ExpiredTimelocks::None),
|
||||
(false, true) => Ok(ExpiredTimelocks::Cancel),
|
||||
(false, false) => Ok(ExpiredTimelocks::Punish),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait_for_cancel_timelock_to_expire<W>(
|
||||
bitcoin_wallet: &W,
|
||||
cancel_timelock: Timelock,
|
||||
lock_tx_id: ::bitcoin::Txid,
|
||||
) -> Result<()>
|
||||
where
|
||||
W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight,
|
||||
{
|
||||
let tx_lock_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await;
|
||||
|
||||
poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -0,0 +1,49 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Add;
|
||||
|
||||
/// Represent a timelock, expressed in relative block height as defined in
|
||||
/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki).
|
||||
/// E.g. The timelock expires 10 blocks after the reference transaction is
|
||||
/// mined.
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(transparent)]
|
||||
pub struct Timelock(u32);
|
||||
|
||||
impl Timelock {
|
||||
pub const fn new(number_of_blocks: u32) -> Self {
|
||||
Self(number_of_blocks)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Timelock> for u32 {
|
||||
fn from(timelock: Timelock) -> Self {
|
||||
timelock.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent a block height, or block number, expressed in absolute block
|
||||
/// count. E.g. The transaction was included in block #655123, 655123 block
|
||||
/// after the genesis block.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct BlockHeight(u32);
|
||||
|
||||
impl From<BlockHeight> for u32 {
|
||||
fn from(height: BlockHeight) -> Self {
|
||||
height.0
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockHeight {
|
||||
pub const fn new(block_height: u32) -> Self {
|
||||
Self(block_height)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Timelock> for BlockHeight {
|
||||
type Output = BlockHeight;
|
||||
|
||||
fn add(self, rhs: Timelock) -> Self::Output {
|
||||
BlockHeight(self.0 + rhs.0)
|
||||
}
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
use ::bitcoin::{util::psbt::PartiallySignedTransaction, Address, Amount, Transaction, Txid};
|
||||
use anyhow::{Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||
use bitcoin_harness::{bitcoind_rpc::PsbtBase64, BitcoindRpcApi};
|
||||
use reqwest::Url;
|
||||
use std::time::Duration;
|
||||
use tokio::time::interval;
|
||||
|
||||
use crate::{
|
||||
bitcoin::{
|
||||
timelocks::BlockHeight, BroadcastSignedTransaction, BuildTxLockPsbt, GetBlockHeight,
|
||||
GetRawTransaction, Network, SignTxLock, TransactionBlockHeight, TxLock,
|
||||
WaitForTransactionFinality, WatchForRawTransaction,
|
||||
},
|
||||
config::Config,
|
||||
};
|
||||
|
||||
pub const TX_LOCK_MINE_TIMEOUT: u64 = 3600;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet {
|
||||
pub inner: bitcoin_harness::Wallet,
|
||||
pub network: bitcoin::Network,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result<Self> {
|
||||
let wallet = bitcoin_harness::Wallet::new(name, url).await?;
|
||||
|
||||
Ok(Self {
|
||||
inner: wallet,
|
||||
network,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn balance(&self) -> Result<Amount> {
|
||||
let balance = self.inner.balance().await?;
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
pub async fn new_address(&self) -> Result<Address> {
|
||||
self.inner.new_address().await.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
|
||||
let fee = self
|
||||
.inner
|
||||
.get_wallet_transaction(txid)
|
||||
.await
|
||||
.map(|res| {
|
||||
res.fee.map(|signed_amount| {
|
||||
signed_amount
|
||||
.abs()
|
||||
.to_unsigned()
|
||||
.expect("Absolute value is always positive")
|
||||
})
|
||||
})?
|
||||
.context("Rpc response did not contain a fee")?;
|
||||
|
||||
Ok(fee)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BuildTxLockPsbt for Wallet {
|
||||
async fn build_tx_lock_psbt(
|
||||
&self,
|
||||
output_address: Address,
|
||||
output_amount: Amount,
|
||||
) -> Result<PartiallySignedTransaction> {
|
||||
let psbt = self.inner.fund_psbt(output_address, output_amount).await?;
|
||||
let as_hex = base64::decode(psbt)?;
|
||||
|
||||
let psbt = bitcoin::consensus::deserialize(&as_hex)?;
|
||||
|
||||
Ok(psbt)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SignTxLock for Wallet {
|
||||
async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result<Transaction> {
|
||||
let psbt = PartiallySignedTransaction::from(tx_lock);
|
||||
|
||||
let psbt = bitcoin::consensus::serialize(&psbt);
|
||||
let as_base64 = base64::encode(psbt);
|
||||
|
||||
let psbt = self
|
||||
.inner
|
||||
.wallet_process_psbt(PsbtBase64(as_base64))
|
||||
.await?;
|
||||
let PsbtBase64(signed_psbt) = PsbtBase64::from(psbt);
|
||||
|
||||
let as_hex = base64::decode(signed_psbt)?;
|
||||
let psbt: PartiallySignedTransaction = bitcoin::consensus::deserialize(&as_hex)?;
|
||||
|
||||
let tx = psbt.extract_tx();
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BroadcastSignedTransaction for Wallet {
|
||||
async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result<Txid> {
|
||||
let txid = self.inner.send_raw_transaction(transaction).await?;
|
||||
tracing::info!("Bitcoin tx broadcasted! TXID = {}", txid);
|
||||
Ok(txid)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
|
||||
// to `ConstantBackoff`.
|
||||
#[async_trait]
|
||||
impl WatchForRawTransaction for Wallet {
|
||||
async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction {
|
||||
(|| async { Ok(self.inner.get_raw_transaction(txid).await?) })
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetRawTransaction for Wallet {
|
||||
// todo: potentially replace with option
|
||||
async fn get_raw_transaction(&self, txid: Txid) -> Result<Transaction> {
|
||||
Ok(self.inner.get_raw_transaction(txid).await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl GetBlockHeight for Wallet {
|
||||
async fn get_block_height(&self) -> BlockHeight {
|
||||
let height = (|| async { Ok(self.inner.client.getblockcount().await?) })
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried");
|
||||
|
||||
BlockHeight::new(height)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl TransactionBlockHeight for Wallet {
|
||||
async fn transaction_block_height(&self, txid: Txid) -> BlockHeight {
|
||||
#[derive(Debug)]
|
||||
enum Error {
|
||||
Io,
|
||||
NotYetMined,
|
||||
}
|
||||
|
||||
let height = (|| async {
|
||||
let block_height = self
|
||||
.inner
|
||||
.transaction_block_height(txid)
|
||||
.await
|
||||
.map_err(|_| backoff::Error::Transient(Error::Io))?;
|
||||
|
||||
let block_height =
|
||||
block_height.ok_or_else(|| backoff::Error::Transient(Error::NotYetMined))?;
|
||||
|
||||
Result::<_, backoff::Error<Error>>::Ok(block_height)
|
||||
})
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await
|
||||
.expect("transient errors to be retried");
|
||||
|
||||
BlockHeight::new(height)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WaitForTransactionFinality for Wallet {
|
||||
async fn wait_for_transaction_finality(&self, txid: Txid, config: Config) -> Result<()> {
|
||||
// TODO(Franck): This assumes that bitcoind runs with txindex=1
|
||||
|
||||
// Divide by 4 to not check too often yet still be aware of the new block early
|
||||
// on.
|
||||
let mut interval = interval(config.bitcoin_avg_block_time / 4);
|
||||
|
||||
loop {
|
||||
let tx = self.inner.client.get_raw_transaction_verbose(txid).await?;
|
||||
if let Some(confirmations) = tx.confirmations {
|
||||
if confirmations >= config.bitcoin_finality_confirmations {
|
||||
break;
|
||||
}
|
||||
}
|
||||
interval.tick().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Network for Wallet {
|
||||
fn get_network(&self) -> bitcoin::Network {
|
||||
self.network
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use crate::bitcoin::Timelock;
|
||||
use crate::bitcoin::timelocks::Timelock;
|
||||
use conquer_once::Lazy;
|
||||
use std::time::Duration;
|
||||
|
@ -1,143 +1,281 @@
|
||||
pub mod wallet;
|
||||
|
||||
use ::bitcoin::hashes::core::fmt::Formatter;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||
use monero_harness::rpc::wallet;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use url::Url;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rust_decimal::{
|
||||
prelude::{FromPrimitive, ToPrimitive},
|
||||
Decimal,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
ops::{Add, Mul, Sub},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use crate::{bitcoin, serde::monero_private_key};
|
||||
|
||||
pub use curve25519_dalek::scalar::Scalar;
|
||||
pub use monero::*;
|
||||
pub use wallet::Wallet;
|
||||
|
||||
pub use xmr_btc::monero::*;
|
||||
pub const PICONERO_OFFSET: u64 = 1_000_000_000_000;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet {
|
||||
pub inner: wallet::Client,
|
||||
pub network: Network,
|
||||
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
|
||||
let scalar = Scalar::random(rng);
|
||||
|
||||
PrivateKey::from_scalar(scalar)
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
pub fn new(url: Url, network: Network) -> Self {
|
||||
Self {
|
||||
inner: wallet::Client::new(url),
|
||||
network,
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey);
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the balance of the primary account.
|
||||
pub async fn get_balance(&self) -> Result<Amount> {
|
||||
let amount = self.inner.get_balance(0).await?;
|
||||
impl Add for PrivateViewKey {
|
||||
type Output = Self;
|
||||
|
||||
Ok(Amount::from_piconero(amount))
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Transfer for Wallet {
|
||||
async fn transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
amount: Amount,
|
||||
) -> Result<(TransferProof, Amount)> {
|
||||
let destination_address =
|
||||
Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||
impl From<PrivateViewKey> for PrivateKey {
|
||||
fn from(from: PrivateViewKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicViewKey> for PublicKey {
|
||||
fn from(from: PublicViewKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
let res = self
|
||||
.inner
|
||||
.transfer(0, amount.as_piconero(), &destination_address.to_string())
|
||||
.await?;
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PublicViewKey(PublicKey);
|
||||
|
||||
let tx_hash = TxHash(res.tx_hash);
|
||||
tracing::info!("Monero tx broadcasted!, tx hash: {:?}", tx_hash);
|
||||
let tx_key = PrivateKey::from_str(&res.tx_key)?;
|
||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
|
||||
pub struct Amount(u64);
|
||||
|
||||
let fee = Amount::from_piconero(res.fee);
|
||||
impl Amount {
|
||||
pub const ZERO: Self = Self(0);
|
||||
/// 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.
|
||||
pub fn from_piconero(amount: u64) -> Self {
|
||||
Amount(amount)
|
||||
}
|
||||
|
||||
let transfer_proof = TransferProof::new(tx_hash, tx_key);
|
||||
tracing::debug!(" Transfer proof: {:?}", transfer_proof);
|
||||
pub fn as_piconero(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
Ok((transfer_proof, fee))
|
||||
pub fn parse_monero(amount: &str) -> Result<Self> {
|
||||
let decimal = Decimal::from_str(amount)?;
|
||||
let piconeros_dec =
|
||||
decimal.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
|
||||
let piconeros = piconeros_dec
|
||||
.to_u64()
|
||||
.ok_or_else(|| OverflowError(amount.to_owned()))?;
|
||||
Ok(Amount(piconeros))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CreateWalletForOutput for Wallet {
|
||||
async fn create_and_load_wallet_for_output(
|
||||
&self,
|
||||
private_spend_key: PrivateKey,
|
||||
private_view_key: PrivateViewKey,
|
||||
) -> Result<()> {
|
||||
let public_spend_key = PublicKey::from_private_key(&private_spend_key);
|
||||
let public_view_key = PublicKey::from_private_key(&private_view_key.into());
|
||||
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;
|
||||
|
||||
let address = Address::standard(self.network, public_spend_key, public_view_key);
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self
|
||||
.inner
|
||||
.generate_from_keys(
|
||||
&address.to_string(),
|
||||
&private_spend_key.to_string(),
|
||||
&PrivateKey::from(private_view_key).to_string(),
|
||||
)
|
||||
.await?;
|
||||
impl Mul<u64> for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
Ok(())
|
||||
fn mul(self, rhs: u64) -> Self::Output {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
|
||||
// to `ConstantBackoff`.
|
||||
impl From<Amount> for u64 {
|
||||
fn from(from: Amount) -> u64 {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Amount {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let mut decimal = Decimal::from(self.0);
|
||||
decimal
|
||||
.set_scale(12)
|
||||
.expect("12 is smaller than max precision of 28");
|
||||
write!(f, "{} XMR", decimal)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
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
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TxHash(pub String);
|
||||
|
||||
impl From<TxHash> for String {
|
||||
fn from(from: TxHash) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Transfer {
|
||||
async fn transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
amount: Amount,
|
||||
) -> anyhow::Result<(TransferProof, Amount)>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WatchForTransfer for Wallet {
|
||||
pub trait WatchForTransfer {
|
||||
async fn watch_for_transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
transfer_proof: TransferProof,
|
||||
expected_amount: Amount,
|
||||
amount: Amount,
|
||||
expected_confirmations: u32,
|
||||
) -> Result<(), InsufficientFunds> {
|
||||
enum Error {
|
||||
TxNotFound,
|
||||
InsufficientConfirmations,
|
||||
InsufficientFunds { expected: Amount, actual: Amount },
|
||||
}
|
||||
|
||||
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||
|
||||
let res = (|| async {
|
||||
// NOTE: Currently, this is conflating IO errors with the transaction not being
|
||||
// in the blockchain yet, or not having enough confirmations on it. All these
|
||||
// errors warrant a retry, but the strategy should probably differ per case
|
||||
let proof = self
|
||||
.inner
|
||||
.check_tx_key(
|
||||
&String::from(transfer_proof.tx_hash()),
|
||||
&transfer_proof.tx_key().to_string(),
|
||||
&address.to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| backoff::Error::Transient(Error::TxNotFound))?;
|
||||
|
||||
if proof.received != expected_amount.as_piconero() {
|
||||
return Err(backoff::Error::Permanent(Error::InsufficientFunds {
|
||||
expected: expected_amount,
|
||||
actual: Amount::from_piconero(proof.received),
|
||||
}));
|
||||
}
|
||||
|
||||
if proof.confirmations < expected_confirmations {
|
||||
return Err(backoff::Error::Transient(Error::InsufficientConfirmations));
|
||||
}
|
||||
|
||||
Ok(proof)
|
||||
})
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await;
|
||||
|
||||
if let Err(Error::InsufficientFunds { expected, actual }) = res {
|
||||
return Err(InsufficientFunds { expected, actual });
|
||||
};
|
||||
|
||||
Ok(())
|
||||
) -> Result<(), InsufficientFunds>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||
#[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")]
|
||||
pub struct InsufficientFunds {
|
||||
pub expected: Amount,
|
||||
pub actual: Amount,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait CreateWalletForOutput {
|
||||
async fn create_and_load_wallet_for_output(
|
||||
&self,
|
||||
private_spend_key: PrivateKey,
|
||||
private_view_key: PrivateViewKey,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
|
||||
#[error("Overflow, cannot convert {0} to u64")]
|
||||
pub struct OverflowError(pub String);
|
||||
|
||||
#[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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,147 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use backoff::{backoff::Constant as ConstantBackoff, future::FutureOperation as _};
|
||||
use monero::{Address, Network, PrivateKey, PublicKey};
|
||||
use monero_harness::rpc::wallet;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
use url::Url;
|
||||
|
||||
use crate::monero::{
|
||||
Amount, CreateWalletForOutput, InsufficientFunds, PrivateViewKey, PublicViewKey, Transfer,
|
||||
TransferProof, TxHash, WatchForTransfer,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Wallet {
|
||||
pub inner: wallet::Client,
|
||||
pub network: Network,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
pub fn new(url: Url, network: Network) -> Self {
|
||||
Self {
|
||||
inner: wallet::Client::new(url),
|
||||
network,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the balance of the primary account.
|
||||
pub async fn get_balance(&self) -> Result<Amount> {
|
||||
let amount = self.inner.get_balance(0).await?;
|
||||
|
||||
Ok(Amount::from_piconero(amount))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Transfer for Wallet {
|
||||
async fn transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
amount: Amount,
|
||||
) -> Result<(TransferProof, Amount)> {
|
||||
let destination_address =
|
||||
Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||
|
||||
let res = self
|
||||
.inner
|
||||
.transfer(0, amount.as_piconero(), &destination_address.to_string())
|
||||
.await?;
|
||||
|
||||
let tx_hash = TxHash(res.tx_hash);
|
||||
tracing::info!("Monero tx broadcasted!, tx hash: {:?}", tx_hash);
|
||||
let tx_key = PrivateKey::from_str(&res.tx_key)?;
|
||||
|
||||
let fee = Amount::from_piconero(res.fee);
|
||||
|
||||
let transfer_proof = TransferProof::new(tx_hash, tx_key);
|
||||
tracing::debug!(" Transfer proof: {:?}", transfer_proof);
|
||||
|
||||
Ok((transfer_proof, fee))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CreateWalletForOutput for Wallet {
|
||||
async fn create_and_load_wallet_for_output(
|
||||
&self,
|
||||
private_spend_key: PrivateKey,
|
||||
private_view_key: PrivateViewKey,
|
||||
) -> Result<()> {
|
||||
let public_spend_key = PublicKey::from_private_key(&private_spend_key);
|
||||
let public_view_key = PublicKey::from_private_key(&private_view_key.into());
|
||||
|
||||
let address = Address::standard(self.network, public_spend_key, public_view_key);
|
||||
|
||||
let _ = self
|
||||
.inner
|
||||
.generate_from_keys(
|
||||
&address.to_string(),
|
||||
&private_spend_key.to_string(),
|
||||
&PrivateKey::from(private_view_key).to_string(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed
|
||||
// to `ConstantBackoff`.
|
||||
|
||||
#[async_trait]
|
||||
impl WatchForTransfer for Wallet {
|
||||
async fn watch_for_transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
transfer_proof: TransferProof,
|
||||
expected_amount: Amount,
|
||||
expected_confirmations: u32,
|
||||
) -> Result<(), InsufficientFunds> {
|
||||
enum Error {
|
||||
TxNotFound,
|
||||
InsufficientConfirmations,
|
||||
InsufficientFunds { expected: Amount, actual: Amount },
|
||||
}
|
||||
|
||||
let address = Address::standard(self.network, public_spend_key, public_view_key.into());
|
||||
|
||||
let res = (|| async {
|
||||
// NOTE: Currently, this is conflating IO errors with the transaction not being
|
||||
// in the blockchain yet, or not having enough confirmations on it. All these
|
||||
// errors warrant a retry, but the strategy should probably differ per case
|
||||
let proof = self
|
||||
.inner
|
||||
.check_tx_key(
|
||||
&String::from(transfer_proof.tx_hash()),
|
||||
&transfer_proof.tx_key().to_string(),
|
||||
&address.to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| backoff::Error::Transient(Error::TxNotFound))?;
|
||||
|
||||
if proof.received != expected_amount.as_piconero() {
|
||||
return Err(backoff::Error::Permanent(Error::InsufficientFunds {
|
||||
expected: expected_amount,
|
||||
actual: Amount::from_piconero(proof.received),
|
||||
}));
|
||||
}
|
||||
|
||||
if proof.confirmations < expected_confirmations {
|
||||
return Err(backoff::Error::Transient(Error::InsufficientConfirmations));
|
||||
}
|
||||
|
||||
Ok(proof)
|
||||
})
|
||||
.retry(ConstantBackoff::new(Duration::from_secs(1)))
|
||||
.await;
|
||||
|
||||
if let Err(Error::InsufficientFunds { expected, actual }) = res {
|
||||
return Err(InsufficientFunds { expected, actual });
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
pub mod alice;
|
||||
pub mod bob;
|
@ -1,31 +0,0 @@
|
||||
[package]
|
||||
name = "xmr-btc"
|
||||
version = "0.1.0"
|
||||
authors = ["CoBloX Team <team@coblox.tech>"]
|
||||
edition = "2018"
|
||||
|
||||
# TODO: Check for stale dependencies, this looks like its a bit of a mess.
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
async-trait = "0.1"
|
||||
bitcoin = { version = "0.25", features = ["rand", "serde"] }
|
||||
conquer-once = "0.3"
|
||||
cross-curve-dleq = { git = "https://github.com/comit-network/cross-curve-dleq", rev = "eddcdea1d1f16fa33ef581d1744014ece535c920", features = ["serde"] }
|
||||
curve25519-dalek = "2"
|
||||
ecdsa_fun = { git = "https://github.com/LLFourn/secp256kfun", rev = "cdfbc766045ea678a41780919d6228dd5acee3be", features = ["libsecp_compat", "serde"] }
|
||||
ed25519-dalek = { version = "1.0.0-pre.4", features = ["serde"] }# Cannot be 1 because they depend on curve25519-dalek version 3
|
||||
futures = "0.3"
|
||||
genawaiter = "0.99.1"
|
||||
miniscript = { version = "4", features = ["serde"] }
|
||||
monero = { version = "0.9", features = ["serde_support"] }
|
||||
rand = "0.7"
|
||||
rust_decimal = "1.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
sha2 = "0.9"
|
||||
thiserror = "1"
|
||||
tokio = { version = "0.2", default-features = false, features = ["time"] }
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_cbor = "0.11"
|
@ -1,43 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::{bitcoin, monero};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
Message0(Message0),
|
||||
Message1(Message1),
|
||||
Message2(Message2),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message0 {
|
||||
pub(crate) A: bitcoin::PublicKey,
|
||||
pub(crate) S_a_monero: monero::PublicKey,
|
||||
pub(crate) S_a_bitcoin: bitcoin::PublicKey,
|
||||
pub(crate) dleq_proof_s_a: cross_curve_dleq::Proof,
|
||||
pub(crate) v_a: monero::PrivateViewKey,
|
||||
pub(crate) redeem_address: bitcoin::Address,
|
||||
pub(crate) punish_address: bitcoin::Address,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message1 {
|
||||
pub(crate) tx_cancel_sig: Signature,
|
||||
pub(crate) tx_refund_encsig: EncryptedSignature,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message2 {
|
||||
pub tx_lock_proof: monero::TransferProof,
|
||||
}
|
||||
|
||||
impl_try_from_parent_enum!(Message0, Message);
|
||||
impl_try_from_parent_enum!(Message1, Message);
|
||||
impl_try_from_parent_enum!(Message2, Message);
|
||||
|
||||
impl_from_child_enum!(Message0, Message);
|
||||
impl_from_child_enum!(Message1, Message);
|
||||
impl_from_child_enum!(Message2, Message);
|
@ -1,49 +0,0 @@
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::Result;
|
||||
use ecdsa_fun::{adaptor::EncryptedSignature, Signature};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
Message0(Message0),
|
||||
Message1(Message1),
|
||||
Message2(Message2),
|
||||
Message3(Message3),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message0 {
|
||||
pub(crate) B: bitcoin::PublicKey,
|
||||
pub(crate) S_b_monero: monero::PublicKey,
|
||||
pub(crate) S_b_bitcoin: bitcoin::PublicKey,
|
||||
pub(crate) dleq_proof_s_b: cross_curve_dleq::Proof,
|
||||
pub(crate) v_b: monero::PrivateViewKey,
|
||||
pub(crate) refund_address: bitcoin::Address,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message1 {
|
||||
pub(crate) tx_lock: bitcoin::TxLock,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message2 {
|
||||
pub(crate) tx_punish_sig: Signature,
|
||||
pub(crate) tx_cancel_sig: Signature,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message3 {
|
||||
pub tx_redeem_encsig: EncryptedSignature,
|
||||
}
|
||||
|
||||
impl_try_from_parent_enum!(Message0, Message);
|
||||
impl_try_from_parent_enum!(Message1, Message);
|
||||
impl_try_from_parent_enum!(Message2, Message);
|
||||
impl_try_from_parent_enum!(Message3, Message);
|
||||
|
||||
impl_from_child_enum!(Message0, Message);
|
||||
impl_from_child_enum!(Message1, Message);
|
||||
impl_from_child_enum!(Message2, Message);
|
||||
impl_from_child_enum!(Message3, Message);
|
@ -1,91 +0,0 @@
|
||||
#![warn(
|
||||
unused_extern_crates,
|
||||
missing_debug_implementations,
|
||||
missing_copy_implementations,
|
||||
rust_2018_idioms,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::fallible_impl_from,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::dbg_macro
|
||||
)]
|
||||
#![cfg_attr(not(test), warn(clippy::unwrap_used))]
|
||||
#![forbid(unsafe_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ExpiredTimelocks {
|
||||
None,
|
||||
Cancel,
|
||||
Punish,
|
||||
}
|
||||
|
||||
#[macro_use]
|
||||
mod utils {
|
||||
|
||||
macro_rules! impl_try_from_parent_enum {
|
||||
($type:ident, $parent:ident) => {
|
||||
impl TryFrom<$parent> for $type {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(from: $parent) -> Result<Self> {
|
||||
if let $parent::$type(inner) = from {
|
||||
Ok(inner)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Failed to convert parent state to child state"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_try_from_parent_enum_for_boxed {
|
||||
($type:ident, $parent:ident) => {
|
||||
impl TryFrom<$parent> for $type {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(from: $parent) -> Result<Self> {
|
||||
if let $parent::$type(inner) = from {
|
||||
Ok(*inner)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Failed to convert parent state to child state"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_from_child_enum {
|
||||
($type:ident, $parent:ident) => {
|
||||
impl From<$type> for $parent {
|
||||
fn from(from: $type) -> Self {
|
||||
$parent::$type(from)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_from_child_enum_for_boxed {
|
||||
($type:ident, $parent:ident) => {
|
||||
impl From<$type> for $parent {
|
||||
fn from(from: $type) -> Self {
|
||||
$parent::$type(Box::new(from))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub mod alice;
|
||||
pub mod bitcoin;
|
||||
pub mod bob;
|
||||
pub mod config;
|
||||
pub mod monero;
|
||||
pub mod serde;
|
||||
pub mod transport;
|
||||
|
||||
pub use cross_curve_dleq;
|
@ -1,274 +0,0 @@
|
||||
use crate::serde::monero_private_key;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::{Add, Mul, Sub};
|
||||
|
||||
use bitcoin::hashes::core::fmt::Formatter;
|
||||
pub use curve25519_dalek::scalar::Scalar;
|
||||
pub use monero::*;
|
||||
use rust_decimal::{
|
||||
prelude::{FromPrimitive, ToPrimitive},
|
||||
Decimal,
|
||||
};
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
pub const PICONERO_OFFSET: u64 = 1_000_000_000_000;
|
||||
|
||||
pub fn random_private_key<R: RngCore + CryptoRng>(rng: &mut R) -> PrivateKey {
|
||||
let scalar = Scalar::random(rng);
|
||||
|
||||
PrivateKey::from_scalar(scalar)
|
||||
}
|
||||
|
||||
pub fn private_key_from_secp256k1_scalar(scalar: crate::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))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PrivateViewKey(#[serde(with = "monero_private_key")] PrivateKey);
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for PrivateViewKey {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrivateViewKey> for PrivateKey {
|
||||
fn from(from: PrivateViewKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicViewKey> for PublicKey {
|
||||
fn from(from: PublicViewKey) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PublicViewKey(PublicKey);
|
||||
|
||||
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, PartialOrd)]
|
||||
pub struct Amount(u64);
|
||||
|
||||
impl Amount {
|
||||
pub const ZERO: Self = Self(0);
|
||||
/// 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.
|
||||
pub fn from_piconero(amount: u64) -> Self {
|
||||
Amount(amount)
|
||||
}
|
||||
|
||||
pub fn as_piconero(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn parse_monero(amount: &str) -> Result<Self> {
|
||||
let decimal = Decimal::from_str(amount)?;
|
||||
let piconeros_dec =
|
||||
decimal.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
|
||||
let piconeros = piconeros_dec
|
||||
.to_u64()
|
||||
.ok_or_else(|| OverflowError(amount.to_owned()))?;
|
||||
Ok(Amount(piconeros))
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<u64> for Amount {
|
||||
type Output = Amount;
|
||||
|
||||
fn mul(self, rhs: u64) -> Self::Output {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Amount> for u64 {
|
||||
fn from(from: Amount) -> u64 {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Amount {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let mut decimal = Decimal::from(self.0);
|
||||
decimal
|
||||
.set_scale(12)
|
||||
.expect("12 is smaller than max precision of 28");
|
||||
write!(f, "{} XMR", decimal)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
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
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TxHash(pub String);
|
||||
|
||||
impl From<TxHash> for String {
|
||||
fn from(from: TxHash) -> Self {
|
||||
from.0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Transfer {
|
||||
async fn transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
amount: Amount,
|
||||
) -> anyhow::Result<(TransferProof, Amount)>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait WatchForTransfer {
|
||||
async fn watch_for_transfer(
|
||||
&self,
|
||||
public_spend_key: PublicKey,
|
||||
public_view_key: PublicViewKey,
|
||||
transfer_proof: TransferProof,
|
||||
amount: Amount,
|
||||
expected_confirmations: u32,
|
||||
) -> Result<(), InsufficientFunds>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, thiserror::Error)]
|
||||
#[error("transaction does not pay enough: expected {expected:?}, got {actual:?}")]
|
||||
pub struct InsufficientFunds {
|
||||
pub expected: Amount,
|
||||
pub actual: Amount,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait CreateWalletForOutput {
|
||||
async fn create_and_load_wallet_for_output(
|
||||
&self,
|
||||
private_spend_key: PrivateKey,
|
||||
private_view_key: PrivateViewKey,
|
||||
) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
|
||||
#[error("Overflow, cannot convert {0} to u64")]
|
||||
pub struct OverflowError(pub String);
|
||||
|
||||
#[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())
|
||||
);
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait SendMessage<SendMsg> {
|
||||
async fn send_message(&mut self, message: SendMsg) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ReceiveMessage<RecvMsg> {
|
||||
async fn receive_message(&mut self) -> Result<RecvMsg>;
|
||||
}
|
Loading…
Reference in New Issue