Skip to content

Commit 3abb5df

Browse files
authored
Merge pull request #211 from arkade-os/feat/delegate
Suport 3rd party delegator (second attempt)
2 parents 8fc72a1 + 98ce20b commit 3abb5df

28 files changed

Lines changed: 1906 additions & 79 deletions

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ members = [
33
"ark-fees",
44
"ark-client",
55
"ark-core",
6+
"ark-delegator",
67
"ark-grpc",
78
"ark-rest",
89
"ark-bdk-wallet",

ark-client-sample/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ anyhow = "1"
1818
ark-bdk-wallet = { path = "../ark-bdk-wallet" }
1919
ark-client = { path = "../ark-client", features = ["default", "sqlite", "test-utils"] }
2020
ark-core = { path = "../ark-core" }
21+
ark-delegator = { path = "../ark-delegator" }
2122
ark-grpc = { path = "../ark-grpc", features = ["test-utils"] }
2223
async-trait = "0.1"
2324
bip39 = "2"

ark-client-sample/alice/ark.config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ ark_server_url = "http://localhost:7070"
22
esplora_url = "http://localhost:3000"
33
swap_storage_path = "./alice/swap_storage.sqlite"
44
boltz_url = "http://localhost:9001"
5+
# Optional: delegate-aware wallet addresses (required for watcher delegation tests)
6+
# delegator_pubkey = "03f458d498611caee1e9f07141f3fba3e64dfd0ee0111187e634740592f10415eb"
7+
# historical_delegator_pubkeys = ["03f458d498611caee1e9f07141f3fba3e64dfd0ee0111187e634740592f10415eb"]
58

69
# ark_server_url = "http://localhost:7070"
710
# esplora_url = "https://mutinynet.com/api"

ark-client-sample/bob/ark.config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ ark_server_url = "http://localhost:7070"
22
esplora_url = "http://localhost:3000"
33
swap_storage_path = "./bob/swap_storage.sqlite"
44
boltz_url = "http://localhost:9001"
5+
# Optional: delegate-aware wallet addresses
6+
# delegator_pubkey = "03f458d498611caee1e9f07141f3fba3e64dfd0ee0111187e634740592f10415eb"
7+
# historical_delegator_pubkeys = ["03f458d498611caee1e9f07141f3fba3e64dfd0ee0111187e634740592f10415eb"]

ark-client-sample/justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ settle-notes actor notes:
4848
subscribe actor address:
4949
cargo run -p ark-client-sample -- --config ./{{ actor }}/ark.config.toml --mnemonic ./{{ actor }}/ark.mnemonic subscribe {{ address }}
5050

51+
# Run delegated VTXO watcher in foreground (use another terminal to trigger activity)
52+
watch-delegated actor delegator_url:
53+
cargo run -p ark-client-sample -- --config ./{{ actor }}/ark.config.toml --mnemonic ./{{ actor }}/ark.mnemonic watch-delegated --delegator-url {{ delegator_url }}
54+
5155
# Send coins to an Onchain address from a given actor, e.g. just send-onchain bob bc1... 1234
5256
send-onchain actor address amount:
5357
cargo run -p ark-client-sample -- --config ./{{ actor }}/ark.config.toml --mnemonic ./{{ actor }}/ark.mnemonic send-onchain {{ address }} {{ amount }}

ark-client-sample/mutinynet/ark.config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ ark_server_url = "https://mutinynet.arkade.sh"
22
esplora_url = "https://mutinynet.com/api"
33
swap_storage_path = "./mutinynet/swap_storage.sqlite"
44
boltz_url = "https://api.boltz.mutinynet.arkade.sh"
5+
# Optional: delegate-aware wallet addresses
6+
# delegator_pubkey = "02c7f08d57d07f4924444db42848aac7d03a918d8d68157953639121d0ecbbbb8a"
7+
# historical_delegator_pubkeys = ["02c7f08d57d07f4924444db42848aac7d03a918d8d68157953639121d0ecbbbb8a"]

ark-client-sample/src/main.rs

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use ark_core::server::SubscriptionResponse;
3030
use ark_core::ArkAddress;
3131
use ark_core::ArkNote;
3232
use ark_core::ExplorerUtxo;
33+
use ark_delegator::DelegatorClient;
3334
use ark_grpc::test_utils as grpc_test_utils;
3435
use bitcoin::address::NetworkUnchecked;
3536
use bitcoin::bip32::Xpriv;
@@ -113,6 +114,12 @@ enum Commands {
113114
/// The Ark address to subscribe to.
114115
address: ArkAddressCli,
115116
},
117+
/// Run the delegated VTXO watcher in the foreground.
118+
WatchDelegated {
119+
/// Delegator API base URL (e.g. https://delegator.example.com).
120+
#[arg(long)]
121+
delegator_url: String,
122+
},
116123
/// Send on-chain to address
117124
SendOnchain {
118125
/// Where to send the funds to
@@ -316,6 +323,8 @@ struct Config {
316323
esplora_url: String,
317324
swap_storage_path: String,
318325
boltz_url: String,
326+
delegator_pubkey: Option<String>,
327+
historical_delegator_pubkeys: Option<Vec<String>>,
319328
}
320329

321330
#[derive(Serialize)]
@@ -359,6 +368,17 @@ fn format_timestamp(unix_secs: i64) -> Result<String> {
359368
Ok(ts.to_string())
360369
}
361370

371+
fn parse_delegator_pubkey(pk: &str) -> Result<bitcoin::XOnlyPublicKey> {
372+
if let Ok(xonly) = pk.parse::<bitcoin::XOnlyPublicKey>() {
373+
return Ok(xonly);
374+
}
375+
376+
let full = pk
377+
.parse::<bitcoin::PublicKey>()
378+
.map_err(|e| anyhow!("invalid delegator_pubkey '{pk}': {e}"))?;
379+
Ok(full.into())
380+
}
381+
362382
#[tokio::main]
363383
#[allow(clippy::unwrap_in_result)]
364384
async fn main() -> Result<()> {
@@ -409,6 +429,20 @@ async fn main() -> Result<()> {
409429
.map_err(|e| anyhow!(e))?,
410430
);
411431

432+
let delegator_pk = config
433+
.delegator_pubkey
434+
.as_deref()
435+
.map(parse_delegator_pubkey)
436+
.transpose()?;
437+
438+
let historical_delegator_pks = config
439+
.historical_delegator_pubkeys
440+
.clone()
441+
.unwrap_or_default()
442+
.into_iter()
443+
.map(|pk| parse_delegator_pubkey(pk.as_str()))
444+
.collect::<Result<Vec<_>>>()?;
445+
412446
match (cli.mnemonic, cli.seed) {
413447
(Some(_), Some(_)) => bail!("specify either --mnemonic or --seed, not both"),
414448
(None, None) => bail!("specify either --mnemonic or --seed"),
@@ -439,6 +473,8 @@ async fn main() -> Result<()> {
439473
storage,
440474
config.boltz_url,
441475
Duration::from_secs(30),
476+
delegator_pk,
477+
historical_delegator_pks.clone(),
442478
)
443479
.connect()
444480
.await
@@ -464,6 +500,8 @@ async fn main() -> Result<()> {
464500
storage,
465501
config.boltz_url,
466502
Duration::from_secs(30),
503+
delegator_pk,
504+
historical_delegator_pks.clone(),
467505
)
468506
.connect()
469507
.await
@@ -476,11 +514,12 @@ async fn main() -> Result<()> {
476514
Ok(())
477515
}
478516

479-
async fn run_command<K: KeyProvider>(
517+
async fn run_command<K: KeyProvider + 'static>(
480518
command: Commands,
481519
client: ark_client::Client<EsploraClient, Wallet<InMemoryDb>, SqliteSwapStorage, K>,
482520
esplora_client: Arc<EsploraClient>,
483521
) -> Result<()> {
522+
let client = Arc::new(client);
484523
client.discover_keys(20).await.map_err(|e| anyhow!(e))?;
485524

486525
match &command {
@@ -645,9 +684,8 @@ async fn run_command<K: KeyProvider>(
645684
while let Some(result) = subscription_stream.next().await {
646685
match result {
647686
Ok(SubscriptionResponse::Event(e)) => {
648-
if let Some(psbt) = e.tx {
649-
let tx = &psbt.unsigned_tx;
650-
let output = tx.output.to_vec().iter().find_map(|out| {
687+
if let Some(tx) = e.tx {
688+
let output = tx.output.iter().find_map(|out| {
651689
if out.script_pubkey == address.0.to_p2tr_script_pubkey() {
652690
Some(out.clone())
653691
} else {
@@ -683,6 +721,48 @@ async fn run_command<K: KeyProvider>(
683721

684722
println!("Subscription stream ended");
685723
}
724+
Commands::WatchDelegated { delegator_url } => {
725+
let delegator = Arc::new(DelegatorClient::new(delegator_url.clone()));
726+
let info = delegator.info().await.map_err(|e| anyhow!(e))?;
727+
let expected_delegator = parse_delegator_pubkey(&info.pubkey)?;
728+
let configured_delegator = client
729+
.delegator_pk()
730+
.ok_or_else(|| anyhow!("delegator_pubkey is not configured in ark.config.toml"))?;
731+
if configured_delegator != expected_delegator {
732+
bail!(
733+
"configured delegator_pubkey {} does not match {} returned by {}",
734+
configured_delegator,
735+
expected_delegator,
736+
delegator_url
737+
);
738+
}
739+
tracing::info!(
740+
pubkey = %info.pubkey,
741+
fee = %info.fee,
742+
delegator_address = %info.delegator_address,
743+
"Starting delegated VTXO watcher"
744+
);
745+
746+
if client
747+
.get_offchain_addresses()
748+
.map_err(|e| anyhow!(e))?
749+
.is_empty()
750+
{
751+
let (addr, _) = client.get_offchain_address().map_err(|e| anyhow!(e))?;
752+
tracing::info!(
753+
address = %addr,
754+
"Derived first offchain address so watcher has scripts to subscribe to"
755+
);
756+
}
757+
758+
let _watcher = client.start_vtxo_watcher(delegator);
759+
tracing::info!(
760+
"Watcher running. Keep this process open and run other commands in parallel."
761+
);
762+
763+
futures::future::pending::<()>().await;
764+
unreachable!("pending future never resolves");
765+
}
686766
Commands::SendOnchain { address, amount } => {
687767
let network = client.server_info.network;
688768
let checked_address = address.clone().require_network(network)?;

ark-client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ workspace = true
1212

1313
[dependencies]
1414
ark-core = { path = "../ark-core", version = "0.8.0" }
15+
ark-delegator = { path = "../ark-delegator", version = "0.8.0" }
1516
ark-fees = { path = "../ark-fees", version = "0.8.0" }
1617
async-stream = "0.3"
1718
async-trait = "0.1"

ark-client/src/batch.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1249,7 +1249,7 @@ where
12491249
let now = now.as_secs();
12501250
let expire_at = now + (2 * 60);
12511251

1252-
if let Some(packet) = create_asset_preservation_packet(&vtxo_inputs, &outputs)? {
1252+
if let Some(packet) = create_asset_preservation_packet(&inputs, &outputs)? {
12531253
outputs.push(intent::Output::AssetPacket(packet.to_txout()));
12541254
}
12551255

ark-client/src/error.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ impl IntoError for String {
198198
///
199199
/// This trick was borrowed from `jiff`, which borrowed it from `anyhow`.
200200
pub trait ErrorContext {
201+
type Output;
202+
201203
/// Contextualize the given consequent error with this (`self`) error as
202204
/// the cause.
203205
///
@@ -206,21 +208,20 @@ pub trait ErrorContext {
206208
/// Note that if an `Error` is given for `kind`, then this panics if it has
207209
/// a cause. (Because the cause would otherwise be dropped. An error causal
208210
/// chain is just a linked list, not a tree.)
209-
fn context(self, consequent: impl IntoError) -> Self;
211+
fn context(self, consequent: impl IntoError) -> Self::Output;
210212

211213
/// Like `context`, but hides error construction within a closure.
212214
///
213215
/// This is useful if the creation of the consequent error is not otherwise
214216
/// guarded and when error construction is potentially "costly" (i.e., it
215217
/// allocates). The closure avoids paying the cost of contextual error
216218
/// creation in the happy path.
217-
///
218-
/// Usually this only makes sense to use on a `Result<T, Error>`, otherwise
219-
/// the closure is just executed immediately anyway.
220-
fn with_context<E: IntoError>(self, consequent: impl FnOnce() -> E) -> Self;
219+
fn with_context<E: IntoError>(self, consequent: impl FnOnce() -> E) -> Self::Output;
221220
}
222221

223222
impl ErrorContext for Error {
223+
type Output = Error;
224+
224225
fn context(self, consequent: impl IntoError) -> Error {
225226
let mut err = consequent.into_error();
226227
assert!(
@@ -244,13 +245,30 @@ impl ErrorContext for Error {
244245
}
245246
}
246247

247-
impl<T> ErrorContext for Result<T, Error> {
248+
impl<T, E> ErrorContext for Result<T, E>
249+
where
250+
E: StdError + Send + Sync + 'static,
251+
{
252+
type Output = Result<T, Error>;
253+
248254
fn context(self, consequent: impl IntoError) -> Result<T, Error> {
249-
self.map_err(|err| err.context(consequent))
255+
self.map_err(|err| {
256+
let err: Box<dyn StdError + Send + Sync + 'static> = Box::new(err);
257+
match err.downcast::<Error>() {
258+
Ok(err) => (*err).context(consequent),
259+
Err(err) => Error::ad_hoc(err).context(consequent),
260+
}
261+
})
250262
}
251263

252-
fn with_context<E: IntoError>(self, consequent: impl FnOnce() -> E) -> Result<T, Error> {
253-
self.map_err(|err| err.with_context(consequent))
264+
fn with_context<C: IntoError>(self, consequent: impl FnOnce() -> C) -> Result<T, Error> {
265+
self.map_err(|err| {
266+
let err: Box<dyn StdError + Send + Sync + 'static> = Box::new(err);
267+
match err.downcast::<Error>() {
268+
Ok(err) => (*err).with_context(consequent),
269+
Err(err) => Error::ad_hoc(err).with_context(consequent),
270+
}
271+
})
254272
}
255273
}
256274

0 commit comments

Comments
 (0)