Skip to content

Commit

Permalink
Merge branch 'paulliu/withdrawal_status' into 'master'
Browse files Browse the repository at this point in the history
feat(ckerc20): Add a withdrawal_status method to minter XC-78

We add a public method `withdrawal_status` to the ckETH/ckERC20 minter to return the status of withdrawal requests matching the given search parameter. The parameter can be one of the following three: by recipient address, by from account, or by ckETH burn index.

Because there could be multiple matches, the result is returned as an array. Paging support will be future work if required. 

See merge request dfinity-lab/public/ic!18860
  • Loading branch information
ninegua committed May 7, 2024
2 parents e54d5c9 + 1b0c105 commit 73af37a
Show file tree
Hide file tree
Showing 12 changed files with 685 additions and 148 deletions.
65 changes: 64 additions & 1 deletion rs/ethereum/cketh/minter/cketh_minter.did
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,10 @@ type EthTransaction = record { transaction_hash : text };
// Status of a finalized transaction.
type TxFinalizedStatus = variant {
// Transaction was successful.
Success : EthTransaction;
Success : record {
transaction_hash : text;
effective_transaction_fee: opt nat;
};
// Transaction failed, user got reimbursed.
Reimbursed : record {
transaction_hash : text;
Expand Down Expand Up @@ -226,6 +229,63 @@ type RetrieveEthStatus = variant {

type WithdrawalArg = record { recipient : text; amount : nat };

// Details of a withdrawal request and its status.
type WithdrawalDetail = record {
// Symbol of the withdrawal token (either ckETH or ckERC20 token symbol).
token_symbol : text;

// Amount of tokens in base unit that was withdrawn.
withdrawal_amount : nat;

// Max transaction fee in Wei (transaction fee paid by the sender).
max_transaction_fee : opt nat;

// Withdrawal id (i.e. burn index on the ckETH ledger).
withdrawal_id : nat64;

// Sender's principal.
from : principal;

// Sender's subaccount (if given).
from_subaccount : opt blob;

// Address to send tokens to.
recipient_address : text;

// Withdrawal status
status : WithdrawalStatus;
};

// Status of a withdrawal request.
type WithdrawalStatus = variant {
// Request is pending, i.e. transaction is not yet created.
Pending;

// Transaction created byt not yet sent.
TxCreated;

// Transaction sent but not yet finalized.
TxSent : EthTransaction;

// Transaction already finalized.
TxFinalized : TxFinalizedStatus;
};

// ICRC-1 account type.
type Account = record { owner : principal; subaccount : opt blob };

// Search parameter for withdrawals.
type WithdrawalSearchParameter = variant {
// Search by recipient's ETH address.
ByRecipient : text;

// Search by sender's token account.
BySenderAccount : Account;

// Search by ckETH burn index (which is also used to index ckERC20 withdrawals).
ByWithdrawalId : nat64;
};

type RetrieveEthRequest = record { block_index : nat };

type WithdrawalError = variant {
Expand Down Expand Up @@ -494,6 +554,9 @@ service : (MinterArg) -> {
// Retrieve the status of a Eth withdrawal request.
retrieve_eth_status : (nat64) -> (RetrieveEthStatus);

// Return details of all withdrawals matching the given search parameter.
withdrawal_status : (WithdrawalSearchParameter) -> (vec WithdrawalDetail) query;

// Check if an address is blocked by the minter.
is_address_blocked : (text) -> (bool) query;

Expand Down
67 changes: 63 additions & 4 deletions rs/ethereum/cketh/minter/src/endpoints.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use crate::eth_rpc_client::responses::TransactionReceipt;
use crate::ledger_client::LedgerBurnError;
use crate::state::transactions::EthWithdrawalRequest;
use crate::numeric::LedgerBurnIndex;
use crate::state::{transactions, transactions::EthWithdrawalRequest};
use crate::tx::{SignedEip1559TransactionRequest, TransactionPrice};
use candid::{CandidType, Deserialize, Nat, Principal};
use icrc_ledger_types::icrc1::account::Account;
use minicbor::{Decode, Encode};
use std::fmt::{Display, Formatter};
use std::str::FromStr;

pub mod ckerc20;

Expand Down Expand Up @@ -89,6 +93,14 @@ impl From<&SignedEip1559TransactionRequest> for EthTransaction {
}
}

impl From<&TransactionReceipt> for EthTransaction {
fn from(receipt: &TransactionReceipt) -> Self {
Self {
transaction_hash: receipt.transaction_hash.to_string(),
}
}
}

#[derive(CandidType, Deserialize, Clone, Debug, PartialEq)]
pub struct RetrieveEthRequest {
pub block_index: Nat,
Expand Down Expand Up @@ -116,7 +128,7 @@ pub enum CandidBlockTag {
impl From<EthWithdrawalRequest> for RetrieveEthRequest {
fn from(value: EthWithdrawalRequest) -> Self {
Self {
block_index: candid::Nat::from(value.ledger_burn_index.get()),
block_index: Nat::from(value.ledger_burn_index.get()),
}
}
}
Expand All @@ -132,7 +144,10 @@ pub enum RetrieveEthStatus {

#[derive(CandidType, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
pub enum TxFinalizedStatus {
Success(EthTransaction),
Success {
transaction_hash: String,
effective_transaction_fee: Option<Nat>,
},
PendingReimbursement(EthTransaction),
Reimbursed {
transaction_hash: String,
Expand All @@ -149,7 +164,9 @@ impl Display for RetrieveEthStatus {
RetrieveEthStatus::TxCreated => write!(f, "Created"),
RetrieveEthStatus::TxSent(tx) => write!(f, "Sent({})", tx.transaction_hash),
RetrieveEthStatus::TxFinalized(tx_status) => match tx_status {
TxFinalizedStatus::Success(tx) => write!(f, "Confirmed({})", tx.transaction_hash),
TxFinalizedStatus::Success {
transaction_hash, ..
} => write!(f, "Confirmed({})", transaction_hash),
TxFinalizedStatus::PendingReimbursement(tx) => {
write!(f, "PendingReimbursement({})", tx.transaction_hash)
}
Expand Down Expand Up @@ -205,6 +222,48 @@ impl From<LedgerBurnError> for WithdrawalError {
}
}

#[derive(CandidType, Deserialize, Clone, Eq, PartialEq, Debug)]
pub enum WithdrawalSearchParameter {
ByWithdrawalId(u64),
ByRecipient(String),
BySenderAccount(Account),
}

impl TryFrom<WithdrawalSearchParameter> for transactions::WithdrawalSearchParameter {
type Error = String;

fn try_from(parameter: WithdrawalSearchParameter) -> Result<Self, String> {
use WithdrawalSearchParameter::*;
match parameter {
ByWithdrawalId(index) => Ok(Self::ByWithdrawalId(LedgerBurnIndex::new(index))),
ByRecipient(address) => Ok(Self::ByRecipient(ic_ethereum_types::Address::from_str(
&address,
)?)),
BySenderAccount(account) => Ok(Self::BySenderAccount(account)),
}
}
}

#[derive(CandidType, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
pub struct WithdrawalDetail {
pub withdrawal_id: u64,
pub recipient_address: String,
pub from: Principal,
pub from_subaccount: Option<[u8; 32]>,
pub token_symbol: String,
pub withdrawal_amount: Nat,
pub max_transaction_fee: Option<Nat>,
pub status: WithdrawalStatus,
}

#[derive(CandidType, Deserialize, Debug, PartialEq, Eq, Hash, Clone)]
pub enum WithdrawalStatus {
Pending,
TxCreated,
TxSent(EthTransaction),
TxFinalized(TxFinalizedStatus),
}

#[derive(CandidType, Deserialize, Clone, Debug, PartialEq)]
pub struct AddCkErc20Token {
pub chain_id: Nat,
Expand Down
48 changes: 46 additions & 2 deletions rs/ethereum/cketh/minter/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use ic_cketh_minter::endpoints::events::{
};
use ic_cketh_minter::endpoints::{
AddCkErc20Token, Eip1559TransactionPrice, Erc20Balance, GasFeeEstimate, MinterInfo,
RetrieveEthRequest, RetrieveEthStatus, WithdrawalArg, WithdrawalError,
RetrieveEthRequest, RetrieveEthStatus, WithdrawalArg, WithdrawalDetail, WithdrawalError,
WithdrawalSearchParameter,
};
use ic_cketh_minter::eth_logs::{EventSource, ReceivedErc20Event, ReceivedEthEvent};
use ic_cketh_minter::guard::retrieve_withdraw_guard;
Expand All @@ -26,7 +27,9 @@ use ic_cketh_minter::state::transactions::{
Erc20WithdrawalRequest, EthWithdrawalRequest, Reimbursed, ReimbursementIndex,
ReimbursementRequest,
};
use ic_cketh_minter::state::{lazy_call_ecdsa_public_key, mutate_state, read_state, State, STATE};
use ic_cketh_minter::state::{
lazy_call_ecdsa_public_key, mutate_state, read_state, transactions, State, STATE,
};
use ic_cketh_minter::tx::lazy_refresh_gas_fee_estimate;
use ic_cketh_minter::withdraw::{
process_reimbursement, process_retrieve_eth_requests, CKERC20_WITHDRAWAL_TRANSACTION_GAS_LIMIT,
Expand All @@ -39,6 +42,7 @@ use ic_cketh_minter::{
};
use ic_ethereum_types::Address;
use std::collections::BTreeSet;
use std::convert::TryFrom;
use std::str::FromStr;
use std::time::Duration;

Expand Down Expand Up @@ -283,6 +287,46 @@ async fn retrieve_eth_status(block_index: u64) -> RetrieveEthStatus {
read_state(|s| s.eth_transactions.transaction_status(&ledger_burn_index))
}

#[query]
async fn withdrawal_status(parameter: WithdrawalSearchParameter) -> Vec<WithdrawalDetail> {
use transactions::WithdrawalRequest::*;
let parameter = transactions::WithdrawalSearchParameter::try_from(parameter).unwrap();
read_state(|s| {
s.eth_transactions
.withdrawal_status(&parameter)
.into_iter()
.map(|(request, status, tx)| WithdrawalDetail {
withdrawal_id: *request.cketh_ledger_burn_index().as_ref(),
recipient_address: request.payee().to_string(),
token_symbol: match request {
CkEth(_) => "ckETH".to_string(),
CkErc20(r) => s
.ckerc20_token_symbol(&r.erc20_contract_address)
.unwrap()
.to_string(),
},
withdrawal_amount: match request {
CkEth(r) => r.withdrawal_amount.into(),
CkErc20(r) => r.withdrawal_amount.into(),
},
max_transaction_fee: match (request, tx) {
(CkEth(_), None) => None,
(CkEth(r), Some(tx)) => {
r.withdrawal_amount.checked_sub(tx.amount).map(|x| x.into())
}
(CkErc20(r), _) => Some(r.max_transaction_fee.into()),
},
from: request.from(),
from_subaccount: request
.from_subaccount()
.clone()
.map(|subaccount| subaccount.0),
status,
})
.collect()
})
}

#[update]
async fn withdraw_erc20(
WithdrawErc20Arg {
Expand Down
3 changes: 1 addition & 2 deletions rs/ethereum/cketh/minter/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,7 @@ impl State {
let tx_fee = receipt.effective_transaction_fee();
let tx = self
.eth_transactions
.finalized_tx
.get_alt(withdrawal_id)
.get_finalized_transaction(withdrawal_id)
.expect("BUG: missing finalized transaction");
let charged_tx_fee = tx.transaction_price().max_transaction_fee();
let unspent_tx_fee = charged_tx_fee.checked_sub(tx_fee).expect(
Expand Down
34 changes: 18 additions & 16 deletions rs/ethereum/cketh/minter/src/state/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ fn state_equivalence() {
Eip1559Signature, Eip1559TransactionRequest, SignedTransactionRequest, TransactionRequest,
};
use ic_cdk::api::management_canister::ecdsa::EcdsaPublicKeyResponse;
use maplit::btreemap;
use maplit::{btreemap, btreeset};

fn source(txhash: &str, index: u64) -> EventSource {
EventSource {
Expand Down Expand Up @@ -847,12 +847,25 @@ fn state_equivalence() {
..withdrawal_request1.clone()
};
let eth_transactions = EthTransactions {
withdrawal_requests: vec![
pending_withdrawal_requests: vec![
withdrawal_request1.clone().into(),
withdrawal_request2.clone().into(),
]
.into_iter()
.collect(),
processed_withdrawal_requests: btreemap! {
LedgerBurnIndex::new(4) => EthWithdrawalRequest {
withdrawal_amount: Wei::new(1_000_000_000_000),
ledger_burn_index: LedgerBurnIndex::new(4),
destination: "0xA776Cc20DFdCCF0c3ba89cB9Fb0f10Aba5b98f52".parse().unwrap(),
from: "ezu3d-2mifu-k3bh4-oqhrj-mbrql-5p67r-pp6pr-dbfra-unkx5-sxdtv-rae"
.parse()
.unwrap(),
from_subaccount: None,
created_at: Some(1699527697000000000),
}.into(),
withdrawal_request1.ledger_burn_index => withdrawal_request1.clone().into(),
},
created_tx: singleton_map(
2,
4,
Expand Down Expand Up @@ -943,18 +956,7 @@ fn state_equivalence() {
.expect("valid receipt"),
),
next_nonce: TransactionNonce::new(3),
maybe_reimburse: btreemap! {
LedgerBurnIndex::new(4) => EthWithdrawalRequest {
withdrawal_amount: Wei::new(1_000_000_000_000),
ledger_burn_index: LedgerBurnIndex::new(4),
destination: "0xA776Cc20DFdCCF0c3ba89cB9Fb0f10Aba5b98f52".parse().unwrap(),
from: "ezu3d-2mifu-k3bh4-oqhrj-mbrql-5p67r-pp6pr-dbfra-unkx5-sxdtv-rae"
.parse()
.unwrap(),
from_subaccount: None,
created_at: Some(1699527697000000000),
}.into()
},
maybe_reimburse: btreeset! { LedgerBurnIndex::new(4) },
reimbursement_requests: btreemap! {
ReimbursementIndex::CkEth { ledger_burn_index: LedgerBurnIndex::new(3) } => ReimbursementRequest {
transaction_hash: Some("0x06afc3c693dc2ba2c19b5c287c4dddce040d766bea5fd13c8a7268b04aa94f2d"
Expand Down Expand Up @@ -1145,7 +1147,7 @@ fn state_equivalence() {
Ok(()),
state.is_equivalent_to(&State {
eth_transactions: EthTransactions {
withdrawal_requests: vec![
pending_withdrawal_requests: vec![
withdrawal_request2.clone().into(),
withdrawal_request1.clone().into()
]
Expand All @@ -1162,7 +1164,7 @@ fn state_equivalence() {
Ok(()),
state.is_equivalent_to(&State {
eth_transactions: EthTransactions {
withdrawal_requests: vec![withdrawal_request1.into()].into_iter().collect(),
pending_withdrawal_requests: vec![withdrawal_request1.into()].into_iter().collect(),
..eth_transactions.clone()
},
..state.clone()
Expand Down
Loading

0 comments on commit 73af37a

Please sign in to comment.