Integrate recoverable native purchases, registered rentals and explicit payment consent
This commit is contained in:
@@ -20,7 +20,7 @@ pub(crate) use invites::notify_join;
|
||||
// Crate-internal: peer-joined resolves the granted trust level by matching
|
||||
// the acceptor's invite_token against our stored outgoing invites.
|
||||
pub(crate) use storage::load_invites;
|
||||
pub(crate) use storage::load_unique_payment_peer;
|
||||
pub(crate) use storage::{load_unique_payment_peer, load_unique_payment_peer_by_did};
|
||||
#[allow(unused_imports)]
|
||||
pub use storage::{
|
||||
add_node, fips_npub_for_onion, load_nodes, load_removed_dids, record_peer_transport,
|
||||
|
||||
@@ -101,6 +101,41 @@ pub(crate) async fn load_unique_payment_peer(
|
||||
Ok(peer)
|
||||
}
|
||||
|
||||
/// Resolve a purchase seller by raw identity before any display deduplication.
|
||||
pub(crate) async fn load_unique_payment_peer_by_did(
|
||||
data_dir: &Path,
|
||||
did: &str,
|
||||
) -> Result<FederatedNode> {
|
||||
let _guard = FEDERATION_STORE_LOCK.lock().await;
|
||||
let content = fs::read(data_dir.join(FEDERATION_DIR).join(NODES_FILE))
|
||||
.await
|
||||
.context("Could not read payment peer bindings")?;
|
||||
let file: NodesFile =
|
||||
serde_json::from_slice(&content).context("Invalid payment peer bindings")?;
|
||||
let matching: Vec<_> = file.nodes.iter().filter(|peer| peer.did == did).collect();
|
||||
anyhow::ensure!(
|
||||
matching.len() == 1,
|
||||
"Payment seller identity binding is missing or ambiguous"
|
||||
);
|
||||
let peer = matching[0];
|
||||
let onion = peer.onion.strip_suffix(".onion").unwrap_or(&peer.onion);
|
||||
anyhow::ensure!(
|
||||
!onion.is_empty()
|
||||
&& file
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| node.onion.strip_suffix(".onion").unwrap_or(&node.onion) == onion)
|
||||
.count()
|
||||
== 1,
|
||||
"Payment seller address binding is missing or ambiguous"
|
||||
);
|
||||
anyhow::ensure!(
|
||||
crate::identity::did_key_from_pubkey_hex(&peer.pubkey)? == peer.did,
|
||||
"Payment seller identity does not match its public key"
|
||||
);
|
||||
Ok(peer.clone())
|
||||
}
|
||||
|
||||
/// Lock-free body of `load_nodes`. Callers that already hold
|
||||
/// `FEDERATION_STORE_LOCK` (i.e. other functions in this module composing a
|
||||
/// multi-step critical section) must call this instead of `load_nodes` to
|
||||
@@ -547,6 +582,41 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn payment_did_resolution_rejects_duplicates_hidden_by_display_merging() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let key = hex::encode([1u8; 32]);
|
||||
let did = crate::identity::did_key_from_pubkey_hex(&key).unwrap();
|
||||
let mut original = make_node(&did, "a.onion");
|
||||
original.pubkey = key;
|
||||
save_nodes(dir.path(), &[original.clone()]).await.unwrap();
|
||||
assert_eq!(
|
||||
load_unique_payment_peer_by_did(dir.path(), &did)
|
||||
.await
|
||||
.unwrap()
|
||||
.onion,
|
||||
"a.onion"
|
||||
);
|
||||
let mut alternate = original.clone();
|
||||
alternate.onion = "b.onion".into();
|
||||
save_nodes(dir.path(), &[original.clone(), alternate])
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(load_unique_payment_peer_by_did(dir.path(), &did)
|
||||
.await
|
||||
.is_err());
|
||||
let mut collision = original.clone();
|
||||
collision.did = crate::identity::did_key_from_pubkey_hex(&hex::encode([2u8; 32])).unwrap();
|
||||
collision.pubkey = hex::encode([2u8; 32]);
|
||||
save_nodes(dir.path(), &[collision, original])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(load_nodes(dir.path()).await.unwrap().len(), 1);
|
||||
assert!(load_unique_payment_peer_by_did(dir.path(), &did)
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dedup_nodes_by_onion_collapses_same_onion() {
|
||||
// Two entries share an onion (same physical node under two dids) — must
|
||||
|
||||
Reference in New Issue
Block a user