• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

payjoin / rust-payjoin / 21486039766

29 Jan 2026 04:23PM UTC coverage: 83.673% (+0.5%) from 83.203%
21486039766

Pull #1296

github

web-flow
Merge 741fb9cbc into 1b6cc38b5
Pull Request #1296: Add standalone metrics service to Payjoin-service

130 of 144 new or added lines in 7 files covered. (90.28%)

18 existing lines in 3 files now uncovered.

10224 of 12219 relevant lines covered (83.67%)

431.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.07
/payjoin-test-utils/src/lib.rs
1
use std::result::Result;
2
use std::str::FromStr;
3
use std::sync::Arc;
4
use std::time::Duration;
5

6
use axum_server::tls_rustls::RustlsConfig;
7
use bitcoin::{Amount, Psbt};
8
pub use corepc_node; // re-export for convenience
9
use corepc_node::AddressType;
10
use http::StatusCode;
11
use ohttp::hpke::{Aead, Kdf, Kem};
12
use ohttp::{KeyId, SymmetricSuite};
13
use once_cell::sync::{Lazy, OnceCell};
14
use payjoin::io::{fetch_ohttp_keys_with_cert, Error as IOError};
15
use payjoin::OhttpKeys;
16
use rcgen::Certificate;
17
use reqwest::{Client, ClientBuilder};
18
use rustls::pki_types::CertificateDer;
19
use rustls::RootCertStore;
20
use tempfile::tempdir;
21
use tokio::task::JoinHandle;
22
use tracing::Level;
23
use tracing_subscriber::{EnvFilter, FmtSubscriber};
24

25
pub type BoxError = Box<dyn std::error::Error + 'static>;
26
pub type BoxSendSyncError = Box<dyn std::error::Error + Send + Sync>;
27

28
pub use payjoin::persist::test_utils::InMemoryTestPersister;
29
pub use payjoin::persist::SessionPersister;
30

31
static INIT_TRACING: OnceCell<()> = OnceCell::new();
32

33
pub fn init_tracing() {
17✔
34
    INIT_TRACING.get_or_init(|| {
17✔
35
        let subscriber = FmtSubscriber::builder()
2✔
36
            .with_env_filter(EnvFilter::from_default_env())
2✔
37
            .with_test_writer()
2✔
38
            .finish();
2✔
39

40
        tracing::subscriber::set_global_default(subscriber)
2✔
41
            .expect("failed to set global default subscriber");
2✔
42
    });
2✔
43
}
17✔
44

45
pub struct TestServices {
46
    cert: Certificate,
47
    directory: (u16, Option<JoinHandle<Result<(), BoxSendSyncError>>>),
48
    ohttp_relay: (u16, Option<JoinHandle<Result<(), BoxSendSyncError>>>),
49
    http_agent: Arc<Client>,
50
}
51

52
impl TestServices {
53
    pub async fn initialize() -> Result<Self, BoxSendSyncError> {
10✔
54
        // TODO add a UUID, and cleanup guard to delete after on successful run
55
        let cert = local_cert_key();
10✔
56
        let cert_der = cert.cert.der().to_vec();
10✔
57
        let key_der = cert.signing_key.serialize_der();
10✔
58
        let cert_key = (cert_der.clone(), key_der);
10✔
59

60
        let mut root_store = RootCertStore::empty();
10✔
61
        root_store.add(CertificateDer::from(cert.cert.der().to_vec())).unwrap();
10✔
62

63
        let directory = init_directory(cert_key, root_store.clone()).await?;
10✔
64
        let ohttp_relay = init_ohttp_relay(root_store).await?;
10✔
65

66
        let http_agent: Arc<Client> = Arc::new(http_agent(cert_der)?);
10✔
67

68
        Ok(Self {
10✔
69
            cert: cert.cert,
10✔
70
            directory: (directory.0, Some(directory.1)),
10✔
71
            ohttp_relay: (ohttp_relay.0, Some(ohttp_relay.1)),
10✔
72
            http_agent,
10✔
73
        })
10✔
74
    }
10✔
75

76
    pub fn cert(&self) -> Vec<u8> { self.cert.der().to_vec() }
10✔
77

78
    pub fn directory_url(&self) -> String { format!("https://localhost:{}", self.directory.0) }
27✔
79

80
    pub fn take_directory_handle(&mut self) -> JoinHandle<Result<(), BoxSendSyncError>> {
9✔
81
        self.directory.1.take().expect("directory handle not found")
9✔
82
    }
9✔
83

84
    pub fn ohttp_relay_url(&self) -> String { format!("http://localhost:{}", self.ohttp_relay.0) }
49✔
85

UNCOV
86
    pub fn ohttp_gateway_url(&self) -> String {
×
87
        format!("{}/.well-known/ohttp-gateway", self.directory_url())
×
88
    }
×
89

90
    pub fn take_ohttp_relay_handle(&mut self) -> JoinHandle<Result<(), BoxSendSyncError>> {
8✔
91
        self.ohttp_relay.1.take().expect("ohttp relay handle not found")
8✔
92
    }
8✔
93

94
    pub fn http_agent(&self) -> Arc<Client> { self.http_agent.clone() }
27✔
95

96
    pub async fn wait_for_services_ready(&self) -> Result<(), &'static str> {
10✔
97
        wait_for_service_ready(&self.ohttp_relay_url(), self.http_agent()).await?;
10✔
98
        wait_for_service_ready(&self.directory_url(), self.http_agent()).await?;
10✔
99
        Ok(())
10✔
100
    }
