-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
260 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters