mirror of
https://github.com/mimblewimble/grin-wallet.git
synced 2025-02-01 17:01:10 +03:00
Contracts - Test fixes + warning cleanups (#694)
* clean up warnings in libwallet crate * clean up warnings in controller crate * update all contract tests with awareness of new proof structure
This commit is contained in:
parent
3da7695742
commit
fa78d72d35
22 changed files with 94 additions and 76 deletions
|
@ -483,13 +483,12 @@ where
|
||||||
/// TODO
|
/// TODO
|
||||||
pub fn verify_payment_proof_invoice(
|
pub fn verify_payment_proof_invoice(
|
||||||
&self,
|
&self,
|
||||||
keychain_mask: Option<&SecretKey>,
|
|
||||||
recipient_address: &DalekPublicKey,
|
recipient_address: &DalekPublicKey,
|
||||||
proof: &InvoiceProof,
|
proof: &InvoiceProof,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut w_lock = self.wallet_inst.lock();
|
let mut w_lock = self.wallet_inst.lock();
|
||||||
let w = w_lock.lc_provider()?.wallet_inst()?;
|
let w = w_lock.lc_provider()?.wallet_inst()?;
|
||||||
foreign::verify_payment_proof_invoice(&mut **w, keychain_mask, recipient_address, proof)
|
foreign::verify_payment_proof_invoice(&mut **w, recipient_address, proof)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -513,21 +513,43 @@ pub trait OwnerRpc {
|
||||||
```
|
```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
TODO: Full docs once API has stabilised
|
||||||
|
*/
|
||||||
fn init_send_tx(&self, token: Token, args: InitTxArgs) -> Result<VersionedSlate, Error>;
|
fn init_send_tx(&self, token: Token, args: InitTxArgs) -> Result<VersionedSlate, Error>;
|
||||||
fn contract_new(&self, token: Token, args: ContractNewArgsAPI)
|
|
||||||
-> Result<VersionedSlate, Error>;
|
/**
|
||||||
|
TODO: Implement
|
||||||
|
*/
|
||||||
// fn contract_setup(
|
// fn contract_setup(
|
||||||
// &self,
|
// &self,
|
||||||
// token: Token,
|
// token: Token,
|
||||||
// slate: VersionedSlate,
|
// slate: VersionedSlate,
|
||||||
// args: ContractSetupArgsAPI,
|
// args: ContractSetupArgsAPI,
|
||||||
// ) -> Result<VersionedSlate, Error>;
|
// ) -> Result<VersionedSlate, Error>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
TODO: Full docs once API has stabilised
|
||||||
|
*/
|
||||||
|
|
||||||
|
fn contract_new(&self, token: Token, args: ContractNewArgsAPI)
|
||||||
|
-> Result<VersionedSlate, Error>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
TODO: Full docs once API has stabilised
|
||||||
|
*/
|
||||||
|
|
||||||
fn contract_sign(
|
fn contract_sign(
|
||||||
&self,
|
&self,
|
||||||
token: Token,
|
token: Token,
|
||||||
slate: VersionedSlate,
|
slate: VersionedSlate,
|
||||||
args: ContractSetupArgsAPI,
|
args: ContractSetupArgsAPI,
|
||||||
) -> Result<VersionedSlate, Error>;
|
) -> Result<VersionedSlate, Error>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
TODO: Full docs once API has stabilised
|
||||||
|
*/
|
||||||
|
|
||||||
fn contract_revoke(
|
fn contract_revoke(
|
||||||
&self,
|
&self,
|
||||||
token: Token,
|
token: Token,
|
||||||
|
|
|
@ -184,6 +184,7 @@ pub fn open_local_wallet(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates the given number of wallets and spawns a thread that runs the wallet proxy
|
// Creates the given number of wallets and spawns a thread that runs the wallet proxy
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn create_wallets(
|
pub fn create_wallets(
|
||||||
wallets_def: Vec<Vec<(&'static str, u64)>>, // a vector of boolean that represent whether we mine into a wallet
|
wallets_def: Vec<Vec<(&'static str, u64)>>, // a vector of boolean that represent whether we mine into a wallet
|
||||||
test_dir: &'static str,
|
test_dir: &'static str,
|
||||||
|
|
|
@ -24,9 +24,8 @@ use self::core::global;
|
||||||
use self::keychain::{ExtKeychain, Keychain};
|
use self::keychain::{ExtKeychain, Keychain};
|
||||||
use grin_wallet_libwallet as libwallet;
|
use grin_wallet_libwallet as libwallet;
|
||||||
use impls::test_framework::{self};
|
use impls::test_framework::{self};
|
||||||
use libwallet::contract::my_fee_contribution;
|
|
||||||
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
|
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
|
||||||
use libwallet::{Slate, SlateState, TxLogEntryType};
|
use libwallet::{Slate, SlateState};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -38,7 +37,7 @@ use common::{clean_output_dir, create_wallets, setup};
|
||||||
/// contract accounts testing (mostly the same as accounts.rs)
|
/// contract accounts testing (mostly the same as accounts.rs)
|
||||||
fn contract_accounts_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
fn contract_accounts_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
||||||
// create two wallets with some extra accounts and don't mine anything in them
|
// create two wallets with some extra accounts and don't mine anything in them
|
||||||
let (wallets, chain, stopper, mut bh) = create_wallets(
|
let (wallets, chain, stopper, _bh) = create_wallets(
|
||||||
vec![
|
vec![
|
||||||
vec![
|
vec![
|
||||||
("default", 0),
|
("default", 0),
|
||||||
|
@ -145,6 +144,7 @@ fn contract_accounts_impl(test_dir: &'static str) -> Result<(), libwallet::Error
|
||||||
|
|
||||||
let mut slate = Slate::blank(0, true); // this gets overriden below
|
let mut slate = Slate::blank(0, true); // this gets overriden below
|
||||||
|
|
||||||
|
let mut sender_address = None;
|
||||||
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
|
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
|
||||||
// Send wallet inititates a standard transaction with --send=5
|
// Send wallet inititates a standard transaction with --send=5
|
||||||
let args = &ContractNewArgsAPI {
|
let args = &ContractNewArgsAPI {
|
||||||
|
@ -155,17 +155,21 @@ fn contract_accounts_impl(test_dir: &'static str) -> Result<(), libwallet::Error
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
slate = api.contract_new(m, args)?;
|
slate = api.contract_new(m, args)?;
|
||||||
|
sender_address = Some(api.get_slatepack_address(mask1, 0)?.pub_key);
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
assert_eq!(slate.state, SlateState::Standard1);
|
assert_eq!(slate.state, SlateState::Standard1);
|
||||||
|
|
||||||
|
let mut recipient_address = None;
|
||||||
wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| {
|
wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| {
|
||||||
// Receive wallet calls --receive=5
|
// Receive wallet calls --receive=5
|
||||||
let args = &ContractSetupArgsAPI {
|
let args = &mut ContractSetupArgsAPI {
|
||||||
net_change: Some(5_000_000_000),
|
net_change: Some(5_000_000_000),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
args.proof_args.sender_address = sender_address;
|
||||||
slate = api.contract_sign(m, &slate, args)?;
|
slate = api.contract_sign(m, &slate, args)?;
|
||||||
|
recipient_address = Some(api.get_slatepack_address(mask2, 0)?.pub_key);
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
assert_eq!(slate.state, SlateState::Standard2);
|
assert_eq!(slate.state, SlateState::Standard2);
|
||||||
|
@ -184,7 +188,6 @@ fn contract_accounts_impl(test_dir: &'static str) -> Result<(), libwallet::Error
|
||||||
api.post_tx(m, &slate, false)?;
|
api.post_tx(m, &slate, false)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
bh += 1;
|
|
||||||
|
|
||||||
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
|
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
|
||||||
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
|
let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?;
|
||||||
|
|
|
@ -18,15 +18,10 @@ extern crate grin_wallet_impls as impls;
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
use grin_core as core;
|
use grin_core as core;
|
||||||
use grin_keychain as keychain;
|
|
||||||
|
|
||||||
use self::core::global;
|
|
||||||
use self::keychain::{ExtKeychain, Keychain};
|
|
||||||
use grin_wallet_libwallet as libwallet;
|
use grin_wallet_libwallet as libwallet;
|
||||||
use impls::test_framework::{self};
|
|
||||||
use libwallet::contract::my_fee_contribution;
|
use libwallet::contract::my_fee_contribution;
|
||||||
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
|
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
|
||||||
use libwallet::{Slate, SlateState, TxLogEntryType};
|
use libwallet::{Slate, SlateState};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -38,7 +33,7 @@ use common::{clean_output_dir, create_wallets, setup};
|
||||||
/// contract accounts testing when switching between accounts during transaction building
|
/// contract accounts testing when switching between accounts during transaction building
|
||||||
fn contract_accounts_switch_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
fn contract_accounts_switch_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
||||||
// create two wallets with some extra accounts and don't mine anything in them
|
// create two wallets with some extra accounts and don't mine anything in them
|
||||||
let (wallets, chain, stopper, mut bh) = create_wallets(
|
let (wallets, _chain, stopper, _bh) = create_wallets(
|
||||||
vec![
|
vec![
|
||||||
vec![("default", 0), ("account1", 1), ("account2", 2)],
|
vec![("default", 0), ("account1", 1), ("account2", 2)],
|
||||||
vec![("default", 0), ("account1", 3), ("account2", 4)],
|
vec![("default", 0), ("account1", 3), ("account2", 4)],
|
||||||
|
@ -118,10 +113,11 @@ fn contract_accounts_switch_impl(test_dir: &'static str) -> Result<(), libwallet
|
||||||
}
|
}
|
||||||
wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| {
|
wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| {
|
||||||
// Receive wallet calls --receive=5
|
// Receive wallet calls --receive=5
|
||||||
let args = &ContractSetupArgsAPI {
|
let args = &mut ContractSetupArgsAPI {
|
||||||
net_change: Some(5_000_000_000),
|
net_change: Some(5_000_000_000),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
args.proof_args.suppress_proof = true;
|
||||||
slate = api.contract_sign(m, &slate, args)?;
|
slate = api.contract_sign(m, &slate, args)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
|
@ -20,10 +20,9 @@ extern crate log;
|
||||||
use grin_wallet_libwallet as libwallet;
|
use grin_wallet_libwallet as libwallet;
|
||||||
|
|
||||||
use grin_core::consensus;
|
use grin_core::consensus;
|
||||||
use impls::test_framework::{self};
|
|
||||||
use libwallet::contract::my_fee_contribution;
|
use libwallet::contract::my_fee_contribution;
|
||||||
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs};
|
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
|
||||||
use libwallet::{OutputCommitMapping, OutputStatus, Slate, SlateState, TxLogEntryType};
|
use libwallet::{OutputStatus, Slate, SlateState};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -35,7 +34,7 @@ use common::{clean_output_dir, create_wallets, setup};
|
||||||
/// contract new with --add-outputs
|
/// contract new with --add-outputs
|
||||||
fn contract_early_lock_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
fn contract_early_lock_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> {
|
||||||
// create a single wallet and mine 5 blocks
|
// create a single wallet and mine 5 blocks
|
||||||
let (wallets, chain, stopper, mut bh) =
|
let (wallets, _chain, stopper, _bh) =
|
||||||
create_wallets(vec![vec![("default", 5)]], test_dir).unwrap();
|
create_wallets(vec![vec![("default", 5)]], test_dir).unwrap();
|
||||||
let send_wallet = wallets[0].0.clone();
|
let send_wallet = wallets[0].0.clone();
|
||||||
let send_mask = wallets[0].1.as_ref();
|
let send_mask = wallets[0].1.as_ref();
|
||||||
|
|
|
@ -22,13 +22,10 @@ extern crate grin_wallet_controller as wallet;
|
||||||
extern crate grin_wallet_impls as impls;
|
extern crate grin_wallet_impls as impls;
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
use grin_core::consensus::KERNEL_WEIGHT;
|
|
||||||
use grin_wallet_libwallet as libwallet;
|
use grin_wallet_libwallet as libwallet;
|
||||||
|
|
||||||
use grin_util::static_secp_instance;
|
|
||||||
use impls::test_framework::{self};
|
use impls::test_framework::{self};
|
||||||
use libwallet::contract::my_fee_contribution;
|
use libwallet::contract::my_fee_contribution;
|
||||||
use libwallet::contract::proofs::{InvoiceProof, ProofWitness};
|
|
||||||
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
|
use libwallet::contract::types::{ContractNewArgsAPI, ContractSetupArgsAPI};
|
||||||
use libwallet::{Slate, SlateState, TxLogEntryType};
|
use libwallet::{Slate, SlateState, TxLogEntryType};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
@ -70,7 +67,7 @@ fn contract_early_proofs_test_impl(test_dir: &'static str) -> Result<(), libwall
|
||||||
let mut recipient_address = None;
|
let mut recipient_address = None;
|
||||||
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
|
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
|
||||||
// Receive wallet calls --receive=5
|
// Receive wallet calls --receive=5
|
||||||
let mut args = &mut ContractSetupArgsAPI {
|
let args = &mut ContractSetupArgsAPI {
|
||||||
net_change: Some(5_000_000_000),
|
net_change: Some(5_000_000_000),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
@ -145,7 +142,7 @@ fn contract_early_proofs_test_impl(test_dir: &'static str) -> Result<(), libwall
|
||||||
|
|
||||||
let mut invoice_proof = None;
|
let mut invoice_proof = None;
|
||||||
// Now some time has passed, sender retrieves and verify the payment proof
|
// Now some time has passed, sender retrieves and verify the payment proof
|
||||||
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
|
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, _m| {
|
||||||
// Extract the stored data as an invoice proof
|
// Extract the stored data as an invoice proof
|
||||||
invoice_proof =
|
invoice_proof =
|
||||||
Some(api.retrieve_payment_proof_invoice(send_mask, true, None, Some(slate.id))?);
|
Some(api.retrieve_payment_proof_invoice(send_mask, true, None, Some(slate.id))?);
|
||||||
|
@ -160,14 +157,10 @@ fn contract_early_proofs_test_impl(test_dir: &'static str) -> Result<(), libwall
|
||||||
|
|
||||||
wallet::controller::foreign_single_use(recv_wallet.clone(), recv_mask.cloned(), |api| {
|
wallet::controller::foreign_single_use(recv_wallet.clone(), recv_mask.cloned(), |api| {
|
||||||
let mut proof = serde_json::from_str(&invoice_proof_json).unwrap();
|
let mut proof = serde_json::from_str(&invoice_proof_json).unwrap();
|
||||||
api.verify_payment_proof_invoice(recv_mask, recipient_address.as_ref().unwrap(), &proof)?;
|
api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof)?;
|
||||||
// tweak something and it shouldn't verify
|
// tweak something and it shouldn't verify
|
||||||
proof.amount = 400000;
|
proof.amount = 400000;
|
||||||
let retval = api.verify_payment_proof_invoice(
|
let retval = api.verify_payment_proof_invoice(recipient_address.as_ref().unwrap(), &proof);
|
||||||
recv_mask,
|
|
||||||
recipient_address.as_ref().unwrap(),
|
|
||||||
&proof,
|
|
||||||
);
|
|
||||||
assert!(retval.is_err());
|
assert!(retval.is_err());
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
|
@ -45,13 +45,14 @@ fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
||||||
|
|
||||||
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
|
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
|
||||||
// Receive wallet inititates an invoice transaction with --receive=5
|
// Receive wallet inititates an invoice transaction with --receive=5
|
||||||
let args = &ContractNewArgsAPI {
|
let args = &mut ContractNewArgsAPI {
|
||||||
setup_args: ContractSetupArgsAPI {
|
setup_args: ContractSetupArgsAPI {
|
||||||
net_change: Some(5_000_000_000),
|
net_change: Some(5_000_000_000),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
args.setup_args.proof_args.suppress_proof = true;
|
||||||
slate = api.contract_new(m, args)?;
|
slate = api.contract_new(m, args)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
@ -70,9 +71,11 @@ fn contract_rsr_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
||||||
|
|
||||||
// Receive wallet finalizes and posts
|
// Receive wallet finalizes and posts
|
||||||
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
|
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
|
||||||
let args = &ContractSetupArgsAPI {
|
let args = &mut ContractSetupArgsAPI {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
// TODO: This possibly shouldn't be needed here if no proof?
|
||||||
|
args.proof_args.suppress_proof = true;
|
||||||
slate = api.contract_sign(m, &slate, args)?;
|
slate = api.contract_sign(m, &slate, args)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
|
@ -45,7 +45,7 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
||||||
|
|
||||||
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
|
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
|
||||||
// Send wallet inititates a standard transaction with --send=5
|
// Send wallet inititates a standard transaction with --send=5
|
||||||
let args = &ContractNewArgsAPI {
|
let args = &mut ContractNewArgsAPI {
|
||||||
setup_args: ContractSetupArgsAPI {
|
setup_args: ContractSetupArgsAPI {
|
||||||
net_change: Some(-5_000_000_000),
|
net_change: Some(-5_000_000_000),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -59,10 +59,11 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
||||||
|
|
||||||
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
|
wallet::controller::owner_single_use(Some(recv_wallet.clone()), recv_mask, None, |api, m| {
|
||||||
// Receive wallet calls --receive=5
|
// Receive wallet calls --receive=5
|
||||||
let args = &ContractSetupArgsAPI {
|
let args = &mut ContractSetupArgsAPI {
|
||||||
net_change: Some(5_000_000_000),
|
net_change: Some(5_000_000_000),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
args.proof_args.suppress_proof = true;
|
||||||
slate = api.contract_sign(m, &slate, args)?;
|
slate = api.contract_sign(m, &slate, args)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
@ -70,9 +71,10 @@ fn contract_srs_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error>
|
||||||
|
|
||||||
// Send wallet finalizes and posts
|
// Send wallet finalizes and posts
|
||||||
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
|
wallet::controller::owner_single_use(Some(send_wallet.clone()), send_mask, None, |api, m| {
|
||||||
let args = &ContractSetupArgsAPI {
|
let args = &mut ContractSetupArgsAPI {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
args.proof_args.suppress_proof = true;
|
||||||
slate = api.contract_sign(m, &slate, args)?;
|
slate = api.contract_sign(m, &slate, args)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
|
@ -21,8 +21,6 @@ use std::io::{Read, Write};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::blake2::blake2b::{Blake2b, Blake2bResult};
|
use crate::blake2::blake2b::{Blake2b, Blake2bResult};
|
||||||
|
|
||||||
use crate::keychain::{ChildNumber, ExtKeychain, Identifier, Keychain, SwitchCommitmentType};
|
use crate::keychain::{ChildNumber, ExtKeychain, Identifier, Keychain, SwitchCommitmentType};
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
//! Generic implementation of owner API functions
|
//! Generic implementation of owner API functions
|
||||||
use grin_util::secp::pedersen::Commitment;
|
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
use crate::api_impl::owner::contract_new as owner_contract_new;
|
use crate::api_impl::owner::contract_new as owner_contract_new;
|
||||||
|
@ -230,7 +229,6 @@ where
|
||||||
/// Verify an invoice payment proof
|
/// Verify an invoice payment proof
|
||||||
pub fn verify_payment_proof_invoice<'a, T: ?Sized, C, K>(
|
pub fn verify_payment_proof_invoice<'a, T: ?Sized, C, K>(
|
||||||
w: &mut T,
|
w: &mut T,
|
||||||
keychain_mask: Option<&SecretKey>,
|
|
||||||
recipient_address: &DalekPublicKey,
|
recipient_address: &DalekPublicKey,
|
||||||
proof: &InvoiceProof,
|
proof: &InvoiceProof,
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
|
@ -239,13 +237,7 @@ where
|
||||||
C: NodeClient + 'a,
|
C: NodeClient + 'a,
|
||||||
K: Keychain + 'a,
|
K: Keychain + 'a,
|
||||||
{
|
{
|
||||||
let (mut client, parent_key_id, keychain) = {
|
let mut client = w.w2n_client().clone();
|
||||||
(
|
|
||||||
w.w2n_client().clone(),
|
|
||||||
w.parent_key_id(),
|
|
||||||
w.keychain(keychain_mask)?,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let wd = match proof.witness_data.clone() {
|
let wd = match proof.witness_data.clone() {
|
||||||
Some(w) => w,
|
Some(w) => w,
|
||||||
|
@ -256,7 +248,7 @@ where
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (retrieved_kernel, index) = match client.get_kernel(&wd.kernel_commitment, None, None) {
|
let (retrieved_kernel, _) = match client.get_kernel(&wd.kernel_commitment, None, None) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(Error::PaymentProof(format!(
|
return Err(Error::PaymentProof(format!(
|
||||||
"Error retrieving kernel from chain: {}",
|
"Error retrieving kernel from chain: {}",
|
||||||
|
|
|
@ -45,7 +45,6 @@ use ed25519_dalek::SecretKey as DalekSecretKey;
|
||||||
use ed25519_dalek::Verifier;
|
use ed25519_dalek::Verifier;
|
||||||
|
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::convert::{TryFrom, TryInto};
|
||||||
use std::ops::Index;
|
|
||||||
use std::sync::mpsc::Sender;
|
use std::sync::mpsc::Sender;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -713,7 +712,10 @@ where
|
||||||
sender_address: Some(sender_address.to_ed25519()?),
|
sender_address: Some(sender_address.to_ed25519()?),
|
||||||
receiver_address: a.pub_key,
|
receiver_address: a.pub_key,
|
||||||
promise_signature: None,
|
promise_signature: None,
|
||||||
timestamp: DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc),
|
timestamp: DateTime::<Utc>::from_utc(
|
||||||
|
NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
|
||||||
|
Utc,
|
||||||
|
),
|
||||||
memo: None,
|
memo: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,6 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
//! Implementation of contract revoke
|
//! Implementation of contract revoke
|
||||||
|
|
||||||
use std::default;
|
|
||||||
|
|
||||||
use crate::contract::types::{ContractRevokeArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs};
|
use crate::contract::types::{ContractRevokeArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs};
|
||||||
use crate::contract::{new, sign};
|
use crate::contract::{new, sign};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
|
@ -23,10 +23,10 @@ use crate::types::{NodeClient, WalletBackend};
|
||||||
|
|
||||||
/// View contract
|
/// View contract
|
||||||
pub fn view<'a, T: ?Sized, C, K>(
|
pub fn view<'a, T: ?Sized, C, K>(
|
||||||
w: &mut T,
|
_w: &mut T,
|
||||||
keychain_mask: Option<&SecretKey>,
|
_keychain_mask: Option<&SecretKey>,
|
||||||
slate: &mut Slate,
|
slate: &mut Slate,
|
||||||
encrypted_for: &str,
|
_encrypted_for: &str,
|
||||||
) -> Result<ContractView, Error>
|
) -> Result<ContractView, Error>
|
||||||
where
|
where
|
||||||
T: WalletBackend<'a, C, K>,
|
T: WalletBackend<'a, C, K>,
|
||||||
|
|
|
@ -40,6 +40,7 @@ use ed25519_dalek::{Signer, Verifier};
|
||||||
use grin_util::secp::Message;
|
use grin_util::secp::Message;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
/// All elements required to validate a proof within a single struct
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ProofWitness {
|
pub struct ProofWitness {
|
||||||
/// Kernel index, supplied so verifiers can look up kernel
|
/// Kernel index, supplied so verifiers can look up kernel
|
||||||
|
@ -58,8 +59,8 @@ pub struct ProofWitness {
|
||||||
pub sender_partial_sig: Signature,
|
pub sender_partial_sig: Signature,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payment proof, to be extracted from slates for
|
/// Payment proof, to be extracted from slates for
|
||||||
// signing (when wrapped as PaymentProofBin) or json export from stored tx data
|
/// signing (when wrapped as PaymentProofBin) or json export from stored tx data
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct InvoiceProof {
|
pub struct InvoiceProof {
|
||||||
/// Proof type, 0x00 legacy (though this will use StoredProofInfo above, 1 invoice, 2 Sender nonce)
|
/// Proof type, 0x00 legacy (though this will use StoredProofInfo above, 1 invoice, 2 Sender nonce)
|
||||||
|
@ -212,7 +213,9 @@ impl InvoiceProof {
|
||||||
};
|
};
|
||||||
|
|
||||||
let timestamp = match slate.payment_proof.as_ref() {
|
let timestamp = match slate.payment_proof.as_ref() {
|
||||||
Some(p) => NaiveDateTime::from_timestamp(p.timestamp.timestamp(), 0).timestamp(),
|
Some(p) => NaiveDateTime::from_timestamp_opt(p.timestamp.timestamp(), 0)
|
||||||
|
.unwrap()
|
||||||
|
.timestamp(),
|
||||||
None => 0,
|
None => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -239,6 +242,7 @@ impl InvoiceProof {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sign the invoice proof, provided all fields are populated
|
||||||
pub fn sign(&self, sec_key: &SecretKey) -> Result<(DalekSignature, DalekPublicKey), Error> {
|
pub fn sign(&self, sec_key: &SecretKey) -> Result<(DalekSignature, DalekPublicKey), Error> {
|
||||||
let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) {
|
let d_skey = match DalekSecretKey::from_bytes(&sec_key.0) {
|
||||||
Ok(k) => k,
|
Ok(k) => k,
|
||||||
|
@ -258,6 +262,7 @@ impl InvoiceProof {
|
||||||
Ok((keypair.sign(&sig_data_bin), pub_key))
|
Ok((keypair.sign(&sig_data_bin), pub_key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verify the signature of the invoice proof
|
||||||
pub fn verify_promise_signature(
|
pub fn verify_promise_signature(
|
||||||
&self,
|
&self,
|
||||||
recipient_address: &DalekPublicKey,
|
recipient_address: &DalekPublicKey,
|
||||||
|
@ -284,6 +289,8 @@ impl InvoiceProof {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verify signature and proof against a given kernel message (kernel lookup is beyond the scope
|
||||||
|
/// of this module)
|
||||||
pub fn verify_witness(
|
pub fn verify_witness(
|
||||||
&self,
|
&self,
|
||||||
recipient_address: &DalekPublicKey,
|
recipient_address: &DalekPublicKey,
|
||||||
|
@ -369,7 +376,7 @@ where
|
||||||
// TODO: Just generating invoice (type 1) for now
|
// TODO: Just generating invoice (type 1) for now
|
||||||
let (invoice_proof, promise_signature, receiver_address) =
|
let (invoice_proof, promise_signature, receiver_address) =
|
||||||
generate_invoice_signature(wallet, keychain_mask, slate, context, proof_args)?;
|
generate_invoice_signature(wallet, keychain_mask, slate, context, proof_args)?;
|
||||||
let timestamp = NaiveDateTime::from_timestamp(Utc::now().timestamp(), 0);
|
let timestamp = NaiveDateTime::from_timestamp_opt(Utc::now().timestamp(), 0).unwrap();
|
||||||
let timestamp = DateTime::<Utc>::from_utc(timestamp, Utc);
|
let timestamp = DateTime::<Utc>::from_utc(timestamp, Utc);
|
||||||
|
|
||||||
let proof = PaymentInfo {
|
let proof = PaymentInfo {
|
||||||
|
@ -407,7 +414,9 @@ where
|
||||||
let recp_key =
|
let recp_key =
|
||||||
address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?;
|
address::address_from_derivation_path(&keychain, &parent_key_id, derivation_index)?;
|
||||||
|
|
||||||
invoice_proof.timestamp = NaiveDateTime::from_timestamp(Utc::now().timestamp(), 0).timestamp();
|
invoice_proof.timestamp = NaiveDateTime::from_timestamp_opt(Utc::now().timestamp(), 0)
|
||||||
|
.unwrap()
|
||||||
|
.timestamp();
|
||||||
let (sig, addr) = invoice_proof.sign(&recp_key)?;
|
let (sig, addr) = invoice_proof.sign(&recp_key)?;
|
||||||
Ok((invoice_proof, sig, addr))
|
Ok((invoice_proof, sig, addr))
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,9 +26,10 @@ use super::types::ProofArgs;
|
||||||
use crate::contract::proofs::InvoiceProof;
|
use crate::contract::proofs::InvoiceProof;
|
||||||
use ed25519_dalek::PublicKey as DalekPublicKey;
|
use ed25519_dalek::PublicKey as DalekPublicKey;
|
||||||
|
|
||||||
|
/// TODO: Removed for now, consider secp error in sign function
|
||||||
/// The secret key we replace the actual key with after we have signed with the Context keys. This is
|
/// The secret key we replace the actual key with after we have signed with the Context keys. This is
|
||||||
/// to prevent possibility of signing with the same key twice.
|
/// to prevent possibility of signing with the same key twice.
|
||||||
pub const SEC_KEY_FAKE: [u8; 32] = [0; 32];
|
/// pub const SEC_KEY_FAKE: [u8; 32] = [0; 32];
|
||||||
|
|
||||||
/// Add payment proof data to slate, noop for sender
|
/// Add payment proof data to slate, noop for sender
|
||||||
pub fn add_payment_proof<'a, T: ?Sized, C, K>(
|
pub fn add_payment_proof<'a, T: ?Sized, C, K>(
|
||||||
|
@ -46,7 +47,7 @@ where
|
||||||
{
|
{
|
||||||
// TODO: Implement. Consider adding this function to the Slate itself so they can easily be versioned
|
// TODO: Implement. Consider adding this function to the Slate itself so they can easily be versioned
|
||||||
// e.g. slate.add_payment_proof_data()
|
// e.g. slate.add_payment_proof_data()
|
||||||
trace!("contract::slate::add_payment_proof => called");
|
debug!("contract::slate::add_payment_proof => called");
|
||||||
// If we're a recipient, generate proof unless explicity told not to
|
// If we're a recipient, generate proof unless explicity told not to
|
||||||
if let Some(ref c) = net_change {
|
if let Some(ref c) = net_change {
|
||||||
if *c > 0 && !proof_args.suppress_proof && slate.payment_proof.is_none() {
|
if *c > 0 && !proof_args.suppress_proof && slate.payment_proof.is_none() {
|
||||||
|
|
|
@ -219,6 +219,8 @@ impl Default for ContractView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Arguments for contract revoke function
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct ContractRevokeArgsAPI {
|
pub struct ContractRevokeArgsAPI {
|
||||||
/// Tx id to cancel
|
/// Tx id to cancel
|
||||||
|
|
|
@ -26,8 +26,6 @@ use crate::{address, Error, OutputData, OutputStatus, TxLogEntry};
|
||||||
use grin_core::core::FeeFields;
|
use grin_core::core::FeeFields;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::proofs::InvoiceProof;
|
|
||||||
|
|
||||||
/// Creates an initial TxLogEntry without input/output or kernel information
|
/// Creates an initial TxLogEntry without input/output or kernel information
|
||||||
pub fn create_tx_log_entry(
|
pub fn create_tx_log_entry(
|
||||||
slate: &Slate,
|
slate: &Slate,
|
||||||
|
|
|
@ -257,7 +257,7 @@ pub enum Error {
|
||||||
#[error("Proof Address decoding: {0}")]
|
#[error("Proof Address decoding: {0}")]
|
||||||
AddressDecoding(String),
|
AddressDecoding(String),
|
||||||
|
|
||||||
// Payment proof - no sender address provided or found in slate
|
/// Payment proof - no sender address provided or found in slate
|
||||||
#[error("Sender address has not been provided")]
|
#[error("Sender address has not been provided")]
|
||||||
NoSenderAddressProvided,
|
NoSenderAddressProvided,
|
||||||
|
|
||||||
|
|
|
@ -1600,7 +1600,10 @@ impl From<&PaymentInfoV4> for PaymentInfo {
|
||||||
sender_address: Some(sender_address),
|
sender_address: Some(sender_address),
|
||||||
receiver_address,
|
receiver_address,
|
||||||
promise_signature: receiver_signature,
|
promise_signature: receiver_signature,
|
||||||
timestamp: DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc),
|
timestamp: DateTime::<Utc>::from_utc(
|
||||||
|
NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
|
||||||
|
Utc,
|
||||||
|
),
|
||||||
memo: None,
|
memo: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,7 +142,7 @@ impl VersionedCoinbase {
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use crate::grin_core::core::transaction::{FeeFields, OutputFeatures};
|
use crate::grin_core::core::transaction::OutputFeatures;
|
||||||
use crate::grin_util::from_hex;
|
use crate::grin_util::from_hex;
|
||||||
use crate::grin_util::secp::key::PublicKey;
|
use crate::grin_util::secp::key::PublicKey;
|
||||||
use crate::grin_util::secp::pedersen::{Commitment, RangeProof};
|
use crate::grin_util::secp::pedersen::{Commitment, RangeProof};
|
||||||
|
@ -155,7 +155,6 @@ pub mod tests {
|
||||||
use ed25519_dalek::Signature as DalekSignature;
|
use ed25519_dalek::Signature as DalekSignature;
|
||||||
use grin_core::global::{set_local_chain_type, ChainTypes};
|
use grin_core::global::{set_local_chain_type, ChainTypes};
|
||||||
use grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType};
|
use grin_keychain::{ExtKeychain, Keychain, SwitchCommitmentType};
|
||||||
use grin_wallet_util::byte_ser::from_bytes;
|
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
|
||||||
// Populate a test internal slate with all fields to test conversions
|
// Populate a test internal slate with all fields to test conversions
|
||||||
|
@ -224,7 +223,7 @@ pub mod tests {
|
||||||
let b = from_hex(raw_pubkey_str).unwrap();
|
let b = from_hex(raw_pubkey_str).unwrap();
|
||||||
let d_pkey = DalekPublicKey::from_bytes(&b).unwrap();
|
let d_pkey = DalekPublicKey::from_bytes(&b).unwrap();
|
||||||
// Need to remove milliseconds component for comparison. Won't be serialized
|
// Need to remove milliseconds component for comparison. Won't be serialized
|
||||||
let ts = NaiveDateTime::from_timestamp(Utc::now().timestamp(), 0);
|
let ts = NaiveDateTime::from_timestamp_opt(Utc::now().timestamp(), 0).unwrap();
|
||||||
let ts = DateTime::<Utc>::from_utc(ts, Utc);
|
let ts = DateTime::<Utc>::from_utc(ts, Utc);
|
||||||
let pm = PaymentMemo {
|
let pm = PaymentMemo {
|
||||||
memo_type: 1,
|
memo_type: 1,
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
//! Wraps a V5 Slate into a V5 Binary slate
|
//! Wraps a V5 Slate into a V5 Binary slate
|
||||||
|
|
||||||
use crate::grin_core::core::transaction::{FeeFields, OutputFeatures};
|
use crate::grin_core::core::transaction::OutputFeatures;
|
||||||
use crate::grin_core::ser as grin_ser;
|
use crate::grin_core::ser as grin_ser;
|
||||||
use crate::grin_core::ser::{Readable, Reader, Writeable, Writer};
|
use crate::grin_core::ser::{Readable, Reader, Writeable, Writer};
|
||||||
use crate::grin_keychain::BlindingFactor;
|
use crate::grin_keychain::BlindingFactor;
|
||||||
|
@ -24,9 +24,7 @@ use crate::grin_util::secp::Signature;
|
||||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
use ed25519_dalek::PublicKey as DalekPublicKey;
|
use ed25519_dalek::PublicKey as DalekPublicKey;
|
||||||
use ed25519_dalek::Signature as DalekSignature;
|
use ed25519_dalek::Signature as DalekSignature;
|
||||||
use grin_wallet_util::byte_ser::from_bytes;
|
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::convert::{TryFrom, TryInto};
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::slate_versions::v5::{
|
use crate::slate_versions::v5::{
|
||||||
CommitsV5, KernelFeaturesArgsV5, ParticipantDataV5, PaymentInfoV5, PaymentMemoV5, SlateStateV5,
|
CommitsV5, KernelFeaturesArgsV5, ParticipantDataV5, PaymentInfoV5, PaymentMemoV5, SlateStateV5,
|
||||||
|
@ -249,7 +247,8 @@ impl Readable for ProofWrap {
|
||||||
let saddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?).unwrap();
|
let saddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?).unwrap();
|
||||||
let raddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?).unwrap();
|
let raddr = DalekPublicKey::from_bytes(&reader.read_fixed_bytes(32)?).unwrap();
|
||||||
let ts_raw: i64 = reader.read_i64().unwrap();
|
let ts_raw: i64 = reader.read_i64().unwrap();
|
||||||
let ts = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(ts_raw, 0), Utc);
|
let ts =
|
||||||
|
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp_opt(ts_raw, 0).unwrap(), Utc);
|
||||||
let psig = match reader.read_u8()? {
|
let psig = match reader.read_u8()? {
|
||||||
0 => None,
|
0 => None,
|
||||||
1 | _ => Some(DalekSignature::try_from(&reader.read_fixed_bytes(64)?[..]).unwrap()),
|
1 | _ => Some(DalekSignature::try_from(&reader.read_fixed_bytes(64)?[..]).unwrap()),
|
||||||
|
@ -488,7 +487,7 @@ fn slate_v5_serialize_deserialize() {
|
||||||
let b = from_hex(raw_pubkey_str).unwrap();
|
let b = from_hex(raw_pubkey_str).unwrap();
|
||||||
let d_pkey = DalekPublicKey::from_bytes(&b).unwrap();
|
let d_pkey = DalekPublicKey::from_bytes(&b).unwrap();
|
||||||
// Need to remove milliseconds component for comparison. Won't be serialized
|
// Need to remove milliseconds component for comparison. Won't be serialized
|
||||||
let ts = NaiveDateTime::from_timestamp(Utc::now().timestamp(), 0);
|
let ts = NaiveDateTime::from_timestamp_opt(Utc::now().timestamp(), 0).unwrap();
|
||||||
let ts = DateTime::<Utc>::from_utc(ts, Utc);
|
let ts = DateTime::<Utc>::from_utc(ts, Utc);
|
||||||
let pm = PaymentMemoV5 {
|
let pm = PaymentMemoV5 {
|
||||||
memo_type: 0,
|
memo_type: 0,
|
||||||
|
|
Loading…
Reference in a new issue