Verify recovery support before spending and route node catalogs through nginx
This commit is contained in:
@@ -849,10 +849,19 @@ pub async fn send_token_recoverable(
|
||||
} else {
|
||||
let mut denominations = amount_to_denominations(amount_sats);
|
||||
denominations.extend(amount_to_denominations(excess));
|
||||
let prepared = mint_client(data_dir, &binding.mint_url)
|
||||
.await?
|
||||
let client = mint_client(data_dir, &binding.mint_url).await?;
|
||||
let prepared = client
|
||||
.prepare_swap_at_least(&proofs, &denominations, amount_sats)
|
||||
.await?;
|
||||
// Establish recovery support before reserving or spending inputs.
|
||||
// Newly derived outputs must not already exist at the mint.
|
||||
let existing = client.restore_prepared_swap(&prepared).await.map_err(|_| {
|
||||
anyhow::anyhow!("The mint could not verify payment recovery support; no funds spent")
|
||||
})?;
|
||||
anyhow::ensure!(
|
||||
existing.is_none(),
|
||||
"Payment outputs already exist at the mint; no funds spent"
|
||||
);
|
||||
Request::Swap(prepared)
|
||||
};
|
||||
journal.prepare(binding.clone(), request).await?
|
||||
|
||||
@@ -991,13 +991,13 @@ impl MintClient {
|
||||
let echoed: Vec<BlindedMessageRequest> = serde_json::from_value(
|
||||
body.get("outputs")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::json!([])),
|
||||
.context("Mint restore response omitted outputs")?,
|
||||
)
|
||||
.context("Failed to parse restored outputs")?;
|
||||
let signatures: Vec<BlindSignature> = serde_json::from_value(
|
||||
body.get("signatures")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::json!([])),
|
||||
.context("Mint restore response omitted signatures")?,
|
||||
)
|
||||
.context("Failed to parse restored signatures")?;
|
||||
|
||||
|
||||
@@ -717,6 +717,32 @@ async fn journal_recovers_lost_swap_reply_and_commits_change_once() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recoverable_send_rejects_broken_recovery_before_reserving_or_spending() {
|
||||
let mint = Mint::start(0, None).await;
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let mut wallet = WalletState::default();
|
||||
wallet.mint_url = mint.url.clone();
|
||||
wallet.add_proofs(&mint.url, vec![proof(ACTIVE, 8)]);
|
||||
save_wallet(root.path(), &wallet).await.unwrap();
|
||||
*mint.restore_reply.lock().unwrap() = Some(json!({}));
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let context = "ab".repeat(32);
|
||||
let error = send_token_recoverable(
|
||||
root.path(), &id, EcashNetwork::Mainnet, &mint.url, 4, &context,
|
||||
).await.unwrap_err();
|
||||
assert!(error.to_string().contains("recovery support"));
|
||||
assert_eq!(load_wallet(root.path()).await.unwrap().balance(), 8);
|
||||
assert!(mint.requests.lock().unwrap().is_empty());
|
||||
// Once the mint responds correctly the same unspent operation can proceed.
|
||||
*mint.restore_reply.lock().unwrap() = None;
|
||||
assert!(send_token_recoverable(
|
||||
root.path(), &id, EcashNetwork::Mainnet, &mint.url, 4, &context,
|
||||
).await.is_ok());
|
||||
assert_eq!(mint.requests.lock().unwrap().len(), 1);
|
||||
assert_eq!(load_wallet(root.path()).await.unwrap().balance(), 4);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recoverable_send_reuses_original_operation_after_ambiguous_mint_response() {
|
||||
let mint = Mint::start(0, None).await;
|
||||
@@ -916,6 +942,10 @@ async fn seed_restore_matches_points_and_rejects_foreign_or_duplicated_metadata(
|
||||
let mut wrong_amount = signature.clone();
|
||||
wrong_amount["amount"] = json!(8);
|
||||
for response in [
|
||||
json!({}),
|
||||
json!({"outputs":[]}),
|
||||
json!({"signatures":[]}),
|
||||
json!({"outputs":null,"signatures":[]}),
|
||||
json!({"outputs":[output.clone(),output.clone()],"signatures":[signature.clone(),signature.clone()]}),
|
||||
json!({"outputs":[foreign],"signatures":[signature.clone()]}),
|
||||
json!({"outputs":[output.clone()],"signatures":[wrong_keyset]}),
|
||||
|
||||
Reference in New Issue
Block a user