Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions minter/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ pub const MAX_CONCURRENT_RPC_CALLS: usize = 10;
/// https://docs.internetcomputer.org/references/ic-interface-spec#ic-http_request
pub const MAX_HTTP_OUTCALL_RESPONSE_BYTES: u64 = 2_000_000;

/// Cycles to attach for `getTransaction` RPC calls.
pub const GET_TRANSACTION_CYCLES: u128 = 50_000_000_000;

/// Cycles to attach for `getSignatureStatuses` RPC calls.
pub const GET_SIGNATURE_STATUSES_CYCLES: u128 = 1_000_000_000_000;

Expand Down
61 changes: 16 additions & 45 deletions minter/src/deposit/manual/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use crate::{
address::{account_address, lazy_get_schnorr_master_key},
constants::GET_TRANSACTION_CYCLES,
cycles::{charge_caller_cycles, check_caller_available_cycles},
deposit::get_deposit_amount_to_address,
deposit::fetch_and_validate_deposit,
guard::process_deposit_guard,
ledger::mint,
rpc::get_transaction,
runtime::CanisterRuntime,
state::{
Deposit,
Expand Down Expand Up @@ -35,7 +34,7 @@ pub async fn process_deposit<R: CanisterRuntime>(
deposit_amount,
amount_to_mint,
} = match read_state(|state| state.deposit_status(&deposit_id)) {
None => try_accept_deposit(&runtime, account, signature, deposit_id).await?,
None => try_accept_deposit(&runtime, account, signature).await?,
Some(DepositStatus::Processing {
deposit_amount,
amount_to_mint,
Expand Down Expand Up @@ -70,57 +69,29 @@ async fn try_accept_deposit<R: CanisterRuntime>(
runtime: &R,
account: Account,
signature: Signature,
deposit_id: DepositId,
) -> Result<Deposit, ProcessDepositError> {
let (cycles_to_attach, deposit_consolidation_fee) = read_state(|state| {
let (cycles_to_attach, deposit_consolidation_fee, fee) = read_state(|state| {
(
state.process_deposit_required_cycles(),
state.deposit_consolidation_fee(),
state.manual_deposit_fee(),
)
});
check_caller_available_cycles(runtime, cycles_to_attach)?;

// Reserve the consolidation fee and forward the rest to the HTTP outcall
let cycles_for_rpc = cycles_to_attach.saturating_sub(deposit_consolidation_fee);
let maybe_transaction = get_transaction(runtime, signature, cycles_for_rpc)
.await
.map_err(|e| {
log!(
Priority::Info,
"Error fetching transaction for deposit {deposit_id:?}: {e}"
);
ProcessDepositError::from(e)
})?;
let result = fetch_and_validate_deposit(runtime, account, signature, fee).await;

// Charge the actual RPC cost plus the consolidation fee
let rpc_cost = cycles_for_rpc.saturating_sub(runtime.msg_cycles_refunded());
charge_caller_cycles(runtime, rpc_cost + deposit_consolidation_fee);
// Always charge for the RPC call; additionally charge the consolidation fee if a deposit is found
let rpc_cost = GET_TRANSACTION_CYCLES.saturating_sub(runtime.msg_cycles_refunded());
let cycles_to_charge = rpc_cost
+ if result.is_ok() {
deposit_consolidation_fee
} else {
0
};
charge_caller_cycles(runtime, cycles_to_charge);

let transaction = match maybe_transaction {
Some(transaction) => Ok(transaction),
None => Err(ProcessDepositError::TransactionNotFound),
}?;

let master_key = lazy_get_schnorr_master_key(runtime).await;
let deposit_address = account_address(&master_key, &account);
let deposit_amount =
get_deposit_amount_to_address(transaction, deposit_address).map_err(|e| {
log!(
Priority::Info,
"Error parsing deposit transaction with signature {signature}: {e}"
);
ProcessDepositError::InvalidDepositTransaction(e.to_string())
})?;
let minimum_deposit_amount = read_state(|state| state.minimum_deposit_amount());
if deposit_amount < minimum_deposit_amount {
return Err(ProcessDepositError::ValueTooSmall {
minimum_deposit_amount,
deposit_amount,
});
}
let amount_to_mint = deposit_amount
.checked_sub(read_state(|state| state.manual_deposit_fee()))
.expect("BUG: deposit amount is less than manual deposit fee");
let (deposit_id, deposit_amount, amount_to_mint) = result?;

mutate_state(|state| {
process_event(
Expand Down
29 changes: 20 additions & 9 deletions minter/src/deposit/manual/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{
constants::GET_TRANSACTION_CYCLES,
deposit::manual::process_deposit,
state::event::{DepositId, EventType},
storage::reset_events,
Expand Down Expand Up @@ -64,7 +65,8 @@ async fn should_return_error_if_get_transaction_fails() {
init_state();
init_schnorr_master_key();

let runtime = runtime_with_time_and_cycles().add_stub_error(IcError::CallPerformFailed);
let runtime =
runtime_with_time_and_cycles_no_deposit().add_stub_error(IcError::CallPerformFailed);

let result = process_deposit(
runtime,
Expand All @@ -85,7 +87,7 @@ async fn should_return_error_if_transaction_not_found() {
init_state();
init_schnorr_master_key();

let runtime = runtime_with_time_and_cycles()
let runtime = runtime_with_time_and_cycles_no_deposit()
.add_stub_response(GetTransactionResult::Consistent(Ok(None)));

let result = process_deposit(
Expand All @@ -107,7 +109,8 @@ async fn should_return_error_if_transaction_not_valid_deposit() {
let get_transaction_response = GetTransactionResult::Consistent(Ok(Some(
deposit_transaction_to_wrong_address().try_into().unwrap(),
)));
let runtime = runtime_with_time_and_cycles().add_stub_response(get_transaction_response);
let runtime =
runtime_with_time_and_cycles_no_deposit().add_stub_response(get_transaction_response);

let result = process_deposit(
runtime,
Expand Down Expand Up @@ -135,7 +138,8 @@ async fn should_fail_if_deposit_amount_is_below_minimum() {
let get_transaction_response = GetTransactionResult::Consistent(Ok(Some(
legacy_deposit_transaction().try_into().unwrap(),
)));
let runtime = runtime_with_time_and_cycles().add_stub_response(get_transaction_response);
let runtime =
runtime_with_time_and_cycles_no_deposit().add_stub_response(get_transaction_response);

let result = process_deposit(
runtime,
Expand Down Expand Up @@ -443,14 +447,21 @@ async fn should_allow_deposits_to_multiple_accounts_with_single_transaction() {
}

fn runtime_with_time_and_cycles() -> TestCanisterRuntime {
// Cycles forwarded to the RPC call = total - consolidation fee
let cycles_for_rpc = PROCESS_DEPOSIT_REQUIRED_CYCLES - DEPOSIT_CONSOLIDATION_FEE;
// Simulate the RPC canister refunding most of the forwarded cycles
let refunded: u128 = cycles_for_rpc - 100_000_000_000;
let rpc_cost = cycles_for_rpc - refunded;
let rpc_cost = 25_000_000_000u128;
Comment thread
lpahlavi marked this conversation as resolved.
Outdated
let refunded = GET_TRANSACTION_CYCLES - rpc_cost;
TestCanisterRuntime::new()
.with_increasing_time()
.add_msg_cycles_available(PROCESS_DEPOSIT_REQUIRED_CYCLES)
.add_msg_cycles_refunded(refunded)
.add_msg_cycles_accept(rpc_cost + DEPOSIT_CONSOLIDATION_FEE)
}

fn runtime_with_time_and_cycles_no_deposit() -> TestCanisterRuntime {
Comment thread
lpahlavi marked this conversation as resolved.
Outdated
let rpc_cost = 25_000_000_000u128;
let refunded = GET_TRANSACTION_CYCLES - rpc_cost;
TestCanisterRuntime::new()
.with_increasing_time()
.add_msg_cycles_available(PROCESS_DEPOSIT_REQUIRED_CYCLES)
.add_msg_cycles_refunded(refunded)
.add_msg_cycles_accept(rpc_cost)
}
58 changes: 58 additions & 0 deletions minter/src/deposit/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
use crate::{
address::{account_address, lazy_get_schnorr_master_key},
rpc::get_transaction,
runtime::CanisterRuntime,
state::{event::DepositId, read_state},
};
use canlog::log;
use cksol_types::ProcessDepositError;
use cksol_types_internal::log::Priority;
use icrc_ledger_types::icrc1::account::Account;
use sol_rpc_types::Lamport;
use solana_address::Address;
use solana_signature::Signature;
use solana_transaction_status_client_types::{
EncodedConfirmedTransactionWithStatusMeta, UiTransactionError,
};
Expand All @@ -11,6 +22,53 @@ mod tests;
pub mod automatic;
pub mod manual;

pub async fn fetch_and_validate_deposit<R: CanisterRuntime>(
runtime: &R,
account: Account,
signature: Signature,
fee: Lamport,
) -> Result<(DepositId, Lamport, Lamport), ProcessDepositError> {
let deposit_id = DepositId { account, signature };
let master_key = lazy_get_schnorr_master_key(runtime).await;
let deposit_address = account_address(&master_key, &account);

let maybe_transaction = get_transaction(runtime, signature).await.map_err(|e| {
log!(
Priority::Info,
"Error fetching transaction for deposit {deposit_id:?}: {e}"
);
ProcessDepositError::from(e)
})?;

let transaction = match maybe_transaction {
Some(t) => t,
None => return Err(ProcessDepositError::TransactionNotFound),
};

let deposit_amount =
get_deposit_amount_to_address(transaction, deposit_address).map_err(|e| {
log!(
Priority::Info,
"Error parsing deposit transaction with signature {signature}: {e}"
);
ProcessDepositError::InvalidDepositTransaction(e.to_string())
})?;

let minimum_deposit_amount = read_state(|s| s.minimum_deposit_amount());
if deposit_amount < minimum_deposit_amount {
return Err(ProcessDepositError::ValueTooSmall {
minimum_deposit_amount,
deposit_amount,
});
}

let amount_to_mint = deposit_amount
.checked_sub(fee)
.expect("BUG: deposit amount is less than fee");

Ok((deposit_id, deposit_amount, amount_to_mint))
}

pub fn get_deposit_amount_to_address(
transaction: EncodedConfirmedTransactionWithStatusMeta,
deposit_address: Address,
Expand Down
7 changes: 4 additions & 3 deletions minter/src/rpc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::{
constants::{GET_SIGNATURE_STATUSES_CYCLES, MAX_HTTP_OUTCALL_RESPONSE_BYTES},
constants::{
GET_SIGNATURE_STATUSES_CYCLES, GET_TRANSACTION_CYCLES, MAX_HTTP_OUTCALL_RESPONSE_BYTES,
},
runtime::CanisterRuntime,
state::read_state,
};
Expand All @@ -22,15 +24,14 @@ mod tests;
pub async fn get_transaction<R: CanisterRuntime>(
runtime: &R,
signature: Signature,
cycles_to_attach: u128,
) -> Result<Option<EncodedConfirmedTransactionWithStatusMeta>, GetTransactionError> {
let result = read_state(|state| state.sol_rpc_client(runtime.inter_canister_call_runtime()))
.get_transaction(signature)
.with_encoding(GetTransactionEncoding::Base64)
.with_commitment(CommitmentLevel::Finalized)
.with_max_supported_transaction_version(0)
.with_response_size_estimate(MAX_HTTP_OUTCALL_RESPONSE_BYTES)
.with_cycles(cycles_to_attach)
.with_cycles(GET_TRANSACTION_CYCLES)
.try_send()
.await;
match result? {
Expand Down
60 changes: 14 additions & 46 deletions minter/src/rpc/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
get_transaction, submit_transaction,
},
test_fixtures::{
PROCESS_DEPOSIT_REQUIRED_CYCLES, confirmed_block,
confirmed_block,
deposit::{legacy_deposit_transaction, legacy_deposit_transaction_signature},
init_state,
runtime::TestCanisterRuntime,
Expand All @@ -28,16 +28,9 @@ mod get_transaction_tests {
async fn should_fail_if_get_transaction_fails() {
init_state();

let runtime = TestCanisterRuntime::new()
.add_msg_cycles_available(PROCESS_DEPOSIT_REQUIRED_CYCLES)
.add_stub_error(IcError::CallPerformFailed);
let runtime = TestCanisterRuntime::new().add_stub_error(IcError::CallPerformFailed);

let result = get_transaction(
&runtime,
legacy_deposit_transaction_signature(),
PROCESS_DEPOSIT_REQUIRED_CYCLES,
)
.await;
let result = get_transaction(&runtime, legacy_deposit_transaction_signature()).await;

assert_eq!(
result,
Expand All @@ -56,15 +49,9 @@ mod get_transaction_tests {
});

let runtime = TestCanisterRuntime::new()
.add_msg_cycles_available(PROCESS_DEPOSIT_REQUIRED_CYCLES)
.add_stub_response(MultiRpcResult::Consistent(Err(rpc_error.clone())));

let result = get_transaction(
&runtime,
legacy_deposit_transaction_signature(),
PROCESS_DEPOSIT_REQUIRED_CYCLES,
)
.await;
let result = get_transaction(&runtime, legacy_deposit_transaction_signature()).await;

assert_eq!(result, Err(GetTransactionError::RpcError(rpc_error)));
}
Expand All @@ -84,16 +71,10 @@ mod get_transaction_tests {
),
];

let runtime = TestCanisterRuntime::new()
.add_msg_cycles_available(PROCESS_DEPOSIT_REQUIRED_CYCLES)
.add_stub_response(MultiRpcResult::Inconsistent(results));
let runtime =
TestCanisterRuntime::new().add_stub_response(MultiRpcResult::Inconsistent(results));

let result = get_transaction(
&runtime,
legacy_deposit_transaction_signature(),
PROCESS_DEPOSIT_REQUIRED_CYCLES,
)
.await;
let result = get_transaction(&runtime, legacy_deposit_transaction_signature()).await;

assert_eq!(result, Err(GetTransactionError::InconsistentRpcResults));
}
Expand All @@ -102,16 +83,10 @@ mod get_transaction_tests {
async fn should_return_empty_if_transaction_not_found() {
init_state();

let runtime = TestCanisterRuntime::new()
.add_msg_cycles_available(PROCESS_DEPOSIT_REQUIRED_CYCLES)
.add_stub_response(MultiRpcResult::Consistent(Ok(None)));
let runtime =
TestCanisterRuntime::new().add_stub_response(MultiRpcResult::Consistent(Ok(None)));

let result = get_transaction(
&runtime,
legacy_deposit_transaction_signature(),
PROCESS_DEPOSIT_REQUIRED_CYCLES,
)
.await;
let result = get_transaction(&runtime, legacy_deposit_transaction_signature()).await;

assert_eq!(result, Ok(None))
}
Expand All @@ -120,18 +95,11 @@ mod get_transaction_tests {
async fn should_return_transaction() {
init_state();

let runtime = TestCanisterRuntime::new()
.add_msg_cycles_available(PROCESS_DEPOSIT_REQUIRED_CYCLES)
.add_stub_response(MultiRpcResult::Consistent(Ok(Some(
legacy_deposit_transaction().try_into().unwrap(),
))));
let runtime = TestCanisterRuntime::new().add_stub_response(MultiRpcResult::Consistent(Ok(
Some(legacy_deposit_transaction().try_into().unwrap()),
)));

let result = get_transaction(
&runtime,
legacy_deposit_transaction_signature(),
PROCESS_DEPOSIT_REQUIRED_CYCLES,
)
.await;
let result = get_transaction(&runtime, legacy_deposit_transaction_signature()).await;

assert_eq!(result, Ok(Some(legacy_deposit_transaction())))
}
Expand Down
Loading