10✔
101

102
    pub async fn fetch_ohttp_keys(&self) -> Result<OhttpKeys, IOError> {
8✔
103
        fetch_ohttp_keys_with_cert(
8✔
104
            self.ohttp_relay_url().as_str(),
8✔
105
            self.directory_url().as_str(),
8✔
106
            self.cert(),
8✔
107
        )
8✔
108
        .await
8✔
109
    }
8✔
110
}
111

112
pub async fn init_directory(
10✔
113
    local_cert_key: (Vec<u8>, Vec<u8>),
10✔
114
    root_store: RootCertStore,
10✔
115
) -> std::result::Result<
10✔
116
    (u16, tokio::task::JoinHandle<std::result::Result<(), BoxSendSyncError>>),
10✔
117
    BoxSendSyncError,
10✔
118
> {
10✔
119
    let tempdir = tempdir()?;
10✔
120
    let config = payjoin_service::config::Config {
10✔
121
        listener: "[::]:0".parse().expect("valid listener address"), // let OS assign a free port
10✔
122
        storage_dir: tempdir.path().to_path_buf(),
10✔
123
        timeout: Duration::from_secs(2),
10✔
124
        metrics_port: 0,
10✔
125
    };
10✔
126

127
    let tls_config = RustlsConfig::from_der(vec![local_cert_key.0], local_cert_key.1).await?;
10✔
128

129
    let (port, _metrics_port, handle) =
10✔
130
        payjoin_service::serve_manual_tls(config, Some(tls_config), root_store)
10✔
131
            .await
10✔
132
            .map_err(|e| e.to_string())?;
10✔
133

134
    let handle = tokio::spawn(async move {
10✔
135
        let _tempdir = tempdir; // keep the tempdir until the directory shuts down
10✔
136
        handle.await.map_err(|e| e.to_string())?.map_err(|e| e.to_string().into())
10✔
UNCOV
137
    });
×
138

139
    Ok((port, handle))
10✔
140
}
10✔
141

142
async fn init_ohttp_relay(
10✔
143
    root_store: RootCertStore,
10✔
144
) -> std::result::Result<
10✔
145
    (u16, tokio::task::JoinHandle<std::result::Result<(), BoxSendSyncError>>),
10✔
146
    BoxSendSyncError,
10✔
147
> {
10✔
148
    let tempdir = tempdir()?;
10✔
149
    let config = payjoin_service::config::Config {
10✔
150
        listener: "[::]:0".parse().expect("valid listener address"), // let OS assign a free port
10✔
151
        storage_dir: tempdir.path().to_path_buf(),
10✔
152
        timeout: Duration::from_secs(2),
10✔
153
        metrics_port: 0,
10✔
154
    };
10✔
155

156
    let (port, _metrics_port, handle) = payjoin_service::serve_manual_tls(config, None, root_store)
10✔
157
        .await
10✔
158
        .map_err(|e| e.to_string())?;
10✔
159

160
    let handle = tokio::spawn(async move {
10✔
161
        let _tempdir = tempdir; // keep the tempdir until the relay shuts down
10✔
162
        handle.await.map_err(|e| e.to_string())?.map_err(|e| e.to_string().into())
10✔
UNCOV
163
    });
×
164

165
    Ok((port, handle))
10✔
166
}
10✔
167

