Merge branch 'client_submodules' into rpc_tests
This commit is contained in:
commit
389d17974e
@ -35,7 +35,6 @@ use transaction::LocalizedTransaction;
|
||||
use extras::TransactionAddress;
|
||||
use filter::Filter;
|
||||
use log_entry::LocalizedLogEntry;
|
||||
use util::keys::store::SecretStore;
|
||||
use block_queue::{BlockQueue, BlockQueueInfo};
|
||||
use blockchain::{BlockChain, BlockProvider, TreeRoute};
|
||||
use client::{BlockId, TransactionId, ClientConfig, BlockChainClient};
|
||||
@ -114,7 +113,6 @@ pub struct Client<V = CanonVerifier> where V: Verifier {
|
||||
author: RwLock<Address>,
|
||||
extra_data: RwLock<Bytes>,
|
||||
verifier: PhantomData<V>,
|
||||
secret_store: Arc<RwLock<SecretStore>>,
|
||||
}
|
||||
|
||||
const HISTORY: u64 = 1000;
|
||||
@ -150,9 +148,6 @@ impl<V> Client<V> where V: Verifier {
|
||||
let panic_handler = PanicHandler::new_in_arc();
|
||||
panic_handler.forward_from(&block_queue);
|
||||
|
||||
let secret_store = Arc::new(RwLock::new(SecretStore::new()));
|
||||
secret_store.write().unwrap().try_import_existing();
|
||||
|
||||
Ok(Arc::new(Client {
|
||||
chain: chain,
|
||||
engine: engine,
|
||||
@ -166,7 +161,6 @@ impl<V> Client<V> where V: Verifier {
|
||||
author: RwLock::new(Address::new()),
|
||||
extra_data: RwLock::new(Vec::new()),
|
||||
verifier: PhantomData,
|
||||
secret_store: secret_store,
|
||||
}))
|
||||
}
|
||||
|
||||
@ -190,11 +184,6 @@ impl<V> Client<V> where V: Verifier {
|
||||
last_hashes
|
||||
}
|
||||
|
||||
/// Secret store (key manager)
|
||||
pub fn secret_store(&self) -> &Arc<RwLock<SecretStore>> {
|
||||
&self.secret_store
|
||||
}
|
||||
|
||||
fn check_and_close_block(&self, block: &PreverifiedBlock) -> Result<ClosedBlock, ()> {
|
||||
let engine = self.engine.deref().deref();
|
||||
let header = &block.header;
|
||||
|
@ -53,6 +53,7 @@ use ethsync::{EthSync, SyncConfig, SyncProvider};
|
||||
use docopt::Docopt;
|
||||
use daemonize::Daemonize;
|
||||
use number_prefix::{binary_prefix, Standalone, Prefixed};
|
||||
use util::keys::store::*;
|
||||
|
||||
fn die_with_message(msg: &str) -> ! {
|
||||
println!("ERROR: {}", msg);
|
||||
@ -195,7 +196,7 @@ fn setup_log(init: &Option<String>) {
|
||||
}
|
||||
|
||||
#[cfg(feature = "rpc")]
|
||||
fn setup_rpc_server(client: Arc<Client>, sync: Arc<EthSync>, url: &str, cors_domain: &str, apis: Vec<&str>) -> Option<Arc<PanicHandler>> {
|
||||
fn setup_rpc_server(client: Arc<Client>, sync: Arc<EthSync>, secret_store: Arc<AccountService>, url: &str, cors_domain: &str, apis: Vec<&str>) -> Option<Arc<PanicHandler>> {
|
||||
use rpc::v1::*;
|
||||
|
||||
let server = rpc::RpcServer::new();
|
||||
@ -204,7 +205,7 @@ fn setup_rpc_server(client: Arc<Client>, sync: Arc<EthSync>, url: &str, cors_dom
|
||||
"web3" => server.add_delegate(Web3Client::new().to_delegate()),
|
||||
"net" => server.add_delegate(NetClient::new(&sync).to_delegate()),
|
||||
"eth" => {
|
||||
server.add_delegate(EthClient::new(&client, &sync).to_delegate());
|
||||
server.add_delegate(EthClient::new(&client, &sync, &secret_store).to_delegate());
|
||||
server.add_delegate(EthFilterClient::new(&client).to_delegate());
|
||||
}
|
||||
_ => {
|
||||
@ -414,6 +415,9 @@ impl Configuration {
|
||||
// Sync
|
||||
let sync = EthSync::register(service.network(), sync_config, client);
|
||||
|
||||
// Secret Store
|
||||
let account_service = Arc::new(AccountService::new());
|
||||
|
||||
// Setup rpc
|
||||
if self.args.flag_jsonrpc || self.args.flag_rpc {
|
||||
let url = format!("{}:{}",
|
||||
@ -424,7 +428,7 @@ impl Configuration {
|
||||
let cors = self.args.flag_rpccorsdomain.as_ref().unwrap_or(&self.args.flag_jsonrpc_cors);
|
||||
// TODO: use this as the API list.
|
||||
let apis = self.args.flag_rpcapi.as_ref().unwrap_or(&self.args.flag_jsonrpc_apis);
|
||||
let server_handler = setup_rpc_server(service.client(), sync.clone(), &url, cors, apis.split(",").collect());
|
||||
let server_handler = setup_rpc_server(service.client(), sync.clone(), account_service.clone(), &url, cors, apis.split(",").collect());
|
||||
if let Some(handler) = server_handler {
|
||||
panic_handler.forward_from(handler.deref());
|
||||
}
|
||||
|
@ -28,22 +28,25 @@ use ethcore::views::*;
|
||||
use ethcore::ethereum::Ethash;
|
||||
use ethcore::ethereum::denominations::shannon;
|
||||
use v1::traits::{Eth, EthFilter};
|
||||
use v1::types::{Block, BlockTransactions, BlockNumber, Bytes, SyncStatus, SyncInfo, Transaction, OptionalValue, Index, Filter, Log};
|
||||
use v1::types::{Block, BlockTransactions, BlockNumber, Bytes, SyncStatus, SyncInfo, Transaction, TransactionRequest, OptionalValue, Index, Filter, Log};
|
||||
use v1::helpers::{PollFilter, PollManager};
|
||||
use util::keys::store::AccountProvider;
|
||||
|
||||
/// Eth rpc implementation.
|
||||
pub struct EthClient<C, S> where C: BlockChainClient, S: SyncProvider {
|
||||
pub struct EthClient<C, S, A> where C: BlockChainClient, S: SyncProvider, A: AccountProvider {
|
||||
client: Weak<C>,
|
||||
sync: Weak<S>,
|
||||
accounts: Weak<A>,
|
||||
hashrates: RwLock<HashMap<H256, u64>>,
|
||||
}
|
||||
|
||||
impl<C, S> EthClient<C, S> where C: BlockChainClient, S: SyncProvider {
|
||||
impl<C, S, A> EthClient<C, S, A> where C: BlockChainClient, S: SyncProvider, A: AccountProvider {
|
||||
/// Creates new EthClient.
|
||||
pub fn new(client: &Arc<C>, sync: &Arc<S>) -> Self {
|
||||
pub fn new(client: &Arc<C>, sync: &Arc<S>, accounts: &Arc<A>) -> Self {
|
||||
EthClient {
|
||||
client: Arc::downgrade(client),
|
||||
sync: Arc::downgrade(sync),
|
||||
accounts: Arc::downgrade(accounts),
|
||||
hashrates: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
@ -94,7 +97,7 @@ impl<C, S> EthClient<C, S> where C: BlockChainClient, S: SyncProvider {
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, S> Eth for EthClient<C, S> where C: BlockChainClient + 'static, S: SyncProvider + 'static {
|
||||
impl<C, S, A> Eth for EthClient<C, S, A> where C: BlockChainClient + 'static, S: SyncProvider + 'static, A: AccountProvider + 'static {
|
||||
fn protocol_version(&self, params: Params) -> Result<Value, Error> {
|
||||
match params {
|
||||
Params::None => to_value(&U256::from(take_weak!(self.sync).status().protocol_version)),
|
||||
@ -158,7 +161,7 @@ impl<C, S> Eth for EthClient<C, S> where C: BlockChainClient + 'static, S: SyncP
|
||||
}
|
||||
}
|
||||
|
||||
fn block_transaction_count(&self, params: Params) -> Result<Value, Error> {
|
||||
fn block_transaction_count_by_hash(&self, params: Params) -> Result<Value, Error> {
|
||||
from_params::<(H256,)>(params)
|
||||
.and_then(|(hash,)| match take_weak!(self.client).block(BlockId::Hash(hash)) {
|
||||
Some(bytes) => to_value(&BlockView::new(&bytes).transactions_count()),
|
||||
@ -166,6 +169,17 @@ impl<C, S> Eth for EthClient<C, S> where C: BlockChainClient + 'static, S: SyncP
|
||||
})
|
||||
}
|
||||
|
||||
fn block_transaction_count_by_number(&self, params: Params) -> Result<Value, Error> {
|
||||
from_params::<(BlockNumber,)>(params)
|
||||
.and_then(|(block_number,)| match block_number {
|
||||
BlockNumber::Pending => to_value(&take_weak!(self.sync).status().transaction_queue_pending),
|
||||
_ => match take_weak!(self.client).block(block_number.into()) {
|
||||
Some(bytes) => to_value(&BlockView::new(&bytes).transactions_count()),
|
||||
None => Ok(Value::Null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn block_uncles_count(&self, params: Params) -> Result<Value, Error> {
|
||||
from_params::<(H256,)>(params)
|
||||
.and_then(|(hash,)| match take_weak!(self.client).block(BlockId::Hash(hash)) {
|
||||
@ -252,6 +266,24 @@ impl<C, S> Eth for EthClient<C, S> where C: BlockChainClient + 'static, S: SyncP
|
||||
to_value(&true)
|
||||
})
|
||||
}
|
||||
|
||||
fn send_transaction(&self, params: Params) -> Result<Value, Error> {
|
||||
from_params::<(TransactionRequest, )>(params)
|
||||
.and_then(|(transaction_request, )| {
|
||||
let accounts = take_weak!(self.accounts);
|
||||
match accounts.account_secret(&transaction_request.from) {
|
||||
Ok(secret) => {
|
||||
let sync = take_weak!(self.sync);
|
||||
let (transaction, _) = transaction_request.to_eth();
|
||||
let signed_transaction = transaction.sign(&secret);
|
||||
let hash = signed_transaction.hash();
|
||||
sync.insert_transaction(signed_transaction);
|
||||
to_value(&hash)
|
||||
},
|
||||
Err(_) => { to_value(&U256::zero()) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Eth filter rpc implementation.
|
||||
|
@ -20,30 +20,28 @@ use jsonrpc_core::*;
|
||||
use v1::traits::Personal;
|
||||
use util::keys::store::*;
|
||||
use util::Address;
|
||||
use std::sync::RwLock;
|
||||
|
||||
/// Account management (personal) rpc implementation.
|
||||
pub struct PersonalClient {
|
||||
secret_store: Weak<RwLock<SecretStore>>,
|
||||
accounts: Weak<AccountProvider>,
|
||||
}
|
||||
|
||||
impl PersonalClient {
|
||||
/// Creates new PersonalClient
|
||||
pub fn new(store: &Arc<RwLock<SecretStore>>) -> Self {
|
||||
pub fn new(store: &Arc<AccountProvider>) -> Self {
|
||||
PersonalClient {
|
||||
secret_store: Arc::downgrade(store),
|
||||
accounts: Arc::downgrade(store),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Personal for PersonalClient {
|
||||
fn accounts(&self, _: Params) -> Result<Value, Error> {
|
||||
let store_wk = take_weak!(self.secret_store);
|
||||
let store = store_wk.read().unwrap();
|
||||
let store = take_weak!(self.accounts);
|
||||
match store.accounts() {
|
||||
Ok(account_list) => {
|
||||
Ok(Value::Array(account_list.iter()
|
||||
.map(|&(account, _)| Value::String(format!("{:?}", account)))
|
||||
.map(|&account| Value::String(format!("{:?}", account)))
|
||||
.collect::<Vec<Value>>())
|
||||
)
|
||||
}
|
||||
@ -54,8 +52,7 @@ impl Personal for PersonalClient {
|
||||
fn new_account(&self, params: Params) -> Result<Value, Error> {
|
||||
from_params::<(String, )>(params).and_then(
|
||||
|(pass, )| {
|
||||
let store_wk = take_weak!(self.secret_store);
|
||||
let mut store = store_wk.write().unwrap();
|
||||
let store = take_weak!(self.accounts);
|
||||
match store.new_account(&pass) {
|
||||
Ok(address) => Ok(Value::String(format!("{:?}", address))),
|
||||
Err(_) => Err(Error::internal_error())
|
||||
@ -67,8 +64,7 @@ impl Personal for PersonalClient {
|
||||
fn unlock_account(&self, params: Params) -> Result<Value, Error> {
|
||||
from_params::<(Address, String, u64)>(params).and_then(
|
||||
|(account, account_pass, _)|{
|
||||
let store_wk = take_weak!(self.secret_store);
|
||||
let store = store_wk.read().unwrap();
|
||||
let store = take_weak!(self.accounts);
|
||||
match store.unlock_account(&account, &account_pass) {
|
||||
Ok(_) => Ok(Value::Bool(true)),
|
||||
Err(_) => Ok(Value::Bool(false)),
|
||||
|
@ -55,12 +55,15 @@ pub trait Eth: Sized + Send + Sync + 'static {
|
||||
|
||||
/// Returns block with given number.
|
||||
fn block_by_number(&self, _: Params) -> Result<Value, Error> { rpc_unimplemented!() }
|
||||
|
||||
|
||||
/// Returns the number of transactions sent from given address at given time (block number).
|
||||
fn transaction_count(&self, _: Params) -> Result<Value, Error> { rpc_unimplemented!() }
|
||||
|
||||
/// Returns the number of transactions in a block.
|
||||
fn block_transaction_count(&self, _: Params) -> Result<Value, Error> { rpc_unimplemented!() }
|
||||
/// Returns the number of transactions in a block given block hash.
|
||||
fn block_transaction_count_by_hash(&self, _: Params) -> Result<Value, Error> { rpc_unimplemented!() }
|
||||
|
||||
/// Returns the number of transactions in a block given block number.
|
||||
fn block_transaction_count_by_number(&self, _: Params) -> Result<Value, Error> { rpc_unimplemented!() }
|
||||
|
||||
/// Returns the number of uncles in a given block.
|
||||
fn block_uncles_count(&self, _: Params) -> Result<Value, Error> { rpc_unimplemented!() }
|
||||
@ -130,8 +133,8 @@ pub trait Eth: Sized + Send + Sync + 'static {
|
||||
delegate.add_method("eth_balance", Eth::balance);
|
||||
delegate.add_method("eth_getStorageAt", Eth::storage_at);
|
||||
delegate.add_method("eth_getTransactionCount", Eth::transaction_count);
|
||||
delegate.add_method("eth_getBlockTransactionCountByHash", Eth::block_transaction_count);
|
||||
delegate.add_method("eth_getBlockTransactionCountByNumber", Eth::block_transaction_count);
|
||||
delegate.add_method("eth_getBlockTransactionCountByHash", Eth::block_transaction_count_by_hash);
|
||||
delegate.add_method("eth_getBlockTransactionCountByNumber", Eth::block_transaction_count_by_number);
|
||||
delegate.add_method("eth_getUncleCountByBlockHash", Eth::block_uncles_count);
|
||||
delegate.add_method("eth_getUncleCountByBlockNumber", Eth::block_uncles_count);
|
||||
delegate.add_method("eth_code", Eth::code_at);
|
||||
|
@ -15,7 +15,9 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use rustc_serialize::hex::ToHex;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde::{Serialize, Serializer, Deserialize, Deserializer, Error};
|
||||
use serde::de::Visitor;
|
||||
use util::common::FromHex;
|
||||
|
||||
/// Wrapper structure around vector of bytes.
|
||||
#[derive(Debug)]
|
||||
@ -26,6 +28,7 @@ impl Bytes {
|
||||
pub fn new(bytes: Vec<u8>) -> Bytes {
|
||||
Bytes(bytes)
|
||||
}
|
||||
pub fn to_vec(self) -> Vec<u8> { let Bytes(x) = self; x }
|
||||
}
|
||||
|
||||
impl Default for Bytes {
|
||||
@ -36,7 +39,7 @@ impl Default for Bytes {
|
||||
}
|
||||
|
||||
impl Serialize for Bytes {
|
||||
fn serialize<S>(&self, serializer: &mut S) -> Result<(), S::Error>
|
||||
fn serialize<S>(&self, serializer: &mut S) -> Result<(), S::Error>
|
||||
where S: Serializer {
|
||||
let mut serialized = "0x".to_owned();
|
||||
serialized.push_str(self.0.to_hex().as_ref());
|
||||
@ -44,6 +47,32 @@ impl Serialize for Bytes {
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for Bytes {
|
||||
fn deserialize<D>(deserializer: &mut D) -> Result<Bytes, D::Error>
|
||||
where D: Deserializer {
|
||||
deserializer.deserialize(BytesVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct BytesVisitor;
|
||||
|
||||
impl Visitor for BytesVisitor {
|
||||
type Value = Bytes;
|
||||
|
||||
fn visit_str<E>(&mut self, value: &str) -> Result<Self::Value, E> where E: Error {
|
||||
if value.len() >= 2 && &value[0..2] == "0x" {
|
||||
Ok(Bytes::new(FromHex::from_hex(&value[2..]).unwrap_or_else(|_| vec![])))
|
||||
} else {
|
||||
Err(Error::custom("invalid hex"))
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_string<E>(&mut self, value: String) -> Result<Self::Value, E> where E: Error {
|
||||
self.visit_str(value.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -33,3 +33,5 @@ pub use self::log::Log;
|
||||
pub use self::optionals::OptionalValue;
|
||||
pub use self::sync::{SyncStatus, SyncInfo};
|
||||
pub use self::transaction::Transaction;
|
||||
pub use self::transaction::TransactionRequest;
|
||||
|
||||
|
@ -17,6 +17,8 @@
|
||||
use util::numbers::*;
|
||||
use ethcore::transaction::{LocalizedTransaction, Action};
|
||||
use v1::types::{Bytes, OptionalValue};
|
||||
use serde::{Deserializer, Error};
|
||||
use ethcore;
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Transaction {
|
||||
@ -37,6 +39,35 @@ pub struct Transaction {
|
||||
pub input: Bytes
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct TransactionRequest {
|
||||
pub from: Address,
|
||||
pub to: Option<Address>,
|
||||
#[serde(rename="gasPrice")]
|
||||
pub gas_price: Option<U256>,
|
||||
pub gas: Option<U256>,
|
||||
pub value: Option<U256>,
|
||||
pub data: Bytes,
|
||||
pub nonce: Option<U256>,
|
||||
}
|
||||
|
||||
impl TransactionRequest {
|
||||
/// maps transaction request to the transaction that can be signed and inserted
|
||||
pub fn to_eth(self) -> (ethcore::transaction::Transaction, Address) {
|
||||
(ethcore::transaction::Transaction {
|
||||
nonce: self.nonce.unwrap_or(U256::zero()),
|
||||
action: match self.to {
|
||||
None => ethcore::transaction::Action::Create,
|
||||
Some(addr) => ethcore::transaction::Action::Call(addr)
|
||||
},
|
||||
gas: self.gas.unwrap_or(U256::zero()),
|
||||
gas_price: self.gas_price.unwrap_or(U256::zero()),
|
||||
value: self.value.unwrap_or(U256::zero()),
|
||||
data: self.data.to_vec()
|
||||
}, self.from)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalizedTransaction> for Transaction {
|
||||
fn from(t: LocalizedTransaction) -> Transaction {
|
||||
Transaction {
|
||||
|
@ -142,6 +142,8 @@ pub struct SyncStatus {
|
||||
pub num_active_peers: usize,
|
||||
/// Heap memory used in bytes
|
||||
pub mem_used: usize,
|
||||
/// Number of pending transactions in queue
|
||||
pub transaction_queue_pending: usize,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
@ -257,6 +259,7 @@ impl ChainSync {
|
||||
blocks_total: match self.highest_block { Some(x) if x > self.starting_block => x - self.starting_block, _ => 0 },
|
||||
num_peers: self.peers.len(),
|
||||
num_active_peers: self.peers.values().filter(|p| p.asking != PeerAsking::Nothing).count(),
|
||||
transaction_queue_pending: self.transaction_queue.lock().unwrap().status().pending,
|
||||
mem_used:
|
||||
// TODO: https://github.com/servo/heapsize/pull/50
|
||||
// self.downloading_hashes.heap_size_of_children()
|
||||
|
@ -78,6 +78,59 @@ struct AccountUnlock {
|
||||
expires: DateTime<UTC>,
|
||||
}
|
||||
|
||||
/// Basic account management trait
|
||||
pub trait AccountProvider : Send + Sync {
|
||||
/// Lists all accounts
|
||||
fn accounts(&self) -> Result<Vec<Address>, ::std::io::Error>;
|
||||
/// Unlocks account with the password provided
|
||||
fn unlock_account(&self, account: &Address, pass: &str) -> Result<(), EncryptedHashMapError>;
|
||||
/// Creates account
|
||||
fn new_account(&self, pass: &str) -> Result<Address, ::std::io::Error>;
|
||||
/// Returns secret for unlocked account
|
||||
fn account_secret(&self, account: &Address) -> Result<crypto::Secret, SigningError>;
|
||||
/// Returns secret for unlocked account
|
||||
fn sign(&self, account: &Address, message: &H256) -> Result<crypto::Signature, SigningError>;
|
||||
}
|
||||
|
||||
/// Thread-safe accounts management
|
||||
pub struct AccountService {
|
||||
secret_store: RwLock<SecretStore>,
|
||||
}
|
||||
|
||||
impl AccountProvider for AccountService {
|
||||
/// Lists all accounts
|
||||
fn accounts(&self) -> Result<Vec<Address>, ::std::io::Error> {
|
||||
Ok(try!(self.secret_store.read().unwrap().accounts()).iter().map(|&(addr, _)| addr).collect::<Vec<Address>>())
|
||||
}
|
||||
/// Unlocks account with the password provided
|
||||
fn unlock_account(&self, account: &Address, pass: &str) -> Result<(), EncryptedHashMapError> {
|
||||
self.secret_store.read().unwrap().unlock_account(account, pass)
|
||||
}
|
||||
/// Creates account
|
||||
fn new_account(&self, pass: &str) -> Result<Address, ::std::io::Error> {
|
||||
self.secret_store.write().unwrap().new_account(pass)
|
||||
}
|
||||
/// Returns secret for unlocked account
|
||||
fn account_secret(&self, account: &Address) -> Result<crypto::Secret, SigningError> {
|
||||
self.secret_store.read().unwrap().account_secret(account)
|
||||
}
|
||||
/// Returns secret for unlocked account
|
||||
fn sign(&self, account: &Address, message: &H256) -> Result<crypto::Signature, SigningError> {
|
||||
self.secret_store.read().unwrap().sign(account, message)
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountService {
|
||||
/// New account service with the default location
|
||||
pub fn new() -> AccountService {
|
||||
let secret_store = RwLock::new(SecretStore::new());
|
||||
secret_store.write().unwrap().try_import_existing();
|
||||
AccountService {
|
||||
secret_store: secret_store
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretStore {
|
||||
/// new instance of Secret Store in default home directory
|
||||
pub fn new() -> SecretStore {
|
||||
|
Loading…
Reference in New Issue
Block a user