Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 103 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ chrono = { version = "0.4.43", features = ["serde"] }
# crypto
base64 = { version = "0.22.1" }
bech32 = { version = "0.11.1" }
blake2 = { version = "0.10.6" }
bitcoin = { version = "0.32.7" }
bitcoincash-addr = { version = "0.5.2" }
blake2b_simd = { version = "1.0.4" }
bs58 = { version = "0.5.1", features = ["check"] }
hex = { version = "0.4.3" }
crc = { version = "3.4.0" }
Expand Down
10 changes: 8 additions & 2 deletions crates/gem_bitcoin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ publish = false

[features]
default = []
rpc = ["dep:chain_traits", "dep:gem_client"]
signer = ["dep:signer", "dep:gem_hash", "dep:hex"]
rpc = ["dep:chain_traits", "dep:gem_client", "signer"]
signer = ["dep:bech32", "dep:bitcoin", "dep:bitcoincash-addr", "dep:bs58", "dep:gem_hash", "dep:hex", "dep:signer"]
reqwest = ["gem_client/reqwest"]
unit_tests = ["signer"]
chain_integration_tests = ["rpc", "reqwest", "primitives/testkit", "settings/testkit"]
Expand All @@ -29,8 +29,14 @@ serde_serializers = { path = "../serde_serializers", features = ["bigint"] }
signer = { path = "../signer", optional = true }
gem_hash = { path = "../gem_hash", optional = true }
hex = { workspace = true, optional = true }
bitcoin = { workspace = true, optional = true }
bitcoincash-addr = { workspace = true, optional = true }
bech32 = { workspace = true, optional = true }
bs58 = { workspace = true, optional = true }

[dev-dependencies]
gem_client = { path = "../gem_client", features = ["testkit"] }
tokio = { workspace = true, features = ["macros", "rt"] }
reqwest = { workspace = true }
primitives = { path = "../primitives", features = ["testkit"] }
settings = { path = "../settings", features = ["testkit"] }
85 changes: 85 additions & 0 deletions crates/gem_bitcoin/src/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use bitcoin::ScriptBuf;
use primitives::{Address as AddressTrait, BitcoinChain, Chain};

use crate::signer::address::script_for_address;

#[derive(Debug, Clone)]
pub struct BitcoinAddress {
chain: BitcoinChain,
address: String,
script_pubkey: ScriptBuf,
}

impl BitcoinAddress {
pub fn try_parse_for_chain(address: &str, chain: BitcoinChain) -> Option<Self> {
let script_pubkey = script_for_address(chain, address).ok()?.script_pubkey;
Some(Self {
chain,
address: address.to_string(),
script_pubkey,
})
}

pub fn is_valid_for_chain(address: &str, chain: Chain) -> bool {
BitcoinChain::from_chain(chain).is_some_and(|chain| Self::try_parse_for_chain(address, chain).is_some())
}

pub fn bitcoin_chain(&self) -> BitcoinChain {
self.chain
}
}

impl AddressTrait for BitcoinAddress {
fn try_parse(address: &str) -> Option<Self> {
[
BitcoinChain::Bitcoin,
BitcoinChain::BitcoinCash,
BitcoinChain::Litecoin,
BitcoinChain::Doge,
BitcoinChain::Zcash,
]
.into_iter()
.find_map(|chain| Self::try_parse_for_chain(address, chain))
}

fn as_bytes(&self) -> &[u8] {
self.script_pubkey.as_bytes()
}

fn encode(&self) -> String {
self.address.clone()
}
}

pub fn validate_address(address: &str, chain: Chain) -> bool {
BitcoinAddress::is_valid_for_chain(address, chain)
}

#[cfg(test)]
mod tests {
use super::*;
use primitives::Address as AddressTrait;

#[test]
fn test_validate_address() {
let bitcoin = BitcoinAddress::mock();
let bitcoin_cash = BitcoinAddress::mock_with_chain(BitcoinChain::BitcoinCash);
let litecoin = BitcoinAddress::mock_with_chain(BitcoinChain::Litecoin);
let doge = BitcoinAddress::mock_with_chain(BitcoinChain::Doge);
let zcash = BitcoinAddress::mock_with_chain(BitcoinChain::Zcash);

assert!(validate_address(&bitcoin.encode(), Chain::Bitcoin));
assert!(validate_address(&bitcoin_cash.encode(), Chain::BitcoinCash));
assert!(validate_address(bitcoin_cash.encode().strip_prefix("bitcoincash:").unwrap(), Chain::BitcoinCash));
assert!(validate_address(&litecoin.encode(), Chain::Litecoin));
assert!(validate_address(&doge.encode(), Chain::Doge));
assert!(validate_address(&zcash.encode(), Chain::Zcash));
assert!(!validate_address(&bitcoin.encode(), Chain::Litecoin));
assert!(!validate_address("invalid", Chain::Bitcoin));

let parsed = BitcoinAddress::try_parse_for_chain(&bitcoin.encode(), BitcoinChain::Bitcoin).unwrap();
assert_eq!(parsed.bitcoin_chain().get_chain(), Chain::Bitcoin);
assert_eq!(parsed.encode(), bitcoin.encode());
assert_eq!(hex::encode(parsed.as_bytes()), "0014751e76e8199196d454941c45d1b3a323f1433bd6");
}
}
34 changes: 34 additions & 0 deletions crates/gem_bitcoin/src/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use bitcoin::hashes::{Hash, hash160 as bitcoin_hash160, sha256d};
use primitives::SignerError;

pub(crate) const HASH160_LEN: usize = 20;

pub(crate) fn double_sha256(bytes: &[u8]) -> [u8; 32] {
sha256d::Hash::hash(bytes).to_byte_array()
}

pub(crate) fn hash160(bytes: &[u8]) -> [u8; HASH160_LEN] {
bitcoin_hash160::Hash::hash(bytes).to_byte_array()
}

pub(crate) fn public_key_hash(public_key: &[u8]) -> [u8; HASH160_LEN] {
hash160(public_key)
}

pub(crate) fn hash20(bytes: &[u8]) -> Result<[u8; HASH160_LEN], SignerError> {
bytes.try_into().map_err(SignerError::from_display)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_hashes() {
assert_eq!(hex::encode(double_sha256(b"")), "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456");
assert_eq!(hex::encode(hash160(b"")), "b472a266d0bd89c13706a4132ccfb16f7c3b9fcb");
assert_eq!(public_key_hash(b""), hash160(b""));
assert_eq!(hash20(&[1u8; HASH160_LEN]).unwrap(), [1u8; HASH160_LEN]);
assert!(hash20(&[1u8; HASH160_LEN - 1]).is_err());
}
}
9 changes: 9 additions & 0 deletions crates/gem_bitcoin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
pub mod models;

#[cfg(feature = "signer")]
pub(crate) mod hash;

#[cfg(feature = "signer")]
pub mod address;

#[cfg(feature = "rpc")]
pub mod provider;

Expand All @@ -15,5 +21,8 @@ pub mod testkit;
#[cfg(feature = "rpc")]
pub use provider::map_transaction;

#[cfg(feature = "signer")]
pub use address::{BitcoinAddress, validate_address};

#[cfg(feature = "rpc")]
pub use rpc::client::BitcoinClient;
Loading
Loading