168
/// generate or get a DER encoded localhost cert and key.
169
pub fn local_cert_key() -> rcgen::CertifiedKey<rcgen::KeyPair> {
16✔
170
    rcgen::generate_simple_self_signed(vec!["0.0.0.0".to_string(), "localhost".to_string()])
16✔
171
        .expect("Failed to generate cert")
16✔
172
}
16✔
173

174
pub fn init_bitcoind() -> Result<corepc_node::Node, BoxError> {
19✔
175
    let bitcoind_exe = corepc_node::exe_path()?;
19✔
176
    let mut conf = corepc_node::Conf::default();
19✔
177
    conf.view_stdout = tracing::enabled!(target: "corepc", Level::TRACE);
19✔
178
    // conf.args.push("-txindex");
179
    let bitcoind = corepc_node::Node::with_conf(bitcoind_exe, &conf)?;
19✔
180
    Ok(bitcoind)
19✔
181
}
19✔
182

183
pub fn init_bitcoind_sender_receiver(
19✔
184
    sender_address_type: Option<AddressType>,
19✔
185
    receiver_address_type: Option<AddressType>,
19✔
186
) -> Result<(corepc_node::Node, corepc_node::Client, corepc_node::Client), BoxError> {
19✔
187
    let bitcoind = init_bitcoind()?;
19✔
188
    let mut wallets = create_and_fund_wallets(
19✔
189
        &bitcoind,
19✔
190
        vec![("receiver", receiver_address_type), ("sender", sender_address_type)],
19✔
UNCOV
191
    )?;
×
192
    let receiver = wallets.pop().expect("receiver to exist");
19✔
193
    let sender = wallets.pop().expect("sender to exist");
19✔
194

195
    Ok((bitcoind, receiver, sender))
19✔
196
}
19✔
197

198
fn create_and_fund_wallets<W: AsRef<str>>(
19✔
199
    bitcoind: &corepc_node::Node,
19✔
200
    wallets: Vec<(W, Option<AddressType>)>,
19✔
201
) -> Result<Vec<corepc_node::Client>, BoxError> {
19✔
202
    let mut funded_wallets = vec![];
19✔
203
    let funding_wallet = bitcoind.create_wallet("funding_wallet")?;
19✔
204
    let funding_address = funding_wallet.new_address()?;
19✔
205
    // 100 blocks would work here, we add a extra block to cover fees between transfers
206
    bitcoind.client.generate_to_address(101 + wallets.len(), &funding_address)?;
19✔
207
    for (wallet_name, address_type) in wallets {
38✔
208
        let wallet = bitcoind.create_wallet(wallet_name)?;
38✔
209
        let address = wallet.get_new_address(None, address_type)?.into_model()?.0.assume_checked();
38✔
210
        funding_wallet.send_to_address(&address, Amount::from_btc(50.0)?)?;
38✔
211
        funded_wallets.push(wallet);
38✔
212
    }
213
    // Mine the block which funds the different wallets
214
    bitcoind.client.generate_to_address(1, &funding_address)?;
19✔
215

216
    for wallet in funded_wallets.iter() {
38✔
217
        let balances = wallet.get_balances()?.into_model()?;
38✔
218
        assert_eq!(
38✔
219
            balances.mine.trusted,
220
            Amount::from_btc(50.0)?,
38✔
221
            "wallet doesn't have expected amount of bitcoin"
222
        );
223
    }
224

225
    Ok(funded_wallets)
19✔
226
}
19✔
227

228
pub fn http_agent(cert_der: Vec<u8>) -> Result<Client, BoxSendSyncError> {
13✔
229
    Ok(http_agent_builder(cert_der).build()?)
13✔
230
}
13✔
231

