Skip to content

Commit

Permalink
Merge pull request #2242 from mickvandijke/metamask-integration
Browse files Browse the repository at this point in the history
API 3rd party transaction signer integration
  • Loading branch information
mickvandijke authored Oct 18, 2024
2 parents ebba7d6 + 37d9022 commit 0ccde60
Show file tree
Hide file tree
Showing 19 changed files with 461 additions and 78 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions autonomi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ fs = ["tokio/fs", "data"]
local = ["sn_networking/local", "test_utils/local", "sn_evm/local"]
registers = ["data"]
loud = []
external-signer = ["sn_evm/external-signer", "data"]

[dependencies]
bip39 = "2.0.0"
Expand Down Expand Up @@ -53,6 +54,7 @@ wasm-bindgen-futures = "0.4.43"
serde-wasm-bindgen = "0.6.5"

[dev-dependencies]
alloy = { version = "0.4.2", default-features = false, features = ["std", "reqwest-rustls-tls", "provider-anvil-node", "sol-types", "json", "signers", "contract", "signer-local", "network"] }
eyre = "0.6.5"
sha2 = "0.10.6"
sn_logging = { path = "../sn_logging", version = "0.2.33" }
Expand Down
2 changes: 2 additions & 0 deletions autonomi/src/client/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ pub enum PutError {
VaultXorName,
#[error("A network error occurred.")]
Network(#[from] NetworkError),
#[error("Error occurred during cost estimation.")]
CostError(#[from] CostError),
#[error("Error occurred during payment.")]
PayError(#[from] PayError),
#[error("Serialization error: {0}")]
Expand Down
102 changes: 102 additions & 0 deletions autonomi/src/client/external_signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use crate::client::data::{DataAddr, PutError};
use crate::client::utils::extract_quote_payments;
use crate::self_encryption::encrypt;
use crate::Client;
use bytes::Bytes;
use sn_evm::{ProofOfPayment, QuotePayment};
use sn_networking::PayeeQuote;
use sn_protocol::storage::Chunk;
use std::collections::HashMap;
use xor_name::XorName;

#[allow(unused_imports)]
pub use sn_evm::external_signer::*;

impl Client {
/// Upload a piece of data to the network. This data will be self-encrypted.
/// Payment will not be done automatically as opposed to the regular `data_put`, so the proof of payment has to be provided.
/// Returns the Data Address at which the data was stored.
pub async fn data_put_with_proof_of_payment(
&self,
data: Bytes,
proof: HashMap<XorName, ProofOfPayment>,
) -> Result<DataAddr, PutError> {
let (data_map_chunk, chunks, _) = encrypt_data(data)?;
self.upload_data_map(&proof, &data_map_chunk).await?;
self.upload_chunks(&chunks, &proof).await?;
Ok(*data_map_chunk.address().xorname())
}

/// Get quotes for data.
/// Returns a cost map, data payments to be executed and a list of free (already paid for) chunks.
pub async fn get_quotes_for_data(
&self,
data: Bytes,
) -> Result<
(
HashMap<XorName, PayeeQuote>,
Vec<QuotePayment>,
Vec<XorName>,
),
PutError,
> {
// Encrypt the data as chunks
let (_data_map_chunk, _chunks, xor_names) = encrypt_data(data)?;
let cost_map = self.get_store_quotes(xor_names.into_iter()).await?;
let (quote_payments, free_chunks) = extract_quote_payments(&cost_map);
Ok((cost_map, quote_payments, free_chunks))
}

async fn upload_data_map(
&self,
payment_proofs: &HashMap<XorName, ProofOfPayment>,
data_map_chunk: &Chunk,
) -> Result<(), PutError> {
let map_xor_name = data_map_chunk.name();

if let Some(proof) = payment_proofs.get(map_xor_name) {
debug!("Uploading data map chunk: {map_xor_name:?}");
self.chunk_upload_with_payment(data_map_chunk.clone(), proof.clone())
.await
.inspect_err(|err| error!("Error uploading data map chunk: {err:?}"))
} else {
Ok(())
}
}

async fn upload_chunks(
&self,
chunks: &[Chunk],
payment_proofs: &HashMap<XorName, ProofOfPayment>,
) -> Result<(), PutError> {
debug!("Uploading {} chunks", chunks.len());
for chunk in chunks {
if let Some(proof) = payment_proofs.get(chunk.name()) {
let address = *chunk.address();
self.chunk_upload_with_payment(chunk.clone(), proof.clone())
.await
.inspect_err(|err| error!("Error uploading chunk {address:?} :{err:?}"))?;
}
}
Ok(())
}
}

/// Encrypts data as chunks.
///
/// Returns the data map chunk, file chunks and a list of all content addresses including the data map.
fn encrypt_data(data: Bytes) -> Result<(Chunk, Vec<Chunk>, Vec<XorName>), PutError> {
let now = sn_networking::target_arch::Instant::now();
let result = encrypt(data)?;

debug!("Encryption took: {:.2?}", now.elapsed());

let map_xor_name = *result.0.address().xorname();
let mut xor_names = vec![map_xor_name];

for chunk in &result.1 {
xor_names.push(*chunk.name());
}

Ok((result.0, result.1, xor_names))
}
2 changes: 2 additions & 0 deletions autonomi/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub mod address;
pub mod archive;
#[cfg(feature = "data")]
pub mod data;
#[cfg(feature = "external-signer")]
pub mod external_signer;
#[cfg(feature = "fs")]
pub mod fs;
#[cfg(feature = "registers")]
Expand Down
36 changes: 6 additions & 30 deletions autonomi/src/client/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@
// KIND, either express or implied. Please review the Licences for the specific language governing
// permissions and limitations relating to use of the SAFE Network Software.

use std::{
collections::{BTreeMap, HashMap},
num::NonZero,
};
use std::{collections::HashMap, num::NonZero};

use bytes::Bytes;
use libp2p::kad::{Quorum, Record};
use rand::{thread_rng, Rng};
use self_encryption::{decrypt_full_set, DataMap, EncryptedChunk};
use sn_evm::{EvmWallet, ProofOfPayment, QuoteHash, QuotePayment, TxHash};
use sn_evm::{EvmWallet, ProofOfPayment, QuotePayment};
use sn_networking::{
GetRecordCfg, Network, NetworkError, PayeeQuote, PutRecordCfg, VerificationKind,
};
Expand All @@ -26,12 +23,12 @@ use sn_protocol::{
};
use xor_name::XorName;

use crate::self_encryption::DataMapLevel;

use super::{
data::{CostError, GetError, PayError, PutError},
Client,
};
use crate::self_encryption::DataMapLevel;
use crate::utils::payment_proof_from_quotes_and_payments;

impl Client {
/// Fetch and decrypt all chunks in the data map.
Expand Down Expand Up @@ -163,7 +160,7 @@ impl Client {
.await
.map_err(|err| PayError::from(err.0))?;

let proofs = construct_proofs(&cost_map, &payments);
let proofs = payment_proof_from_quotes_and_payments(&cost_map, &payments);

trace!(
"Chunk payments of {} chunks completed. {} chunks were free / already paid for",
Expand Down Expand Up @@ -229,7 +226,7 @@ async fn fetch_store_quote(
}

/// Form to be executed payments and already executed payments from a cost map.
fn extract_quote_payments(
pub(crate) fn extract_quote_payments(
cost_map: &HashMap<XorName, PayeeQuote>,
) -> (Vec<QuotePayment>, Vec<XorName>) {
let mut to_be_paid = vec![];
Expand All @@ -249,24 +246,3 @@ fn extract_quote_payments(

(to_be_paid, already_paid)
}

/// Construct payment proofs from cost map and payments map.
fn construct_proofs(
cost_map: &HashMap<XorName, PayeeQuote>,
payments: &BTreeMap<QuoteHash, TxHash>,
) -> HashMap<XorName, ProofOfPayment> {
cost_map
.iter()
.filter_map(|(xor_name, (_, _, quote))| {
payments.get(&quote.hash()).map(|tx_hash| {
(
*xor_name,
ProofOfPayment {
quote: quote.clone(),
tx_hash: *tx_hash,
},
)
})
})
.collect()
}
3 changes: 3 additions & 0 deletions autonomi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ extern crate tracing;
pub mod client;
#[cfg(feature = "data")]
mod self_encryption;
mod utils;

pub use sn_evm::get_evm_network_from_env;
pub use sn_evm::EvmNetwork;
pub use sn_evm::EvmWallet as Wallet;
pub use sn_evm::RewardsAddress;
#[cfg(feature = "external-signer")]
pub use utils::payment_proof_from_quotes_and_payments;

#[doc(no_inline)] // Place this under 'Re-exports' in the docs.
pub use bytes::Bytes;
Expand Down
24 changes: 24 additions & 0 deletions autonomi/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use sn_evm::{ProofOfPayment, QuoteHash, TxHash};
use sn_networking::PayeeQuote;
use std::collections::{BTreeMap, HashMap};
use xor_name::XorName;

pub fn payment_proof_from_quotes_and_payments(
quotes: &HashMap<XorName, PayeeQuote>,
payments: &BTreeMap<QuoteHash, TxHash>,
) -> HashMap<XorName, ProofOfPayment> {
quotes
.iter()
.filter_map(|(xor_name, (_, _, quote))| {
payments.get(&quote.hash()).map(|tx_hash| {
(
*xor_name,
ProofOfPayment {
quote: quote.clone(),
tx_hash: *tx_hash,
},
)
})
})
.collect()
}
91 changes: 91 additions & 0 deletions autonomi/tests/external_signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#![cfg(feature = "external-signer")]

use alloy::network::TransactionBuilder;
use alloy::providers::Provider;
use autonomi::Client;
use sn_evm::{QuoteHash, TxHash};
use sn_logging::LogBuilder;
use std::collections::BTreeMap;
use std::time::Duration;
use test_utils::evm::get_funded_wallet;
use test_utils::{gen_random_data, peers_from_env};
use tokio::time::sleep;

// Example of how put would be done using external signers.
#[tokio::test]
async fn external_signer_put() -> eyre::Result<()> {
let _log_appender_guard =
LogBuilder::init_single_threaded_tokio_test("external_signer_put", false);

let client = Client::connect(&peers_from_env()?).await?;
let wallet = get_funded_wallet();
let data = gen_random_data(1024 * 1024 * 10);

let (quotes, quote_payments, _free_chunks) = client.get_quotes_for_data(data.clone()).await?;

// Form quotes payment transaction data
let pay_for_quotes_calldata = autonomi::client::external_signer::pay_for_quotes_calldata(
wallet.network(),
quote_payments.into_iter(),
)?;

// Init an external wallet provider. In the webapp, this would be MetaMask for example
let provider = wallet.to_provider();

// Form approve to spend tokens transaction data
let approve_calldata = autonomi::client::external_signer::approve_to_spend_tokens_calldata(
wallet.network(),
pay_for_quotes_calldata.approve_spender,
pay_for_quotes_calldata.approve_amount,
);

// Prepare approve to spend tokens transaction
let transaction_request = provider
.transaction_request()
.with_to(approve_calldata.1)
.with_input(approve_calldata.0);

// Send approve to spend tokens transaction
let _tx_hash = provider
.send_transaction(transaction_request)
.await?
.watch()
.await?;

let mut payments: BTreeMap<QuoteHash, TxHash> = Default::default();

// Execute all quote payment transactions in batches
for (calldata, quote_hashes) in pay_for_quotes_calldata.batched_calldata_map {
// Prepare batched quote payments transaction
let transaction_request = provider
.transaction_request()
.with_to(pay_for_quotes_calldata.to)
.with_input(calldata);

// Send batched quote payments transaction
let tx_hash = provider
.send_transaction(transaction_request)
.await?
.watch()
.await?;

// Add to payments to be later use to construct the proofs
for quote_hash in quote_hashes {
payments.insert(quote_hash, tx_hash);
}
}

// Payment proofs
let proofs = autonomi::payment_proof_from_quotes_and_payments(&quotes, &payments);

let addr = client
.data_put_with_proof_of_payment(data.clone(), proofs)
.await?;

sleep(Duration::from_secs(10)).await;

let data_fetched = client.data_get(addr).await?;
assert_eq!(data, data_fetched, "data fetched should match data put");

Ok(())
}
1 change: 1 addition & 0 deletions evmlib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ version = "0.1.0"
[features]
wasm-bindgen = ["alloy/wasm-bindgen"]
local = []
external-signer = []

[dependencies]
alloy = { version = "0.4.2", default-features = false, features = ["std", "reqwest-rustls-tls", "provider-anvil-node", "sol-types", "json", "signers", "contract", "signer-local", "network"] }
Expand Down
1 change: 1 addition & 0 deletions evmlib/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ pub type QuoteHash = Hash;
pub type Amount = U256;
pub type QuotePayment = (QuoteHash, Address, Amount);
pub type EthereumWallet = alloy::network::EthereumWallet;
pub type Calldata = alloy::primitives::Bytes;
Loading

0 comments on commit 0ccde60

Please sign in to comment.