Skip to content

Commit 63d2f1b

Browse files
authored
Add PaymentAddress, PubKey, SecretKey, File and Timestamp types, Mint types, no extra encoding (#717)
1 parent 84c6dd8 commit 63d2f1b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+2666
-2136
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
* Document versioning scheme
44
* Rework WASM API to return `TSResult<T> = { Success: T } | { Error: JsErrorData }` without triggering exceptions
55
* Rework `sum` and `currency` into a coherent `Sum` type that's ready for multi-currency and exchange rates (breaking DB change)
6-
* Add strong types for `SchnorrSignature`, `Sha256Hash`, `BlockId` (breaking DB change)
6+
* Refactor the transport layer to distinguish between protocol and the rest and to use `borsh` for serialization on our side
7+
* Add strong types for `SchnorrSignature`, `Sha256Hash`, `BlockId`, `File` types, `Mint` types and use `PublicKey` and `SecretKey` in protocol types (breaking DB change)
8+
* Use bytes without encoding for bill data (breaking DB change)
9+
* Fix plaintext-chain rendering - the nesting of `data` now works properly and one `JSON.parse` call is enough (breaking API change)
710

811
# 0.4.12
912

crates/bcr-ebill-api/src/data/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ pub use bcr_ebill_core::nostr_contact;
1717
pub use bcr_ebill_core::notification;
1818
pub use bcr_ebill_core::signature;
1919
pub use bcr_ebill_core::sum;
20+
pub use bcr_ebill_core::timestamp;
2021
pub use bcr_ebill_core::zip;
2122

23+
pub use bcr_ebill_core::BitcoinAddress;
2224
pub use bcr_ebill_core::File;
2325
pub use bcr_ebill_core::GeneralSearchFilterItemType;
2426
pub use bcr_ebill_core::GeneralSearchResult;

crates/bcr-ebill-api/src/external/bitcoin.rs

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use crate::get_config;
22
use async_trait::async_trait;
33
use bcr_ebill_core::{
4-
PublicKey, ServiceTraitBounds,
4+
BitcoinAddress, PublicKey, ServiceTraitBounds,
55
bill::{InMempoolData, PaidData, PaymentState},
66
sum::Sum,
7+
timestamp::Timestamp,
78
};
89
use bitcoin::{Network, secp256k1::Scalar};
910
use log::debug;
@@ -46,35 +47,35 @@ use mockall::automock;
4647
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
4748
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
4849
pub trait BitcoinClientApi: ServiceTraitBounds {
49-
async fn get_address_info(&self, address: &str) -> Result<AddressInfo>;
50+
async fn get_address_info(&self, address: &BitcoinAddress) -> Result<AddressInfo>;
5051

51-
async fn get_transactions(&self, address: &str) -> Result<Transactions>;
52+
async fn get_transactions(&self, address: &BitcoinAddress) -> Result<Transactions>;
5253

5354
async fn get_last_block_height(&self) -> Result<u64>;
5455

5556
/// Checks payment by iterating over the transactions on the address in chronological order, until
5657
/// the target amount is filled, returning the respective payment status
5758
async fn check_payment_for_address(
5859
&self,
59-
address: &str,
60+
address: &BitcoinAddress,
6061
target_amount: u64,
6162
) -> Result<PaymentState>;
6263

6364
fn get_address_to_pay(
6465
&self,
6566
bill_public_key: &PublicKey,
6667
holder_public_key: &PublicKey,
67-
) -> Result<String>;
68+
) -> Result<BitcoinAddress>;
6869

69-
fn generate_link_to_pay(&self, address: &str, sum: &Sum, message: &str) -> String;
70+
fn generate_link_to_pay(&self, address: &BitcoinAddress, sum: &Sum, message: &str) -> String;
7071

7172
fn get_combined_private_descriptor(
7273
&self,
7374
pkey: &bitcoin::PrivateKey,
7475
pkey_to_combine: &bitcoin::PrivateKey,
7576
) -> Result<String>;
7677

77-
fn get_mempool_link_for_address(&self, address: &str) -> String;
78+
fn get_mempool_link_for_address(&self, address: &BitcoinAddress) -> String;
7879
}
7980

