Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
85 changes: 54 additions & 31 deletions packages/rs-platform-wallet/tests/e2e/cases/transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,16 @@ use crate::framework::prelude::*;
// the empirical chain-time ceiling sidesteps the bug until #3040
// lands at the dpp layer.

/// Gross credits the bank submits when funding `addr_1`. The bank
/// uses `[ReduceOutput(0)]`, so addr_1 actually receives
/// `FUNDING_CREDITS − bank_fee`. Sized well above the chain-time
/// fee (~15M empirically) so addr_1 retains enough headroom to
/// fund the test's own self-transfer (see #3040 comment above).
/// Credits the bank delivers to `addr_1`. The bank uses
/// `[DeductFromInput(0)]`, so addr_1 receives this exact amount;
/// the bank's fee is absorbed by the bank's own input. Sized well
/// above the chain-time fee (~15M empirically) so addr_1 has
/// enough headroom for the self-transfer (see #3040 comment above).
const FUNDING_CREDITS: u64 = 100_000_000;

/// Lower bound on what addr_1 must receive after the bank's fee
/// deduction before the test proceeds. Pinned well below the raw
/// gross so the wait isn't sensitive to fee fluctuations across
/// protocol versions.
/// Safety floor for the addr_1 wait. Under `[DeductFromInput(0)]`
/// addr_1 receives FUNDING_CREDITS exactly; the floor is kept as a
/// guard against an empty/stale observation slipping through.
const FUNDING_FLOOR: u64 = 70_000_000;

