From bdb9826aba9fb780a0c1413575129541daffee47 Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 14 Jul 2026 21:56:21 +0100 Subject: [PATCH 1/4] feat(wallet): Ark protocol support via barkd sidecar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit barkd (Ark wallet daemon, pinned 0.3.0, checksum-verified release binary) packaged as an installable app; thin HTTP bridge in wallet/ark_client.rs mirrors the fedimint_client pattern — the bark SDK stays out of the node binary. wallet.ark-* RPCs cover status/balance/address/send/invoice/ board/offboard/history/configure; Ark movements merge into the unified ecash history (kind="ark") and spendable Ark sats into total_sats. Signet defaults (Second's public Ark server) until Ark matures. Co-Authored-By: Claude Fable 5 --- app-catalog/catalog.json | 19 + apps/barkd/Dockerfile | 32 ++ apps/barkd/entrypoint.sh | 15 + apps/barkd/manifest.yml | 76 +++ core/archipelago/src/api/rpc/ark.rs | 223 ++++++++ core/archipelago/src/api/rpc/dispatcher.rs | 11 + core/archipelago/src/api/rpc/mod.rs | 1 + core/archipelago/src/api/rpc/wallet.rs | 11 +- .../src/container/docker_packages.rs | 7 + core/archipelago/src/port_allocator.rs | 1 + core/archipelago/src/server.rs | 1 + core/archipelago/src/wallet/ark_client.rs | 479 ++++++++++++++++++ core/archipelago/src/wallet/mod.rs | 1 + scripts/image-versions.sh | 7 + 14 files changed, 881 insertions(+), 3 deletions(-) create mode 100644 apps/barkd/Dockerfile create mode 100644 apps/barkd/entrypoint.sh create mode 100644 apps/barkd/manifest.yml create mode 100644 core/archipelago/src/api/rpc/ark.rs create mode 100644 core/archipelago/src/wallet/ark_client.rs diff --git a/app-catalog/catalog.json b/app-catalog/catalog.json index a3c3aaa3..ca1b6a22 100644 --- a/app-catalog/catalog.json +++ b/app-catalog/catalog.json @@ -298,6 +298,25 @@ ] } }, + { + "id": "barkd", + "title": "Ark Wallet", + "version": "0.3.0", + "description": "Ark protocol wallet daemon (barkd). Lets the node hold self-custodial off-chain bitcoin via an Ark server; the wallet talks to it over a local REST API. Signet by default while Ark matures.", + "icon": "/assets/img/app-icons/bark.png", + "author": "Second", + "category": "money", + "dockerImage": "146.59.87.168:3000/lfg2025/barkd:0.3.0", + "repoUrl": "https://gitlab.com/ark-bitcoin/bark", + "containerConfig": { + "ports": [ + "3535:3535" + ], + "volumes": [ + "/var/lib/archipelago/barkd:/data" + ] + } + }, { "id": "jellyfin", "title": "Jellyfin", diff --git a/apps/barkd/Dockerfile b/apps/barkd/Dockerfile new file mode 100644 index 00000000..6421fbd9 --- /dev/null +++ b/apps/barkd/Dockerfile @@ -0,0 +1,32 @@ +# barkd — Ark protocol wallet daemon (https://gitlab.com/ark-bitcoin/bark). +# No official upstream image exists (their GitLab registry is empty), so we +# package the pinned, checksum-verified release binary ourselves and push to +# the node registry — same approach as fmcd. Keep the version in lockstep with +# the REST shapes coded in core/archipelago/src/wallet/ark_client.rs (0.3.0). +FROM debian:bookworm-slim + +ARG BARKD_VERSION=0.3.0 +ARG BARKD_SHA256=8562fa27386bae666ed62fa95c92d40f7bdb20d22525f75799adfc16adaaedb3 + +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates curl && \ + curl -fsSL "https://gitlab.com/api/v4/projects/ark-bitcoin%2Fbark/packages/generic/release-assets/bark-${BARKD_VERSION}/barkd-${BARKD_VERSION}-linux-x86_64" \ + -o /usr/local/bin/barkd && \ + echo "${BARKD_SHA256} /usr/local/bin/barkd" | sha256sum -c - && \ + chmod a+x /usr/local/bin/barkd && \ + apt-get purge -y curl && apt-get autoremove -y && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +COPY entrypoint.sh /entrypoint.sh +RUN chmod a+x /entrypoint.sh + +# The wallet itself is created over REST by the node's Ark bridge +# (wallet.ark-* RPCs) — the container just runs the daemon. +ENV BARKD_DATADIR=/data \ + BARKD_BIND_HOST=0.0.0.0 \ + BARKD_BIND_PORT=3535 + +EXPOSE 3535 +VOLUME /data + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/apps/barkd/entrypoint.sh b/apps/barkd/entrypoint.sh new file mode 100644 index 00000000..1f8481cd --- /dev/null +++ b/apps/barkd/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Install the node-provided auth secret (64-char hex from the manifest's +# generated barkd-secret) so the wallet bridge can derive the matching Bearer +# token, then start the daemon. Without BARKD_SECRET, barkd generates its own +# random token in the datadir and the bridge won't authenticate — so treat a +# failed refresh as fatal rather than starting an unreachable daemon. +set -eu + +if [ -n "${BARKD_SECRET:-}" ]; then + # `secret refresh` prints the Bearer token on stdout — never log it. + barkd secret refresh --secret "$BARKD_SECRET" >/dev/null + unset BARKD_SECRET +fi + +exec barkd diff --git a/apps/barkd/manifest.yml b/apps/barkd/manifest.yml new file mode 100644 index 00000000..5ba3d40d --- /dev/null +++ b/apps/barkd/manifest.yml @@ -0,0 +1,76 @@ +app: + id: barkd + name: Ark Wallet + version: 0.3.0 + description: Ark protocol wallet daemon (barkd). Lets the node hold self-custodial off-chain bitcoin via an Ark server; the wallet talks to it over a local REST API. Signet by default while Ark matures. + + container: + # barkd packaged from the pinned upstream release binary (no usable + # upstream image exists — their registry is empty). Built from + # apps/barkd/Dockerfile and pushed to the node registry. Pin the tag to + # match the REST shapes coded in core/archipelago/src/wallet/ark_client.rs + # (validated against barkd 0.3.0 on signet, 2026-07-14). + image: 146.59.87.168:3000/lfg2025/barkd:0.3.0 + pull_policy: if-not-present + network: archy-net + # The entrypoint installs the shared secret below via `barkd secret + # refresh` (so the wallet bridge can derive the matching Bearer token) and + # execs the daemon. The Ark wallet itself is created over REST by the + # bridge on first use (wallet.ark-* RPCs) with the node's ark_config + # (default: Second's public signet server) — no host provisioning needed. + generated_secrets: + - name: barkd-secret + kind: hex32 + secret_env: + - key: BARKD_SECRET + secret_file: barkd-secret + data_uid: "1000:1000" + + dependencies: + - storage: 1Gi + + resources: + # barkd is a single wallet daemon (SQLite + a gRPC conn to the Ark server + # + esplora polling); steady state is tiny. Cap it so a stuck sync can't + # starve the node. + cpu_limit: 1 + memory_limit: 512Mi + disk_limit: 1Gi + + security: + readonly_root: true + # Needs outbound HTTPS to the Ark server (ark.signet.2nd.dev) and the + # esplora chain source, plus the published REST port for the wallet + # bridge. No inbound requirements beyond that. + network_policy: bridge + + ports: + # barkd REST bound to 3535 in-container (BARKD_BIND_PORT); 3535 is free on + # the host (see port_allocator.rs). The Rust bridge targets + # http://127.0.0.1:3535. + - host: 3535 + container: 3535 + protocol: tcp + + volumes: + # Holds the wallet DB, mnemonic and auth token. ARK funds are recoverable + # on-chain from this datadir (unilateral exit) — include it in backups. + - type: bind + source: /var/lib/archipelago/barkd + target: /data + options: [rw] + + environment: + - BARKD_DATADIR=/data + - BARKD_BIND_HOST=0.0.0.0 + - BARKD_BIND_PORT=3535 + + # All /api/v1/* routes require the Bearer token, so an HTTP probe would 401 + # forever — use a TCP probe like fmcd (the host-side lifecycle layer + # verifies reachability). + health_check: + type: tcp + endpoint: localhost:3535 + interval: 30s + timeout: 5s + retries: 3 diff --git a/core/archipelago/src/api/rpc/ark.rs b/core/archipelago/src/api/rpc/ark.rs new file mode 100644 index 00000000..27209f3f --- /dev/null +++ b/core/archipelago/src/api/rpc/ark.rs @@ -0,0 +1,223 @@ +//! Ark protocol RPCs — bridge to the `barkd` sidecar. +//! +//! Companion to the Cashu RPCs in [`super::wallet`] and the Fedimint RPCs in +//! [`super::fedimint`]. Holding VTXOs, joining rounds and unilateral exits are +//! delegated to the barkd container via [`crate::wallet::ark_client::ArkClient`]; +//! here we expose the node's JSON-RPC surface. barkd keeps its own movement +//! history, so unlike Fedimint there is no local transaction log. + +use super::RpcHandler; +use crate::wallet::ark_client::{self, ArkClient}; +use anyhow::Result; + +impl RpcHandler { + /// `wallet.ark-status` — sidecar reachability, wallet fingerprint, network + /// and Ark server parameters. Soft-fails into `available: false` so the + /// settings UI can render an install/enable hint instead of an error. + pub(super) async fn handle_wallet_ark_status(&self) -> Result { + let config = ark_client::load_config(&self.config.data_dir).await; + let client = match ArkClient::from_node(&self.config.data_dir).await { + Ok(c) => c, + Err(_) => { + return Ok(serde_json::json!({ + "available": false, + "wallet_ready": false, + "config": config, + })) + } + }; + // Make sure the wallet exists before reporting (idempotent, cheap once + // created). + let _ = ark_client::ensure_wallet(&self.config.data_dir).await; + + let wallet = client.wallet_info().await.ok(); + let info = client.ark_info().await.ok(); + Ok(serde_json::json!({ + "available": true, + "wallet_ready": wallet.is_some(), + "wallet": wallet, + "ark_info": info, + "config": config, + })) + } + + /// `wallet.ark-balance` — off-chain (spendable + pending) and on-chain + /// sats. Soft-fails to zeros so unified balances still render. + pub(super) async fn handle_wallet_ark_balance(&self) -> Result { + let client = match ArkClient::from_node(&self.config.data_dir).await { + Ok(c) => c, + Err(_) => { + return Ok(serde_json::json!({ + "balance_sats": 0, + "spendable_sats": 0, + "pending_sats": 0, + "onchain_sats": 0, + })) + } + }; + let bal = client.balance().await.unwrap_or_else(|_| serde_json::json!({})); + let sat = |key: &str| bal.get(key).and_then(|v| v.as_u64()).unwrap_or(0); + let spendable = sat("spendable_sat"); + let pending = sat("pending_in_round_sat") + + sat("pending_board_sat") + + sat("pending_lightning_send_sat") + + sat("claimable_lightning_receive_sat") + + bal.get("pending_exit_sat").and_then(|v| v.as_u64()).unwrap_or(0); + let onchain = client + .onchain_balance() + .await + .ok() + .and_then(|b| { + b.get("total_sat") + .or_else(|| b.get("confirmed_sat")) + .and_then(|v| v.as_u64()) + }) + .unwrap_or(0); + Ok(serde_json::json!({ + "balance_sats": spendable, + "spendable_sats": spendable, + "pending_sats": pending, + "onchain_sats": onchain, + })) + } + + /// `wallet.ark-address` — fresh Ark (`tark1…`) receive address; pass + /// `{"onchain": true}` for an on-chain boarding address instead. + pub(super) async fn handle_wallet_ark_address( + &self, + params: Option, + ) -> Result { + let _ = ark_client::ensure_wallet(&self.config.data_dir).await; + let client = ArkClient::from_node(&self.config.data_dir).await?; + let onchain = params + .as_ref() + .and_then(|p| p.get("onchain")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let address = if onchain { + client.onchain_address().await? + } else { + client.ark_address().await? + }; + Ok(serde_json::json!({ "address": address, "onchain": onchain })) + } + + /// `wallet.ark-send` — pay an Ark address, BOLT11 invoice, LNURL or + /// lightning address from Ark funds. + pub(super) async fn handle_wallet_ark_send( + &self, + params: Option, + ) -> Result { + let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; + let destination = params + .get("destination") + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|s| !s.is_empty()) + .ok_or_else(|| anyhow::anyhow!("Missing destination"))?; + // Optional for BOLT11 invoices that carry their own amount. + let amount_sats = params.get("amount_sats").and_then(|v| v.as_u64()); + if amount_sats == Some(0) { + return Err(anyhow::anyhow!("Amount must be greater than zero")); + } + let comment = params.get("comment").and_then(|v| v.as_str()); + + let client = ArkClient::from_node(&self.config.data_dir).await?; + let movement = client.send(destination, amount_sats, comment).await?; + Ok(serde_json::json!({ + "sent": true, + "movement": movement, + })) + } + + /// `wallet.ark-invoice` — BOLT11 invoice that lands as Ark funds when paid. + pub(super) async fn handle_wallet_ark_invoice( + &self, + params: Option, + ) -> Result { + let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; + let amount_sats = params + .get("amount_sats") + .and_then(|v| v.as_u64()) + .filter(|&v| v > 0) + .ok_or_else(|| anyhow::anyhow!("Missing amount_sats"))?; + + let _ = ark_client::ensure_wallet(&self.config.data_dir).await; + let client = ArkClient::from_node(&self.config.data_dir).await?; + let res = client.lightning_invoice(amount_sats).await?; + Ok(res) + } + + /// `wallet.ark-board` — lift on-chain funds into Ark VTXOs. Omitting + /// `amount_sats` boards everything. + pub(super) async fn handle_wallet_ark_board( + &self, + params: Option, + ) -> Result { + let amount_sats = params + .as_ref() + .and_then(|p| p.get("amount_sats")) + .and_then(|v| v.as_u64()); + if amount_sats == Some(0) { + return Err(anyhow::anyhow!("Amount must be greater than zero")); + } + let client = ArkClient::from_node(&self.config.data_dir).await?; + let res = client.board(amount_sats).await?; + Ok(res) + } + + /// `wallet.ark-offboard` — collaboratively move all VTXOs back on-chain, + /// optionally to a provided address (defaults to the wallet's own). + pub(super) async fn handle_wallet_ark_offboard( + &self, + params: Option, + ) -> Result { + let address = params + .as_ref() + .and_then(|p| p.get("address")) + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|s| !s.is_empty()); + let client = ArkClient::from_node(&self.config.data_dir).await?; + let res = client.offboard_all(address).await?; + Ok(res) + } + + /// `wallet.ark-history` — barkd movements mapped to the unified + /// transaction shape (kind = "ark"), newest first. + pub(super) async fn handle_wallet_ark_history(&self) -> Result { + let mut transactions = ark_client::load_ark_txs(&self.config.data_dir).await; + transactions.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); + Ok(serde_json::json!({ "transactions": transactions })) + } + + /// `wallet.ark-configure` — set the Ark server / esplora / network used + /// when the barkd wallet is (re)created. Does NOT migrate an existing + /// wallet: barkd binds a wallet to its Ark server at creation. + pub(super) async fn handle_wallet_ark_configure( + &self, + params: Option, + ) -> Result { + let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; + let mut config = ark_client::load_config(&self.config.data_dir).await; + for (key, field) in [ + ("network", &mut config.network as &mut String), + ("ark_server", &mut config.ark_server), + ("esplora", &mut config.esplora), + ] { + if let Some(v) = params.get(key).and_then(|v| v.as_str()) { + let v = v.trim(); + if !v.is_empty() { + *field = v.to_string(); + } + } + } + if !matches!(config.network.as_str(), "signet" | "mainnet" | "regtest") { + return Err(anyhow::anyhow!( + "network must be one of: signet, mainnet, regtest" + )); + } + ark_client::save_config(&self.config.data_dir, &config).await?; + Ok(serde_json::json!({ "config": config })) + } +} diff --git a/core/archipelago/src/api/rpc/dispatcher.rs b/core/archipelago/src/api/rpc/dispatcher.rs index cb95a64d..ddd783e3 100644 --- a/core/archipelago/src/api/rpc/dispatcher.rs +++ b/core/archipelago/src/api/rpc/dispatcher.rs @@ -258,6 +258,17 @@ impl RpcHandler { "wallet.fedimint-leave" => self.handle_wallet_fedimint_leave(params).await, "wallet.fedimint-balance" => self.handle_wallet_fedimint_balance().await, + // Ark protocol (via barkd sidecar) + "wallet.ark-status" => self.handle_wallet_ark_status().await, + "wallet.ark-balance" => self.handle_wallet_ark_balance().await, + "wallet.ark-address" => self.handle_wallet_ark_address(params).await, + "wallet.ark-send" => self.handle_wallet_ark_send(params).await, + "wallet.ark-invoice" => self.handle_wallet_ark_invoice(params).await, + "wallet.ark-board" => self.handle_wallet_ark_board(params).await, + "wallet.ark-offboard" => self.handle_wallet_ark_offboard(params).await, + "wallet.ark-history" => self.handle_wallet_ark_history().await, + "wallet.ark-configure" => self.handle_wallet_ark_configure(params).await, + // Container registries "registry.list" => self.handle_registry_list().await, "registry.add" => self.handle_registry_add(params).await, diff --git a/core/archipelago/src/api/rpc/mod.rs b/core/archipelago/src/api/rpc/mod.rs index d0328b82..32730469 100644 --- a/core/archipelago/src/api/rpc/mod.rs +++ b/core/archipelago/src/api/rpc/mod.rs @@ -1,4 +1,5 @@ mod analytics; +mod ark; mod auth; mod backup_rpc; mod bitcoin; diff --git a/core/archipelago/src/api/rpc/wallet.rs b/core/archipelago/src/api/rpc/wallet.rs index d1d5217a..67e9c379 100644 --- a/core/archipelago/src/api/rpc/wallet.rs +++ b/core/archipelago/src/api/rpc/wallet.rs @@ -1,5 +1,5 @@ use super::RpcHandler; -use crate::wallet::{ecash, fedimint_client, profits}; +use crate::wallet::{ark_client, ecash, fedimint_client, profits}; use anyhow::Result; /// A Cashu token (NUT-00 `cashuA`/`cashuB`, or our legacy `cashuSend_` form) @@ -21,13 +21,16 @@ impl RpcHandler { Ok(client) => client.total_balance_sats().await.unwrap_or(0), Err(_) => 0, }; + // Spendable Ark (barkd) balance, same best-effort contract. + let ark_sats = ark_client::spendable_sats_or_zero(&self.config.data_dir).await; Ok(serde_json::json!({ // `balance_sats` stays Cashu-only for back-compat; `total_sats` is the - // spendable amount across Cashu + Fedimint. + // spendable amount across Cashu + Fedimint + Ark. "balance_sats": cashu_sats, "cashu_sats": cashu_sats, "fedimint_sats": fedimint_sats, - "total_sats": cashu_sats + fedimint_sats, + "ark_sats": ark_sats, + "total_sats": cashu_sats + fedimint_sats + ark_sats, "proof_count": wallet.proofs.iter().filter(|p| !p.spent && !p.reserved).count(), "mint_url": wallet.mint_url, })) @@ -181,6 +184,8 @@ impl RpcHandler { let wallet = ecash::load_wallet(&self.config.data_dir).await?; let mut transactions = wallet.transactions; transactions.extend(fedimint_client::load_fedimint_txs(&self.config.data_dir).await); + // Ark movements from barkd (kind="ark"), best-effort like Fedimint. + transactions.extend(ark_client::load_ark_txs(&self.config.data_dir).await); // Sort by RFC-3339 timestamp descending (string compare is valid for // same-offset RFC-3339), newest first. transactions.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); diff --git a/core/archipelago/src/container/docker_packages.rs b/core/archipelago/src/container/docker_packages.rs index f99ceb32..a3fe1f0a 100644 --- a/core/archipelago/src/container/docker_packages.rs +++ b/core/archipelago/src/container/docker_packages.rs @@ -372,6 +372,13 @@ fn get_app_metadata(app_id: &str) -> AppMetadata { repo: "https://github.com/minmoto/fmcd".to_string(), tier: "", }, + "barkd" | "bark" => AppMetadata { + title: "Ark Wallet".to_string(), + description: "Ark protocol wallet daemon (barkd) — self-custodial off-chain bitcoin via an Ark server (signet)".to_string(), + icon: "/assets/img/app-icons/bark.png".to_string(), + repo: "https://gitlab.com/ark-bitcoin/bark".to_string(), + tier: "", + }, "morphos" | "morphos-server" => AppMetadata { title: "Morphos".to_string(), description: "Self-hosted file converter".to_string(), diff --git a/core/archipelago/src/port_allocator.rs b/core/archipelago/src/port_allocator.rs index 5075ff3e..dd667f76 100644 --- a/core/archipelago/src/port_allocator.rs +++ b/core/archipelago/src/port_allocator.rs @@ -19,6 +19,7 @@ const RESERVED_PORTS: &[u16] = &[ 23000, // BTCPay 8173, 8174, 8175, // Fedimint 8178, // Fedimint client daemon (fedimint-clientd REST) + 3535, // Ark wallet daemon (barkd REST) 8123, // Home Assistant 3000, // Grafana 11434, // Ollama diff --git a/core/archipelago/src/server.rs b/core/archipelago/src/server.rs index 58fc5eda..8f00a74e 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -1590,6 +1590,7 @@ fn fallback_package_port(app_id: &str) -> Option { match app_id { "fedimint" | "fedimintd" => Some(8175), "fedimint-clientd" => Some(8178), + "barkd" => Some(3535), "filebrowser" => Some(8083), "indeedhub" => Some(7778), "nginx-proxy-manager" => Some(8081), diff --git a/core/archipelago/src/wallet/ark_client.rs b/core/archipelago/src/wallet/ark_client.rs new file mode 100644 index 00000000..9aa4ae4a --- /dev/null +++ b/core/archipelago/src/wallet/ark_client.rs @@ -0,0 +1,479 @@ +//! Thin HTTP bridge to the `barkd` sidecar container (Ark protocol). +//! +//! Same shape as [`super::fedimint_client`]: the heavy `bark-wallet` SDK stays +//! OUT of this binary. The `barkd` daemon (in `apps/barkd`) holds the Ark +//! wallet (VTXOs, rounds, unilateral exits) and we speak its REST API +//! (`/api/v1/*`, Bearer auth). Endpoint/JSON shapes target barkd 0.3.0 and +//! must be pinned to the vendored image tag. +//! +//! ARK is on-chain-anchored: VTXOs expire (`vtxo_expiry_delta` blocks) and the +//! barkd daemon refreshes them by joining rounds on its own — the bridge never +//! has to schedule anything. Unlike Cashu/Fedimint, funds survive the sidecar +//! dying (the wallet mnemonic in barkd's datadir can unilaterally exit +//! on-chain), so back up `/var/lib/archipelago/barkd`. + +use anyhow::{Context, Result}; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use tokio::fs; + +const BARKD_TIMEOUT_SECS: u64 = 15; +/// Send/board/offboard can wait on Ark round participation (signet rounds run +/// every 5 minutes), so give mutating calls generous room. +const BARKD_HEAVY_TIMEOUT_SECS: u64 = 120; + +/// Default host port the `barkd` container is mapped to (its in-container +/// REST port; 3535 is unused elsewhere on the node — see `port_allocator`). +const DEFAULT_BARKD_URL: &str = "http://127.0.0.1:3535"; + +/// Shared secret between the barkd container and this bridge. The barkd +/// manifest generates it via `generated_secrets: [{barkd-secret, hex32}]`; the +/// container entrypoint installs it with `barkd secret refresh --secret` and +/// the bridge derives the matching Bearer token from the same file. +const BARKD_SECRET: &str = "barkd-secret"; + +/// Wallet configuration used when the bridge has to create the barkd wallet +/// (first use). Persisted so operators can point at their own Ark server. +/// Defaults target Second's public signet deployment while Ark matures — +/// mainnet needs an explicit opt-in edit of `wallet/ark_config.json`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ArkConfig { + pub network: String, + pub ark_server: String, + pub esplora: String, +} + +impl Default for ArkConfig { + fn default() -> Self { + Self { + network: "signet".to_string(), + ark_server: "https://ark.signet.2nd.dev".to_string(), + esplora: "https://esplora.signet.2nd.dev".to_string(), + } + } +} + +const ARK_CONFIG_FILE: &str = "wallet/ark_config.json"; + +pub async fn load_config(data_dir: &Path) -> ArkConfig { + match fs::read_to_string(data_dir.join(ARK_CONFIG_FILE)).await { + Ok(s) => serde_json::from_str(&s).unwrap_or_default(), + Err(_) => ArkConfig::default(), + } +} + +pub async fn save_config(data_dir: &Path, config: &ArkConfig) -> Result<()> { + let dir = data_dir.join("wallet"); + fs::create_dir_all(&dir) + .await + .context("Failed to create wallet dir")?; + let content = serde_json::to_string_pretty(config).context("Failed to serialize ark config")?; + fs::write(data_dir.join(ARK_CONFIG_FILE), content) + .await + .context("Failed to write ark config")?; + Ok(()) +} + +/// Encode barkd's Bearer token from the raw 32-byte shared secret: +/// base64url-nopad of `<32-byte secret>` (see barkd `AuthToken`). +fn encode_auth_token(secret: &[u8; 32]) -> String { + let mut buf = Vec::with_capacity(33); + buf.push(0u8); + buf.extend_from_slice(secret); + URL_SAFE_NO_PAD.encode(&buf) +} + +fn secret_hex_to_token(hex: &str) -> Result { + let hex = hex.trim(); + if hex.len() != 64 || !hex.chars().all(|c| c.is_ascii_hexdigit()) { + anyhow::bail!("barkd-secret must be exactly 64 hex characters"); + } + let mut secret = [0u8; 32]; + for (i, byte) in secret.iter_mut().enumerate() { + *byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).expect("validated hex"); + } + Ok(encode_auth_token(&secret)) +} + +/// HTTP client for a `barkd` instance. +pub struct ArkClient { + base_url: String, + token: String, + client: reqwest::Client, +} + +impl ArkClient { + pub fn new(base_url: &str, token: &str) -> Result { + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(BARKD_HEAVY_TIMEOUT_SECS)) + .build() + .context("Failed to build HTTP client for barkd")?; + Ok(Self { + base_url: base_url.trim_end_matches('/').to_string(), + token: token.to_string(), + client, + }) + } + + /// Resolve URL + auth token from env / node secret, with sane defaults. + /// URL: `BARKD_URL` else the default mapped port. Token: `BARKD_TOKEN` + /// (already-encoded Bearer token) else derived from the shared + /// `barkd-secret` the manifest generated for the container. + pub async fn from_node(data_dir: &Path) -> Result { + let base_url = std::env::var("BARKD_URL").unwrap_or_else(|_| DEFAULT_BARKD_URL.to_string()); + let token = match std::env::var("BARKD_TOKEN") { + Ok(t) if !t.is_empty() => t, + _ => { + let path = data_dir.join("secrets").join(BARKD_SECRET); + let hex = fs::read_to_string(&path).await.context( + "Ark wallet not configured (no BARKD_TOKEN and no barkd-secret \ + secret). Install the Ark (barkd) app.", + )?; + secret_hex_to_token(&hex)? + } + }; + Self::new(&base_url, &token) + } + + fn auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder { + req.bearer_auth(&self.token) + } + + async fn get(&self, path: &str) -> Result { + let url = format!("{}{}", self.base_url, path); + let resp = self + .auth(self.client.get(&url)) + .timeout(std::time::Duration::from_secs(BARKD_TIMEOUT_SECS)) + .send() + .await + .with_context(|| format!("barkd GET {path} failed (is it running?)"))?; + Self::parse(resp, path).await + } + + async fn post(&self, path: &str, body: serde_json::Value) -> Result { + let url = format!("{}{}", self.base_url, path); + let resp = self + .auth(self.client.post(&url)) + .json(&body) + .send() + .await + .with_context(|| format!("barkd POST {path} failed (is it running?)"))?; + Self::parse(resp, path).await + } + + async fn parse(resp: reqwest::Response, path: &str) -> Result { + let status = resp.status(); + let text = resp.text().await.unwrap_or_default(); + if !status.is_success() { + // barkd errors are `{"message": "..."}`; surface the message. + let msg = serde_json::from_str::(&text) + .ok() + .and_then(|v| v.get("message").and_then(|m| m.as_str()).map(String::from)) + .unwrap_or(text); + anyhow::bail!("barkd {path} returned {status}: {msg}"); + } + if text.is_empty() { + return Ok(serde_json::json!({})); + } + serde_json::from_str(&text) + .with_context(|| format!("barkd {path} returned non-JSON: {text}")) + } + + /// `GET /api/v1/wallet` — wallet info (fingerprint, network, config). + /// Errors with "No wallet set" until `create_wallet` has run. + pub async fn wallet_info(&self) -> Result { + self.get("/api/v1/wallet").await + } + + /// `POST /api/v1/wallet/create` — create (or restore, with a mnemonic) the + /// barkd wallet. Idempotent guard is on the caller (`ensure_wallet`). + pub async fn create_wallet(&self, config: &ArkConfig) -> Result { + self.post( + "/api/v1/wallet/create", + serde_json::json!({ + "network": config.network, + "ark_server": config.ark_server, + "chain_source": { "esplora": { "url": config.esplora } }, + }), + ) + .await + } + + /// `GET /api/v1/wallet/balance` — off-chain balance breakdown, in sats. + pub async fn balance(&self) -> Result { + self.get("/api/v1/wallet/balance").await + } + + /// Spendable off-chain sats (0 on any missing field, never an error once + /// the call itself succeeds). + pub async fn spendable_sats(&self) -> Result { + let bal = self.balance().await?; + Ok(bal.get("spendable_sat").and_then(|v| v.as_u64()).unwrap_or(0)) + } + + /// `GET /api/v1/onchain/balance` — the wallet's on-chain (boarding) funds. + pub async fn onchain_balance(&self) -> Result { + self.get("/api/v1/onchain/balance").await + } + + /// `POST /api/v1/wallet/addresses/next` — fresh Ark (`tark1…`) address. + pub async fn ark_address(&self) -> Result { + let res = self.post("/api/v1/wallet/addresses/next", serde_json::json!({})).await?; + res.get("address") + .and_then(|v| v.as_str()) + .map(String::from) + .ok_or_else(|| anyhow::anyhow!("barkd address: no address in response")) + } + + /// `POST /api/v1/onchain/addresses/next` — fresh on-chain boarding address. + pub async fn onchain_address(&self) -> Result { + let res = self.post("/api/v1/onchain/addresses/next", serde_json::json!({})).await?; + res.get("address") + .and_then(|v| v.as_str()) + .map(String::from) + .ok_or_else(|| anyhow::anyhow!("barkd onchain address: no address in response")) + } + + /// `POST /api/v1/wallet/send` — pay an Ark address, BOLT11 invoice, LNURL + /// or lightning address from off-chain funds. Returns the movement barkd + /// reports for the payment. + pub async fn send( + &self, + destination: &str, + amount_sats: Option, + comment: Option<&str>, + ) -> Result { + self.post( + "/api/v1/wallet/send", + serde_json::json!({ + "destination": destination, + "amount_sat": amount_sats, + "comment": comment, + }), + ) + .await + } + + /// `POST /api/v1/lightning/receives/invoice` — BOLT11 invoice that lands + /// as an Ark VTXO when paid. + pub async fn lightning_invoice(&self, amount_sats: u64) -> Result { + self.post( + "/api/v1/lightning/receives/invoice", + serde_json::json!({ "amount_sat": amount_sats }), + ) + .await + } + + /// `POST /api/v1/boards/board-amount` (or `board-all` when `amount_sats` + /// is None) — lift on-chain funds into Ark VTXOs. + pub async fn board(&self, amount_sats: Option) -> Result { + match amount_sats { + Some(sats) => { + self.post( + "/api/v1/boards/board-amount", + serde_json::json!({ "amount_sat": sats }), + ) + .await + } + None => self.post("/api/v1/boards/board-all", serde_json::json!({})).await, + } + } + + /// `POST /api/v1/wallet/offboard/all` — move all VTXOs back on-chain via a + /// collaborative round. + pub async fn offboard_all(&self, address: Option<&str>) -> Result { + self.post( + "/api/v1/wallet/offboard/all", + serde_json::json!({ "address": address }), + ) + .await + } + + /// `GET /api/v1/wallet/movements` — barkd's own movement history. This is + /// authoritative (includes receives we never initiated), so unlike the + /// Fedimint bridge there is no local tx log to maintain. + pub async fn movements(&self) -> Result> { + let res = self.get("/api/v1/wallet/movements").await?; + Ok(res.as_array().cloned().unwrap_or_default()) + } + + /// `GET /api/v1/wallet/ark-info` — connected Ark server parameters. + pub async fn ark_info(&self) -> Result { + self.get("/api/v1/wallet/ark-info").await + } +} + +/// Idempotently make sure barkd has a wallet, creating one with the node's +/// Ark config on first use. Best-effort no-op when the sidecar isn't +/// installed/running yet — mirrors `fedimint_client::ensure_default_federation`. +pub async fn ensure_wallet(data_dir: &Path) -> Result<()> { + let client = match ArkClient::from_node(data_dir).await { + Ok(c) => c, + Err(_) => return Ok(()), // barkd not configured yet + }; + if client.wallet_info().await.is_ok() { + return Ok(()); + } + let config = load_config(data_dir).await; + match client.create_wallet(&config).await { + Ok(_) => { + tracing::info!( + "created barkd Ark wallet ({} via {})", + config.network, + config.ark_server + ); + // Persist the effective config so the settings UI shows what the + // wallet was actually created with. + let _ = save_config(data_dir, &config).await; + } + Err(e) => tracing::debug!("barkd wallet auto-create skipped: {e}"), + } + Ok(()) +} + +/// Total spendable Ark sats, soft-failing to 0 when the sidecar is not +/// installed or unreachable so unified balances still render. +pub async fn spendable_sats_or_zero(data_dir: &Path) -> u64 { + match ArkClient::from_node(data_dir).await { + Ok(client) => client.spendable_sats().await.unwrap_or(0), + Err(_) => 0, + } +} + +/// Map barkd movements into unified [`EcashTransaction`] history entries +/// (kind = "ark"). Best-effort: empty on any error, never blocks history. +pub async fn load_ark_txs(data_dir: &Path) -> Vec { + let client = match ArkClient::from_node(data_dir).await { + Ok(c) => c, + Err(_) => return Vec::new(), + }; + let movements = match client.movements().await { + Ok(m) => m, + Err(_) => return Vec::new(), + }; + movements + .iter() + .filter_map(movement_to_tx) + .collect() +} + +/// Convert one barkd `Movement` into an [`EcashTransaction`]. `None` for +/// zero-delta movements (e.g. internal refreshes) so history stays meaningful. +fn movement_to_tx(m: &serde_json::Value) -> Option { + use crate::wallet::ecash::{EcashTransaction, TransactionType}; + + let delta = m.get("effective_balance_sat").and_then(|v| v.as_i64())?; + if delta == 0 { + return None; + } + let tx_type = if delta < 0 { + TransactionType::Send + } else { + TransactionType::Receive + }; + // `time` holds created/updated/completed; prefer the completion time. + let timestamp = m + .get("time") + .and_then(|t| { + t.get("completed_at") + .or_else(|| t.get("updated_at")) + .or_else(|| t.get("created_at")) + }) + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(); + // Describe via the recipient list (send) or receive source when present. + let peer = m + .get("sent_to") + .or_else(|| m.get("received_on")) + .and_then(|v| v.as_array()) + .and_then(|a| a.first()) + .and_then(|d| { + d.get("destination") + .or_else(|| d.get("address")) + .or_else(|| d.get("invoice")) + .and_then(|v| v.as_str()) + }) + .unwrap_or_default() + .to_string(); + let subsystem = m + .get("subsystem") + .map(|s| match s { + serde_json::Value::String(v) => v.clone(), + other => other + .as_object() + .and_then(|o| o.keys().next().cloned()) + .unwrap_or_default(), + }) + .unwrap_or_default(); + let description = if delta < 0 { + format!("Sent via Ark{}", suffix(&subsystem)) + } else { + format!("Received via Ark{}", suffix(&subsystem)) + }; + Some(EcashTransaction { + id: format!("ark-{}", m.get("id").and_then(|v| v.as_u64()).unwrap_or(0)), + tx_type, + amount_sats: delta.unsigned_abs(), + timestamp, + description, + mint_url: String::new(), + peer, + kind: "ark".to_string(), + }) +} + +fn suffix(subsystem: &str) -> String { + if subsystem.is_empty() { + String::new() + } else { + format!(" ({subsystem})") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn token_encoding_matches_barkd_format() { + // barkd token = base64url-nopad(0x00 || secret); 33 bytes -> 44 chars. + let token = secret_hex_to_token(&"ab".repeat(32)).unwrap(); + assert_eq!(token.len(), 44); + let bytes = URL_SAFE_NO_PAD.decode(&token).unwrap(); + assert_eq!(bytes.len(), 33); + assert_eq!(bytes[0], 0); + assert_eq!(&bytes[1..], &[0xabu8; 32]); + } + + #[test] + fn token_rejects_bad_secret() { + assert!(secret_hex_to_token("deadbeef").is_err(), "too short"); + assert!(secret_hex_to_token(&"zz".repeat(32)).is_err(), "not hex"); + } + + #[test] + fn movement_maps_to_history_entry() { + let m = serde_json::json!({ + "id": 7, + "effective_balance_sat": -1500, + "time": { "completed_at": "2026-07-14T12:00:00Z" }, + "sent_to": [{ "destination": "tark1abc" }], + "subsystem": "arkoor", + }); + let tx = movement_to_tx(&m).expect("mapped"); + assert_eq!(tx.amount_sats, 1500); + assert_eq!(tx.kind, "ark"); + assert_eq!(tx.peer, "tark1abc"); + assert!(matches!( + tx.tx_type, + crate::wallet::ecash::TransactionType::Send + )); + + // Zero-delta refresh movements are dropped. + let refresh = serde_json::json!({ "id": 8, "effective_balance_sat": 0 }); + assert!(movement_to_tx(&refresh).is_none()); + } +} diff --git a/core/archipelago/src/wallet/mod.rs b/core/archipelago/src/wallet/mod.rs index 098543f9..9f7f92f5 100644 --- a/core/archipelago/src/wallet/mod.rs +++ b/core/archipelago/src/wallet/mod.rs @@ -1,6 +1,7 @@ // WIP Cashu/ecash wallet — many helpers defined for future callers. #![allow(dead_code)] +pub mod ark_client; pub mod bdhke; pub mod cashu; pub mod ecash; diff --git a/scripts/image-versions.sh b/scripts/image-versions.sh index a2f17e22..d8f25951 100644 --- a/scripts/image-versions.sh +++ b/scripts/image-versions.sh @@ -64,6 +64,13 @@ FEDIMINT_GATEWAY_IMAGE="$ARCHY_REGISTRY/gatewayd:v0.10.0" # federation first (the interim default is node-local). See docs/dual-ecash-design.md. FMCD_IMAGE="$ARCHY_REGISTRY/fmcd:0.8.1" +# Ark (bark) +# barkd = Ark wallet daemon, packaged from the pinned upstream release binary +# (apps/barkd/Dockerfile). Signet-only default config; keep the tag in +# lockstep with core/archipelago/src/wallet/ark_client.rs REST shapes. Not in +# the bundled CONTAINER_IMAGES list — install via the barkd app manifest. +BARKD_IMAGE="$ARCHY_REGISTRY/barkd:0.3.0" + # Media REDIS_IMAGE="$ARCHY_REGISTRY/redis:7.4.8" From 6fa0aa46a945753684fcef16860266358ae44225 Mon Sep 17 00:00:00 2001 From: Dorian Date: Tue, 14 Jul 2026 21:57:12 +0100 Subject: [PATCH 2/4] feat(ui): Ark wallet tab, balances, history badge + demo mocks Ark tab in Wallet Settings (status, balances, receive address, board/offboard, server config via wallet.ark-*), teal Ark badge in the transactions modal, Ark row on the home wallet card (hidden until barkd reports a balance), official bark icon, and wallet.ark-* mocks so the public demo renders the tab. Co-Authored-By: Claude Fable 5 --- neode-ui/mock-backend.js | 64 +++++ neode-ui/public/assets/img/app-icons/bark.png | Bin 0 -> 12984 bytes neode-ui/public/catalog.json | 19 ++ neode-ui/scripts/download-app-icons.js | 4 + neode-ui/src/components/TransactionsModal.vue | 5 +- .../src/components/WalletSettingsModal.vue | 227 +++++++++++++++++- neode-ui/src/views/Home.vue | 5 +- neode-ui/src/views/home/HomeWalletCard.vue | 11 +- 8 files changed, 329 insertions(+), 6 deletions(-) create mode 100644 neode-ui/public/assets/img/app-icons/bark.png diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 75b4ae66..20dbc648 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -132,6 +132,11 @@ const SEED_WALLET = { channel_sats: 8_250_000, ecash_sats: 250_000, ecash_tokens: 12, + // Ark (barkd sidecar) demo balances — signet defaults + ark_sats: 75_000, + ark_pending_sats: 0, + ark_onchain_sats: 20_000, + ark_config: { network: 'signet', ark_server: 'https://ark.signet.2nd.dev', esplora: 'https://esplora.signet.2nd.dev' }, block_height: 892451, transactions: [ { tx_hash: 'ab12cd34ef5678901234567890abcdef12345678', amount_sats: 2_000_000, direction: 'incoming', num_confirmations: 142, block_height: 892310, time_stamp: Math.floor(Date.now()/1000) - 86400, label: 'Channel funding', total_fees: 0, dest_addresses: [] }, @@ -2209,6 +2214,63 @@ app.post('/rpc/v1', (req, res) => { const total = (mockState.federations || []).reduce((s, f) => s + (f.balance_sats || 0), 0) return res.json({ result: { balance_sats: total } }) } + case 'wallet.ark-status': { + return res.json({ + result: { + available: true, + wallet_ready: true, + wallet: { network: walletState.ark_config.network }, + ark_info: { server: walletState.ark_config.ark_server }, + config: walletState.ark_config, + }, + }) + } + case 'wallet.ark-balance': { + return res.json({ + result: { + balance_sats: walletState.ark_sats, + spendable_sats: walletState.ark_sats, + pending_sats: walletState.ark_pending_sats, + onchain_sats: walletState.ark_onchain_sats, + }, + }) + } + case 'wallet.ark-address': { + const onchain = params?.onchain === true + const address = onchain ? 'tb1q' + randomHex(16) : 'tark1' + randomHex(28) + return res.json({ result: { address, onchain } }) + } + case 'wallet.ark-board': { + const amt = params?.amount_sats ?? walletState.ark_onchain_sats + walletState.ark_onchain_sats = Math.max(0, walletState.ark_onchain_sats - amt) + walletState.ark_sats += amt + return res.json({ result: { boarded_sats: amt } }) + } + case 'wallet.ark-offboard': { + const amt = walletState.ark_sats + walletState.ark_sats = 0 + walletState.ark_onchain_sats += amt + return res.json({ result: { offboarded_sats: amt } }) + } + case 'wallet.ark-send': { + const amt = params?.amount_sats || 1000 + walletState.ark_sats = Math.max(0, walletState.ark_sats - amt) + return res.json({ result: { sent: true, movement: { id: 1, effective_balance_sat: -amt } } }) + } + case 'wallet.ark-invoice': { + return res.json({ result: { invoice: 'lntbs' + randomHex(64) } }) + } + case 'wallet.ark-history': { + return res.json({ result: { transactions: [] } }) + } + case 'wallet.ark-configure': { + const cfg = { ...walletState.ark_config } + for (const key of ['network', 'ark_server', 'esplora']) { + if (typeof params?.[key] === 'string' && params[key].trim()) cfg[key] = params[key].trim() + } + walletState.ark_config = cfg + return res.json({ result: { config: cfg } }) + } case 'content.request-onchain': { const price = params?.price_sats || 500 return res.json({ @@ -3225,6 +3287,8 @@ app.post('/rpc/v1', (req, res) => { { type: 'receive', amount_sats: 50000, timestamp: new Date(Date.now() - 86400000).toISOString(), note: 'Minted from Lightning' }, { type: 'send', amount_sats: 5000, timestamp: new Date(Date.now() - 43200000).toISOString(), note: 'Sent ecash token' }, { type: 'receive', amount_sats: 10000, timestamp: new Date(Date.now() - 3600000).toISOString(), note: 'Redeemed token' }, + // Ark movement in the unified history shape (see wallet.ark-history / load_ark_txs) + { id: 'ark-1', tx_type: 'receive', type: 'receive', amount_sats: 75000, timestamp: new Date(Date.now() - 7200000).toISOString(), description: 'Received via Ark', note: 'Received via Ark', mint_url: '', peer: '', kind: 'ark' }, ], }, }) diff --git a/neode-ui/public/assets/img/app-icons/bark.png b/neode-ui/public/assets/img/app-icons/bark.png new file mode 100644 index 0000000000000000000000000000000000000000..e9be9a0b01f1c6125125a008c815f611bf0ddabd GIT binary patch literal 12984 zcmd6O1zQ|Jv+gYJZovZtcXzko?u5lXxD#Y?OGt1F?oM!bOYq?CZh_#CyZQ9obN|5I z=h8iJ?CQ4034jqLU1pol_xAM{&004qkfkz}5=r0QAstL3JxoOBr0#)Or zhtP(%rQTaBWo3X7T1NulK*Rw2UlQn76h!i$x-5tufc-~5R8E8~0QWB%6=(?n#6ryg zfVuyl5;h0)FKVcq9GHJogMb{^f7gM>zl`I?io(zW*;!uS4FJS{{r!S+XlB`=E10m= z(sS2SRuVLKa%45Na5A%G^>%dr%LNE~3qq@omhPsM-i{9M-2}ZwsQy7A2(AC^W}~9~ z2Z_7A2$i0)8l{wzt0g5bD?2MYl_&}&C8e;dg_WR&wCumwp)Cu60Rc94PBuhO!F(?G@D5uyk{B@c1iV zl$(>2i-V1Won81J9RJtYf9Lxr-+$S(-`l#2a{i0ue|7(FF1`Pb|39<+&#`~;3A6pZ zhy^g=gS6h)ZrKYx}eios;(1^}?Vyp@*F@&=u*`aC&mK8syc@A>=$N5*jC zG3-!LxxeMcYAxc_;S5E4V?qB`5>|w`)Vd$dv-R!F z4>ybZnId1m-B4sti27A`gx~shtsOrHRNordM@5KU=l&tAmJhZq?1n9B*sNXs41>ym z1;V-qmq82m*;NV>3J{jh|357?F3hJc8Qrm@)U7QF?g7W@Z-)ynfqx#?VaYFo5JfM7 z6`5;8AOF1Ec9HvTS!eNN)YVY~b_dR}FYyt=7Nv#B29`dVccXq4v{!dR+h0`Fyjy zKbFB~J)TM8)PCs{_(9^45{D<7{MB?BwnvBdOs2P!eu@f3yZ|j#)|}qdpz#N3>lJB&zI|_8tThBJrxO z7hhV5esK=tI{Fl(AegAMq$HoJec^q9wzJk_8#}aZ@8{q1`&?nc(6)WOQr!_><@uJk zf$@ac(q2W!t zVFO|D9J;hdX)V*}3iTtj8JXT&QB2kCuKnGV8t4HOAof&QnZk{}&DmFCd}$8f#(0Ow zyjB#LDz)Upw{=T91f&72c`s6aNs>XfPmE#b@S2&POSbG-L%}Zc#2|Kz-{Frs1;r&3 zo1KRVf~mNM?^_DfcyQtUD?Z>14#Co6t>(ZYY5lb4%%J|W%NE$8HktP;al)>=lGc!; zz~!R@cNuhJWt(!lsJ;zwPOj=2xeVlrjX!}a8|EJN#s2t=@yrt;qPTEdCiB{|Q3H%e z>J@M0QSGqSG#x(g4NHG|`CT3O483nD9JfquNkHt(1G1_U1|J?~vRTJlF0kBRi9Rk) zG{$eV9#_SLbgPNrf!Og{1{EC0yEy^usZYK?^9M)iPt7kS&El#$ACB&?_J?@5ZSfQW z2|)2GDV$%wjKO3ytp_}soo=XUkAC8x*-bGaN2s76qT9=hZRF#fKngl)MF7rP_P_GX z>6vGF%rO|LBfqMv$-e(#?TR9UwsSI%6qFgtNc)Pnfi#w@UYKQ|3|7X-V^;geR#;az^$dlDcVUiL9Fr0mXUj-I@1ql+GuR@d&15UII{mNnDICAtE!kIh z{GKyZWGQiQP09*{6JNwT8>ax27H^gbON4$Cb={}mh0#mG#8V+WU~|IcP*{%os?Z@H zhqTy_GH!SJ3xu5v^m2^SL=rWBV!VB%06B}fYF)rmbHaYT2&IeMWnMO%n1{3SB&hqs zPYhd{MFNUfNXDUn0ZLEKzrVYw`{gh37M5AMF?``iH%rrs2JhMzNkez&86#!A8KC0J zM1uMW?bH#i$En`uhdDVcECCIDE8iHmSQJo_d;8W*vsXANkNnxNGFM`jImD>f6-oFWV{L zv)YlebJc)*AaxO0b_QDV41b>umjfjmT7t+$fqyQ(L)j@5L!0;5U+n#qJ^QE*Bh4lO&u2|&MmeAN; z9qqGtAySZ*PU%5zGbOJ~ zn|wxm9DdJ+c9H>IdhPduMW+zfo+bd1(Sr%UaEeFD(LxxuV^-~ zXujH8oBe!ZtyvB)1H_K?np<}h_7fA38A~|Z=vod$r@w06`J#B%y!|nHb6z{4H9!{l zDRV!t`=zt%&+|Foy1yIw!+>getD#+KVa*G^t`SF8j1?77W>){Q3jikZuov&H^9Z%s z!jo84Fkl>6%0A8%p~?W))<00z!9vzzC^1-dI;OR^1eU(uX_m?NXI@=GVbN-E;=iAg zHK>3Ew&7m@D1)I|F@Q$87sZzmOD0mTW6~$eP zQk*_}H?@{!xjh>bOwJ8!a}x}yL|!6Dyx*0@BkzNLCZ+6fXWoUHB3T(5H%yLidU1+8 zW^O=@92tG~*Swu#3G83D(~4~9-)|IIYp@sP0PZ{zEyyH#CuPQ_LbQWacJEd32TBG^yJ_6)JA-7$z%E$ zqf!}r7YU0gJh{`|Z%zqAnFKoW8OBw(FZK4&v(vnMMm4$XHTGJa{n>Bv&*8EF`THJg zoxuf$l6SC_^+q2^#s7r!wS&-pOlm8CMEMRCAHVP%L-p9f4J&>>=<|%L^DwAaf8P1H zWZ(|r!0HfBs?)MIEvU$r=<)8SCa>K~x9>buc3OgJ5n;4yLEL%QYGQUuQ8UQI`bVH7 ztbw48e~#|1RD?jv0Jj}_s4g;UIohScAIrh_VJDr`eP_jGTP=zHU4BtBK@Ef#>Tw*p zh&ko6_`bV<5S~C!Sih56xdrlaTcx6}2p}%^6qA5^X;!j#y-|<>ciSItDBvOEAt(H6 z$$CO3H6dxb{^veY+}Ty0C8?H=P%Uvh0oCdwF`pu-#dW<>esi&VZ3Mi#(f?*e=F*Q1 zboZml@FqOYDi0_r6CuJytGx4IuY8q{>cf!Fffnf>AfmiY=bqlS`?jYiilo4p|!$F6$B1i z#_hq?pEAW6;JPNVbd)d5pHG|C*VKUvEJk+Mah?F1#R8rLPu=-Y;&OHHR03v@il^77 zKVMMEzHbb4lPbL4b`)y3UGt-G?gkxKd{glze<4^1)3&mtH}46&%2>y1SUL))kN(jhg13LP!*|IKR_xnk+Y@`m zhd7e93ci|ij)~h)u|mlt6XaR(J9a&1dq+5X(0iMlnGxXnzDif{7hl(X9M8rRNj``4 zxo+K?JLNNjwQ(;M>kBTed_EV&?3(b~C3`j@MLs$QTMGqIn;kZ$7c2^|SvhNyxH0anukWh8$)kbLh;p~7-xwL_!NniUv(5I-OSScpDfNe$P6|mZ zdEQawJ~-&%a8TyxxHl-ifX#)@=Voyt;6{q#Ve;wzs%?#Sq|WIJW35XV@5Rl+0auCM zJIlc3=_afGcUS52{<};jwvUvP(g0jh&sJ4vFoakt5BKz^eI{9`V58>(eeTDpQa&IE z^^NnC4~*ZNSG2wS!k3ybN^GOM5L8zFy;O7A1#}4=Nz24HiGfh#oNFKh?wT&ii6O;W z_pz+J%541#=`j5yQ8{24I!7m+0Yq(R^gaDqbLM;H>W%mtt`cs_;QIsNX|IvB>yAhB&6)C zS{y7;x8;o}TT#=C;;t-;7^_l}aK_py^ls=t%MntXu-&#m#_;IkFUBaurKrdGIoVn- zMIeV!OC;?2q<%&PYVo!{C?Ve>60i9lSNa>RKg=~P3GuBkQxhp-Um3EsLTZZFXgKjF zTAWjvE}BSGC9P@rp6~oN+{#cl1o+;A6)Ztz7IV((VUCtOZ|qbFE#2qN+Rn#`30RjH zVq3rP+*^kLL4`U-E;0l(FDAByN$hE;r#Om~n~xXm&UG4vk`}I>;|jXkMZ*ur)e~c? zzl*<=S||!ow$&~=@uam(Z2FB0?AO=dD}w-pUfE3hwQ~gYoxwI&|Qn#57e!SFo)n4pr z_sdCDr@~!quKr%Rj=sWHTW00YxmaCqoQSyZq!=}c1jOfL3f%PXR91h^b{J`d+9GI| zn1yyWsdWLmYS!hef$+Uwek}RyXSLABS9Lp-kCb5plsW>x6Zkjn4Au;zYa%n=w=!s_ zy z$Qe1*8O!wyQMPcG&0<(4%UJL+emlSc&SK9{@OWPJ+0$#yr}$#<-C6P1w9ouI@lwpz z98!dqfnB1Uh7%E{G!r*e*N^R0`*8qJxqY+j)C8+6>w__m8Y1PQ546ZL+$g?Qxk@3T{&oN81y!$D zaw4vKGgr9*MqaBPVG;Gic<8c(#Dcn0AgZK29MiSZ^KGL~z-O@x|#yIV1AMU4okdJ|Dj*ehK(OZK|@am3$y4+{Hzl`NiAb*Wm{(8GotP;l^ai*hBWpP-D@R%RC7iF5(+d$it)EH946-^! z4Sh(~nWT)$M0+5(i{A0OZCf8hCINCxPA$zIRYX!&hv4UOw<=(2!k(edgmWkg-uUx8 zz!uPhSwx&WOZ?S{5=19!^^IHXenhnygW{mWBw6f8?a6T$-Z+~~VIKe#3e3R54s5bOf5W^1NA zY;$xsD_w8^|6Z;=@nQ#@SA5JI4SLM-$%8m$4DWKpDY*xp8%c7Kbi_Fs&+qrMtNSNY z-lMJb8^X)XYs0-1U2ng9f?D$xe}bO6ub;~j#bMCmht-^K7OhFd9||!i^ii$C;$THb zRP@i6tv~=vv)TK$Q{J;2l&4(o4JdBsz=Dyxz2Hr`vbuFORQQYRs?#~*UI-Q14MXc!>tJ-`aK-ROgLp$`H&CG_XhUo*6 zo8;9cg;nS98JTl+FY&ijjgAj#iDOc*lazlUHF$(VKSz-TtjF#O|3a^ zA=3PhZR9sETG9HO;i`pv*g)_B)TXP-ZA_^M6E>YNg(ojoOa8idgAk-OPG8VZYhk9g zYe{ROu}C~*FN)ob+l_6VPv~Y^Miw6=&Dwo&3e_zsZpms!@rj}il2@9?lNCNe1<`^d zJBeew?g(CLb`fu)skx_h{f!e_gctI|AsxLy~NOpiJJn?G+Y!>~CWzSE^EdQmMfh=eDBs2O5-@}2nm;oBt3)*S}Vi_0!8MWZ>k3x3aIy;M_12LUyEPzX*LnIfE%j`*odVl>1GwMD- zsp6DZnz0(Wo^n|}9gCo`-)n#=g!{jwe(AYl^Gn??sPY~ zLggtErjJ1BuMSQkALVF6G_|s5Gj$M4dGL;-ARwu#848yjX-IB*oH)5FS}Ye8Ftc8| zA|*%+N+m6IqT6__sB59Zi0V5$rEj;~GZC8_a3TZls8`+(_dQNDXjL(5|7&P91n0sX zq(&r8Z#bNFC8&xnwsY>%@wj1vLQ0K20gbL{CT|tY+RQL6x(4RFp1jDPx-ud=s0w6| z=+VG>e*;(mO+k?(c-M%r2Won7vB$T6NyUAD+P1_r*SLRd5J`j!w&oRQ;be~sBs-}- zIGV942;?eRHx44|?kDt~pJt`fK}s%Mzi~ONKvH9m%uK!s?BR_t&Tz11F@CW7Wac&f zam4T8u=uZ281uWRhGsZUA>Xk4Myi}Y;iq$b=|ky?0jD5BJA3$1NqHN z)KS%%BlpIBpSmPjg{PkBpy15x#B`+(M#eI5XRp#C;v<7{-b%I&rWfHO#JE#iFKBnf zW>@f&up-B(8Ohc$nl#`5NOtQ+il(1jS!4s zVm1(jgXdYCT|7klsRF|3UfmaT=J6xPE8X`KNLi!2=?mm`U7NX>LzP8oG zDvp&oQ;P^jxan$8Z>f9ulxm(FGML0nhn+1P-}#T6*n|3tlUyvgr(8;m0J$1~CApvM zN5UOOLnx>05K;Cm^8j*s4b=TQsDQfGAl7kpn^)e8GcduAEnkOS_k=nzmsE6i2|5yR zNM13eqQrtU4PeDeML?Ar&O%F`4|PXn+Rs=O<-4%o2lTc=p%DnoUr4(aL%}{pv>R)+ zD$23A>%&OnOdec#?1^k$PrrUXW!%Io)y~%orYOv2R(=Ey6zNxCVQH8e`qw)T2LoQa zsnG3Pd>p~;u_9^{)V2U4c8<2AE}ljK!$lLlulT-bvonl&h);ih_gyEo={J3I7heb% zR_*!?Y9Kmc2V@3U!2rHTxPyoE6!}~dK3V%~t8yd>x2mR`VhG#)#{Cf0koy2Pmm(xO)A1zVb6@qaU82dm+v;az#KMsSv0qag&vZ^BwSG!Wr>b zWIQRau!HLc>=0#!A)KCeuYis7E~r=2uf?Q6{)5VbZr8&sRsGnb0&KM2I$T=ETz{)1 z#DZ4m^uTjc0}suCp@B{v5tl zz(|uEf93^fUmQnhnXB#lWVZn}6f`2zA7Kpaa4?NAkJ=doKSKrO0JJ^*XA3tmgS(9a zsUm1VI-7D;NQ{S;KC!(2>HYqd^LbJXHUsfW!o@c}{l09Qtn}iK0orrIzQG7;88igl z&8Lma$sB}5kIFm+b3q=%I|l=@Ux;qKk6jal9j4G7G*2Uge2+q3%k<1!&qRVLrMCcB@`9F;C`L?K=ZFIwwr)hx*#CvI<k`uT^GF(SPk>)uBs_qIA$I`?{wrKb6eLRMce7fH#tK?ktZVTXQnm7YLz2(fjc!>XAV%`Ba6L^ zWJ>Q~8b?H1`PS9)^#JTC!uahX{i-oIuF#Y!y}PrjtUqnTk3sbS=e}Z1x8q1V$t93J zBjs(2PQ!Bgd^(Y4yF2^Vdbo*4tqjj@v{U7Wt^=eXzsK|ykG#`0zjNd70eT3ah%%wl z@hYqZ7zC|+82Ace*>m0KU=_vV1`riMFgTs+REIig8bM}d4*#rEx}M zAB-xVMqI#;`Otj;E8WQ4k_veFv}_Fx>Ck=k&O1;PQAb*apG(Ogkq1J8Kdk7#-+%7L zT+Rp@x2@_}dr%dE!fx%1nVC?$U@a}{;Tx#K^xI`{iI*HKe$r{^Zr=?{{9Zn`bs0#U zm#Y^;?8#YW0>)3WG5h7ZIBV#r9#pO^`4ZYX1?H4)3vXsOvibNZEM>$PGN%$>iU1(j zL*aufKf&?M=4u9)wF*DV05Ss1*A^W|Fp{}<;SH;*W&abVvRaOzHPedM*6TnWWqE-@ zVA5Ln3}%(NkcE0Kj!W+4NkA6%IBNZAkI%5P`D$D!Cdi2m0m2dT$nvQ%E{ve3v2+C* zKwD!V36&?w;4K8Yv;GDQ#RIc+n@Q`$azm!blt)@ZjW6OVQL`@XE>n0qg}*vl;A!RU zrxw-3&x>-5Q*Z3+H1Z_=f&etr{%$NE>Rj(5{~2QVb-EOcNroZk~}3H z9ip-PB_JEjM8dzkulBOZNz0!yAWKQxKB{M&miX>YIjH<$nd8y;+?Vhw+_TvB>q60h ziG1_R!!bH)qt&a?a`R(t!>y=?@VwsgEUvy%Sm3jiS(bAzvQ^e-e%k{1!vs8`QM^1q zdrkPNT95~rvLfV?-CArmiP>4qaVXdx!?yEoE9Ugn;Ylmo?{h~9a{Asd?`Q%2(i1}M z8>g$iF{y-Kf@YMfMHqn9yVQa$vhXk3u!jptfEy7UJ(}lvEx0#?lG0M)ynX@#Nc~%B#hj0PT|m1{h#AyVCQgx}0_+Mo5)vqk5>GAF^sF zNi-Z9JFKXiJz;yzr^x{`Dd>H` zPbJf??cl^A01i5}@ni-CouPD$d(zmns8wJUW0V@=vInuT$T%}X;M4R>w7tpd~>W;!{6}R9>1H>=FIi81T{C=~@Zu!ynf91e#p2 zX}zIY7wA6|&%QTQ{J|B1IZ&LbHCz34kZ-BpL~VkSI;^SVeNIWKz2TZ!yksRi!M}}m zW>5^iJ2bT<>i+m~SPojhH@s0ybyO2F*D&e!D+}fvnPUxmLH&67(`A9u*7SGSaTI$&B_a0h@%1|n2`apw)?~x zl7tc}=+684w`lX~2}3p+Dh>P2PJ(LD2D(sh0fFFNUldHxWy*TOkr*^DhT~AmL+*iTh5SJPzkpz|DXd|!ND0n7fT%-m=3K2^; z>qElQUbwK9svVl4Oa=XzXoyoQBPp>>uSX9AK6x!e)lzT!C;PTKvlBzian!DID+zjL z8qk!Bbg}Uh$HvC#&9Pz_$&xjt;X_Hyu6M=#Q{1VY-=>ppI`}(2ABQlzPue1u{%9ov*owW=oaD6%A5?Cmb*031+9p6iK*{IhxU6m|pxoM9lLL0?GjFxhXj(Az7fnR*Yzi_5;-S|44C6_qzw{z1aSciHblB@L0~hCzTa zc|b+Db^zg#RtTro73P7}s$r;gl!Dbt7(2Gf&TIOWX42=BPZ5Hu@Q=ItFof`$8yp9i zaiaTPSs~Q-?rFO3PBr)IvI@&-4!K#O{n+EI9R{#|X)!LG zYa9S-3gp{EN)Y=#lya9*y2UES74@4Ff1Gkz;FezmV6V{F0j(csjBV_y#4)${)b$t9ecDQIC~zB9ShXD*`vThh*{3`x#q12%C^OS+^ViDis=XA9qNv)gR&8rfhl zh2p43*W|M+abSSO2{DBYaIm%3NGX~opbq7Ay|ein9hjh_>W~(^%%fS)-Qt)(ME$)o z443Yym!~pD2O6ObR?Ox%wV>dJF(a#9VmEQtQb4#F^DrSoYLzCE(~@?Y;o#Y4D=VC_ zaG}2vE(m+2mT8F8Ep{>6Y((Af{oSYCQSuz5FcF`@>S;+V<~nq=j9+-+;D^|E7Oac7 z7-Q_&_+N4z#UZXDBbu$(RTnMSd&Q@>R@Ca3#J`-1_7dvLKU@L6NbAcj-J+-he)P7E zhVR*TWeANeT!W@@{Q1LvlD0UPRS>wG^e*s-Qr*-#`ZH$`oP){ZFp|~qON}?Jf3g3h z85v^7fA{&Js9vD%P$6sLHPA*j3ons1L zh#${{sS;oWv7X7n$uzUqmcl;7!Uf*hEN0% zfuQKDS>wv#u5S(y=Sv+Dc)8n_5zX`I8!-U?4iyygfNX#HVhlp~l8!id^DU&W%_*U2 zM7Ofd79Bt=C5Y0pFBzZ9KbY1*yRq~W&r zig+`Q?WNDf5y{hYy(rs5qefoPlN^1#`7&d*l@u)AOfS|Uz#J^1eI!|#KdXlS9t02> z8fBA0gk7W>9D8xheTg>8_%I^vBZ=q>0y8jzHk5IH*IW*gBtXLITRPH+0ES?uiW&&A zZdGn%@MT-wA+YhV;(s-ED0XUm!4p8|u@`h$2{OZRj*Y&~G{~w+pxs{KBL*ByRK#C5 zd^GvEC3a{f%O0}8Pnk3JWM7~pnTGwI7#-N6IPUsg>BK|9ExAm4hkKckSL8SZju_*| zh@F`lg9lDcE~0Q*Pp)}YQ0aafZU+fSJZd9LSo&%!U+Y{U|eZN}WhhfX+a~w&kOEf<%M! zJNDclHRamJBBTnB9g?_KHOVw#K6H5C59adxz30@x=VHP|SkvcdDcbEPaq{Rlw&_T~ zu~h{Q1fPFR;E))jB5pN!S-o$KKG#)$+G7^g@{VWKOHv>#6Kyg?cNHINc($+>V=_@pfM> z^nC${?EQ@QtP=iA7!`sq@&YAr@H${3K}C?kSIqCxww{jvT@GAEGzVC@Ba3Vc2g0hr z3w;40$@2W0^~B!jL7C-~04F?HocRq z^k6m{zw%U;#<<+8(=9kf$=MNE?T8~)y-N0~q^8&rQ{WGA3Ddb<# zxMY_wWUc)EbBV1rl?+Qs0FYX?0uDjX`%`RIzhK|T8uR3YpR!!Pb%Y2+_h$u`S)+QU z=%?_*{6q@^gM^-QeKAsh;efz`n1%;Ox9K1-(mVUg2I*5zZKfCI3>d4hG>~P=^wZNZ zV$eOCDdSl2&(PWULrX)Mxb$BPo9Un&SZRjAyM2R6OWtmNERY)NB_TodwP5#qUa0q} z6&0h{RJDc7XdelThzDwIHEq4GX`IU!K87aRHDZYSayVZeq_heGw}v0EwR2hzx@uP5 oq!@9M2dL{JUW5*IMEntpoMua;bJ}D1`zps<85QX&NigL909B?bn*aa+ literal 0 HcmV?d00001 diff --git a/neode-ui/public/catalog.json b/neode-ui/public/catalog.json index a3c3aaa3..ca1b6a22 100644 --- a/neode-ui/public/catalog.json +++ b/neode-ui/public/catalog.json @@ -298,6 +298,25 @@ ] } }, + { + "id": "barkd", + "title": "Ark Wallet", + "version": "0.3.0", + "description": "Ark protocol wallet daemon (barkd). Lets the node hold self-custodial off-chain bitcoin via an Ark server; the wallet talks to it over a local REST API. Signet by default while Ark matures.", + "icon": "/assets/img/app-icons/bark.png", + "author": "Second", + "category": "money", + "dockerImage": "146.59.87.168:3000/lfg2025/barkd:0.3.0", + "repoUrl": "https://gitlab.com/ark-bitcoin/bark", + "containerConfig": { + "ports": [ + "3535:3535" + ], + "volumes": [ + "/var/lib/archipelago/barkd:/data" + ] + } + }, { "id": "jellyfin", "title": "Jellyfin", diff --git a/neode-ui/scripts/download-app-icons.js b/neode-ui/scripts/download-app-icons.js index 6fa5ca2f..7cbb9d12 100755 --- a/neode-ui/scripts/download-app-icons.js +++ b/neode-ui/scripts/download-app-icons.js @@ -45,6 +45,10 @@ const customIconUrls = { 'fedimint': [ 'https://raw.githubusercontent.com/fedibtc/fedimint-ui/master/apps/router/public/favicon.svg', ], + // Official bark project avatar (Ark protocol wallet daemon) + 'bark': [ + 'https://gitlab.com/uploads/-/system/project/avatar/75519706/bark-smiling-square-white-2.jpg', + ], } const iconDir = path.join(__dirname, '../public/assets/img/app-icons') diff --git a/neode-ui/src/components/TransactionsModal.vue b/neode-ui/src/components/TransactionsModal.vue index 8d174364..a2a831af 100644 --- a/neode-ui/src/components/TransactionsModal.vue +++ b/neode-ui/src/components/TransactionsModal.vue @@ -52,7 +52,7 @@ {{ kindLabel(tx) }} @@ -90,7 +90,7 @@ interface WalletTransaction { label: string block_height: number // Which rail the transaction happened on; absent = onchain (older backends) - kind?: 'onchain' | 'lightning' | 'cashu' | 'fedimint' + kind?: 'onchain' | 'lightning' | 'cashu' | 'fedimint' | 'ark' } defineProps<{ @@ -114,6 +114,7 @@ function kindLabel(tx: WalletTransaction): string { if (tx.kind === 'lightning') return '⚡ Lightning' if (tx.kind === 'cashu') return 'Cashu' if (tx.kind === 'fedimint') return 'Fedimint' + if (tx.kind === 'ark') return 'Ark' return '' } diff --git a/neode-ui/src/components/WalletSettingsModal.vue b/neode-ui/src/components/WalletSettingsModal.vue index 38f5a666..57d403fe 100644 --- a/neode-ui/src/components/WalletSettingsModal.vue +++ b/neode-ui/src/components/WalletSettingsModal.vue @@ -148,11 +148,107 @@ Joining federations lands with the Fedimint client backend.

+ + +
+
+

+ Ark holds self-custodial off-chain bitcoin via an Ark server. Funds stay recoverable on-chain even if the server disappears. +

+ Not installed + {{ arkStatus.config.network }} +
+ +
Checking Ark wallet…
+ + +
diff --git a/neode-ui/src/views/Home.vue b/neode-ui/src/views/Home.vue index fcb963c3..e06072f6 100644 --- a/neode-ui/src/views/Home.vue +++ b/neode-ui/src/views/Home.vue @@ -133,6 +133,7 @@ :wallet-lightning="walletLightning" :wallet-ecash="walletEcash" :wallet-fedimint="walletFedimint" + :wallet-ark="walletArk" :wallet-transactions="walletTransactions" :is-dev="isDev" @show-send="showSendModal = true" @@ -515,6 +516,7 @@ const showSendModal = ref(false); const showReceiveModal = ref(false); const sho async function devFaucet() { try { await rpcClient.call({ method: 'dev.faucet', params: { amount_sats: 1_000_000 } }); await loadWeb5Status() } catch { /* ignore */ } } const walletConnected = ref(false); const walletOnchain = ref(0); const walletLightning = ref(0); const walletEcash = ref(0); const walletFedimint = ref(0) +const walletArk = ref(0) const walletTransactions = ref([]) // Overlay the explorer above the current page — never navigate away. @@ -532,7 +534,7 @@ interface EcashTransaction { description: string mint_url: string peer: string - kind: 'cashu' | 'fedimint' + kind: 'cashu' | 'fedimint' | 'ark' } function ecashToWalletTransaction(tx: EcashTransaction): WalletTransaction { @@ -557,6 +559,7 @@ async function loadWeb5Status() { try { const res = await rpcClient.call<{ balance_sats: number; channel_balance_sats: number }>({ method: 'lnd.getinfo', timeout: 5000 }); walletOnchain.value = res.balance_sats || 0; walletLightning.value = res.channel_balance_sats || 0; walletConnected.value = true } catch { walletConnected.value = false } try { const res = await rpcClient.call<{ balance_sats: number }>({ method: 'wallet.ecash-balance', timeout: 5000 }); walletEcash.value = res.balance_sats ?? 0 } catch { /* keep last-known balance */ } try { const res = await rpcClient.call<{ balance_sats: number }>({ method: 'wallet.fedimint-balance', timeout: 5000 }); walletFedimint.value = res.balance_sats ?? 0 } catch { /* keep last-known balance */ } + try { const res = await rpcClient.call<{ spendable_sats: number }>({ method: 'wallet.ark-balance', timeout: 5000 }); walletArk.value = res.spendable_sats ?? 0 } catch { /* keep last-known balance */ } // Merge LND transactions with ecash/Fedimint history (wallet.ecash-history // already unifies both) — previously only LND transactions were fetched // here, so any Cashu or Fedimint receive (e.g. a TollGate payment) never diff --git a/neode-ui/src/views/home/HomeWalletCard.vue b/neode-ui/src/views/home/HomeWalletCard.vue index 94c5fcbe..2c8d174b 100644 --- a/neode-ui/src/views/home/HomeWalletCard.vue +++ b/neode-ui/src/views/home/HomeWalletCard.vue @@ -136,6 +136,14 @@ {{ walletFedimint.toLocaleString() }} sats + +
+
+ + Ark +
+ {{ (walletArk ?? 0).toLocaleString() }} sats +