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
19 changes: 18 additions & 1 deletion crates/e2e-move-tests/src/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ pub struct MoveHarness {
pub chain: MockChain,
pub vm: InitiaVM,
pub api: MockAPI,
/// Optional fee payer injected into every execution Env (not the initialize Env).
pub fee_payer: Option<AccountAddress>,
}

pub fn path_in_crate<S>(relative: S) -> PathBuf
Expand All @@ -66,7 +68,17 @@ impl MoveHarness {
let chain = MockChain::new();
let api = MockAPI::empty();

Self { chain, vm, api }
Self {
chain,
vm,
api,
fee_payer: None,
}
}

/// Sets the fee payer to be injected into the `Env` for all subsequent execution calls.
pub fn set_fee_payer(&mut self, fee_payer: Option<AccountAddress>) {
self.fee_payer = fee_payer;
}

pub fn initialize(&mut self) {
Expand All @@ -80,6 +92,7 @@ impl MoveHarness {
1,
Self::generate_random_hash().try_into().unwrap(),
Self::generate_random_hash().try_into().unwrap(),
None,
);

let output = self
Expand Down Expand Up @@ -182,6 +195,7 @@ impl MoveHarness {
1,
Self::generate_random_hash().try_into().unwrap(),
Self::generate_random_hash().try_into().unwrap(),
self.fee_payer,
);

self.vm.execute_view_function(
Expand Down Expand Up @@ -335,6 +349,7 @@ impl MoveHarness {
1,
Self::generate_random_hash().try_into().unwrap(),
Self::generate_random_hash().try_into().unwrap(),
self.fee_payer,
);

let state = self.chain.create_state();
Expand All @@ -361,6 +376,7 @@ impl MoveHarness {
1,
Self::generate_random_hash().try_into().unwrap(),
Self::generate_random_hash().try_into().unwrap(),
self.fee_payer,
);

let state = self.chain.create_state();
Expand Down Expand Up @@ -391,6 +407,7 @@ impl MoveHarness {
1,
Self::generate_random_hash().try_into().unwrap(),
Self::generate_random_hash().try_into().unwrap(),
self.fee_payer,
);

let mut table_resolver = MockTableState::new(state);
Expand Down
1 change: 1 addition & 0 deletions crates/e2e-move-tests/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod solana_derivable_account_abstraction;
mod staking;
mod std_coin;
mod table;
mod transaction_context;
mod view_output;

#[cfg(feature = "testing")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "TransactionContextTests"
version = "0.0.0"

[dependencies]
InitiaStdlib = { local = "../../../../../../precompile/modules/initia_stdlib" }

[addresses]
std = "0x1"
test = "0x2"
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module test::TxContextTests {
use std::option::Option;
use initia_std::transaction_context;

/// Stores the fee_payer observed during an entry function call.
struct FeePayerStore has key {
value: Option<address>,
}

/// Stores the senders observed during an entry function call.
struct SendersStore has key {
value: vector<address>,
}

/// Entry function: reads fee_payer() from the current transaction context and
/// stores it as a resource under the caller's account.
public entry fun store_fee_payer(sender: &signer) {
let fp = transaction_context::fee_payer();
move_to(sender, FeePayerStore { value: fp });
}

/// Entry function: reads senders() from the current transaction context and
/// stores them as a resource under the caller's account.
public entry fun store_senders(sender: &signer) {
let s = transaction_context::senders();
move_to(sender, SendersStore { value: s });
}

/// Two-signer variant of `store_senders` to exercise the multi-sender path
/// (Move entry functions require the signer arg count to match the senders vector length).
public entry fun store_senders_two(sender: &signer, _co_signer: &signer) {
let s = transaction_context::senders();
move_to(sender, SendersStore { value: s });
}
}
168 changes: 168 additions & 0 deletions crates/e2e-move-tests/src/tests/transaction_context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use crate::MoveHarness;
use initia_move_natives::code::UpgradePolicy;
use move_core_types::account_address::AccountAddress;
use move_core_types::identifier::Identifier;
use move_core_types::language_storage::StructTag;
use std::str::FromStr;

/// `FeePayerStore` mirrors the Move struct `test::TxContextTests::FeePayerStore`.
#[derive(serde::Deserialize, Debug, PartialEq)]
struct FeePayerStore {
// Option<address> in Move BCS = vector<address> of length 0 or 1.
value: Option<AccountAddress>,
}

/// `SendersStore` mirrors the Move struct `test::TxContextTests::SendersStore`.
#[derive(serde::Deserialize, Debug, PartialEq)]
struct SendersStore {
value: Vec<AccountAddress>,
}

/// Verifies that `fee_payer` set on the harness `Env` flows through into Move via
/// `transaction_context::fee_payer()`.
#[test]
fn test_fee_payer_flows_from_env_to_move() {
let deployer_addr =
AccountAddress::from_hex_literal("0x2").expect("0x2 account should be parseable");
let path = "src/tests/transaction_context.data/pack";
let mut h = MoveHarness::new();

h.initialize();

// Publish the test module.
let output = h
.publish_package(&deployer_addr, path, UpgradePolicy::Compatible)
.expect("publish should succeed");
h.commit(output, true);

let struct_tag = StructTag {
address: deployer_addr,
module: Identifier::from_str("TxContextTests").unwrap(),
name: Identifier::from_str("FeePayerStore").unwrap(),
type_args: vec![],
};

// Sender address used to call the entry function and store the resource.
let sender = AccountAddress::from_hex_literal("0x42").unwrap();

// --- Case 1: fee_payer = None ---
// Default harness has fee_payer = None; verify that None is stored.
assert!(
h.fee_payer.is_none(),
"harness should start with None fee_payer"
);

let output = h
.run_entry_function(
vec![sender],
str::parse("0x2::TxContextTests::store_fee_payer").unwrap(),
vec![],
vec![],
)
.expect("entry function should succeed");
h.commit(output, true);

let stored: FeePayerStore = h
.read_resource(&sender, struct_tag.clone())
.expect("FeePayerStore resource should exist after entry call");
assert_eq!(
stored.value, None,
"expected None fee_payer when Env has None"
);

// --- Case 2: fee_payer = Some(0xCAFE) ---
let expected_fee_payer =
AccountAddress::from_hex_literal("0xCAFE").expect("0xCAFE should be parseable");
h.set_fee_payer(Some(expected_fee_payer));

// Use a fresh sender so there is no existing FeePayerStore resource.
let sender2 = AccountAddress::from_hex_literal("0x43").unwrap();

let output = h
.run_entry_function(
vec![sender2],
str::parse("0x2::TxContextTests::store_fee_payer").unwrap(),
vec![],
vec![],
)
.expect("entry function should succeed with fee_payer set");
h.commit(output, true);

let stored2: FeePayerStore = h
.read_resource(&sender2, struct_tag)
.expect("FeePayerStore resource should exist after entry call");
assert_eq!(
stored2.value,
Some(expected_fee_payer),
"fee_payer should match what was set on Env"
);
}

/// Verifies that the `senders` vector passed into the VM flows through into Move
/// via `transaction_context::senders()`.
#[test]
fn test_senders_flow_from_env_to_move() {
let deployer_addr =
AccountAddress::from_hex_literal("0x2").expect("0x2 account should be parseable");
let path = "src/tests/transaction_context.data/pack";
let mut h = MoveHarness::new();

h.initialize();

let output = h
.publish_package(&deployer_addr, path, UpgradePolicy::Compatible)
.expect("publish should succeed");
h.commit(output, true);

let struct_tag = StructTag {
address: deployer_addr,
module: Identifier::from_str("TxContextTests").unwrap(),
name: Identifier::from_str("SendersStore").unwrap(),
type_args: vec![],
};

// --- Case 1: single sender ---
let sender_a = AccountAddress::from_hex_literal("0x42").unwrap();

let output = h
.run_entry_function(
vec![sender_a],
str::parse("0x2::TxContextTests::store_senders").unwrap(),
vec![],
vec![],
)
.expect("entry function should succeed");
h.commit(output, true);

let stored: SendersStore = h
.read_resource(&sender_a, struct_tag.clone())
.expect("SendersStore resource should exist after entry call");
assert_eq!(
stored.value,
vec![sender_a],
"senders() should equal the single sender passed to the VM"
);

// --- Case 2: multiple senders (multi-agent style) ---
let sender_b = AccountAddress::from_hex_literal("0x43").unwrap();
let sender_c = AccountAddress::from_hex_literal("0x44").unwrap();

let output = h
.run_entry_function(
vec![sender_b, sender_c],
str::parse("0x2::TxContextTests::store_senders_two").unwrap(),
vec![],
vec![],
)
.expect("entry function with multiple senders should succeed");
h.commit(output, true);

let stored: SendersStore = h
.read_resource(&sender_b, struct_tag)
.expect("SendersStore resource should exist for first sender");
assert_eq!(
stored.value,
vec![sender_b, sender_c],
"senders() should preserve the full senders vector"
);
}
3 changes: 3 additions & 0 deletions crates/gas/src/initia_stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ crate::macros::define_gas_parameters!(
[transaction_context_generate_unique_address_base: InternalGas, "transaction_context.generate_unique_address.base", 735],
[transaction_context_entry_function_payload_base: InternalGas, "transaction_context.entry_function_payload.base", 735],
[transaction_context_entry_function_payload_per_byte_in_str: InternalGasPerByte, "transaction_context.entry_function_payload.per_abstract_memory_unit", 18],
[transaction_context_senders_base: InternalGas, "transaction_context.senders.base", 735],
[transaction_context_senders_per_address: InternalGasPerArg, "transaction_context.senders.per_address", 18],
[transaction_context_fee_payer_base: InternalGas, "transaction_context.fee_payer.base", 735],

// Note(Gas): These are SDK gas cost, so use `SCALING` factor
[staking_delegate_base: InternalGas, "staking.delegate.base", 50_000 * SCALING],
Expand Down
Loading
Loading