Skip to content

Commit

Permalink
Upgrade to testcontainers 0.15
Browse files Browse the repository at this point in the history
  • Loading branch information
ikmckenz committed Feb 24, 2024
1 parent 9e07344 commit ad6baa8
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 2 deletions.
8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,23 @@ bitcoin = { version = "0.29", features = ["serde"] }
bitcoincore-rpc-json = "0.16"
futures = "0.3.5"
hex = "0.4.2"
hmac = "0.12.1"
jsonrpc_client = { version = "0.7", features = ["reqwest"] }
rand = "0.8.5"
reqwest = { version = "0.11", default-features = false, features = ["json"] }
serde = "1.0"
serde_json = "1.0"
testcontainers = "0.14"
sha2 = "0.10.8"
testcontainers = "0.15"
thiserror = "1.0"
tokio = { version = "1.0", features = ["time"] }
tracing = "0.1"
url = "2"

[dev-dependencies]
bitcoincore-rpc = "0.18.0"
pretty_env_logger = "0.5.0"
spectral = "0.6"
tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] }

[features]
Expand Down
249 changes: 249 additions & 0 deletions src/img.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
use std::fmt;

use hex::encode;
use hmac::{Hmac, Mac};
use rand::{thread_rng, Rng};
use sha2::Sha256;
use testcontainers::{core::WaitFor, Image, ImageArgs};

const NAME: &str = "coblox/bitcoin-core";
const TAG: &str = "0.21.0";
const BITCOIND_STARTUP_MESSAGE: &str = "bitcoind startup sequence completed.";

#[derive(Debug, Default, Copy, Clone)]
pub struct BitcoinCore;

#[derive(Debug, Clone, Copy)]
pub enum Network {
Mainnet,
Testnet,
Regtest,
}

#[derive(Debug, Clone, Copy)]
pub enum AddressType {
Legacy,
P2shSegwit,
Bech32,
}

impl fmt::Display for AddressType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
AddressType::Legacy => "legacy",
AddressType::P2shSegwit => "p2sh-segwit",
AddressType::Bech32 => "bech32",
})
}
}

#[derive(Clone, Debug)]
pub struct RpcAuth {
pub username: String,
pub password: String,
pub salt: String,
}

impl RpcAuth {
pub fn username(&self) -> &str {
&self.username
}

pub fn password(&self) -> &str {
&self.password
}

pub fn new(username: String) -> Self {
let salt = Self::generate_salt();
let password = Self::generate_password();

RpcAuth {
username,
password,
salt,
}
}

fn generate_salt() -> String {
let mut buffer = [0u8; 16];
thread_rng().fill(&mut buffer[..]);
encode(buffer)
}

fn generate_password() -> String {
let mut buffer = [0u8; 32];
thread_rng().fill(&mut buffer[..]);

encode(buffer)
}

fn encode_password(&self) -> String {
let mut mac = Hmac::<Sha256>::new_from_slice(self.salt.as_bytes()).unwrap();
mac.update(self.password.as_bytes().as_ref());

let result = mac.finalize().into_bytes();

encode(result)
}

pub fn encode(&self) -> String {
format!("{}:{}${}", self.username, self.salt, self.encode_password())
}
}

#[derive(Debug, Clone)]
pub struct BitcoinCoreImageArgs {
pub server: bool,
pub network: Network,
pub print_to_console: bool,
pub tx_index: bool,
pub rpc_bind: String,
pub rpc_allowip: String,
pub rpc_auth: RpcAuth,
pub accept_non_std_txn: Option<bool>,
pub rest: bool,
pub fallback_fee: Option<f64>,
pub address_type: AddressType,
}

impl Default for BitcoinCoreImageArgs {
fn default() -> Self {
BitcoinCoreImageArgs {
server: true,
network: Network::Regtest,
print_to_console: true,
rpc_auth: RpcAuth::new(String::from("bitcoin")),
tx_index: true,
rpc_bind: "0.0.0.0".to_string(), // This allows to bind on all ports
rpc_allowip: "0.0.0.0/0".to_string(),
accept_non_std_txn: Some(false),
rest: true,
fallback_fee: Some(0.0002),
address_type: AddressType::Bech32,
}
}
}

