-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add bria-client to stablesats #411
Changes from 21 commits
0f78ef5
a8307fa
09f68e1
5567aa4
f089fd6
e228bac
168901d
2fc0e3c
23e5bc7
9ef98eb
2c5b60d
cb6d6c4
3eeb951
5a1d25f
be9643d
410e984
26169f1
2d66f03
54e20eb
a54c6f0
7963fd5
3690e79
0ff6553
ce719db
3638486
28ca36c
c3a3d8e
deaee76
aa31bf4
afefa21
8c66cb9
16a7f8c
94c3611
7974c0a
3e436a3
c5cbb15
24398af
21d97b2
8c5fa08
d469313
7a7d0da
a60f7fd
0c71d60
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,4 +12,5 @@ members = [ | |
"bitfinex-client", | ||
"bitfinex-price", | ||
"galoy-client", | ||
"bria-client", | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
[package] | ||
name = "bria-client" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[features] | ||
|
||
fail-on-warnings = [] | ||
|
||
[dependencies] | ||
shared = { path = "../shared", package = "stablesats-shared" } | ||
|
||
anyhow = "1.0.72" | ||
chrono = { version = "0.4", features = [ | ||
"clock", | ||
"serde", | ||
], default-features = false } | ||
prost = "0.11" | ||
tonic = "0.9" | ||
axum-core = "0.3.4" | ||
tokio = "1.29.1" | ||
futures = "0.3.27" | ||
thiserror = "1.0.40" | ||
serde = { version = "1.0.158", features = ["derive"] } | ||
rust_decimal = "1.29.0" | ||
tracing = "0.1.37" | ||
opentelemetry = { version = "0.18.0", features = ["trace"] } | ||
tracing-opentelemetry = "0.18.0" | ||
rust_decimal_macros = "1.29.0" | ||
rusty-money = "0.4.1" | ||
serde_json = "1.0.93" | ||
serde_with = { version = "2.3.1", features = ["chrono_0_4"] } | ||
url = "2.4.0" | ||
async-trait = "0.1.67" | ||
prost-wkt-types = { version = "0.4.2", features = ["vendored-protoc"]} | ||
|
||
[build-dependencies] | ||
protobuf-src = { version = "1.1.0" } | ||
tonic-build = { version = "0.8", features = ["prost"] } | ||
|
||
[dev-dependencies] | ||
anyhow = "1.0.70" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
println!("cargo:rerun-if-changed=migrations"); | ||
std::env::set_var("PROTOC", protobuf_src::protoc()); | ||
|
||
tonic_build::configure() | ||
.type_attribute(".", "#[derive(serde::Serialize)]") | ||
.type_attribute(".", "#[serde(rename_all = \"camelCase\")]") | ||
.extern_path(".google.protobuf.Struct", "::prost_wkt_types::Struct") | ||
.compile(&["../proto/bria/bria_service.proto"], &["../proto"])?; | ||
Ok(()) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
use super::{config::BriaClientConfig, proto}; | ||
use crate::error::BriaClientError; | ||
|
||
type ProtoClient = proto::bria_service_client::BriaServiceClient<tonic::transport::Channel>; | ||
|
||
pub const PROFILE_API_KEY_HEADER: &str = "x-bria-api-key"; | ||
|
||
#[derive(Debug)] | ||
pub struct OnchainAddress { | ||
pub address: String, | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
pub struct BriaClient { | ||
config: BriaClientConfig, | ||
proto_client: ProtoClient, | ||
} | ||
|
||
impl BriaClient { | ||
pub async fn connect(config: BriaClientConfig) -> Result<Self, BriaClientError> { | ||
let proto_client = ProtoClient::connect(config.url.clone()) | ||
.await | ||
.map_err(|_| BriaClientError::ConnectionError(config.url.clone()))?; | ||
|
||
if config.key.is_empty() { | ||
return Err(BriaClientError::EmptyKey); | ||
} | ||
|
||
Ok(Self { | ||
config, | ||
proto_client, | ||
}) | ||
} | ||
|
||
pub fn inject_auth_token<T>( | ||
&self, | ||
mut request: tonic::Request<T>, | ||
) -> Result<tonic::Request<T>, BriaClientError> { | ||
let key = &self.config.key; | ||
request.metadata_mut().insert( | ||
PROFILE_API_KEY_HEADER, | ||
tonic::metadata::MetadataValue::try_from(key) | ||
.map_err(|_| BriaClientError::CouldNotCreateMetadataValue)?, | ||
); | ||
Ok(request) | ||
} | ||
|
||
pub async fn onchain_address(&mut self) -> Result<OnchainAddress, BriaClientError> { | ||
match self.get_address().await { | ||
Ok(addr) => Ok(addr), | ||
Err(_) => self.new_address().await, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't eat this error - only do new_address if the error is NotFound. |
||
} | ||
} | ||
|
||
async fn get_address(&mut self) -> Result<OnchainAddress, BriaClientError> { | ||
let request = tonic::Request::new(proto::GetAddressRequest { | ||
identifier: Some(proto::get_address_request::Identifier::ExternalId( | ||
self.config.external_id.clone(), | ||
)), | ||
}); | ||
|
||
self.proto_client | ||
.get_address(self.inject_auth_token(request)?) | ||
.await | ||
.ok() | ||
.and_then(|res| { | ||
res.into_inner() | ||
.address | ||
.map(|addr| OnchainAddress { address: addr }) | ||
}) | ||
.ok_or(BriaClientError::AddressNotFound) | ||
} | ||
|
||
async fn new_address(&mut self) -> Result<OnchainAddress, BriaClientError> { | ||
let request = tonic::Request::new(proto::NewAddressRequest { | ||
wallet_name: self.config.wallet_name.clone(), | ||
external_id: Some(self.config.external_id.clone()), | ||
metadata: None, | ||
}); | ||
self.proto_client | ||
.new_address(self.inject_auth_token(request)?) | ||
.await | ||
.map(|res| OnchainAddress { | ||
address: res.into_inner().address, | ||
}) | ||
.map_err(|e| BriaClientError::CouldNotGenerateNewAddress(e.message().to_string())) | ||
} | ||
|
||
pub async fn send_onchain_payment( | ||
&mut self, | ||
destination: String, | ||
satoshis: u64, | ||
metadata: Option<serde_json::Value>, | ||
) -> Result<String, BriaClientError> { | ||
let request = tonic::Request::new(proto::SubmitPayoutRequest { | ||
wallet_name: self.config.wallet_name.clone(), | ||
payout_queue_name: self.config.payout_queue_name.clone(), | ||
destination: Some(proto::submit_payout_request::Destination::OnchainAddress( | ||
destination, | ||
)), | ||
satoshis, | ||
external_id: None, | ||
metadata: metadata.map(serde_json::from_value).transpose()?, | ||
}); | ||
|
||
let response = self | ||
.proto_client | ||
.submit_payout(self.inject_auth_token(request)?) | ||
.await | ||
.map_err(|e| BriaClientError::CouldNotSendOnchainPayment(e.message().to_string()))?; | ||
Ok(response.into_inner().id) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
use serde::{Deserialize, Serialize}; | ||
|
||
#[derive(Debug, Clone, Deserialize, Serialize, Default)] | ||
pub struct BriaClientConfig { | ||
#[serde(default = "default_url")] | ||
pub url: String, | ||
#[serde(default)] | ||
pub key: String, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we be more specific here? |
||
#[serde(default)] | ||
pub wallet_name: String, | ||
#[serde(default)] | ||
pub payout_queue_name: String, | ||
#[serde(default)] | ||
pub external_id: String, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about |
||
} | ||
|
||
fn default_url() -> String { | ||
"http://localhost:2742".to_string() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
#[allow(clippy::all)] | ||
pub mod proto { | ||
tonic::include_proto!("services.bria.v1"); | ||
} | ||
|
||
mod bria_client; | ||
mod config; | ||
|
||
pub use bria_client::*; | ||
pub use config::*; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
use thiserror::Error; | ||
|
||
#[derive(Error, Debug)] | ||
pub enum BriaClientError { | ||
#[error("Couldn't connect to bria at url: {0}")] | ||
ConnectionError(String), | ||
#[error("Bria key cannot be empty")] | ||
EmptyKey, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we should need this error. |
||
#[error("Couldn't create MetadataValue")] | ||
CouldNotCreateMetadataValue, | ||
#[error("Couldn't find address for the given external_id")] | ||
AddressNotFound, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When is this returned? This should be handled internally. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when we call |
||
#[error("Couldn't generate a new address: {0}")] | ||
CouldNotGenerateNewAddress(String), | ||
#[error("Couldn't send onchain payment: {0}")] | ||
CouldNotSendOnchainPayment(String), | ||
#[error("Could not parse Send Onchain Payment Metadata: {0}")] | ||
CouldNotParseSendOnchainPaymentMetadata(serde_json::Error), | ||
} | ||
|
||
impl From<serde_json::Error> for BriaClientError { | ||
fn from(err: serde_json::Error) -> BriaClientError { | ||
BriaClientError::CouldNotParseSendOnchainPaymentMetadata(err) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is too late to validate. It should be asserted in cli.