UNCOV
232
pub fn init_bitcoind_multi_sender_single_reciever(
×
UNCOV
233
    number_of_senders: usize,
×
UNCOV
234
) -> Result<(corepc_node::Node, Vec<corepc_node::Client>, corepc_node::Client), BoxError> {
×
UNCOV
235
    let bitcoind = init_bitcoind()?;
×
UNCOV
236
    let wallets_to_create =
×
237
        (0..number_of_senders + 1).map(|i| (format!("sender_{i}"), None)).collect::<Vec<_>>();
×
238
    let mut wallets = create_and_fund_wallets(&bitcoind, wallets_to_create)?;
×
239
    let receiver = wallets.pop().expect("receiver to exist");
×
240
    let senders = wallets;
×
241

242
    Ok((bitcoind, senders, receiver))
×
243
}
×
244

245
fn http_agent_builder(cert_der: Vec<u8>) -> ClientBuilder {
13✔
246
    ClientBuilder::new().http1_only().use_rustls_tls().add_root_certificate(
13✔
247
        reqwest::tls::Certificate::from_der(cert_der.as_slice())
13✔
248
            .expect("cert_der should be a valid DER-encoded certificate"),
13✔
249
    )
250
}
13✔
251

252
const TESTS_TIMEOUT: Duration = Duration::from_secs(20);
253
const WAIT_SERVICE_INTERVAL: Duration = Duration::from_secs(3);
254

255
pub async fn wait_for_service_ready(
24✔
256
    service_url: &str,
24✔
257
    agent: Arc<Client>,
24✔
258
) -> Result<(), &'static str> {
24✔
259
    let health_url = format!("{}/health", service_url.trim_end_matches("/"));
24✔
260
    let start = std::time::Instant::now();
24✔
261

262
    while start.elapsed() < TESTS_TIMEOUT {
24✔
263
        let request_result =
24✔
264
            agent.get(health_url.clone()).send().await.map_err(|_| "Bad request")?;
24✔
265
        match request_result.status() {
24✔
266
            StatusCode::OK => return Ok(()),
24✔
UNCOV
267
            StatusCode::NOT_FOUND => return Err("Endpoint not found"),
×
UNCOV
268
            _ => std::thread::sleep(WAIT_SERVICE_INTERVAL),
×
269
        }
270
    }
271

272
    Err("Timeout waiting for service to be ready")
×
273
}
24✔
274

275
pub static EXAMPLE_URL: &str = "https://example.com";
276

277
pub const KEY_ID: KeyId = 1;
278
pub const KEM: Kem = Kem::K256Sha256;
279
pub const SYMMETRIC: &[SymmetricSuite] =
280
    &[ohttp::SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)];
281

282
// https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-span_idtestvectorsspanTest_vectors
283
// | InputScriptType | Original PSBT Fee rate | maxadditionalfeecontribution | additionalfeeoutputindex|
284
// |-----------------|-----------------------|------------------------------|-------------------------|
285
// | P2SH-P2WPKH     |  2 sat/vbyte          | 0.00000182                   | 0                       |
286

287
pub const QUERY_PARAMS: &str = "maxadditionalfeecontribution=182&additionalfeeoutputindex=0";
288

289
/// From the BIP-78 test vector
290
pub const ORIGINAL_PSBT: &str = "cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=";
291

292
/// From the BIP-174 test vector
293
pub const INVALID_PSBT: &str = "AgAAAAEmgXE3Ht/yhek3re6ks3t4AAwFZsuzrWRkFxPKQhcb9gAAAABqRzBEAiBwsiRRI+a/R01gxbUMBD1MaRpdJDXwmjSnZiqdwlF5CgIgATKcqdrPKAvfMHQOwDkEIkIsgctFg5RXrrdvwS7dlbMBIQJlfRGNM1e44PTCzUbbezn22cONmnCry5st5dyNv+TOMf7///8C09/1BQAAAAAZdqkU0MWZA8W6woaHYOkP1SGkZlqnZSCIrADh9QUAAAAAF6kUNUXm4zuDLEcFDyTT7rk8nAOUi8eHsy4TAA&#61;&#61;";
294