/// Gross credits the test wallet submits in its self-transfer to
Expand Down Expand Up @@ -82,14 +81,19 @@ async fn transfer_between_two_platform_addresses() {
.await
.expect("derive addr_1");

// Snapshot bank balance before funding so we can derive the fee
// the bank's input actually paid (invisible to the test wallet).
let bank_pre = s.ctx.bank().total_credits().await;

s.ctx
.bank()
.fund_address(&addr_1, FUNDING_CREDITS)
.await
.expect("bank.fund_address");

// Bank uses `[ReduceOutput(0)]`, so addr_1 receives
// `FUNDING_CREDITS − bank_fee`. Wait on the post-fee floor.
// Bank uses `[DeductFromInput(0)]`: addr_1 receives FUNDING_CREDITS
// exactly. Wait on the safety floor; the exact-amount assertion
// follows after the test wallet syncs.
wait_for_balance(&s.test_wallet, &addr_1, FUNDING_FLOOR, STEP_TIMEOUT)
.await
.expect("addr_1 funding never observed");
Expand All @@ -116,31 +120,38 @@ async fn transfer_between_two_platform_addresses() {
.await
.expect("addr_2 transfer never observed");

// Re-sync so the cached view reflects post-transfer state across
// BOTH addresses, then derive bank- and transfer-fee shares from
// observed balances.
// Re-sync test wallet so the cached view reflects post-transfer
// state across BOTH addresses.
s.test_wallet
.sync_balances()
.await
.expect("post-transfer sync");
let balances = s.test_wallet.balances().await;
let received = balances.get(&addr_2).copied().unwrap_or(0);
let remaining = balances.get(&addr_1).copied().unwrap_or(0);
let observed_total = received.saturating_add(remaining);
// Bank's `ReduceOutput(0)` charged its fee against addr_1's
// funding output: the wallet's total post-transfer is
// `FUNDING_CREDITS − bank_fee − transfer_fee`. Each fee is the
// amount each ReduceOutput step trimmed off its respective
// output; together they equal `FUNDING_CREDITS − observed_total`.
let total_fees = FUNDING_CREDITS.saturating_sub(observed_total);
// The transfer fee is the share TRANSFER_CREDITS lost while
// crossing addr_1 -> addr_2.
// crossing addr_1 -> addr_2 via `[ReduceOutput(0)]`.
let transfer_fee = TRANSFER_CREDITS.saturating_sub(received);
let bank_fee = total_fees.saturating_sub(transfer_fee);

// Resync the bank to get its post-funding balance, then derive
// the fee the bank's input absorbed under `[DeductFromInput(0)]`.
s.ctx
.bank()
.sync_balances()
.await
.expect("bank post-funding sync");
let bank_post = s.ctx.bank().total_credits().await;
// bank_pre - bank_post = FUNDING_CREDITS + bank_fee
let bank_fee = bank_pre
.saturating_sub(bank_post)
.saturating_sub(FUNDING_CREDITS);

tracing::info!(
target: "platform_wallet::e2e::cases::transfer",
?addr_1,
?addr_2,
bank_pre,
bank_post,
funded = FUNDING_CREDITS,
received,
remaining,
Expand All @@ -149,14 +160,25 @@ async fn transfer_between_two_platform_addresses() {
"post-transfer balance snapshot"
);

assert!(
received >= TRANSFER_FLOOR,
"addr_2 must hold at least TRANSFER_FLOOR ({TRANSFER_FLOOR}); observed {received}"
// Under [ReduceOutput(0)], the protocol deducts the transfer fee
// from output[0] — addr_2's received amount — not from addr_1's
// residual. So addr_1 retains FUNDING_CREDITS - TRANSFER_CREDITS
// and addr_2 receives TRANSFER_CREDITS - transfer_fee.
assert_eq!(
remaining,
FUNDING_CREDITS - TRANSFER_CREDITS,
"addr_1 must retain FUNDING_CREDITS - TRANSFER_CREDITS \
(transfer_fee is deducted from addr_2's amount, not addr_1's residual). \
observed remaining={remaining} expected={}",
FUNDING_CREDITS - TRANSFER_CREDITS,
);
assert!(
received < TRANSFER_CREDITS,
"addr_2 must hold less than TRANSFER_CREDITS ({TRANSFER_CREDITS}) \
after `ReduceOutput(0)` fee deduction; observed {received}"
assert_eq!(
received,
TRANSFER_CREDITS - transfer_fee,
"addr_2 must receive TRANSFER_CREDITS minus the transfer fee \
(ReduceOutput(0) deducts fee from the transferred amount). \
observed received={received} expected={}",
TRANSFER_CREDITS - transfer_fee,
);
assert!(
transfer_fee > 0,
Expand All @@ -168,7 +190,8 @@ async fn transfer_between_two_platform_addresses() {
);
assert!(
bank_fee > 0,
"bank funding must charge a non-zero fee (observed_total={observed_total})"
"bank funding must charge a non-zero fee to its own input \
(bank_pre={bank_pre} bank_post={bank_post} funded={FUNDING_CREDITS})"
);

s.teardown().await.expect("teardown");
Expand Down
13 changes: 9 additions & 4 deletions packages/rs-platform-wallet/tests/e2e/framework/bank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ use tokio::sync::Mutex as AsyncMutex;
use simple_signer::signer::SimpleSigner;

use super::config::Config;
use super::wallet_factory::{
default_fee_strategy, DEFAULT_ACCOUNT_INDEX_PUB, DEFAULT_KEY_CLASS_PUB,
};
use super::wallet_factory::{bank_fee_strategy, DEFAULT_ACCOUNT_INDEX_PUB, DEFAULT_KEY_CLASS_PUB};
use super::{make_platform_signer, FrameworkError, FrameworkResult};

/// In-process funding mutex — serialises concurrent
Expand Down Expand Up @@ -153,6 +151,13 @@ impl BankWallet {
/// Fund `target` with `credits` from the bank's primary
/// account.
///
/// Recipients receive the **exact** `credits` amount; the fee
/// is deducted from the bank's input via
/// [`bank_fee_strategy`]. The bank therefore consumes
/// `credits + fee` from its own platform-addresses pool —
/// verify the bank balance is sufficiently above
/// `min_bank_credits` before calling.
///
/// Submits the transfer immediately and returns the resulting
/// [`PlatformAddressChangeSet`]. Does NOT wait for the chain to
/// observe the credit — callers follow up with
Expand All @@ -173,7 +178,7 @@ impl BankWallet {
DEFAULT_ACCOUNT_INDEX_PUB,
InputSelection::Auto,
outputs,
default_fee_strategy(),
bank_fee_strategy(),
Some(PlatformVersion::latest()),
&self.signer,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,21 @@ pub(crate) fn default_fee_strategy() -> AddressFundsFeeStrategy {
vec![AddressFundsFeeStrategyStep::ReduceOutput(0)]
}

/// Bank-funding fee strategy: deduct fee from input #0 so the
/// recipient receives the **exact** requested amount.
///
/// Used by [`super::bank::BankWallet::fund_address`] so
/// downstream calls — e.g. `register_identity_from_addresses(
/// {addr: N}, ...)` — don't have to compensate for fee
/// deduction at the recipient.
///
/// Tests that need the alternative `ReduceOutput(0)` semantics
/// (e.g. PA-002b verifying `Σ outputs + fee == input balance`)
/// should call [`default_fee_strategy`] explicitly.
pub(crate) fn bank_fee_strategy() -> AddressFundsFeeStrategy {
vec![AddressFundsFeeStrategyStep::DeductFromInput(0)]
}

/// Rebalance an explicit-input map so its sum equals `Σ outputs`.
///
/// `AddressFundsTransferTransition` validation rejects with
Expand Down
Loading