Skip to content
Closed
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
455 changes: 392 additions & 63 deletions packages/rs-platform-wallet/tests/e2e/TEST_SPEC.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions packages/rs-platform-wallet/tests/e2e/cases/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,22 @@
//! `#[tokio_shared_rt::test(shared)]` entries that share the
//! process-wide [`super::framework::E2eContext`].

// Token tests (Wave 2 — per TEST_SPEC.md ### Tokens (TK))
pub mod tk_001_token_transfer;
pub mod tk_001b_token_transfer_zero;
pub mod tk_001c_token_transfer_after_reissue;
pub mod tk_002_token_claim_perpetual;
pub mod tk_003_register_token_contract;
pub mod tk_004_token_transfer_round_trip;
pub mod tk_005_token_mint;
pub mod tk_005b_token_mint_to_other;
pub mod tk_006_token_burn;
pub mod tk_007_token_freeze;
pub mod tk_008_token_unfreeze;
pub mod tk_009_token_destroy_frozen;
pub mod tk_010_token_pause_resume;
pub mod tk_011_token_price_purchase;
pub mod tk_012_token_update_config;
pub mod tk_013_token_claim_pre_programmed;
pub mod tk_014_token_group_action;
pub mod transfer;
188 changes: 188 additions & 0 deletions packages/rs-platform-wallet/tests/e2e/cases/tk_001_token_transfer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
//! TK-001 — Token transfer between two identities (happy path).
//!
//! Spec: `tests/e2e/TEST_SPEC.md` § TK-001. Owner mints 100 tokens to
//! itself, then transfers 50 to a peer. Pins:
//! - sender token balance drops by exactly the transferred amount,
//! - peer token balance grows by exactly the transferred amount,
//! - sender's identity credit balance drops by `> 0` (token transfer
//! pays its fee in credits, not tokens).
//!
//! Gated behind `#[ignore]` so a stock workspace `cargo test` stays
//! green for contributors that lack the bank mnemonic and live testnet
//! access. Operator setup mirrors `cases/transfer.rs`.

use std::sync::Arc;
use std::time::Duration;

use dash_sdk::platform::Fetch;
use dpp::data_contract::DataContract;
use dpp::identity::accessors::IdentityGettersV0;
use dpp::identity::Identity;

use crate::framework::harness::E2eContext;
use crate::framework::tokens::{
mint_to, setup_with_token_and_two_identities, token_balance_of, wait_for_token_balance,
DEFAULT_TK_FUNDING,
};

/// Tokens minted to the sender before the transfer. Sized comfortably
/// above `TRANSFER_AMOUNT` so the post-transfer assertion can pin the
/// residual without being sensitive to mint-side rounding.
const MINT_AMOUNT: u64 = 100;

/// Tokens moved from owner → peer. Picked to leave a non-zero residual
/// on the sender so we can pin "balance decreased by exactly N" rather
/// than "balance is now zero".
const TRANSFER_AMOUNT: u64 = 50;

/// Per-step deadline for token-balance observations. Longer than the
/// PA-side `wait_for_balance` budget because token reads round-trip
/// the SDK + proof verifier rather than a wallet-cached map.
const STEP_TIMEOUT: Duration = Duration::from_secs(60);

#[tokio_shared_rt::test(shared, flavor = "multi_thread", worker_threads = 12)]
#[ignore = "requires PLATFORM_WALLET_E2E_BANK_MNEMONIC and live testnet access; run with cargo test -- --ignored"]
async fn tk_001_token_transfer_between_identities() {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info,platform_wallet=debug".into()),
)
.with_test_writer()
.try_init();

let ctx = E2eContext::init().await.expect("init e2e context");

let two = setup_with_token_and_two_identities(ctx, DEFAULT_TK_FUNDING)
.await
.expect("setup token + 2 identities");
let contract_id = two.setup.contract_id;
let position = two.setup.token_position;
let owner = &two.setup.owner;
let peer = &two.peer;

// --- mint to owner so it has stock to transfer -------------------
mint_to(ctx, contract_id, position, MINT_AMOUNT, owner, owner)
.await
.expect("mint to owner");
wait_for_token_balance(
ctx,
owner.id,
contract_id,
position,
MINT_AMOUNT,
STEP_TIMEOUT,
)
.await
.expect("mint never observed on owner");