295
/// From the BIP-78 test vector
296
pub const PAYJOIN_PROPOSAL: &str = "cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAQEgqBvXBQAAAAAXqRTeTh6QYcpZE1sDWtXm1HmQRUNU0IcAAQEggIQeAAAAAAAXqRTI8sv5ymFHLIjkZNRrNXSEXZHY1YcBBxcWABRfgGZV5ZJMkgTC1RvlOU9L+e2iEAEIawJHMEQCIGe7e0DfJaVPRYEKWxddL2Pr0G37BoKz0lyNa02O2/tWAiB7ZVgBoF4s8MHocYWWmo4Q1cyV2wl7MX0azlqa8NBENAEhAmXWPPW0G3yE3HajBOb7gO7iKzHSmZ0o0w0iONowcV+tAAAA";
297

298
/// From the BIP-78 test vector
299
pub const PAYJOIN_PROPOSAL_WITH_SENDER_INFO: &str = "cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAQEgqBvXBQAAAAAXqRTeTh6QYcpZE1sDWtXm1HmQRUNU0IcBBBYAFMeKRXJTVYKNVlgHTdUmDV/LaYUwIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUIgICygvBWB5prpfx61y1HDAwo37kYP3YRJBvAjtunBAur3wYSFzWUDEAAIABAACAAAAAgAEAAAABAAAAAAA=";
300

301
/// Input contribution for the receiver, from the BIP78 test vector
302
pub const RECEIVER_INPUT_CONTRIBUTION: &str = "cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQAAAA==";
303

304
pub static PARSED_ORIGINAL_PSBT: Lazy<Psbt> =
305
    Lazy::new(|| Psbt::from_str(ORIGINAL_PSBT).expect("known psbt should parse"));
1✔
306

307
pub static PARSED_PAYJOIN_PROPOSAL: Lazy<Psbt> =
308
    Lazy::new(|| Psbt::from_str(PAYJOIN_PROPOSAL).expect("known psbt should parse"));
1✔
309

310
pub static PARSED_PAYJOIN_PROPOSAL_WITH_SENDER_INFO: Lazy<Psbt> = Lazy::new(|| {
1✔
311
    Psbt::from_str(PAYJOIN_PROPOSAL_WITH_SENDER_INFO).expect("known psbt should parse")
1✔
312
});
1✔
313

314
pub const MULTIPARTY_ORIGINAL_PSBT_ONE: &str = "cHNidP8BAMMCAAAAA9cTcqYjSnJdteUn3Re12vFuVb5xPypYheHze74WBW8EAAAAAAD+////6JHS6JgYdav7tr+Q5bWk95C2aA4AudqZnjeRF0hx33gBAAAAAP7///+d/1mRjJ6snVe9o0EfMM8bIExdlwfaig1j/JnUupqIWwAAAAAA/v///wJO4wtUAgAAABYAFGAEc6cWf7z5a97Xxulg+S7JZyg/kvEFKgEAAAAWABQexWFsGEdXxYNTJLJ1qBCO0nXELwAAAAAAAQCaAgAAAAKa/zFrWjicRDNVCehGj/2jdXGKuZLMzLNUo+uj7PBW8wAAAAAA/v///4HkRlmU9FA3sHckkCQMjGrabNpgAw37XaqUItNFjR5rAAAAAAD+////AgDyBSoBAAAAFgAUGPI/E9xCHmaL6DIsjkaD8LX2mpLg6QUqAQAAABYAFH6MyygysrAmk/nunEFuGGDLf244aAAAAAEBHwDyBSoBAAAAFgAUGPI/E9xCHmaL6DIsjkaD8LX2mpIBCGsCRzBEAiBLJUhV7tja6FdELXDcB6Q3Gd+BMBpkjdHpj3DLnbL6AAIgJmFl3kpWBzkO8yDDl73BMMalSlAOQvfY+hqFRos5DhQBIQLfeq7CCftNEUHcRLZniJOqcgKZEqPc1A40BnEiZHj3FQABAJoCAAAAAp3/WZGMnqydV72jQR8wzxsgTF2XB9qKDWP8mdS6mohbAQAAAAD+////1xNypiNKcl215SfdF7Xa8W5VvnE/KliF4fN7vhYFbwQBAAAAAP7///8CoNkFKgEAAAAWABQchAe9uxzqT1EjLx4jgx9u1Mn6QADyBSoBAAAAFgAUye8yXWX0MvouXYhAFb06xX5kADpoAAAAAQEfAPIFKgEAAAAWABTJ7zJdZfQy+i5diEAVvTrFfmQAOgEIawJHMEQCIF7ihY/YtUPUTOaEdJbg0/HiwKunK398BI67/LknPGqMAiBHBXmL6gTP8PxEGeWswk6T0tCI2Gvwq1zh+wd7h8QCWAEhApM0w2WFlw0eg64Zp3PeyRmxl/7WGHUED8Ul/aX1FiTBAAAAAA==";
315

