use std::{collections::HashMap, env};
use anyswap_sdk::{
bitcoin,
client::HttpClient,
context::SwapContext,
driver::{ChainConfig, Destinations, Driver, MemoryStore, TargetAmount},
elements,
esplora::{BitcoinEsplora, LiquidEsplora},
invoice,
types::{Chain, NATIVE_ASSET, SwapNetwork},
wallet::Wallet,
};
use async_trait::async_trait;
struct Addresses(HashMap<Chain, String>);
#[async_trait]
impl Destinations for Addresses {
async fn claim_address(&self, chain: Chain) -> Result<String, String> {
self.0
.get(&chain)
.cloned()
.ok_or_else(|| format!("no address for {chain}"))
}
async fn refund_address(&self, chain: Chain) -> Result<String, String> {
self.claim_address(chain).await
}
}
const NETWORK: SwapNetwork = SwapNetwork::Regtest;
fn env_var(name: &str) -> anyhow::Result<String> {
env::var(name).map_err(|_| anyhow::anyhow!("{name} is not set"))
}
fn address_chain(arg: &str) -> Option<Chain> {
if arg
.parse::<bitcoin::Address<bitcoin::address::NetworkUnchecked>>()
.is_ok()
{
return Some(Chain::Bitcoin);
}
arg.parse::<elements::Address>().ok().map(|_| Chain::Liquid)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut addresses = HashMap::new();
let mut swaps = Vec::new();
for arg in env::args().skip(1) {
let (chain, spec) = match arg.strip_prefix("liquid:") {
Some(rest) => (Chain::Liquid, rest.to_owned()),
None => (Chain::Bitcoin, arg),
};
match address_chain(&spec) {
Some(chain) => {
addresses.insert(chain, spec);
}
None => swaps.push((chain, spec)),
}
}
if swaps.is_empty() {
anyhow::bail!("usage: swaps [addresses] [liquid:]<sats or bolt11>...");
}
for (chain, _) in &swaps {
if !addresses.contains_key(chain) {
anyhow::bail!("give a {chain} address for the {chain} swaps");
}
}
let (wallet, _mnemonic) = Wallet::generate(NETWORK)?;
let client = HttpClient::new(&env_var("ANYSWAP_SERVER_URL")?, Some(&wallet))?;
let mut ctx = SwapContext::new(wallet);
let mut liquid_esplora = None;
if swaps.iter().any(|(chain, _)| *chain == Chain::Liquid) {
let esplora = LiquidEsplora::new(&env_var("ANYSWAP_LIQUID_ESPLORA_URL")?);
let genesis_hash = esplora.get_block_hash(0).await?;
let info = client.get_info().await?;
let policy_asset = info
.policy
.chain
.get(&Chain::Liquid)
.and_then(|policy| policy.asset.get(NATIVE_ASSET))
.ok_or_else(|| anyhow::anyhow!("the server offers no Liquid asset"))?;
ctx = ctx.with_liquid(&genesis_hash.to_string(), &policy_asset.asset_id)?;
liquid_esplora = Some(esplora);
}
let mut driver = Driver::new(ctx, client.clone(), MemoryStore::default())
.with_chain(
Chain::Bitcoin,
BitcoinEsplora::new(&env_var("ANYSWAP_BITCOIN_ESPLORA_URL")?)?,
ChainConfig::bitcoin(),
)?
.with_destinations(Addresses(addresses))
.with_signals(client.signals());
if let Some(esplora) = liquid_esplora {
driver = driver.with_chain(Chain::Liquid, esplora, ChainConfig::liquid())?;
}
for (chain, spec) in swaps {
if let Ok(receive_amount) = spec.parse::<u64>() {
let quote = driver
.quote(None, Some(chain), TargetAmount::Receive(receive_amount))
.await?;
let swap = driver.swap_out(quote, None).await?;
println!("pay {}", swap.claim_invoice().await?);
} else {
let bolt11 = spec;
let receive_amount = invoice::amount_msat(&bolt11)? / 1000;
let quote = driver
.quote(Some(chain), None, TargetAmount::Receive(receive_amount))
.await?;
let swap = driver.swap_in(quote, bolt11, None).await?;
println!(
"deposit {} sats to {}",
swap.record().await?.send_amount,
swap.address().await?
);
}
}
let mut updates = driver.subscribe();
tokio::spawn(driver.run());
while let Some(swap) = updates.next().await {
if let Some(error) = swap.error() {
println!("{} error: {error}", swap.swap_id());
continue;
}
let record = swap.record().await?;
println!(
"{} {:?} (server: {})",
swap.swap_id(),
record.status,
record.server_status.as_deref().unwrap_or("unreachable")
);
}
Ok(())
}