Skip to content

Commit ceb8420

Browse files
committed
integration of frigate ephemeral scanning
1 parent b9a4fb7 commit ceb8420

5 files changed

Lines changed: 243 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/v2/src/main.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use bdk_sp::{
1414
hex::{DisplayHex, FromHex},
1515
key::Secp256k1,
1616
script::PushBytesBuf,
17-
secp256k1::{PublicKey, Scalar},
17+
secp256k1::{PublicKey, Scalar, SecretKey},
1818
Address, Amount, Block, BlockHash, FeeRate, Network, OutPoint, PrivateKey, ScriptBuf,
1919
Sequence, Transaction, TxOut, Txid,
2020
},
@@ -34,6 +34,7 @@ use bdk_sp_oracles::{
3434
TrustedPeer, UnboundedReceiver, Warning,
3535
},
3636
filters::kyoto::{FilterEvent, FilterSubscriber},
37+
frigate::{self, FrigateClient, FrigateListener, History, SubscribeRequest},
3738
tweaks::blindbit::{BlindbitSubscriber, TweakEvent},
3839
};
3940
use bdk_sp_wallet::{
@@ -161,6 +162,24 @@ pub enum Commands {
161162
#[clap(long)]
162163
hash: Option<BlockHash>,
163164
},
165+
166+
ScanFrigrate {
167+
#[clap(flatten)]
168+
rpc_args: RpcArgs,
169+
/// The scan private key for which outputs will be scanned for.
170+
#[clap(long)]
171+
scan_priv_key: SecretKey,
172+
/// The spend public key for which outputs will be scanned for.
173+
#[clap(long)]
174+
spend_pub_key: PublicKey,
175+
/// An optional start parameter from where the scanning will start. When not specified it starts from Taproot activation height.
176+
#[clap(long)]
177+
start: Option<u64>,
178+
/// Optional list of labels to scan for. It always scan for change index with label 0.
179+
#[clap(long)]
180+
labels: Option<Vec<u32>>,
181+
},
182+
164183
Create {
165184
/// Network
166185
#[clap(long, short, default_value = "signet")]
@@ -567,6 +586,41 @@ async fn main() -> anyhow::Result<()> {
567586
);
568587
}
569588
}
589+
Commands::ScanFrigrate {
590+
rpc_args,
591+
scan_priv_key,
592+
spend_pub_key,
593+
start,
594+
labels,
595+
} => {
596+
let client = FrigateClient::new(
597+
&rpc_args.url,
598+
rpc_args.rpc_user.as_deref(),
599+
rpc_args.rpc_password.as_deref(),
600+
)?;
601+
// send a subscribe request
602+
let subscribe_params = SubscribeRequest {
603+
scan_priv_key,
604+
spend_pub_key: spend_pub_key.into(),
605+
start_height: start,
606+
labels,
607+
};
608+
609+
let (history_tx, mut history_rx) = tokio::sync::mpsc::unbounded_channel::<History>();
610+
let frigate_listener = FrigateListener::new(client, history_tx);
611+
tokio::task::spawn(async move { frigate_listener.run(subscribe_params).await });
612+
613+
loop {
614+
if let Some(history) = history_rx.recv().await {
615+
if history.progress >= 1.0 {
616+
break;
617+
}
618+
// Make another request to .get the txdetails
619+
// Check that the outputs in the txs really belongs to the wallet
620+
// Update the wallet state with them
621+
}
622+
}
623+
}
570624
Commands::Balance => {
571625
fn print_balances<'a>(
572626
title_str: &'a str,

oracles/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ redb = "2.4.0"
1313
rayon = "1.11.0"
1414
reqwest = { version = "0.12.23", features = ["json", "rustls-tls", "http2", "charset"], default-features = false }
1515
serde = { version = "1.0.219", features = ["serde_derive"] }
16-
serde_json = "1.0.142"
16+
serde_json = { version = "1.0.142", features = ["raw_value"]}
1717
url = "2.5.4"
1818
tracing = "0.1.41"
19+
jsonrpc = "=0.18.0"
1920

2021
[lints]
2122
workspace = true

oracles/src/frigate/mod.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use bip157::tokio;
2+
use bip157::tokio::io::{AsyncReadExt, AsyncWriteExt};
3+
use bip157::tokio::sync::mpsc::UnboundedSender;
4+
use bitcoin::{key::TweakedPublicKey, secp256k1::SecretKey, PublicKey};
5+
use jsonrpc::simple_http::{self, SimpleHttpTransport};
6+
use jsonrpc::Client;
7+
use serde::{Deserialize, Serialize};
8+
use serde_json::value::to_raw_value;
9+
use serde_json::Value;
10+
11+
#[derive(Debug)]
12+
pub enum FrigateError {
13+
JsonRpc(jsonrpc::Error),
14+
ParseUrl(url::ParseError),
15+
Serde(serde_json::Error),
16+
}
17+
18+
impl From<serde_json::Error> for FrigateError {
19+
fn from(value: serde_json::Error) -> Self {
20+
FrigateError::Serde(value)
21+
}
22+
}
23+
24+
impl From<url::ParseError> for FrigateError {
25+
fn from(value: url::ParseError) -> Self {
26+
Self::ParseUrl(value)
27+
}
28+
}
29+
30+
impl From<jsonrpc::Error> for FrigateError {
31+
fn from(value: jsonrpc::Error) -> Self {
32+
Self::JsonRpc(value)
33+
}
34+
}
35+
36+
pub struct FrigateClient {
37+
pub host_url: String,
38+
client: Client,
39+
}
40+
41+
#[derive(Serialize, Deserialize)]
42+
pub struct History {
43+
pub height: u64,
44+
pub tx_hash: bitcoin::BlockHash,
45+
pub tweak_key: TweakedPublicKey,
46+
pub progress: f32,
47+
}
48+
49+
#[derive(Serialize, Deserialize)]
50+
pub struct NotifPayload {
51+
scan_private_key: SecretKey,
52+
spend_public_key: PublicKey,
53+
address: String,
54+
labels: Option<Vec<u32>>,
55+
start_height: u64,
56+
progress: f32,
57+
history: Vec<History>,
58+
}
59+
60+
#[derive(Serialize, Deserialize)]
61+
pub struct SubscribeRequest {
62+
pub scan_priv_key: SecretKey,
63+
pub spend_pub_key: PublicKey,
64+
pub start_height: Option<u64>,
65+
pub labels: Option<Vec<u32>>,
66+
}
67+
68+
#[derive(Serialize, Deserialize)]
69+
pub struct UnsubscribeRequest {
70+
pub scan_privkey: SecretKey,
71+
pub spend_pubkey: PublicKey,
72+
}
73+
74+
#[derive(Serialize, Deserialize)]
75+
pub struct GetRequest {
76+
tx_hash: bitcoin::BlockHash,
77+
}
78+
79+
const SUBSCRIBE_RPC_METHOD: &str = "blockchain.silentpayments.subscribe";
80+
const UNSUBSCRIBE_RPC_METHOD: &str = "blockchain.silentpayments.unsubscribe";
81+
const GET_RPC_METHOD: &str = "blockchain.transaction.get";
82+
const SUBSCRIBE_OWNED_OUTPUTS: &str = "blockchain.scripthash.subscribe";
83+
84+
impl FrigateClient {
85+
pub fn new(
86+
host_url: &str,
87+
user: Option<&str>,
88+
password: Option<&str>,
89+
) -> Result<Self, simple_http::Error> {
90+
let transport = SimpleHttpTransport::builder()
91+
.url(host_url)?
92+
.auth(user.unwrap_or(""), password)
93+
.build();
94+
95+
Ok(Self {
96+
host_url: host_url.to_string(),
97+
client: Client::with_transport(transport),
98+
})
99+
}
100+
101+
pub fn subscribe(&self, req: &SubscribeRequest) -> Result<String, FrigateError> {
102+
let params = to_raw_value(&serde_json::json!(req))?;
103+
let req = self
104+
.client
105+
.build_request(SUBSCRIBE_RPC_METHOD, Some(&params));
106+
let res = self.client.send_request(req)?;
107+
Ok(res.result()?)
108+
}
109+
110+
pub fn unsubscribe(&self, req: &UnsubscribeRequest) -> Result<String, FrigateError> {
111+
let params = to_raw_value(&serde_json::json!(req))?;
112+
let req = self
113+
.client
114+
.build_request(UNSUBSCRIBE_RPC_METHOD, Some(&params));
115+
let res = self.client.send_request(req)?;
116+
Ok(res.result()?)
117+
}
118+
119+
pub fn get(&self, req: &GetRequest) -> Result<String, FrigateError> {
120+
let params = to_raw_value(req)?;
121+
let req = self.client.build_request(GET_RPC_METHOD, Some(&params));
122+
Ok("".to_string())
123+
}
124+
}
125+
126+
pub struct FrigateListener {
127+
frigate_client: FrigateClient,
128+
sender: UnboundedSender<History>,
129+
}
130+
131+
impl FrigateListener {
132+
pub fn new(frigate_client: FrigateClient, sender: UnboundedSender<History>) -> Self {
133+
Self {
134+
frigate_client,
135+
sender,
136+
}
137+
}
138+
139+
pub async fn run(&self, params: SubscribeRequest) -> Result<(), FrigateError> {
140+
let raw_subscribe_params = serde_json::json!(params);
141+
let mut tcp_stream = tokio::net::TcpStream::connect(self.frigate_client.host_url.clone())
142+
.await
143+
.map_err(|e| FrigateError::JsonRpc(jsonrpc::Error::Transport(Box::new(e))))?;
144+
let (mut reader, mut writer) = tcp_stream.split();
145+
146+
writer
147+
.write_all(raw_subscribe_params.to_string().as_bytes())
148+
.await
149+
.map_err(|e| FrigateError::JsonRpc(jsonrpc::Error::Transport(Box::new(e))))?;
150+
151+
loop {
152+
let mut buffer = vec![0; 1024];
153+
let n = reader
154+
.read(&mut buffer)
155+
.await
156+
.map_err(|e| FrigateError::JsonRpc(jsonrpc::Error::Transport(Box::new(e))))?;
157+
if n == 0 {
158+
break;
159+
}
160+
let response_str = String::from_utf8_lossy(&buffer[..n]);
161+
let result: Value = serde_json::from_str(&response_str).map_err(FrigateError::Serde)?;
162+
163+
if result["result"].is_string() {
164+
tracing::info!("Subscribed to silent payment address: {:?}", result);
165+
} else if result["params"].is_object() {
166+
let mut history: History =
167+
serde_json::from_value(result["params"]["history"].clone())
168+
.map_err(FrigateError::Serde)?;
169+
history.progress = result["params"]["progress"].as_f64().unwrap_or(0.0) as f32;
170+
let progress = history.progress;
171+
172+
self.sender
173+
.send(history)
174+
.map_err(|e| FrigateError::JsonRpc(jsonrpc::Error::Transport(Box::new(e))))?;
175+
176+
if progress >= 1.0 {
177+
tracing::info!("Scanning completed");
178+
break;
179+
}
180+
}
181+
}
182+
Ok(())
183+
}
184+
}

oracles/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod filters;
2+
pub mod frigate;
23
pub mod tweaks;
34
pub use bip157;

0 commit comments

Comments
 (0)