impl ImageArgs for BitcoinCoreImageArgs {
fn into_iterator(self) -> Box<dyn Iterator<Item = String>> {
let mut args = vec![
format!("-rpcauth={}", self.rpc_auth.encode()),
// Will print a message when bitcoind is fully started
format!("-startupnotify='echo \'{BITCOIND_STARTUP_MESSAGE}\''"),
format!("-addresstype={}", self.address_type),
];

if self.server {
args.push("-server".to_string())
}

match self.network {
Network::Testnet => args.push("-testnet".to_string()),
Network::Regtest => args.push("-regtest".to_string()),
Network::Mainnet => {}
}

if self.tx_index {
args.push("-txindex=1".to_string())
}

if !self.rpc_allowip.is_empty() {
args.push(format!("-rpcallowip={}", self.rpc_allowip));
}

if !self.rpc_bind.is_empty() {
args.push(format!("-rpcbind={}", self.rpc_bind));
}

if self.print_to_console {
args.push("-printtoconsole".to_string())
}

if let Some(accept_non_std_txn) = self.accept_non_std_txn {
if accept_non_std_txn {
args.push("-acceptnonstdtxn=1".to_string());
} else {
args.push("-acceptnonstdtxn=0".to_string());
}
}

if self.rest {
args.push("-rest".to_string())
}

if let Some(fallback_fee) = self.fallback_fee {
args.push(format!("-fallbackfee={fallback_fee}"));
}

Box::new(args.into_iter())
}
}

impl Image for BitcoinCore {
type Args = BitcoinCoreImageArgs;

fn name(&self) -> String {
NAME.to_owned()
}

fn tag(&self) -> String {
TAG.to_owned()
}

fn ready_conditions(&self) -> Vec<WaitFor> {
vec![
WaitFor::message_on_stdout(BITCOIND_STARTUP_MESSAGE),
WaitFor::millis_in_env_var("BITCOIND_ADDITIONAL_SLEEP_PERIOD"),
]
}
}

#[cfg(test)]
mod tests {
use bitcoincore_rpc::RpcApi;
use spectral::{assert_that, prelude::*};
use testcontainers::clients;

use super::*;

#[test]
fn encodes_rpc_auth_correctly() {
let auth = RpcAuth {
username: "bitcoin".to_string(),
password: "54pLR_f7-G6is32LP-7nbhzZSbJs_2zSATtZV_r05yg=".to_string(),
salt: "cb77f0957de88ff388cf817ddbc7273".to_string(),
};

let rpc_auth = auth.encode();

assert_eq!(rpc_auth, "bitcoin:cb77f0957de88ff388cf817ddbc7273$9eaa166ace0d94a29c6eceb831a42458e93faeb79f895a7ee4ce03f4343f8f55".to_string())
}

#[test]
fn coblox_bitcoincore_getnewaddress() {
let _ = pretty_env_logger::try_init();
let docker = clients::Cli::default();
let node = docker.run(BitcoinCore);

let client = {
let host_port = node.get_host_port_ipv4(18443);

let url = format!("http://127.0.0.1:{host_port}");

let auth = &node.image_args().rpc_auth;

bitcoincore_rpc::Client::new(
&url,
bitcoincore_rpc::Auth::UserPass(
auth.username().to_owned(),
auth.password().to_owned(),
),
)
.unwrap()
};

assert_that(&client.create_wallet("miner", None, None, None, None)).is_ok();

assert_that(&client.get_new_address(None, None)).is_ok();
}
}
4 changes: 3 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,16 @@
pub mod bitcoind_rpc;
pub mod bitcoind_rpc_api;
pub mod img;
pub mod wallet;

use reqwest::Url;
use std::time::Duration;
use testcontainers::{clients, images::coblox_bitcoincore::BitcoinCore, Container};
use testcontainers::{clients, Container};

pub use crate::bitcoind_rpc::Client;
pub use crate::bitcoind_rpc_api::BitcoindRpcApi;
pub use crate::img::BitcoinCore;
pub use crate::wallet::Wallet;

pub type Result<T> = std::result::Result<T, Error>;
Expand Down

0 comments on commit ad6baa8

Please sign in to comment.