Dyn liquidity sources (#52)
This commit is contained in:
parent
d78183b2c8
commit
3509edfe3d
8 changed files with 221 additions and 127 deletions
|
@ -4,7 +4,7 @@ use cosmwasm_std::{
|
||||||
};
|
};
|
||||||
use cw2::set_contract_version;
|
use cw2::set_contract_version;
|
||||||
use cw20_base::{
|
use cw20_base::{
|
||||||
contract::{execute_mint, query_balance as cw20_query_balance},
|
contract::query_balance as cw20_query_balance,
|
||||||
state::{MinterData, TokenInfo, TOKEN_INFO},
|
state::{MinterData, TokenInfo, TOKEN_INFO},
|
||||||
};
|
};
|
||||||
use quartz_cw::{handler::RawHandler, state::EPOCH_COUNTER};
|
use quartz_cw::{handler::RawHandler, state::EPOCH_COUNTER};
|
||||||
|
@ -12,7 +12,10 @@ use quartz_cw::{handler::RawHandler, state::EPOCH_COUNTER};
|
||||||
use crate::{
|
use crate::{
|
||||||
error::ContractError,
|
error::ContractError,
|
||||||
msg::{
|
msg::{
|
||||||
execute::{SubmitObligationMsg, SubmitObligationsMsg, SubmitSetoffsMsg},
|
execute::{
|
||||||
|
Cw20Transfer, FaucetMintMsg, SubmitObligationMsg, SubmitObligationsMsg,
|
||||||
|
SubmitSetoffsMsg,
|
||||||
|
},
|
||||||
ExecuteMsg, InstantiateMsg, QueryMsg,
|
ExecuteMsg, InstantiateMsg, QueryMsg,
|
||||||
},
|
},
|
||||||
state::{
|
state::{
|
||||||
|
@ -63,35 +66,6 @@ pub fn instantiate(
|
||||||
};
|
};
|
||||||
TOKEN_INFO.save(deps.storage, &data)?;
|
TOKEN_INFO.save(deps.storage, &data)?;
|
||||||
|
|
||||||
let info = MessageInfo {
|
|
||||||
sender: env.contract.address.clone(),
|
|
||||||
funds: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
execute_mint(
|
|
||||||
deps.branch(),
|
|
||||||
env.clone(),
|
|
||||||
info.clone(),
|
|
||||||
"wasm1qv9nel6lwtrq5jmwruxfndqw7ejskn5ysz53hp".to_owned(),
|
|
||||||
Uint128::new(1000),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
execute_mint(
|
|
||||||
deps.branch(),
|
|
||||||
env.clone(),
|
|
||||||
info.clone(),
|
|
||||||
"wasm1tfxrdcj5kk6rewzmmkku4d9htpjqr0kk6lcftv".to_owned(),
|
|
||||||
Uint128::new(1000),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
execute_mint(
|
|
||||||
deps.branch(),
|
|
||||||
env.clone(),
|
|
||||||
info.clone(),
|
|
||||||
"wasm1gjg72awjl7jvtmq4kjqp3al9p6crstpar8wgn5".to_owned(),
|
|
||||||
Uint128::new(1000),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Response::new()
|
Ok(Response::new()
|
||||||
.add_attribute("method", "instantiate")
|
.add_attribute("method", "instantiate")
|
||||||
.add_attribute("owner", info.sender))
|
.add_attribute("owner", info.sender))
|
||||||
|
@ -106,6 +80,12 @@ pub fn execute(
|
||||||
) -> Result<Response, ContractError> {
|
) -> Result<Response, ContractError> {
|
||||||
match msg {
|
match msg {
|
||||||
ExecuteMsg::Quartz(msg) => msg.handle_raw(deps, &env, &info).map_err(Into::into),
|
ExecuteMsg::Quartz(msg) => msg.handle_raw(deps, &env, &info).map_err(Into::into),
|
||||||
|
ExecuteMsg::FaucetMint(FaucetMintMsg { recipient, amount }) => {
|
||||||
|
execute::faucet_mint(deps, env, recipient, amount)
|
||||||
|
}
|
||||||
|
ExecuteMsg::Transfer(Cw20Transfer { recipient, amount }) => Ok(
|
||||||
|
cw20_base::contract::execute_transfer(deps, env, info, recipient, amount.into())?,
|
||||||
|
),
|
||||||
ExecuteMsg::SubmitObligation(SubmitObligationMsg { ciphertext, digest }) => {
|
ExecuteMsg::SubmitObligation(SubmitObligationMsg { ciphertext, digest }) => {
|
||||||
execute::submit_obligation(deps, ciphertext, digest)
|
execute::submit_obligation(deps, ciphertext, digest)
|
||||||
}
|
}
|
||||||
|
@ -120,7 +100,7 @@ pub fn execute(
|
||||||
Ok(Response::new())
|
Ok(Response::new())
|
||||||
}
|
}
|
||||||
ExecuteMsg::SubmitSetoffs(SubmitSetoffsMsg { setoffs_enc }) => {
|
ExecuteMsg::SubmitSetoffs(SubmitSetoffsMsg { setoffs_enc }) => {
|
||||||
execute::submit_setoffs(deps, env, info, setoffs_enc)
|
execute::submit_setoffs(deps, env, setoffs_enc)
|
||||||
}
|
}
|
||||||
ExecuteMsg::InitClearing => execute::init_clearing(deps),
|
ExecuteMsg::InitClearing => execute::init_clearing(deps),
|
||||||
}
|
}
|
||||||
|
@ -131,6 +111,7 @@ pub mod execute {
|
||||||
|
|
||||||
use cosmwasm_std::{DepsMut, Env, HexBinary, MessageInfo, Response, StdResult};
|
use cosmwasm_std::{DepsMut, Env, HexBinary, MessageInfo, Response, StdResult};
|
||||||
use cw20_base::contract::{execute_burn, execute_mint};
|
use cw20_base::contract::{execute_burn, execute_mint};
|
||||||
|
use k256::ecdsa::VerifyingKey;
|
||||||
use quartz_cw::state::{Hash, EPOCH_COUNTER};
|
use quartz_cw::state::{Hash, EPOCH_COUNTER};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -141,6 +122,28 @@ pub mod execute {
|
||||||
ContractError,
|
ContractError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn faucet_mint(
|
||||||
|
mut deps: DepsMut,
|
||||||
|
env: Env,
|
||||||
|
recipient: String,
|
||||||
|
amount: u64,
|
||||||
|
) -> Result<Response, ContractError> {
|
||||||
|
let info = MessageInfo {
|
||||||
|
sender: env.contract.address.clone(),
|
||||||
|
funds: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
execute_mint(
|
||||||
|
deps.branch(),
|
||||||
|
env.clone(),
|
||||||
|
info.clone(),
|
||||||
|
recipient.to_string(),
|
||||||
|
amount.into(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Response::new().add_attribute("action", "faucet_mint"))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn submit_obligation(
|
pub fn submit_obligation(
|
||||||
deps: DepsMut,
|
deps: DepsMut,
|
||||||
ciphertext: HexBinary,
|
ciphertext: HexBinary,
|
||||||
|
@ -167,11 +170,12 @@ pub mod execute {
|
||||||
|
|
||||||
pub fn append_liquidity_sources(
|
pub fn append_liquidity_sources(
|
||||||
deps: DepsMut,
|
deps: DepsMut,
|
||||||
liquidity_sources: Vec<String>,
|
liquidity_sources: Vec<HexBinary>,
|
||||||
) -> Result<(), ContractError> {
|
) -> Result<(), ContractError> {
|
||||||
|
// validate liquidity sources as public keys
|
||||||
liquidity_sources
|
liquidity_sources
|
||||||
.iter()
|
.iter()
|
||||||
.try_for_each(|ls| deps.api.addr_validate(ls).map(|_| ()))?;
|
.try_for_each(|ls| VerifyingKey::from_sec1_bytes(ls).map(|_| ()))?;
|
||||||
|
|
||||||
// store the liquidity sources
|
// store the liquidity sources
|
||||||
LiquiditySourcesItem::new(¤t_epoch_key(LIQUIDITY_SOURCES_KEY, deps.storage)?)
|
LiquiditySourcesItem::new(¤t_epoch_key(LIQUIDITY_SOURCES_KEY, deps.storage)?)
|
||||||
|
@ -186,7 +190,6 @@ pub mod execute {
|
||||||
pub fn submit_setoffs(
|
pub fn submit_setoffs(
|
||||||
mut deps: DepsMut,
|
mut deps: DepsMut,
|
||||||
env: Env,
|
env: Env,
|
||||||
_info: MessageInfo,
|
|
||||||
setoffs_enc: BTreeMap<RawHash, SettleOff>,
|
setoffs_enc: BTreeMap<RawHash, SettleOff>,
|
||||||
) -> Result<Response, ContractError> {
|
) -> Result<Response, ContractError> {
|
||||||
// store the `BTreeMap<RawHash, RawCipherText>`
|
// store the `BTreeMap<RawHash, RawCipherText>`
|
||||||
|
|
|
@ -13,6 +13,8 @@ pub struct InstantiateMsg(pub QuartzInstantiateMsg);
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum ExecuteMsg {
|
pub enum ExecuteMsg {
|
||||||
Quartz(QuartzExecuteMsg),
|
Quartz(QuartzExecuteMsg),
|
||||||
|
FaucetMint(execute::FaucetMintMsg),
|
||||||
|
Transfer(execute::Cw20Transfer),
|
||||||
SubmitObligation(execute::SubmitObligationMsg),
|
SubmitObligation(execute::SubmitObligationMsg),
|
||||||
SubmitObligations(execute::SubmitObligationsMsg),
|
SubmitObligations(execute::SubmitObligationsMsg),
|
||||||
SubmitSetoffs(execute::SubmitSetoffsMsg),
|
SubmitSetoffs(execute::SubmitSetoffsMsg),
|
||||||
|
@ -22,6 +24,18 @@ pub enum ExecuteMsg {
|
||||||
pub mod execute {
|
pub mod execute {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[cw_serde]
|
||||||
|
pub struct FaucetMintMsg {
|
||||||
|
pub recipient: String,
|
||||||
|
pub amount: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cw_serde]
|
||||||
|
pub struct Cw20Transfer {
|
||||||
|
pub recipient: String,
|
||||||
|
pub amount: u64,
|
||||||
|
}
|
||||||
|
|
||||||
#[cw_serde]
|
#[cw_serde]
|
||||||
pub struct SubmitObligationMsg {
|
pub struct SubmitObligationMsg {
|
||||||
pub ciphertext: HexBinary,
|
pub ciphertext: HexBinary,
|
||||||
|
@ -33,7 +47,7 @@ pub mod execute {
|
||||||
#[cw_serde]
|
#[cw_serde]
|
||||||
pub struct SubmitObligationsMsg {
|
pub struct SubmitObligationsMsg {
|
||||||
pub obligations: Vec<SubmitObligationMsg>,
|
pub obligations: Vec<SubmitObligationMsg>,
|
||||||
pub liquidity_sources: Vec<String>,
|
pub liquidity_sources: Vec<HexBinary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cw_serde]
|
#[cw_serde]
|
||||||
|
@ -69,7 +83,7 @@ pub struct GetAllSetoffsResponse {
|
||||||
|
|
||||||
#[cw_serde]
|
#[cw_serde]
|
||||||
pub struct GetLiquiditySourcesResponse {
|
pub struct GetLiquiditySourcesResponse {
|
||||||
pub liquidity_sources: Vec<String>,
|
pub liquidity_sources: Vec<HexBinary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -10,7 +10,7 @@ pub type RawCipherText = HexBinary;
|
||||||
|
|
||||||
pub type ObligationsItem<'a> = Item<'a, BTreeMap<RawHash, RawCipherText>>;
|
pub type ObligationsItem<'a> = Item<'a, BTreeMap<RawHash, RawCipherText>>;
|
||||||
pub type SetoffsItem<'a> = Item<'a, BTreeMap<RawHash, SettleOff>>;
|
pub type SetoffsItem<'a> = Item<'a, BTreeMap<RawHash, SettleOff>>;
|
||||||
pub type LiquiditySourcesItem<'a> = Item<'a, BTreeSet<String>>;
|
pub type LiquiditySourcesItem<'a> = Item<'a, BTreeSet<HexBinary>>;
|
||||||
|
|
||||||
#[cw_serde]
|
#[cw_serde]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
|
|
|
@ -16,6 +16,7 @@ use mtcs::{
|
||||||
algo::mcmf::primal_dual::PrimalDual, impls::complex_id::ComplexIdMtcs,
|
algo::mcmf::primal_dual::PrimalDual, impls::complex_id::ComplexIdMtcs,
|
||||||
obligation::SimpleObligation, prelude::DefaultMtcs, setoff::SimpleSetoff, Mtcs,
|
obligation::SimpleObligation, prelude::DefaultMtcs, setoff::SimpleSetoff, Mtcs,
|
||||||
};
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use tonic::{Request, Response, Result as TonicResult, Status};
|
use tonic::{Request, Response, Result as TonicResult, Status};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -23,14 +24,18 @@ use crate::{
|
||||||
proto::{clearing_server::Clearing, RunClearingRequest, RunClearingResponse},
|
proto::{clearing_server::Clearing, RunClearingRequest, RunClearingResponse},
|
||||||
};
|
};
|
||||||
|
|
||||||
const BANK_PK: &str = "0216254f4636c4e68ae22d98538851a46810b65162fe37bf57cba6d563617c913e";
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct MtcsService<A> {
|
pub struct MtcsService<A> {
|
||||||
sk: Arc<Mutex<Option<SigningKey>>>,
|
sk: Arc<Mutex<Option<SigningKey>>>,
|
||||||
_attestor: A,
|
_attestor: A,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RunClearingMessage {
|
||||||
|
intents: BTreeMap<RawHash, RawCipherText>,
|
||||||
|
liquidity_sources: Vec<HexBinary>,
|
||||||
|
}
|
||||||
|
|
||||||
impl<A> MtcsService<A>
|
impl<A> MtcsService<A>
|
||||||
where
|
where
|
||||||
A: Attestor,
|
A: Attestor,
|
||||||
|
@ -49,10 +54,12 @@ where
|
||||||
&self,
|
&self,
|
||||||
request: Request<RunClearingRequest>,
|
request: Request<RunClearingRequest>,
|
||||||
) -> TonicResult<Response<RunClearingResponse>> {
|
) -> TonicResult<Response<RunClearingResponse>> {
|
||||||
let message = request.into_inner().message;
|
let message: RunClearingMessage = {
|
||||||
|
let message = request.into_inner().message;
|
||||||
|
serde_json::from_str(&message).map_err(|e| Status::invalid_argument(e.to_string()))?
|
||||||
|
};
|
||||||
|
|
||||||
let digests_ciphertexts: BTreeMap<RawHash, RawCipherText> =
|
let digests_ciphertexts = message.intents;
|
||||||
serde_json::from_str(&message).map_err(|e| Status::invalid_argument(e.to_string()))?;
|
|
||||||
let (digests, ciphertexts): (Vec<_>, Vec<_>) = digests_ciphertexts.into_iter().unzip();
|
let (digests, ciphertexts): (Vec<_>, Vec<_>) = digests_ciphertexts.into_iter().unzip();
|
||||||
|
|
||||||
let sk = self.sk.lock().unwrap();
|
let sk = self.sk.lock().unwrap();
|
||||||
|
@ -64,9 +71,16 @@ where
|
||||||
let mut mtcs = ComplexIdMtcs::wrapping(DefaultMtcs::new(PrimalDual::default()));
|
let mut mtcs = ComplexIdMtcs::wrapping(DefaultMtcs::new(PrimalDual::default()));
|
||||||
let setoffs: Vec<SimpleSetoff<_, i64>> = mtcs.run(obligations).unwrap();
|
let setoffs: Vec<SimpleSetoff<_, i64>> = mtcs.run(obligations).unwrap();
|
||||||
|
|
||||||
|
let liquidity_sources: Vec<_> = message
|
||||||
|
.liquidity_sources
|
||||||
|
.into_iter()
|
||||||
|
.map(|ls| VerifyingKey::from_sec1_bytes(&ls))
|
||||||
|
.collect::<Result<_, _>>()
|
||||||
|
.map_err(|e| Status::invalid_argument(e.to_string()))?;
|
||||||
|
|
||||||
let setoffs_enc: BTreeMap<RawHash, SettleOff> = setoffs
|
let setoffs_enc: BTreeMap<RawHash, SettleOff> = setoffs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(into_settle_offs)
|
.map(|so| into_settle_offs(so, &liquidity_sources))
|
||||||
.zip(digests)
|
.zip(digests)
|
||||||
.map(|(settle_off, digest)| (digest, settle_off))
|
.map(|(settle_off, digest)| (digest, settle_off))
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -76,25 +90,26 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_settle_offs(so: SimpleSetoff<HexBinary, i64>) -> SettleOff {
|
fn into_settle_offs(
|
||||||
|
so: SimpleSetoff<HexBinary, i64>,
|
||||||
|
liquidity_sources: &[VerifyingKey],
|
||||||
|
) -> SettleOff {
|
||||||
let debtor_pk = VerifyingKey::from_sec1_bytes(&so.debtor).unwrap();
|
let debtor_pk = VerifyingKey::from_sec1_bytes(&so.debtor).unwrap();
|
||||||
let creditor_pk = VerifyingKey::from_sec1_bytes(&so.creditor).unwrap();
|
let creditor_pk = VerifyingKey::from_sec1_bytes(&so.creditor).unwrap();
|
||||||
|
|
||||||
let bank_pk = VerifyingKey::from_sec1_bytes(&hex::decode(BANK_PK).unwrap()).unwrap();
|
if let Some(ls_pk) = liquidity_sources.iter().find(|ls| ls == &&debtor_pk) {
|
||||||
let bank_addrs = wasm_address(bank_pk);
|
|
||||||
if debtor_pk == bank_pk {
|
|
||||||
// A setoff on a tender should result in the creditor's (i.e. the tender receiver) balance
|
// A setoff on a tender should result in the creditor's (i.e. the tender receiver) balance
|
||||||
// decreasing by the setoff amount
|
// decreasing by the setoff amount
|
||||||
SettleOff::Transfer(Transfer {
|
SettleOff::Transfer(Transfer {
|
||||||
payer: wasm_address(creditor_pk),
|
payer: wasm_address(creditor_pk),
|
||||||
payee: bank_addrs,
|
payee: wasm_address(*ls_pk),
|
||||||
amount: so.set_off as u64,
|
amount: so.set_off as u64,
|
||||||
})
|
})
|
||||||
} else if creditor_pk == bank_pk {
|
} else if let Some(ls_pk) = liquidity_sources.iter().find(|ls| ls == &&creditor_pk) {
|
||||||
// A setoff on an acceptance should result in the debtor's (i.e. the acceptance initiator)
|
// A setoff on an acceptance should result in the debtor's (i.e. the acceptance initiator)
|
||||||
// balance increasing by the setoff amount
|
// balance increasing by the setoff amount
|
||||||
SettleOff::Transfer(Transfer {
|
SettleOff::Transfer(Transfer {
|
||||||
payer: bank_addrs,
|
payer: wasm_address(*ls_pk),
|
||||||
payee: wasm_address(debtor_pk),
|
payee: wasm_address(debtor_pk),
|
||||||
amount: so.set_off as u64,
|
amount: so.set_off as u64,
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,6 +6,9 @@ use displaydoc::Display;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use subtle_encoding::{bech32::decode as bech32_decode, Error as Bech32DecodeError};
|
use subtle_encoding::{bech32::decode as bech32_decode, Error as Bech32DecodeError};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::ADDRESS_PREFIX;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Parser)]
|
#[derive(Clone, Debug, Parser)]
|
||||||
#[command(author, version, about)]
|
#[command(author, version, about)]
|
||||||
|
@ -61,9 +64,17 @@ pub enum CliCommand {
|
||||||
/// epoch pk
|
/// epoch pk
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
epoch_pk: String,
|
epoch_pk: String,
|
||||||
|
/// liquidity sources' UUIDs
|
||||||
|
#[arg(short, long, num_args = 1.., value_parser = parse_uuid)]
|
||||||
|
liquidity_sources: Vec<Uuid>,
|
||||||
},
|
},
|
||||||
/// Sync set-offs
|
/// Sync set-offs
|
||||||
SyncSetOffs,
|
SyncSetOffs,
|
||||||
|
/// Get address for Uuid
|
||||||
|
GetAddress {
|
||||||
|
#[arg(long, value_parser = parse_uuid)]
|
||||||
|
uuid: Uuid,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Display, Error, Debug)]
|
#[derive(Display, Error, Debug)]
|
||||||
|
@ -76,9 +87,13 @@ pub enum AddressError {
|
||||||
|
|
||||||
fn wasm_address(address_str: &str) -> Result<AccountId, AddressError> {
|
fn wasm_address(address_str: &str) -> Result<AccountId, AddressError> {
|
||||||
let (hr, _) = bech32_decode(address_str).map_err(AddressError::NotBech32Encoded)?;
|
let (hr, _) = bech32_decode(address_str).map_err(AddressError::NotBech32Encoded)?;
|
||||||
if hr != "wasm" {
|
if hr != ADDRESS_PREFIX {
|
||||||
return Err(AddressError::HumanReadableMismatch(hr));
|
return Err(AddressError::HumanReadableMismatch(hr));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(address_str.parse().unwrap())
|
Ok(address_str.parse().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_uuid(uuid_str: &str) -> Result<Uuid, String> {
|
||||||
|
Uuid::parse_str(uuid_str).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
|
@ -12,9 +12,10 @@ use bip32::{
|
||||||
ecdsa::VerifyingKey,
|
ecdsa::VerifyingKey,
|
||||||
sha2::{Digest, Sha256},
|
sha2::{Digest, Sha256},
|
||||||
},
|
},
|
||||||
Language, Mnemonic, Prefix, PrivateKey, Seed, XPrv,
|
Error as Bip32Error, Language, Mnemonic, Prefix, PrivateKey, Seed, XPrv,
|
||||||
};
|
};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use cosmrs::{tendermint::account::Id as TmAccountId, AccountId};
|
||||||
use cosmwasm_std::HexBinary;
|
use cosmwasm_std::HexBinary;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -26,7 +27,7 @@ use crate::{
|
||||||
obligato_client::{http::HttpClient, Client},
|
obligato_client::{http::HttpClient, Client},
|
||||||
types::{
|
types::{
|
||||||
Obligation, ObligatoObligation, ObligatoSetOff, RawEncryptedObligation, RawObligation,
|
Obligation, ObligatoObligation, ObligatoSetOff, RawEncryptedObligation, RawObligation,
|
||||||
RawOffset, RawSetOff, SubmitObligationsMsg,
|
RawOffset, RawSetOff, SubmitObligationsMsg, SubmitObligationsMsgInner,
|
||||||
},
|
},
|
||||||
wasmd_client::{CliWasmdClient, QueryResult, WasmdClient},
|
wasmd_client::{CliWasmdClient, QueryResult, WasmdClient},
|
||||||
};
|
};
|
||||||
|
@ -38,8 +39,7 @@ mod wasmd_client;
|
||||||
|
|
||||||
const MNEMONIC_PHRASE: &str = "clutch debate vintage foster barely primary clown leader sell manual leopard ladder wet must embody story oyster imitate cable alien six square rice wedding";
|
const MNEMONIC_PHRASE: &str = "clutch debate vintage foster barely primary clown leader sell manual leopard ladder wet must embody story oyster imitate cable alien six square rice wedding";
|
||||||
|
|
||||||
const ALICE_ID: &str = "7bfad4e8-d898-4ce2-bbac-1beff7182319";
|
const ADDRESS_PREFIX: &str = "wasm";
|
||||||
const BANK_DEBTOR_ID: &str = "3879fa15-d86e-4464-b679-0a3d78cf3dd3";
|
|
||||||
|
|
||||||
type Sha256Digest = [u8; 32];
|
type Sha256Digest = [u8; 32];
|
||||||
|
|
||||||
|
@ -65,15 +65,38 @@ async fn main() -> Result<(), DynError> {
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
CliCommand::SyncObligations { ref epoch_pk } => {
|
CliCommand::SyncObligations {
|
||||||
sync_obligations(cli.clone(), epoch_pk).await?
|
ref epoch_pk,
|
||||||
}
|
ref liquidity_sources,
|
||||||
|
} => sync_obligations(cli.clone(), epoch_pk, liquidity_sources).await?,
|
||||||
CliCommand::SyncSetOffs => sync_setoffs(cli).await?,
|
CliCommand::SyncSetOffs => sync_setoffs(cli).await?,
|
||||||
|
CliCommand::GetAddress { uuid } => address_from_uuid(uuid)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn address_from_uuid(uuid: Uuid) -> Result<(), DynError> {
|
||||||
|
let seed = global_seed()?;
|
||||||
|
let sk = derive_child_xprv(&seed, uuid);
|
||||||
|
let pk_b = sk.public_key().public_key().to_sec1_bytes();
|
||||||
|
let pk = VerifyingKey::from_sec1_bytes(&pk_b)?;
|
||||||
|
println!("{}", wasm_address(pk));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wasm_address(pk: VerifyingKey) -> String {
|
||||||
|
let tm_pk = TmAccountId::from(pk);
|
||||||
|
AccountId::new(ADDRESS_PREFIX, tm_pk.as_bytes())
|
||||||
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn global_seed() -> Result<Seed, Bip32Error> {
|
||||||
|
let mnemonic = Mnemonic::new(MNEMONIC_PHRASE, Language::English)?;
|
||||||
|
Ok(mnemonic.to_seed("password"))
|
||||||
|
}
|
||||||
|
|
||||||
async fn sync_setoffs(cli: Cli) -> Result<(), DynError> {
|
async fn sync_setoffs(cli: Cli) -> Result<(), DynError> {
|
||||||
let wasmd_client = CliWasmdClient::new(cli.node);
|
let wasmd_client = CliWasmdClient::new(cli.node);
|
||||||
let query_result: QueryResult<QueryAllSetoffsResponse> =
|
let query_result: QueryResult<QueryAllSetoffsResponse> =
|
||||||
|
@ -123,34 +146,48 @@ async fn sync_setoffs(cli: Cli) -> Result<(), DynError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sync_obligations(cli: Cli, epoch_pk: &str) -> Result<(), DynError> {
|
async fn sync_obligations(
|
||||||
|
cli: Cli,
|
||||||
|
epoch_pk: &str,
|
||||||
|
liquidity_sources: &[Uuid],
|
||||||
|
) -> Result<(), DynError> {
|
||||||
let mut intents = {
|
let mut intents = {
|
||||||
let client = HttpClient::new(cli.obligato_url, cli.obligato_key);
|
let client = HttpClient::new(cli.obligato_url.clone(), cli.obligato_key);
|
||||||
client.get_obligations().await.unwrap()
|
client
|
||||||
|
.get_obligations()
|
||||||
|
.await
|
||||||
|
.map_err(|_| cli.obligato_url.to_string())?
|
||||||
};
|
};
|
||||||
|
|
||||||
let bank_id = Uuid::parse_str(BANK_DEBTOR_ID).unwrap();
|
let keys = derive_keys(&mut intents, liquidity_sources)?;
|
||||||
let keys = derive_keys(&mut intents, bank_id)?;
|
|
||||||
write_keys_to_file(cli.keys_file, &keys);
|
write_keys_to_file(cli.keys_file, &keys);
|
||||||
|
|
||||||
add_default_acceptances(&mut intents, bank_id);
|
add_default_acceptances(&mut intents, liquidity_sources);
|
||||||
|
|
||||||
debug!("intents: {intents:?}");
|
debug!("intents: {intents:?}");
|
||||||
|
|
||||||
let intents_enc = {
|
let intents_enc = {
|
||||||
let epoch_pk = VerifyingKey::from_sec1_bytes(&hex::decode(epoch_pk).unwrap()).unwrap();
|
let epoch_pk = VerifyingKey::from_sec1_bytes(&hex::decode(epoch_pk).unwrap()).unwrap();
|
||||||
encrypt_intents(intents, keys, &epoch_pk, cli.obligation_user_map_file)
|
encrypt_intents(intents, &keys, &epoch_pk, cli.obligation_user_map_file)
|
||||||
};
|
};
|
||||||
debug!("Encrypted {} intents", intents_enc.len());
|
debug!("Encrypted {} intents", intents_enc.len());
|
||||||
|
|
||||||
let msg = create_wasm_msg(intents_enc);
|
let liquidity_sources = liquidity_sources
|
||||||
|
.iter()
|
||||||
|
.map(|id| keys[id].private_key().public_key())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let msg = create_wasm_msg(intents_enc, liquidity_sources)?;
|
||||||
let wasmd_client = CliWasmdClient::new(cli.node);
|
let wasmd_client = CliWasmdClient::new(cli.node);
|
||||||
wasmd_client.tx_execute(&cli.contract, &cli.chain_id, 3000000, cli.user, msg)?;
|
wasmd_client.tx_execute(&cli.contract, &cli.chain_id, 3000000, cli.user, msg)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_wasm_msg(obligations_enc: Vec<(Sha256Digest, Vec<u8>)>) -> serde_json::Value {
|
fn create_wasm_msg(
|
||||||
|
obligations_enc: Vec<(Sha256Digest, Vec<u8>)>,
|
||||||
|
liquidity_sources: Vec<VerifyingKey>,
|
||||||
|
) -> Result<serde_json::Value, DynError> {
|
||||||
let obligations_enc: Vec<_> = obligations_enc
|
let obligations_enc: Vec<_> = obligations_enc
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(digest, ciphertext)| {
|
.map(|(digest, ciphertext)| {
|
||||||
|
@ -160,15 +197,23 @@ fn create_wasm_msg(obligations_enc: Vec<(Sha256Digest, Vec<u8>)>) -> serde_json:
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let liquidity_sources = liquidity_sources
|
||||||
|
.into_iter()
|
||||||
|
.map(|pk| HexBinary::from(pk.to_sec1_bytes().as_ref()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
let msg = SubmitObligationsMsg {
|
let msg = SubmitObligationsMsg {
|
||||||
submit_obligations: obligations_enc,
|
submit_obligations: SubmitObligationsMsgInner {
|
||||||
|
obligations: obligations_enc,
|
||||||
|
liquidity_sources,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
serde_json::to_value(msg).unwrap()
|
serde_json::to_value(msg).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encrypt_intents(
|
fn encrypt_intents(
|
||||||
intents: Vec<ObligatoObligation>,
|
intents: Vec<ObligatoObligation>,
|
||||||
keys: HashMap<Uuid, XPrv>,
|
keys: &HashMap<Uuid, XPrv>,
|
||||||
epoch_pk: &VerifyingKey,
|
epoch_pk: &VerifyingKey,
|
||||||
obligation_user_map_file: PathBuf,
|
obligation_user_map_file: PathBuf,
|
||||||
) -> Vec<(Sha256Digest, Vec<u8>)> {
|
) -> Vec<(Sha256Digest, Vec<u8>)> {
|
||||||
|
@ -209,16 +254,18 @@ fn encrypt_intents(
|
||||||
intents_enc
|
intents_enc
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_default_acceptances(obligations: &mut Vec<ObligatoObligation>, bank_id: Uuid) {
|
fn add_default_acceptances(obligations: &mut Vec<ObligatoObligation>, liquidity_sources: &[Uuid]) {
|
||||||
let acceptances = obligations.iter().fold(HashSet::new(), |mut acc, o| {
|
let acceptances = obligations.iter().fold(HashSet::new(), |mut acc, o| {
|
||||||
if o.debtor_id != bank_id {
|
if !liquidity_sources.contains(&o.debtor_id) {
|
||||||
let acceptance = ObligatoObligation {
|
for ls in liquidity_sources {
|
||||||
id: Default::default(),
|
let acceptance = ObligatoObligation {
|
||||||
debtor_id: o.creditor_id,
|
id: Default::default(),
|
||||||
creditor_id: bank_id,
|
debtor_id: o.creditor_id,
|
||||||
amount: u32::MAX as u64,
|
creditor_id: *ls,
|
||||||
};
|
amount: u32::MAX as u64,
|
||||||
acc.insert(acceptance);
|
};
|
||||||
|
acc.insert(acceptance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
acc
|
acc
|
||||||
});
|
});
|
||||||
|
@ -274,41 +321,46 @@ fn write_obligation_user_map_to_file(
|
||||||
|
|
||||||
fn derive_keys(
|
fn derive_keys(
|
||||||
obligations: &mut Vec<ObligatoObligation>,
|
obligations: &mut Vec<ObligatoObligation>,
|
||||||
bank_id: Uuid,
|
liquidity_sources: &[Uuid],
|
||||||
) -> Result<HashMap<Uuid, XPrv>, DynError> {
|
) -> Result<HashMap<Uuid, XPrv>, DynError> {
|
||||||
// Derive a BIP39 seed value using the given password
|
// Derive a BIP39 seed value using the given password
|
||||||
let seed = {
|
let seed = global_seed()?;
|
||||||
let mnemonic = Mnemonic::new(MNEMONIC_PHRASE, Language::English)?;
|
|
||||||
mnemonic.to_seed("password")
|
|
||||||
};
|
|
||||||
|
|
||||||
obligations.sort_by_key(|o| o.debtor_id);
|
obligations.sort_by_key(|o| o.debtor_id);
|
||||||
|
|
||||||
let mut keys = HashMap::new();
|
let mut keys = HashMap::new();
|
||||||
let mut child_num = 0;
|
|
||||||
|
|
||||||
let alice_id = Uuid::parse_str(ALICE_ID).unwrap();
|
for ls in liquidity_sources {
|
||||||
|
keys.entry(*ls)
|
||||||
keys.entry(alice_id)
|
.or_insert_with(|| derive_child_xprv(&seed, *ls));
|
||||||
.or_insert_with(|| derive_child_xprv(&seed, &mut child_num));
|
}
|
||||||
|
|
||||||
keys.entry(bank_id)
|
|
||||||
.or_insert_with(|| derive_child_xprv(&seed, &mut child_num));
|
|
||||||
|
|
||||||
for o in obligations {
|
for o in obligations {
|
||||||
keys.entry(o.debtor_id)
|
keys.entry(o.debtor_id)
|
||||||
.or_insert_with(|| derive_child_xprv(&seed, &mut child_num));
|
.or_insert_with(|| derive_child_xprv(&seed, o.debtor_id));
|
||||||
keys.entry(o.creditor_id)
|
keys.entry(o.creditor_id)
|
||||||
.or_insert_with(|| derive_child_xprv(&seed, &mut child_num));
|
.or_insert_with(|| derive_child_xprv(&seed, o.creditor_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(keys)
|
Ok(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn derive_child_xprv(seed: &Seed, i: &mut usize) -> XPrv {
|
fn derive_child_xprv(seed: &Seed, uuid: Uuid) -> XPrv {
|
||||||
let child_path = format!("m/0/44'/118'/0'/0/{}", i).parse().unwrap();
|
// Hash the UUID using SHA-256
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(uuid.as_bytes());
|
||||||
|
let uuid_digest = hasher.finalize();
|
||||||
|
|
||||||
|
// Convert the hash bytes to a number
|
||||||
|
let uuid_digest_num = u128::from_be_bytes(uuid_digest[..16].try_into().unwrap());
|
||||||
|
|
||||||
|
// Take modulo (2^31 - 1)
|
||||||
|
let address_index = uuid_digest_num % ((1u128 << 31) - 1);
|
||||||
|
|
||||||
|
let child_path = format!("m/0/44'/118'/0'/0/{address_index}")
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
let child_xprv = XPrv::derive_from_path(seed, &child_path);
|
let child_xprv = XPrv::derive_from_path(seed, &child_path);
|
||||||
*i += 1;
|
|
||||||
child_xprv.unwrap()
|
child_xprv.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,52 +368,40 @@ fn derive_child_xprv(seed: &Seed, i: &mut usize) -> XPrv {
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{error::Error, str::FromStr};
|
use std::{error::Error, str::FromStr};
|
||||||
|
|
||||||
use bip32::{Language, Mnemonic, Prefix, PrivateKey, XPrv};
|
use bip32::{Mnemonic, Prefix, PrivateKey, XPrv};
|
||||||
use rand_core::OsRng;
|
use rand_core::OsRng;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{derive_child_xprv, MNEMONIC_PHRASE};
|
use crate::{derive_child_xprv, global_seed};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_mnemonic() {
|
fn test_create_mnemonic() {
|
||||||
// Generate random Mnemonic using the default language (English)
|
// Generate random Mnemonic using the default language (English)
|
||||||
let mnemonic = Mnemonic::random(&mut OsRng, Default::default());
|
let mnemonic = Mnemonic::random(&mut OsRng, Default::default());
|
||||||
|
|
||||||
println!("{}", mnemonic.phrase());
|
println!("{}", mnemonic.phrase());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_enc_dec_for_derived() -> Result<(), Box<dyn Error>> {
|
fn test_enc_dec_for_derived() -> Result<(), Box<dyn Error>> {
|
||||||
let seed = {
|
let seed = global_seed()?;
|
||||||
let mnemonic = Mnemonic::new(MNEMONIC_PHRASE, Language::English)?;
|
|
||||||
mnemonic.to_seed("password")
|
let alice_uuid = Uuid::from_u128(1);
|
||||||
};
|
let alice_sk = derive_child_xprv(&seed, alice_uuid);
|
||||||
|
let alice_pk = alice_sk.private_key().public_key();
|
||||||
|
|
||||||
let mut child_num = 0;
|
|
||||||
let alice_sk = derive_child_xprv(&seed, &mut child_num);
|
|
||||||
let alice_sk_str = alice_sk.to_string(Prefix::XPRV).to_string();
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
alice_sk.private_key().public_key().to_sec1_bytes(),
|
alice_pk.to_sec1_bytes(),
|
||||||
hex::decode("02027e3510f66f1f6c1ea5e3600062255928e518220f7883810cac3fc7fc092057")
|
hex::decode("0219b0b8ee5fe9b317b69119fd15170d79737380c4f020e251b7839096f5513ccf")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into()
|
.into()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let alice_sk_str = alice_sk.to_string(Prefix::XPRV).to_string();
|
||||||
assert_eq!(XPrv::from_str(&alice_sk_str).unwrap(), alice_sk);
|
assert_eq!(XPrv::from_str(&alice_sk_str).unwrap(), alice_sk);
|
||||||
|
|
||||||
let alice_pk = alice_sk.private_key().public_key();
|
|
||||||
assert_eq!(
|
|
||||||
alice_pk.to_sec1_bytes().into_vec(),
|
|
||||||
vec![
|
|
||||||
2, 2, 126, 53, 16, 246, 111, 31, 108, 30, 165, 227, 96, 0, 98, 37, 89, 40, 229, 24,
|
|
||||||
34, 15, 120, 131, 129, 12, 172, 63, 199, 252, 9, 32, 87
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
let msg = r#"{"debtor":"02027e3510f66f1f6c1ea5e3600062255928e518220f7883810cac3fc7fc092057","creditor":"0216254f4636c4e68ae22d98538851a46810b65162fe37bf57cba6d563617c913e","amount":10,"salt":"65c188bcc133add598f7eecc449112f4bf61024345316cff0eb5ce61291991b141073dcd3c543ea142e66fffa8f483dc382043d37e490ef9b8069c489ce94a0b"}"#;
|
let msg = r#"{"debtor":"02027e3510f66f1f6c1ea5e3600062255928e518220f7883810cac3fc7fc092057","creditor":"0216254f4636c4e68ae22d98538851a46810b65162fe37bf57cba6d563617c913e","amount":10,"salt":"65c188bcc133add598f7eecc449112f4bf61024345316cff0eb5ce61291991b141073dcd3c543ea142e66fffa8f483dc382043d37e490ef9b8069c489ce94a0b"}"#;
|
||||||
|
|
||||||
let ciphertext = ecies::encrypt(&alice_pk.to_sec1_bytes(), msg.as_bytes()).unwrap();
|
let ciphertext = ecies::encrypt(&alice_pk.to_sec1_bytes(), msg.as_bytes()).unwrap();
|
||||||
// let ciphertext = hex::decode("0418d9051cbfc86c8ddd57ae43ea3d1ac8b30353a3ecd8c806bb11f0693dfd282d5f07d1de32cbcd933d5ab7cd0aa171c972e75531b915e968f0fdeba78fa3f359c7f3ef7ae2dfffeb19493e9b2418dc774e6e80448a2dc4a7ba657cd4a8456e120977ebe372a57187d53981cc5856fbd63e9c1bdf001ed71c3d50cbaff594561191d33dad852cb782126f480add2cc92758b59eb63de857d299eaa5f09fbc55643a73b1d8206ce83453b5296b566d9f622520679bb3e6d9c8b7a707f33d3093c41dfc0a8267749b4028e9ee0faad0c8df64f1682a348f220585fdd9b9ac411bdaaa6a249b45accc89a80e5af09abb239231aa869e29459e562721b685d98b3da3eeaef14e1c5f3bd20cf27c0cbbae7b5c618e737df9a84f9a040bb472b7254af2cf4ccc76784cf8432080e528f700ca2a082b7020d94f0f5325dd4998c03972a0b39e6670b65be89e7a80aad7af08a393fcf2e103999254380c1f0355d97ddcdfaeed4bcfaf15b578cee1f6d3fd4ceccd85760b9bd714f81698ddf6fbbc06152a9306a5dd0052c722e390470f0c70eeac81a5da0090").unwrap();
|
// println!("{}", hex::encode(&ciphertext));
|
||||||
|
|
||||||
println!("{}", hex::encode(&ciphertext));
|
|
||||||
|
|
||||||
let msg_dec =
|
let msg_dec =
|
||||||
ecies::decrypt(&alice_sk.private_key().to_bytes(), ciphertext.as_slice()).unwrap();
|
ecies::decrypt(&alice_sk.private_key().to_bytes(), ciphertext.as_slice()).unwrap();
|
||||||
|
|
|
@ -4,10 +4,11 @@ use uuid::Uuid;
|
||||||
use crate::{
|
use crate::{
|
||||||
obligato_client::Client,
|
obligato_client::Client,
|
||||||
types::{ObligatoObligation, ObligatoSetOff},
|
types::{ObligatoObligation, ObligatoSetOff},
|
||||||
BANK_DEBTOR_ID,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct MockClient;
|
pub struct MockClient {
|
||||||
|
pub bank: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Client for MockClient {
|
impl Client for MockClient {
|
||||||
|
@ -25,7 +26,7 @@ impl Client for MockClient {
|
||||||
// tender: $ --10--> 1
|
// tender: $ --10--> 1
|
||||||
ObligatoObligation {
|
ObligatoObligation {
|
||||||
id: Uuid::from_u128(2),
|
id: Uuid::from_u128(2),
|
||||||
debtor_id: Uuid::parse_str(BANK_DEBTOR_ID).unwrap(),
|
debtor_id: self.bank,
|
||||||
creditor_id: Uuid::from_u128(1),
|
creditor_id: Uuid::from_u128(1),
|
||||||
amount: 10,
|
amount: 10,
|
||||||
},
|
},
|
||||||
|
|
|
@ -47,7 +47,13 @@ pub struct RawEncryptedObligation {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct SubmitObligationsMsg {
|
pub struct SubmitObligationsMsg {
|
||||||
pub submit_obligations: Vec<RawEncryptedObligation>,
|
pub submit_obligations: SubmitObligationsMsgInner,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SubmitObligationsMsgInner {
|
||||||
|
pub obligations: Vec<RawEncryptedObligation>,
|
||||||
|
pub liquidity_sources: Vec<HexBinary>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||||
|
|
Loading…
Reference in a new issue