316
pub const MULTIPARTY_ORIGINAL_PSBT_TWO: &str = "cHNidP8BAMMCAAAAA9cTcqYjSnJdteUn3Re12vFuVb5xPypYheHze74WBW8EAAAAAAD+////6JHS6JgYdav7tr+Q5bWk95C2aA4AudqZnjeRF0hx33gBAAAAAP7///+d/1mRjJ6snVe9o0EfMM8bIExdlwfaig1j/JnUupqIWwAAAAAA/v///wJO4wtUAgAAABYAFGAEc6cWf7z5a97Xxulg+S7JZyg/kvEFKgEAAAAWABQexWFsGEdXxYNTJLJ1qBCO0nXELwAAAAAAAAEAmgIAAAACnf9ZkYyerJ1XvaNBHzDPGyBMXZcH2ooNY/yZ1LqaiFsBAAAAAP7////XE3KmI0pyXbXlJ90XtdrxblW+cT8qWIXh83u+FgVvBAEAAAAA/v///wKg2QUqAQAAABYAFByEB727HOpPUSMvHiODH27UyfpAAPIFKgEAAAAWABTJ7zJdZfQy+i5diEAVvTrFfmQAOmgAAAABAR8A8gUqAQAAABYAFMnvMl1l9DL6Ll2IQBW9OsV+ZAA6AQhrAkcwRAIgXuKFj9i1Q9RM5oR0luDT8eLAq6crf3wEjrv8uSc8aowCIEcFeYvqBM/w/EQZ5azCTpPS0IjYa/CrXOH7B3uHxAJYASECkzTDZYWXDR6Drhmnc97JGbGX/tYYdQQPxSX9pfUWJMEAAQCaAgAAAAIKOB8lY4eoEupDnxviz0/nAuR2biNFKbdkvckiW5ioPAAAAAAA/v///w0g/mj67592vy29xhnZMGeVtEXN1jD4lU/SMZM8oStqAAAAAAD+////AgDyBSoBAAAAFgAUC3r4YzVSpsWp3knMxbgWIx2R36/g6QUqAQAAABYAFMGlw3hwcx1b+KQGWIfOUxzwrwWkaAAAAAEBHwDyBSoBAAAAFgAUC3r4YzVSpsWp3knMxbgWIx2R368BCGsCRzBEAiA//JH+jonzbzqnKI0Uti16iJcdsXI+6Zu0IAZKlOq6AwIgP0XawCCH73uXKilFqSXQQQrBvmi/Sx44D/A+/MQ/mJYBIQIekOyEpJKpFQd7eHuY6Vt4Qlf0+00Wp529I23hl/EpcQAAAA==";
317

318
/// sha256d of the empty string
319
#[rustfmt::skip]
320
pub const DUMMY32: [u8; 32] = [
321
    0x5d, 0xf6, 0xe0, 0xe2, 0x76, 0x13, 0x59, 0xd3,
322
    0x0a, 0x82, 0x75, 0x05, 0x8e, 0x29, 0x9f, 0xcc,
323
    0x03, 0x81, 0x53, 0x45, 0x45, 0xf5, 0x5c, 0xf4,
324
    0x3e, 0x41, 0x98, 0x3f, 0x5d, 0x4c, 0x94, 0x56,
325
];
326
/// hash160 of the empty string
327
#[rustfmt::skip]
328
pub const DUMMY20: [u8; 20] = [
329
    0xb4, 0x72, 0xa2, 0x66, 0xd0, 0xbd, 0x89, 0xc1, 0x37, 0x06,
330
    0xa4, 0x13, 0x2c, 0xcf, 0xb1, 0x6f, 0x7c, 0x3b, 0x9f, 0xcb,
331
];
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc