Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ pub async fn run() -> Result<()> {
log::info!("🚀 Starting brc721");

let cli = crate::cli::parse();
let ctx = context::Context::from_cli(&cli);
let ctx = context::Context::from_cli(&cli)?;

if let Some(path) = ctx.log_file.as_deref() {
log::info!("📝 Log file: {}", path.to_string_lossy());
Expand Down
33 changes: 18 additions & 15 deletions src/commands/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,11 @@ fn run_register_collection(
rebaseable,
};
let payload = Brc721Payload::RegisterCollection(msg);
let output = Brc721OpReturnOutput::new(payload).into_txout().unwrap();
let output = Brc721OpReturnOutput::new(payload)
.into_txout()
.context("build register-collection op_return output")?;

let passphrase = resolve_passphrase(passphrase);
let passphrase = resolve_passphrase(passphrase)?;
let tx = wallet
.build_tx(output, fee_rate, passphrase)
.context("build tx")?;
Expand Down Expand Up @@ -126,9 +128,11 @@ fn run_register_ownership(
)?;
let payload = Brc721Payload::RegisterOwnership(ownership);

let output = Brc721OpReturnOutput::new(payload).into_txout().unwrap();
let output = Brc721OpReturnOutput::new(payload)
.into_txout()
.context("build register-ownership op_return output")?;

let passphrase = resolve_passphrase(passphrase);
let passphrase = resolve_passphrase(passphrase)?;
let tx = wallet
.build_tx_with_op_return_and_payments(
output,
Expand Down Expand Up @@ -158,7 +162,7 @@ fn run_send_amount(
let wallet = load_wallet(ctx)?;
let amount = Amount::from_sat(amount_sat);
let address = Address::from_str(to)?.require_network(ctx.network)?;
let passphrase = resolve_passphrase(passphrase);
let passphrase = resolve_passphrase(passphrase)?;
let tx = wallet
.build_payment_tx(&address, amount, fee_rate, passphrase)
.context("build payment tx")?;
Expand Down Expand Up @@ -235,7 +239,7 @@ fn run_send_assets(

let address = Address::from_str(to)?.require_network(ctx.network)?;
let dust_amount = Amount::from_sat(dust_sat);
let passphrase = resolve_passphrase(passphrase);
let passphrase = resolve_passphrase(passphrase)?;

let lock_outpoints =
compute_wallet_token_outpoints_to_lock(&storage, &wallet_utxos, &token_outpoints)
Expand Down Expand Up @@ -372,7 +376,7 @@ fn run_mix(
.into_txout()
.context("build mix op_return output")?;

let passphrase = resolve_passphrase(passphrase);
let passphrase = resolve_passphrase(passphrase)?;
let tx = wallet
.build_mix_tx(
&token_outpoints,
Expand Down Expand Up @@ -402,14 +406,13 @@ fn load_wallet(ctx: &context::Context) -> Result<Brc721Wallet> {
Brc721Wallet::load(&ctx.data_dir, ctx.network, &ctx.rpc_url, ctx.auth.clone())
}

fn resolve_passphrase(passphrase: Option<String>) -> SecretString {
passphrase.map(SecretString::from).unwrap_or_else(|| {
SecretString::from(
prompt_passphrase_once()
.expect("prompt")
.unwrap_or_default(),
)
})
fn resolve_passphrase(passphrase: Option<String>) -> Result<SecretString> {
if let Some(passphrase) = passphrase {
return Ok(SecretString::from(passphrase));
}

let passphrase = prompt_passphrase_once().context("prompt passphrase")?;
Ok(SecretString::from(passphrase.unwrap_or_default()))
}

fn parse_outpoints(outpoints: &[String]) -> Result<Vec<OutPoint>> {
Expand Down
23 changes: 13 additions & 10 deletions src/commands/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ fn run_init(
Mnemonic::parse_in(Language::English, mnemonic_str).context("invalid mnemonic")?;

// Resolve passphrase
let passphrase = resolve_passphrase_init(passphrase);
let passphrase = resolve_passphrase_init(passphrase)?;

// Create new wallet
let wallet = Brc721Wallet::create(
Expand Down Expand Up @@ -475,21 +475,24 @@ fn load_local_wallet(ctx: &context::Context) -> Result<LocalWallet> {
LocalWallet::load(&ctx.data_dir, ctx.network).context("loading local wallet")
}

fn resolve_passphrase_init(passphrase: Option<String>) -> SecretString {
passphrase.map(SecretString::from).unwrap_or_else(|| {
SecretString::from(prompt_passphrase().expect("prompt").unwrap_or_default())
})
fn resolve_passphrase_init(passphrase: Option<String>) -> Result<SecretString> {
if let Some(passphrase) = passphrase {
return Ok(SecretString::from(passphrase));
}

let passphrase = prompt_passphrase().context("prompt passphrase")?;
Ok(SecretString::from(passphrase.unwrap_or_default()))
}

fn generate_mnemonic(short: bool) -> Mnemonic {
fn generate_mnemonic(short: bool) -> Result<Mnemonic> {
let entropy_bytes = if short { 16 } else { 32 };
let mut entropy = vec![0u8; entropy_bytes];
OsRng.fill_bytes(&mut entropy);
Mnemonic::from_entropy(&entropy).expect("mnemonic")
Mnemonic::from_entropy(&entropy).context("generate mnemonic")
}

fn run_generate(short: bool) -> Result<()> {
let mnemonic = generate_mnemonic(short);
let mnemonic = generate_mnemonic(short)?;
println!("{}", mnemonic);
Ok(())
}
Expand All @@ -500,15 +503,15 @@ mod tests {

#[test]
fn generate_mnemonic_is_valid_12_words() {
let mnemonic = generate_mnemonic(true);
let mnemonic = generate_mnemonic(true).expect("generate mnemonic");
assert_eq!(mnemonic.word_count(), 12);
let parsed = Mnemonic::parse_in(Language::English, mnemonic.to_string()).expect("parse");
assert_eq!(parsed.word_count(), 12);
}

#[test]
fn generate_mnemonic_is_valid_24_words() {
let mnemonic = generate_mnemonic(false);
let mnemonic = generate_mnemonic(false).expect("generate mnemonic");
assert_eq!(mnemonic.word_count(), 24);
let parsed = Mnemonic::parse_in(Language::English, mnemonic.to_string()).expect("parse");
assert_eq!(parsed.word_count(), 24);
Expand Down
10 changes: 5 additions & 5 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ pub struct Context {
}

impl Context {
pub fn from_cli(cli: &crate::cli::Cli) -> Self {
pub fn from_cli(cli: &crate::cli::Cli) -> Result<Self> {
let auth = match (&cli.rpc_user, &cli.rpc_pass) {
(Some(user), Some(pass)) => Auth::UserPass(user.clone(), pass.clone()),
_ => Auth::None,
};
let rpc_url = Url::parse(&cli.rpc_url).expect("rpc url");
let rpc_url = Url::parse(&cli.rpc_url).context("invalid rpc url")?;
let network = detect_network(&rpc_url, &auth)
.unwrap_or_else(|_| panic!("detect network from node {}", rpc_url));
.with_context(|| format!("detect network from node {}", rpc_url))?;
let mut data_dir = PathBuf::from(&cli.data_dir);
let network_dir = network.to_string();
data_dir.push(network_dir);
Self {
Ok(Self {
network,
data_dir,
rpc_url,
Expand All @@ -40,7 +40,7 @@ impl Context {
log_file: cli.log_file.as_deref().map(PathBuf::from),
reset: cli.reset,
api_listen: cli.api_listen,
}
})
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/parser/brc721_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,14 @@ impl Brc721Parser {
}

let remaining_outputs_len = remaining_outputs.len();
let last_output = remaining_outputs.last().ok_or_else(|| {
Brc721Error::TxError("implicit transfer requires at least one spendable output".into())
})?;
for (input_index, input) in token_inputs.into_iter().enumerate() {
let (dest_vout, dest_txout) = if input_index < remaining_outputs_len {
remaining_outputs[input_index]
} else {
*remaining_outputs
.last()
.expect("non-empty remaining_outputs")
*last_output
};

let dest_owner_h160 = h160_from_script_pubkey(&dest_txout.script_pubkey);
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/brc721_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl Brc721Wallet {
auth: Auth,
) -> Result<Brc721Wallet> {
let seed = mnemonic.to_seed(String::default());
let master_xprv = Xpriv::new_master(network, &seed).expect("master_key");
let master_xprv = Xpriv::new_master(network, &seed).context("create master key")?;
let external = Bip86(master_xprv, KeychainKind::External);
let internal = Bip86(master_xprv, KeychainKind::Internal);

Expand Down
27 changes: 19 additions & 8 deletions src/wallet/remote_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,16 @@ impl RemoteWallet {
],
)
.context("watch wallet created")?;
if ans["name"].as_str().unwrap() != self.watch_name {
return Err(anyhow::anyhow!("Unexpected wallet name: {:?}", ans["name"]));
let created_name = ans
.get("name")
.and_then(|value| value.as_str())
.context("createwallet response missing name")?;
if created_name != self.watch_name {
return Err(anyhow::anyhow!(
"Unexpected wallet name: expected '{}', got '{}'",
self.watch_name,
created_name
));
}
if !ans["warning"].is_null() {
return Err(anyhow::anyhow!(
Expand Down Expand Up @@ -163,12 +171,15 @@ impl RemoteWallet {
.call::<serde_json::Value>("importdescriptors", &[imports])
.context("import descriptor")?;

let arr = ans.as_array().expect("array");
if !arr.iter().all(|e| e["success"].as_bool() == Some(true)) {
return Err(anyhow::anyhow!(
"Failed to import descriptors: {}",
serde_json::to_string_pretty(&ans).unwrap()
));
let arr = ans
.as_array()
.context("importdescriptors response was not an array")?;
if !arr
.iter()
.all(|e| e.get("success").and_then(|v| v.as_bool()) == Some(true))
{
let pretty = serde_json::to_string_pretty(&ans).unwrap_or_else(|_| format!("{ans:?}"));
return Err(anyhow::anyhow!("Failed to import descriptors: {}", pretty));
}
Ok(())
}
Expand Down
4 changes: 3 additions & 1 deletion src/wallet/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ impl Signer {
.create_wallet_no_persist()
.map_err(|e| Brc721Error::WalletError(e.to_string()))?;

let finalized = wallet.sign(psbt, Default::default()).expect("sign");
let finalized = wallet
.sign(psbt, Default::default())
.map_err(|e| Brc721Error::WalletError(e.to_string()))?;
Ok(finalized)
}
}
Expand Down