8081
#[derive(Clone)]
@@ -134,10 +135,11 @@ impl Default for BitcoinClient {
134135
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
135136
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
136137
impl BitcoinClientApi for BitcoinClient {
137-
async fn get_address_info(&self, address: &str) -> Result<AddressInfo> {
138+
async fn get_address_info(&self, address: &BitcoinAddress) -> Result<AddressInfo> {
139+
let addr_str = address.assume_checked_ref().to_string();
138140
let address: AddressInfo = self
139141
.cl
140-
.get(self.request_url(&format!("/address/{address}")))
142+
.get(self.request_url(&format!("/address/{addr_str}")))
141143
.send()
142144
.await
143145
.map_err(Error::from)?
@@ -148,10 +150,11 @@ impl BitcoinClientApi for BitcoinClient {
148150
Ok(address)
149151
}
150152

151-
async fn get_transactions(&self, address: &str) -> Result<Transactions> {
153+
async fn get_transactions(&self, address: &BitcoinAddress) -> Result<Transactions> {
154+
let addr_str = address.assume_checked_ref().to_string();
152155
let transactions: Transactions = self
153156
.cl
154-
.get(self.request_url(&format!("/address/{address}/txs")))
157+
.get(self.request_url(&format!("/address/{addr_str}/txs")))
155158
.send()
156159
.await
157160
.map_err(Error::from)?
@@ -176,10 +179,13 @@ impl BitcoinClientApi for BitcoinClient {
176179

177180
async fn check_payment_for_address(
178181
&self,
179-
address: &str,
182+
address: &BitcoinAddress,
180183
target_amount: u64,
181184
) -> Result<PaymentState> {
182-
debug!("checking if btc address {address} is paid {target_amount}");
185+
debug!(
186+
"checking if btc address {} is paid {target_amount}",
187+
address.assume_checked_ref()
188+
);
183189
// in parallel, get current chain height, transactions and address info for the given address
184190
let (chain_block_height, txs) =
185191
try_join!(self.get_last_block_height(), self.get_transactions(address),)?;
@@ -191,7 +197,7 @@ impl BitcoinClientApi for BitcoinClient {
191197
&self,
192198
bill_public_key: &PublicKey,
193199
holder_public_key: &PublicKey,
194-
) -> Result<String> {
200+
) -> Result<BitcoinAddress> {
195201
let public_key_bill = bitcoin::CompressedPublicKey(*bill_public_key);
196202
let public_key_bill_holder = bitcoin::CompressedPublicKey(*holder_public_key);
197203

@@ -201,12 +207,19 @@ impl BitcoinClientApi for BitcoinClient {
201207
.map_err(Error::from)?;
202208
let pub_key_bill = bitcoin::CompressedPublicKey(public_key_bill);
203209

204-
Ok(bitcoin::Address::p2wpkh(&pub_key_bill, get_config().bitcoin_network()).to_string())
210+
Ok(
211+
bitcoin::Address::p2wpkh(&pub_key_bill, get_config().bitcoin_network())
212+
.as_unchecked()
213+
.to_owned(),
214+
)
205215
}
206216

207-
fn generate_link_to_pay(&self, address: &str, sum: &Sum, message: &str) -> String {
217+
fn generate_link_to_pay(&self, address: &BitcoinAddress, sum: &Sum, message: &str) -> String {
208218
let btc_sum = sum.as_btc_string();
209-
let link = format!("bitcoin:{address}?amount={btc_sum}&message={message}");
219+
let link = format!(
220+
"bitcoin:{}?amount={btc_sum}&message={message}",
221+
address.assume_checked_ref()
222+
);
210223
link
211224
}
212225

@@ -234,21 +247,22 @@ impl BitcoinClientApi for BitcoinClient {
234247
Ok(desc.to_string_with_secret(&kmap))
235248
}
236249

237-
fn get_mempool_link_for_address(&self, address: &str) -> String {
238-
self.link_url(&format!("/address/{address}"))
250+
fn get_mempool_link_for_address(&self, address: &BitcoinAddress) -> String {
251+
self.link_url(&format!("/address/{}", address.assume_checked_ref()))
239252
}
240253
}
241254

242255
fn payment_state_from_transactions(
243256
chain_block_height: u64,
244257
txs: Transactions,
245-
address: &str,
258+
address: &BitcoinAddress,
246259
target_amount: u64,
247260
) -> Result<PaymentState> {
248261
// no transactions - no payment
249262
if txs.is_empty() {
250263
return Ok(PaymentState::NotFound);
251264
}
265+
let addr_string = address.assume_checked_ref().to_string();
252266

253267
let mut total = 0;
254268
let mut tx_filled = None;
@@ -258,7 +272,7 @@ fn payment_state_from_transactions(
258272
for vout in tx.vout.iter() {
259273
// sum up outputs towards the address to check
260274
if let Some(ref addr) = vout.scriptpubkey_address
261-
&& addr == address
275+
&& addr == &addr_string
262276
{
263277
total += vout.value;
264278
}
@@ -274,7 +288,7 @@ fn payment_state_from_transactions(
274288
Some(tx) => {
275289
// in mem pool
276290
if !tx.status.confirmed {
277-
debug!("payment for {address} is in mem pool {}", tx.txid);
291+
debug!("payment for {addr_string} is in mem pool {}", tx.txid);
278292
Ok(PaymentState::InMempool(InMempoolData { tx_id: tx.txid }))
279293
} else {
280294
match (
@@ -285,7 +299,7 @@ fn payment_state_from_transactions(
285299
(Some(block_height), Some(block_time), Some(block_hash)) => {
286300
let confirmations = chain_block_height - block_height + 1;
287301
let paid_data = PaidData {
288-
block_time,
302+
block_time: Timestamp::new(block_time).map_err(|_| Error::InvalidData(format!("Invalid data when checking payment for {addr_string} - invalid block time")))?,
289303
block_hash,
290304
confirmations,
291305
tx_id: tx.txid,
@@ -295,30 +309,30 @@ fn payment_state_from_transactions(
295309
{
296310
// paid and confirmed
297311
debug!(
298-
"payment for {address} is paid and confirmed with {confirmations} confirmations"
312+
"payment for {addr_string} is paid and confirmed with {confirmations} confirmations"
299313
);
300314
Ok(PaymentState::PaidConfirmed(paid_data))
301315
} else {
302316
// paid but not enough confirmations yet
303317
debug!(
304-
"payment for {address} is paid and unconfirmed with {confirmations} confirmations"
318+
"payment for {addr_string} is paid and unconfirmed with {confirmations} confirmations"
305319
);
306320
Ok(PaymentState::PaidUnconfirmed(paid_data))
307321
}
308322
}
309323
_ => {
310324
log::error!(
311-
"Invalid data when checking payment for {address} - confirmed tx, but no metadata"
325+
"Invalid data when checking payment for {addr_string} - confirmed tx, but no metadata"
312326
);
313-
Err(Error::InvalidData(format!("Invalid data when checking payment for {address} - confirmed tx, but no metadata")).into())
327+
Err(Error::InvalidData(format!("Invalid data when checking payment for {addr_string} - confirmed tx, but no metadata")).into())
314328
}
315329
}
316330
}
317331
}
318332
None => {
319333
// not enough funds to cover amount
320334
debug!(
321-
"Not enough funds to cover {target_amount} yet when checking payment for {address}: {total}"
335+
"Not enough funds to cover {target_amount} yet when checking payment for {addr_string}: {total}"
322336
);
323337
Ok(PaymentState::NotFound)
324338
}
@@ -369,14 +383,15 @@ pub struct Status {
369383
#[cfg(test)]
370384
pub mod tests {
371385
use crate::tests::tests::init_test_cfg;
386+
use std::str::FromStr;
372387

373388
use super::*;
374389

375390
#[test]
376391
fn test_payment_state_from_transactions() {
377392
init_test_cfg();
378393
let test_height = 4578915;
379-
let test_addr = "n4n9CNeCkgtEs8wukKEvWC78eEqK4A3E6d";
394+
let test_addr = BitcoinAddress::from_str("n4n9CNeCkgtEs8wukKEvWC78eEqK4A3E6d").unwrap();
380395
let test_amount = 500;
381396
let mut test_tx = Tx {
382397
txid: "".into(),
@@ -390,18 +405,18 @@ pub mod tests {
390405
},
391406
vout: vec![Vout {
392407
value: 500,
393-
scriptpubkey_address: Some(test_addr.to_owned()),
408+
scriptpubkey_address: Some(test_addr.assume_checked_ref().to_string()),
394409
}],
395410
};
396411

397412
let res_empty =
398-
payment_state_from_transactions(test_height, vec![], test_addr, test_amount);
413+
payment_state_from_transactions(test_height, vec![], &test_addr, test_amount);
399414
assert!(matches!(res_empty, Ok(PaymentState::NotFound)));
400415

401416
let res_paid_confirmed = payment_state_from_transactions(
402417
test_height,
403418
vec![test_tx.clone()],
404-
test_addr,
419+
&test_addr,
405420
test_amount,
406421
);
407422
assert!(matches!(
@@ -413,7 +428,7 @@ pub mod tests {
413428
let res_paid_unconfirmed = payment_state_from_transactions(
414429
test_height,
415430
vec![test_tx.clone()],
416-
test_addr,
431+
&test_addr,
417432
test_amount,
418433
);
419434
assert!(matches!(
@@ -427,7 +442,7 @@ pub mod tests {
427442
let res_paid_confirmed_no_data = payment_state_from_transactions(
428443
test_height,
429444
vec![test_tx.clone()],
430-
test_addr,
445+
&test_addr,
431446
test_amount,
432447
);
433448
assert!(matches!(
@@ -441,7 +456,7 @@ pub mod tests {
441456
let res_in_mem_pool = payment_state_from_transactions(
442457
test_height,
443458
vec![test_tx.clone()],
444-
test_addr,
459+
&test_addr,
445460
test_amount,
446461
);
447462
assert!(matches!(res_in_mem_pool, Ok(PaymentState::InMempool(..))));
@@ -450,7 +465,7 @@ pub mod tests {
450465
let res_not_filled = payment_state_from_transactions(
451466
test_height,
452467
vec![test_tx.clone()],
453-
test_addr,
468+
&test_addr,
454469
test_amount,
455470
);
456471
assert!(matches!(res_not_filled, Ok(PaymentState::NotFound)));

crates/bcr-ebill-api/src/external/file_storage.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use async_trait::async_trait;
44
use bcr_ebill_core::ServiceTraitBounds;
55
use nostr::hashes::{
66
Hash,
7-
sha256::{self, Hash as Sha256Hash},
7+
sha256::{self, Hash as Sha256HexHash},
88
};
99
use reqwest::Url;
1010
use serde::Deserialize;
@@ -38,9 +38,9 @@ use mockall::automock;
3838
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
3939
pub trait FileStorageClientApi: ServiceTraitBounds {
4040
/// Upload the given bytes, checking and returning the nostr_hash
41-
async fn upload(&self, relay_url: &url::Url, bytes: Vec<u8>) -> Result<Sha256Hash>;
41+
async fn upload(&self, relay_url: &url::Url, bytes: Vec<u8>) -> Result<Sha256HexHash>;
4242
/// Download the bytes with the given nostr_hash and compare if the hash matches the file
43-
async fn download(&self, relay_url: &url::Url, nostr_hash: &str) -> Result<Vec<u8>>;
43+
async fn download(&self, relay_url: &url::Url, nostr_hash: &Sha256HexHash) -> Result<Vec<u8>>;
4444
}
4545

4646
#[derive(Debug, Clone, Default)]
@@ -81,7 +81,7 @@ pub fn to_url(relay_url: &url::Url, to_join: &str) -> Result<Url> {
8181
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
8282
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
8383
impl FileStorageClientApi for FileStorageClient {
84-
async fn upload(&self, relay_url: &url::Url, bytes: Vec<u8>) -> Result<Sha256Hash> {
84+
async fn upload(&self, relay_url: &url::Url, bytes: Vec<u8>) -> Result<Sha256HexHash> {
8585
// Calculate hash to compare with the hash we get back
8686
let mut hash_engine = sha256::HashEngine::default();
8787
if hash_engine.write_all(&bytes).is_err() {
@@ -108,11 +108,11 @@ impl FileStorageClientApi for FileStorageClient {
108108
Ok(nostr_hash)
109109
}
110110

111-
async fn download(&self, relay_url: &url::Url, nostr_hash: &str) -> Result<Vec<u8>> {
111+
async fn download(&self, relay_url: &url::Url, nostr_hash: &Sha256HexHash) -> Result<Vec<u8>> {
112112
// Make download request
113113
let resp: Vec<u8> = self
114114
.cl
115-
.get(to_url(relay_url, nostr_hash)?)
115+
.get(to_url(relay_url, &nostr_hash.to_string())?)
116116
.send()
117117
.await?
118118
.bytes()
@@ -127,7 +127,7 @@ impl FileStorageClientApi for FileStorageClient {
127127

128128
// Check hash
129129
let hash = sha256::Hash::from_engine(hash_engine);
130-
if hash.to_string() != nostr_hash {
130+
if &hash != nostr_hash {
131131
return Err(Error::InvalidHash.into());
132132
}
133133

@@ -137,5 +137,5 @@ impl FileStorageClientApi for FileStorageClient {
137137

138138
#[derive(Debug, Clone, Deserialize)]
139139
pub struct BlobDescriptorReply {
140-
sha256: Sha256Hash,
140+
sha256: Sha256HexHash,
141141
}

0 commit comments

Comments
 (0)