let owner_tok_pre = token_balance_of(ctx, contract_id, position, owner.id)
.await
.expect("owner token balance pre");
let peer_tok_pre = token_balance_of(ctx, contract_id, position, peer.id)
.await
.expect("peer token balance pre");
let owner_credits_pre = Identity::fetch(ctx.sdk(), owner.id)
.await
.expect("fetch owner identity pre")
.expect("owner identity must exist after registration")
.balance();

assert_eq!(
owner_tok_pre, MINT_AMOUNT,
"owner must hold the just-minted balance (observed={owner_tok_pre} expected={MINT_AMOUNT})"
);
assert_eq!(
peer_tok_pre, 0,
"peer must start with zero token balance (observed={peer_tok_pre})"
);

// --- transfer ----------------------------------------------------
let data_contract = DataContract::fetch(ctx.sdk(), contract_id)
.await
.expect("fetch data contract")
.expect("contract not found on chain");

two.setup
.setup_guard
.base
.test_wallet
.platform_wallet()
.identity()
.token_transfer_with_signer(
Arc::new(data_contract),
position,
owner.id,
peer.id,
TRANSFER_AMOUNT,
&owner.high_key,
owner.signer.as_ref(),
None,
None,
)
.await
.expect("token_transfer_with_signer");

// Wait for the proof-verified peer balance to hit the target.
wait_for_token_balance(
ctx,
peer.id,
contract_id,
position,
TRANSFER_AMOUNT,
STEP_TIMEOUT,
)
.await
.expect("peer balance never observed");

// --- post-transfer reads ----------------------------------------
let owner_tok_post = token_balance_of(ctx, contract_id, position, owner.id)
.await
.expect("owner token balance post");
let peer_tok_post = token_balance_of(ctx, contract_id, position, peer.id)
.await
.expect("peer token balance post");
let owner_credits_post = Identity::fetch(ctx.sdk(), owner.id)
.await
.expect("fetch owner identity post")
.expect("owner identity must still exist post-transfer")
.balance();

let credit_fee = owner_credits_pre.saturating_sub(owner_credits_post);

tracing::info!(
target: "platform_wallet::e2e::cases::tk_001",
owner = ?owner.id,
peer = ?peer.id,
owner_tok_pre,
owner_tok_post,
peer_tok_pre,
peer_tok_post,
owner_credits_pre,
owner_credits_post,
credit_fee,
"post-transfer snapshot"
);

assert_eq!(
owner_tok_post,
owner_tok_pre - TRANSFER_AMOUNT,
"owner token balance must drop by exactly TRANSFER_AMOUNT (observed={owner_tok_post})",
);
assert_eq!(
peer_tok_post,
peer_tok_pre + TRANSFER_AMOUNT,
"peer token balance must rise by exactly TRANSFER_AMOUNT (observed={peer_tok_post})",
);
assert!(
credit_fee > 0,
"token transfer must charge a non-zero credit fee \
(pre={owner_credits_pre} post={owner_credits_post})"
);
assert!(
credit_fee < owner_credits_pre,
"credit fee implausibly large: {credit_fee} >= owner_credits_pre {owner_credits_pre}"
);

two.setup.setup_guard.teardown().await.expect("teardown");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! TK-001b — Token transfer with `amount = 0`.
//!
//! Spec: `tests/e2e/TEST_SPEC.md` § TK-001b. Pins the **(a) Reject**
//! contract: validation surfaces an `InvalidTokenAmountError` (token
//! amount must be `> 0` and `≤ MAX_DISTRIBUTION_PARAM`, see
//! `rs-dpp/.../token_transfer_transition/validate_structure/v0/mod.rs`),
//! no broadcast lands, and both balances stay unchanged.
//!
//! The chain rejects zero-amount before broadcast / proof, so we
//! simply assert the API call returns an error and that the post-call
//! balances match the pre-call ones.

use std::sync::Arc;
use std::time::Duration;

use dash_sdk::platform::Fetch;
use dpp::data_contract::DataContract;
use dpp::identity::accessors::IdentityGettersV0;
use dpp::identity::Identity;

use crate::framework::harness::E2eContext;
use crate::framework::tokens::{
mint_to, setup_with_token_and_two_identities, token_balance_of, wait_for_token_balance,
DEFAULT_TK_FUNDING,
};

