API route for spending online and offline ecash

This commit is contained in:
Simon Wicky
2024-05-07 13:35:21 +02:00
parent d57ff735df
commit 1df5ae9c2e
10 changed files with 424 additions and 71 deletions
+1 -2
View File
@@ -15,8 +15,7 @@ schemars = { workspace = true, features = ["preserve_order"] }
serde = { workspace = true, features = ["derive"] }
ts-rs = { workspace = true, optional = true }
tendermint = { workspace = true }
time = { workspace = true, features = ["serde", "parsing", "formatting"] }
time = { workspace = true }
# for serde on secp256k1 signatures
ecdsa = { version = "0.16", features = ["serde"] }
@@ -7,4 +7,5 @@ pub mod models;
pub use models::{
BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, FreePassRequest,
PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse,
VerificationKeyResponse, VerifyCredentialResponse, VerifyEcashCredentialBody,
};
+65 -5
View File
@@ -4,18 +4,20 @@
use crate::coconut::helpers::issued_credential_plaintext;
use cosmrs::AccountId;
use nym_credentials_interface::{
hash_to_scalar, Attribute, BlindSignRequest, BlindedSignature, Bytable, CoconutError,
CredentialSpendingData, VerificationKey,
BlindedSignature, CompactEcashError, CredentialSpendingData, OldCredentialSpendingData,
PartialCoinIndexSignature, PartialExpirationDateSignature, PublicKeyUser, VerificationKeyAuth,
WithdrawalRequest,
};
use nym_crypto::asymmetric::identity;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::{collections::BTreeMap, fmt::Display};
use tendermint::hash::Hash;
use time::OffsetDateTime;
#[derive(Serialize, Deserialize)]
pub struct VerifyCredentialBody {
/// The cryptographic material required for spending the underlying credential.
pub credential_data: CredentialSpendingData,
pub credential_data: OldCredentialSpendingData,
/// Multisig proposal for releasing funds for the provided bandwidth credential
pub proposal_id: u64,
@@ -26,7 +28,7 @@ pub struct VerifyCredentialBody {
impl VerifyCredentialBody {
pub fn new(
credential_data: CredentialSpendingData,
credential_data: OldCredentialSpendingData,
proposal_id: u64,
gateway_cosmos_addr: AccountId,
) -> VerifyCredentialBody {
@@ -51,6 +53,64 @@ impl VerifyCredentialResponse {
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct VerifyEcashCredentialBody {
/// The cryptographic material required for spending the underlying credential.
pub credential: CredentialSpendingData,
/// Cosmos address of the sender of the credential
pub gateway_cosmos_addr: AccountId,
/// Multisig proposal for releasing funds for the provided bandwidth credential, None for freepasses
pub proposal_id: Option<u64>,
}
impl VerifyEcashCredentialBody {
pub fn new(
credential: CredentialSpendingData,
gateway_cosmos_addr: AccountId,
proposal_id: Option<u64>,
) -> VerifyEcashCredentialBody {
VerifyEcashCredentialBody {
credential,
gateway_cosmos_addr,
proposal_id,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum VerifyEcashCredentialResponse {
InvalidFormat(String),
DoubleSpend,
AlreadySent,
SubmittedTooLate(u64, u64),
Refused,
Accepted,
}
impl Display for VerifyEcashCredentialResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidFormat(reason) => {
write!(f, "Invalid format : {:?}", reason)
}
Self::DoubleSpend => write!(f, "Credential was already spent"),
Self::AlreadySent => write!(f, "This credential was already sent"),
Self::SubmittedTooLate(expected, actual) => {
write!(
f,
"Credential spent too late. Accepted from {:#?}, spent on {:#?}",
OffsetDateTime::from_unix_timestamp(*expected as i64),
OffsetDateTime::from_unix_timestamp(*actual as i64)
)
}
Self::Refused => write!(f, "Credential failed to validate"),
Self::Accepted => write!(f, "Credential was accepted"),
}
}
}
// All strings are base58 encoded representations of structs
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct BlindSignRequestBody {
+214 -54
View File
@@ -7,9 +7,10 @@ use crate::coconut::helpers::{accepted_vote_err, blind_sign};
use crate::coconut::state::State;
use crate::coconut::storage::CoconutStorageExt;
use k256::ecdsa::signature::Verifier;
use nym_api_requests::coconut::models::SpentCredentialsResponse;
use nym_api_requests::coconut::models::{
CredentialsRequestBody, EpochCredentialsResponse, FreePassNonceResponse, FreePassRequest,
IssuedCredentialResponse, IssuedCredentialsResponse,
IssuedCredentialResponse, IssuedCredentialsResponse, VerifyEcashCredentialResponse,
};
use nym_api_requests::coconut::{
BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse,
@@ -17,16 +18,18 @@ use nym_api_requests::coconut::{
};
use nym_coconut_dkg_common::types::EpochId;
use nym_compact_ecash::error::CompactEcashError;
use nym_compact_ecash::identify::IdentifyResult;
use nym_credentials::coconut::bandwidth::bandwidth_credential_params;
use nym_credentials::coconut::utils::{
cred_exp_date_timestamp, freepass_exp_date_timestamp, today_timestamp,
};
use nym_validator_client::nyxd::Coin;
use nym_ecash_contract_common::spend_credential::check_proposal;
use rand::rngs::OsRng;
use rand::RngCore;
use rocket::serde::json::Json;
use rocket::State as RocketState;
use std::ops::Deref;
use time::OffsetDateTime;
use time::{Duration, OffsetDateTime};
mod helpers;
@@ -221,70 +224,227 @@ pub async fn post_blind_sign(
Ok(Json(BlindedSignatureResponse { blinded_signature }))
}
#[post("/verify-bandwidth-credential", data = "<verify_credential_body>")]
pub async fn verify_bandwidth_credential(
verify_credential_body: Json<VerifyCredentialBody>,
#[post("/verify-online-credential", data = "<verify_credential_body>")]
pub async fn verify_online_credential(
verify_credential_body: Json<VerifyEcashCredentialBody>,
state: &RocketState<State>,
) -> Result<Json<VerifyCredentialResponse>> {
) -> Result<Json<VerifyEcashCredentialResponse>> {
let proposal_id = verify_credential_body.proposal_id;
let credential_data = &verify_credential_body.credential_data;
let epoch_id = credential_data.epoch_id;
let theta = &credential_data.verify_credential_request;
let credential_data = &verify_credential_body.credential;
let payment = &credential_data.payment;
let today_date = today_timestamp();
let voucher_value: u64 = if credential_data.typ.is_voucher() {
credential_data
.get_bandwidth_attribute()
.ok_or(CoconutError::MissingBandwidthValue)?
.parse()
.map_err(|source| CoconutError::VoucherValueParsingFailure { source })?
} else {
return Err(CoconutError::NotABandwidthVoucher {
typ: credential_data.typ,
});
};
// TODO: introduce a check to make sure we haven't already voted for this proposal to prevent DDOS
let proposal = state.client.get_proposal(proposal_id).await?;
// Proposal description is the blinded serial number
if !theta.has_blinded_serial_number(&proposal.description)? {
return Err(CoconutError::IncorrectProposal {
reason: String::from("incorrect blinded serial number in description"),
});
//tickets needs a proposal
if proposal_id.is_none() && credential_data.typ.is_ticketbook() {
return Ok(Json(VerifyEcashCredentialResponse::InvalidFormat(
"No proposal for a ticketbook".to_string(),
)));
}
let proposed_release_funds =
funds_from_cosmos_msgs(proposal.msgs).ok_or(CoconutError::IncorrectProposal {
reason: String::from("action is not to release funds"),
})?;
// Credential has not been spent before, and is on its way of being spent
let credential_status = state
//there should be no proposal on a freepass
if proposal_id.is_some() && credential_data.typ.is_free_pass() {
return Ok(Json(VerifyEcashCredentialResponse::InvalidFormat(
"Proposal present for a freepass".to_string(),
)));
}
if today_date != credential_data.spend_date {
state.refuse_proposal_online(proposal_id).await;
return Ok(Json(VerifyEcashCredentialResponse::SubmittedTooLate(
today_date,
credential_data.spend_date,
)));
}
//actual double spend detection with storage
if let Some(previous_payment) = state
.get_credential_by_sn(credential_data.serial_number_b58())
.await?
{
match nym_compact_ecash::identify::identify(
credential_data.payment.clone(),
previous_payment.payment,
credential_data.pay_info.clone(),
previous_payment.pay_info,
) {
IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message?
IdentifyResult::DuplicatePayInfo(_) => {
log::warn!("Identical payInfo");
state.refuse_proposal_online(proposal_id).await;
return Ok(Json(VerifyEcashCredentialResponse::AlreadySent));
}
IdentifyResult::DoubleSpendingPublicKeys(pub_key) => {
//Actual double spending
log::warn!(
"Double spending attempt for key {}",
pub_key.to_base58_string()
);
state.refuse_proposal_online(proposal_id).await;
if credential_data.typ.is_ticketbook() {
state.blacklist(pub_key.to_base58_string()).await;
}
return Ok(Json(VerifyEcashCredentialResponse::DoubleSpend));
}
}
}
//Double spend check with contract
if let Some(spent_credential) = state
.client
.get_spent_credential(theta.blinded_serial_number_bs58())
.get_spent_credential(payment.serial_number_bs58())
.await?
.spend_credential
.ok_or(CoconutError::InvalidCredentialStatus {
status: String::from("Inexistent"),
})?
.status();
if credential_status != SpendCredentialStatus::InProgress {
return Err(CoconutError::InvalidCredentialStatus {
status: format!("{:?}", credential_status),
});
{
if spent_credential.serial_number() == credential_data.serial_number_b58() {
state.refuse_proposal_online(proposal_id).await;
return Ok(Json(VerifyEcashCredentialResponse::DoubleSpend));
}
}
let verification_key = state.verification_key(credential_data.epoch_id).await?;
let params = bandwidth_credential_params();
if credential_data.verify(params, &verification_key).is_err() {
state.refuse_proposal_online(proposal_id).await;
return Ok(Json(VerifyEcashCredentialResponse::Refused));
}
// TODO: introduce a check to make sure we haven't already voted for this proposal to prevent DDOS
if let Some(id) = proposal_id {
let proposal = state.client.get_proposal(id).await?;
// Proposal description is the blinded serial number
if !payment.has_serial_number(&proposal.description)? {
state.client.vote_proposal(id, false, None).await?;
return Ok(Json(VerifyEcashCredentialResponse::InvalidFormat(
String::from("incorrect blinded serial number in description"),
)));
}
if !check_proposal(proposal.msgs) {
state.client.vote_proposal(id, false, None).await?;
return Ok(Json(VerifyEcashCredentialResponse::InvalidFormat(
String::from("action is not to spend_credential"),
)));
}
// Vote yes or no on the proposal based on the verification result
let ret = state.client.vote_proposal(id, true, None).await;
accepted_vote_err(ret)?;
}
//From here, credential is considered spent
//add to bloom filter for fast dup detection
state
.add_spent_credentials(&credential_data.serial_number_b58())
.await;
//store credential
//don't store free pass, as they do not incur rewards
if !credential_data.typ.is_free_pass() {
state
.store_credential(
&verify_credential_body.credential,
&verify_credential_body.gateway_cosmos_addr,
proposal_id.unwrap(), //safety : It's not a freepass, and we checked before that it was not none
)
.await?;
}
Ok(Json(VerifyEcashCredentialResponse::Accepted))
}
#[post("/verify-offline-credential", data = "<verify_credential_body>")]
pub async fn verify_offline_credential(
verify_credential_body: Json<VerifyEcashCredentialBody>,
state: &RocketState<State>,
) -> Result<Json<VerifyEcashCredentialResponse>> {
let credential_data = &verify_credential_body.credential;
let proposal_id = verify_credential_body.proposal_id;
//tickets needs a proposal
if proposal_id.is_none() && credential_data.typ.is_ticketbook() {
return Ok(Json(VerifyEcashCredentialResponse::InvalidFormat(
"No proposal for a ticketbook".to_string(),
)));
}
//there should be no proposal on a freepass
if proposal_id.is_some() && credential_data.typ.is_free_pass() {
return Ok(Json(VerifyEcashCredentialResponse::InvalidFormat(
"Proposal present for a freepass".to_string(),
)));
}
//SW NOTE: Offline scheme, but we still need some check on that, so that client and gateway can't collude and send expired credentials.
//Let's allow the current day (obviously), and the day before (for late sender or around midnight)
let today_date = today_timestamp();
let yesterday_date = today_date - Duration::DAY.whole_seconds() as u64;
if today_date != credential_data.spend_date && yesterday_date != credential_data.spend_date {
state.refuse_proposal(proposal_id).await;
return Ok(Json(VerifyEcashCredentialResponse::SubmittedTooLate(
yesterday_date,
credential_data.spend_date,
)));
}
//actual double spend detection with storage
if let Some(previous_payment) = state
.get_credential_by_sn(credential_data.serial_number_b58())
.await?
{
match nym_compact_ecash::identify::identify(
credential_data.payment.clone(),
previous_payment.payment,
credential_data.pay_info.clone(),
previous_payment.pay_info,
) {
IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message?
IdentifyResult::DuplicatePayInfo(_) => {
log::warn!("Identical payInfo");
state.refuse_proposal(proposal_id).await;
return Ok(Json(VerifyEcashCredentialResponse::AlreadySent));
}
IdentifyResult::DoubleSpendingPublicKeys(pub_key) => {
//Actual double spending
log::warn!(
"Double spending attempt for key {}",
pub_key.to_base58_string()
);
state.refuse_proposal(proposal_id).await;
if credential_data.typ.is_ticketbook() {
state.blacklist(pub_key.to_base58_string()).await;
}
return Ok(Json(VerifyEcashCredentialResponse::DoubleSpend));
}
}
}
let epoch_id = credential_data.epoch_id;
let verification_key = state.verification_key(epoch_id).await?;
let params = bandwidth_credential_params();
let mut vote_yes = credential_data.verify(params, &verification_key);
vote_yes &= Coin::from(proposed_release_funds)
== Coin::new(voucher_value as u128, state.mix_denom.clone());
if credential_data.verify(params, &verification_key).is_err() {
state.refuse_proposal(proposal_id).await;
return Ok(Json(VerifyEcashCredentialResponse::Refused));
}
// Vote yes or no on the proposal based on the verification result
let ret = state
.client
.vote_proposal(proposal_id, vote_yes, None)
//add to bloom filter for fast dup detection
state
.add_spent_credentials(&credential_data.serial_number_b58())
.await;
accepted_vote_err(ret)?;
//store credential
//don't store free pass, as they do not incur rewards
if !credential_data.typ.is_free_pass() {
state
.store_credential(
&verify_credential_body.credential,
&verify_credential_body.gateway_cosmos_addr,
proposal_id.unwrap(), //safety : It's not a freepass, and we checked before that it was not none
)
.await?;
}
state
.accept_and_execute_proposal(proposal_id, credential_data.serial_number_b58())
.await?;
Ok(Json(VerifyEcashCredentialResponse::Accepted))
}
#[get("/spent-credentials")]
pub async fn spent_credentials(
+4 -2
View File
@@ -4,7 +4,6 @@
use crate::coconut::error::Result;
use cw3::{ProposalResponse, VoteResponse};
use cw4::MemberResponse;
use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse;
use nym_coconut_dkg_common::dealer::{
DealerDetails, DealerDetailsResponse, RegisteredDealerDetails,
};
@@ -20,6 +19,7 @@ use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyS
use nym_contracts_common::IdentityKey;
use nym_dkg::Threshold;
use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse;
use nym_ecash_contract_common::spend_credential::EcashSpentCredentialResponse;
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
use nym_validator_client::nyxd::{AccountId, Fee, Hash, TxResponse};
@@ -42,7 +42,9 @@ pub trait Client {
async fn get_spent_credential(
&self,
blinded_serial_number: String,
) -> Result<SpendCredentialResponse>;
) -> Result<EcashSpentCredentialResponse>;
async fn propose_for_blacklist(&self, public_key: String) -> Result<ExecuteResult>;
async fn get_blacklisted_account(
&self,
public_key: String,
+2 -2
View File
@@ -82,8 +82,8 @@ pub enum CoconutError {
)]
TooLongFreePass { expiry_date: OffsetDateTime },
#[error("the provided free pass expiry is set in the past!")]
FreePassExpiryInThePast { expiry_date: OffsetDateTime },
#[error("the spend date provided does not correspnd to today's date. Expected : {expected}, got {got}")]
InvalidSpendDate { expected: u64, got: u64 },
#[error("the received bandwidth voucher did not contain deposit value")]
MissingBandwidthValue,
+12
View File
@@ -13,6 +13,8 @@ use nym_compact_ecash::scheme::keygen::SecretKeyAuth;
use nym_compact_ecash::setup::{sign_coin_indices, CoinIndexSignature, Parameters};
use nym_compact_ecash::utils::BlindedSignature;
use nym_compact_ecash::{PublicKeyUser, VerificationKeyAuth, WithdrawalRequest};
use nym_ecash_contract_common::events::BLACKLIST_PROPOSAL_ID;
use nym_validator_client::nyxd::cosmwasm_client::logs::Log;
use nym_validator_client::nyxd::error::NyxdError::AbciError;
use tokio::sync::RwLock;
@@ -182,4 +184,14 @@ impl ExpirationDateSignatureCache {
signatures.clone().unwrap() // Either we or someone else update the signatures, so they must be there
}
}
/// Search for the proposal id in the given log. It'll be in the LAST wasm event, with attribute key "proposal_id"
pub fn find_proposal_id(logs: &[Log]) -> Option<&cosmwasm_std::Attribute> {
logs.iter()
.rev()
.flat_map(|log| log.events.iter())
.find(|event| event.ty == "wasm")?
.attributes
.iter()
.find(|attr| attr.key == BLACKLIST_PROPOSAL_ID)
}
+6 -1
View File
@@ -47,7 +47,12 @@ where
api_routes::get_current_free_pass_nonce,
api_routes::post_free_pass,
api_routes::post_blind_sign,
api_routes::verify_bandwidth_credential,
api_routes::verify_online_credential,
api_routes::verify_offline_credential,
api_routes::expiration_date_signatures,
api_routes::expiration_date_signatures_timestamp,
api_routes::coin_indices_signatures,
api_routes::spent_credentials,
api_routes::epoch_credentials,
api_routes::issued_credential,
api_routes::issued_credentials,
+105 -2
View File
@@ -1,13 +1,15 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::coconut::client::Client as LocalClient;
use crate::coconut::comm::APICommunicationChannel;
use crate::coconut::deposit::validate_deposit_tx;
use crate::coconut::error::Result;
use crate::coconut::keys::KeyPair;
use crate::coconut::storage::CoconutStorageExt;
use crate::coconut::{client::Client as LocalClient, helpers::find_proposal_id};
use crate::support::storage::NymApiStorage;
use bloomfilter::Bloom;
use cw3::Status;
use nym_api_requests::coconut::helpers::issued_credential_plaintext;
use nym_api_requests::coconut::BlindSignRequestBody;
use nym_compact_ecash::{
@@ -17,6 +19,10 @@ use nym_compact_ecash::{
utils::BlindedSignature,
VerificationKeyAuth,
};
use nym_config::defaults::{BLOOM_BITMAP_SIZE, BLOOM_NUM_HASHES, BLOOM_SIP_KEYS};
use nym_credentials::{coconut::utils::cred_exp_date_timestamp, CredentialSpendingData};
use nym_ecash_contract_common::spend_credential::check_proposal;
use super::{
error::CoconutError,
helpers::{accepted_vote_err, CoinIndexSignatureCache, ExpirationDateSignatureCache},
@@ -107,6 +113,103 @@ impl State {
validate_deposit_tx(request, tx).await
}
pub(crate) async fn accept_and_execute_proposal(
&self,
proposal_id: Option<u64>,
serial_number_bs58: String,
) -> Result<()> {
if let Some(id) = proposal_id {
let proposal = self.client.get_proposal(id).await?;
// Proposal description is the blinded serial number
if serial_number_bs58 != proposal.description {
return Err(CoconutError::IncorrectProposal {
reason: String::from("incorrect blinded serial number in description"),
});
}
if !check_proposal(proposal.msgs) {
return Err(CoconutError::IncorrectProposal {
reason: String::from("action is not to spend_credential"),
});
}
let client = self.client.clone();
tokio::spawn(async move {
let ret = client.vote_proposal(id, true, None).await;
//SW NOTE: What to do if this fails
if let Err(e) = accepted_vote_err(ret) {
log::debug!("Failed to vote on proposal {} - {:?}", id, e);
}
if let Ok(proposal) = client.get_proposal(id).await {
if proposal.status == Status::Passed {
//SW NOTE: What to do if this fails
if let Err(e) = client.execute_proposal(id).await {
log::debug!("Failed to execute proposal {} - {:?}", id, e);
}
}
}
});
}
Ok(())
}
pub(crate) async fn refuse_proposal(&self, proposal_id: Option<u64>) {
if let Some(id) = proposal_id {
let client = self.client.clone();
tokio::spawn(async move {
//whatever is in the proposal, we can refuse it anyway
if let Err(e) = client.vote_proposal(id, false, None).await {
log::debug!("Failed to refuse proposal {:?} - {:?}", id, e)
}
});
}
}
pub(crate) async fn refuse_proposal_online(&self, proposal_id: Option<u64>) {
if let Some(id) = proposal_id {
//whatever is in the proposal, we can refuse it anyway
if let Err(e) = self.client.vote_proposal(id, false, None).await {
log::debug!("Failed to refuse proposal {:?} - {:?}", id, e)
}
}
}
pub(crate) async fn blacklist(&self, public_key: String) {
let client = self.client.clone();
tokio::spawn(async move {
//SW TODO error handling with one log at the end
let response = client.propose_for_blacklist(public_key.clone()).await?;
let proposal_id = find_proposal_id(&response.logs)
.ok_or(CoconutError::ProposalIdError {
reason: "Proposal_ID not found".to_string(),
})?
.value
.parse::<u64>()
.map_err(|_| CoconutError::ProposalIdError {
reason: String::from("proposal id could not be parsed to u64"),
})?;
let proposal = client.get_proposal(proposal_id).await?;
if proposal.status == Status::Open {
if public_key != proposal.description {
return Err(CoconutError::IncorrectProposal {
reason: String::from("incorrect publickey in description"),
});
}
let ret = client.vote_proposal(proposal_id, true, None).await;
accepted_vote_err(ret)?;
if let Ok(proposal) = client.get_proposal(proposal_id).await {
if proposal.status == Status::Passed {
client.execute_proposal(proposal_id).await?
}
}
}
Ok(())
});
}
pub(crate) async fn sign_and_store_credential(
&self,
current_epoch: EpochId,
@@ -164,7 +267,7 @@ impl State {
Ok(())
}
pub async fn verification_key(&self, epoch_id: EpochId) -> Result<VerificationKey> {
pub async fn verification_key(&self, epoch_id: EpochId) -> Result<VerificationKeyAuth> {
self.comm_channel
.aggregated_verification_key(epoch_id)
.await
+14 -3
View File
@@ -8,7 +8,6 @@ use anyhow::Result;
use async_trait::async_trait;
use cw3::{ProposalResponse, VoteResponse};
use cw4::MemberResponse;
use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse;
use nym_coconut_dkg_common::dealing::{
DealerDealingsStatusResponse, DealingChunkInfo, DealingMetadata, DealingStatusResponse,
PartialContractDealing,
@@ -24,6 +23,7 @@ use nym_config::defaults::{ChainDetails, NymNetworkDetails};
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse;
use nym_ecash_contract_common::spend_credential::EcashSpentCredentialResponse;
use nym_mixnet_contract_common::families::FamilyHead;
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::reward_params::RewardingParams;
@@ -37,7 +37,7 @@ use nym_validator_client::nyxd::contract_traits::{NameServiceQueryClient, PagedD
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::nyxd::{
contract_traits::{
CoconutBandwidthQueryClient, DkgQueryClient, DkgSigningClient, GroupQueryClient,
DkgQueryClient, DkgSigningClient, EcashQueryClient, EcashSigningClient, GroupQueryClient,
MixnetQueryClient, MixnetSigningClient, MultisigQueryClient, MultisigSigningClient,
NymContractsProvider, PagedMixnetQueryClient, PagedMultisigQueryClient,
PagedVestingQueryClient, SpDirectoryQueryClient,
@@ -401,12 +401,23 @@ impl crate::coconut::client::Client for Client {
async fn get_spent_credential(
&self,
blinded_serial_number: String,
) -> crate::coconut::error::Result<SpendCredentialResponse> {
) -> crate::coconut::error::Result<EcashSpentCredentialResponse> {
Ok(nyxd_query!(
self,
get_spent_credential(blinded_serial_number).await?
))
}
async fn propose_for_blacklist(
&self,
public_key: String,
) -> crate::coconut::error::Result<ExecuteResult> {
Ok(nyxd_signing!(
self,
propose_for_blacklist(public_key, None).await?
))
}
async fn get_blacklisted_account(
&self,
public_key: String,