Integrate recoverable native purchases, registered rentals and explicit payment consent

This commit is contained in:
archipelago
2026-10-06 22:44:06 -04:00
parent e4eae71314
commit 49703d7e88
63 changed files with 8028 additions and 134 deletions
+1 -1
View File
@@ -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