/// Tokens minted to the sender so the pre-condition (sender holds a
/// non-zero balance) holds. Mirrors TK-001's mint amount.
const MINT_AMOUNT: u64 = 100;

/// Per-step deadline for token-balance observations.
const STEP_TIMEOUT: Duration = Duration::from_secs(60);

#[tokio_shared_rt::test(shared, flavor = "multi_thread", worker_threads = 12)]
#[ignore = "requires PLATFORM_WALLET_E2E_BANK_MNEMONIC and live testnet access; run with cargo test -- --ignored"]
async fn tk_001b_token_transfer_zero_rejected() {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info,platform_wallet=debug".into()),
)
.with_test_writer()
.try_init();

let ctx = E2eContext::init().await.expect("init e2e context");

let two = setup_with_token_and_two_identities(ctx, DEFAULT_TK_FUNDING)
.await
.expect("setup token + 2 identities");
let contract_id = two.setup.contract_id;
let position = two.setup.token_position;
let owner = &two.setup.owner;
let peer = &two.peer;

// Mint to owner so it has stock — without this the transfer might
// fail on insufficient balance instead of the zero-amount guard,
// which would muddy the assertion.
mint_to(ctx, contract_id, position, MINT_AMOUNT, owner, owner)
.await
.expect("mint to owner");
wait_for_token_balance(
ctx,
owner.id,
contract_id,
position,
MINT_AMOUNT,
STEP_TIMEOUT,
)
.await
.expect("mint never observed on owner");

let owner_tok_pre = token_balance_of(ctx, contract_id, position, owner.id)
.await
.expect("owner token balance pre");
let peer_tok_pre = token_balance_of(ctx, contract_id, position, peer.id)
.await
.expect("peer token balance pre");

let data_contract = DataContract::fetch(ctx.sdk(), contract_id)
.await
.expect("fetch data contract")
.expect("contract not found on chain");

let result = two
.setup
.setup_guard
.base
.test_wallet
.platform_wallet()
.identity()
.token_transfer_with_signer(
Arc::new(data_contract),
position,
owner.id,
peer.id,
0,
&owner.high_key,
owner.signer.as_ref(),
None,
None,
)
.await;

// `TransferResult` doesn't implement `Debug`, so use a manual
// match instead of `expect_err`.
let err = match result {
Ok(_) => panic!("zero-amount transfer must be rejected, but the call returned Ok"),
Err(e) => e,
};
let err_msg = format!("{err}");
tracing::info!(
target: "platform_wallet::e2e::cases::tk_001b",
error = %err_msg,
"zero-amount transfer rejected (as expected)"
);

// Pin the typed error shape: rs-dpp surfaces zero amounts as
// InvalidTokenAmount; the SDK preserves the variant in its
// stringified error so a substring match is the cheapest stable
// contract while we wait for a typed-error accessor in dash-sdk.
assert!(
err_msg.contains("InvalidTokenAmount") || err_msg.to_lowercase().contains("amount"),
"rejection must reference the invalid-amount validator \
(observed: {err_msg})"
);

// Re-read balances; both must be unchanged (no broadcast, no fee).
let owner_tok_post = token_balance_of(ctx, contract_id, position, owner.id)
.await
.expect("owner token balance post");
let peer_tok_post = token_balance_of(ctx, contract_id, position, peer.id)
.await
.expect("peer token balance post");
let owner_credits_post = Identity::fetch(ctx.sdk(), owner.id)
.await
.expect("fetch owner identity post")
.expect("owner identity must still exist post-rejection")
.balance();

tracing::info!(
target: "platform_wallet::e2e::cases::tk_001b",
owner = ?owner.id,
peer = ?peer.id,
owner_tok_pre,
owner_tok_post,
peer_tok_pre,
peer_tok_post,
owner_credits_post,
"post-rejection snapshot"
);

assert_eq!(
owner_tok_post, owner_tok_pre,
"rejected transfer must not alter sender token balance"
);
assert_eq!(
peer_tok_post, peer_tok_pre,
"rejected transfer must not alter recipient token balance"
);
assert!(
owner_credits_post > 0,
"owner identity must still hold credits after a rejected transfer \
(observed={owner_credits_post})"
);

two.setup.setup_guard.teardown().await.expect("teardown");
}
Loading
Loading