diff --git a/crates/e2e-move-tests/src/harness.rs b/crates/e2e-move-tests/src/harness.rs index b9f64c36..eab6bf22 100644 --- a/crates/e2e-move-tests/src/harness.rs +++ b/crates/e2e-move-tests/src/harness.rs @@ -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, } pub fn path_in_crate(relative: S) -> PathBuf @@ -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) { + self.fee_payer = fee_payer; } pub fn initialize(&mut self) { @@ -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 @@ -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( @@ -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(); @@ -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(); @@ -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); diff --git a/crates/e2e-move-tests/src/tests/mod.rs b/crates/e2e-move-tests/src/tests/mod.rs index 20376e4a..a9e75858 100644 --- a/crates/e2e-move-tests/src/tests/mod.rs +++ b/crates/e2e-move-tests/src/tests/mod.rs @@ -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")] diff --git a/crates/e2e-move-tests/src/tests/transaction_context.data/pack/Move.toml b/crates/e2e-move-tests/src/tests/transaction_context.data/pack/Move.toml new file mode 100644 index 00000000..acb6dba5 --- /dev/null +++ b/crates/e2e-move-tests/src/tests/transaction_context.data/pack/Move.toml @@ -0,0 +1,10 @@ +[package] +name = "TransactionContextTests" +version = "0.0.0" + +[dependencies] +InitiaStdlib = { local = "../../../../../../precompile/modules/initia_stdlib" } + +[addresses] +std = "0x1" +test = "0x2" diff --git a/crates/e2e-move-tests/src/tests/transaction_context.data/pack/sources/TxContextTests.move b/crates/e2e-move-tests/src/tests/transaction_context.data/pack/sources/TxContextTests.move new file mode 100644 index 00000000..f7d13f4c --- /dev/null +++ b/crates/e2e-move-tests/src/tests/transaction_context.data/pack/sources/TxContextTests.move @@ -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
, + } + + /// Stores the senders observed during an entry function call. + struct SendersStore has key { + value: vector
, + } + + /// 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 }); + } +} diff --git a/crates/e2e-move-tests/src/tests/transaction_context.rs b/crates/e2e-move-tests/src/tests/transaction_context.rs new file mode 100644 index 00000000..df1ab79d --- /dev/null +++ b/crates/e2e-move-tests/src/tests/transaction_context.rs @@ -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
in Move BCS = vector
of length 0 or 1. + value: Option, +} + +/// `SendersStore` mirrors the Move struct `test::TxContextTests::SendersStore`. +#[derive(serde::Deserialize, Debug, PartialEq)] +struct SendersStore { + value: Vec, +} + +/// 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" + ); +} diff --git a/crates/gas/src/initia_stdlib.rs b/crates/gas/src/initia_stdlib.rs index 048bdd1d..7b1dba54 100644 --- a/crates/gas/src/initia_stdlib.rs +++ b/crates/gas/src/initia_stdlib.rs @@ -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], diff --git a/crates/natives/src/transaction_context.rs b/crates/natives/src/transaction_context.rs index 7840545b..1e400f7c 100644 --- a/crates/natives/src/transaction_context.rs +++ b/crates/natives/src/transaction_context.rs @@ -1,5 +1,5 @@ use better_any::{Tid, TidAble}; -use initia_move_gas::NumBytes; +use initia_move_gas::{NumArgs, NumBytes}; use initia_move_types::user_transaction_context::{EntryFunctionPayload, UserTransactionContext}; use move_binary_format::errors::PartialVMError; use move_core_types::{account_address::AccountAddress, vm_status::StatusCode}; @@ -139,6 +139,54 @@ fn native_entry_function_payload_internal( } } +#[allow(clippy::result_large_err)] +fn native_senders( + context: &mut SafeNativeContext, + _ty_args: Vec, + _args: VecDeque, +) -> SafeNativeResult> { + let gas_params = &context.native_gas_params.initia_stdlib; + context.charge(gas_params.transaction_context_senders_base)?; + + let txn_ctx_opt = get_user_transaction_context_opt_from_context(context); + let senders: Vec = match txn_ctx_opt { + Some(ctx) => ctx.senders().to_vec(), + None => { + return Err(SafeNativeError::Abort { + abort_code: ETRANSACTION_CONTEXT_NOT_AVAILABLE, + }); + } + }; + context.charge( + gas_params.transaction_context_senders_per_address * NumArgs::new(senders.len() as u64), + )?; + Ok(smallvec![Value::vector_address(senders)]) +} + +#[allow(clippy::result_large_err)] +fn native_fee_payer( + context: &mut SafeNativeContext, + _ty_args: Vec, + _args: VecDeque, +) -> SafeNativeResult> { + let gas_params = &context.native_gas_params.initia_stdlib; + context.charge(gas_params.transaction_context_fee_payer_base)?; + + let txn_ctx_opt = get_user_transaction_context_opt_from_context(context); + let value = match txn_ctx_opt { + Some(ctx) => match ctx.fee_payer() { + Some(addr) => Value::struct_(Struct::pack(vec![Value::vector_address(vec![addr])])), + None => Value::struct_(Struct::pack(vec![Value::vector_address(vec![])])), + }, + None => { + return Err(SafeNativeError::Abort { + abort_code: ETRANSACTION_CONTEXT_NOT_AVAILABLE, + }); + } + }; + Ok(smallvec![value]) +} + fn create_option_some_value(value: Value) -> Value { Value::struct_(Struct::pack(vec![create_singleton_vector(value)])) } @@ -246,6 +294,90 @@ fn native_test_only_set_transaction_hash( Ok(smallvec![]) } +#[cfg(feature = "testing")] +fn ensure_user_transaction_context(ctx: &mut NativeTransactionContext) { + if ctx.user_transaction_context_opt.is_none() { + ctx.user_transaction_context_opt = Some(UserTransactionContext::new(vec![], None, None)); + } +} + +#[cfg(feature = "testing")] +#[allow(clippy::result_large_err)] +fn native_test_only_set_senders( + context: &mut SafeNativeContext, + _ty_args: Vec, + mut arguments: VecDeque, +) -> SafeNativeResult> { + use crate::safely_pop_arg; + use move_vm_types::values::Vector; + + debug_assert_eq!(arguments.len(), 1); + + let raw_vec = safely_pop_arg!(arguments, Vector) + .unpack_unchecked() + .map_err(SafeNativeError::InvariantViolation)?; + let senders_vec: Vec = raw_vec + .into_iter() + .map(|v| v.value_as::()) + .collect::>() + .map_err(SafeNativeError::InvariantViolation)?; + + let txn_ctx = context + .extensions_mut() + .get_mut::(); + ensure_user_transaction_context(txn_ctx); + let prev = txn_ctx.user_transaction_context_opt.take().unwrap(); + txn_ctx.user_transaction_context_opt = Some(UserTransactionContext::new( + senders_vec, + prev.fee_payer(), + prev.entry_function_payload(), + )); + + Ok(smallvec![]) +} + +#[cfg(feature = "testing")] +#[allow(clippy::result_large_err)] +fn native_test_only_set_fee_payer( + context: &mut SafeNativeContext, + _ty_args: Vec, + mut arguments: VecDeque, +) -> SafeNativeResult> { + use crate::safely_pop_arg; + use move_vm_types::values::Vector; + + debug_assert_eq!(arguments.len(), 1); + + // Move signature: set_fee_payer_internal(fee_payer: vector
) + // length 0 or 1. We pop it as Vector then unpack to get individual addresses. + let raw_vec = safely_pop_arg!(arguments, Vector) + .unpack_unchecked() + .map_err(SafeNativeError::InvariantViolation)?; + let fee_payer_vec: Vec = raw_vec + .into_iter() + .map(|v| v.value_as::()) + .collect::>() + .map_err(SafeNativeError::InvariantViolation)?; + debug_assert!( + fee_payer_vec.len() <= 1, + "set_fee_payer_internal expects a vector of length 0 or 1" + ); + let fee_payer = fee_payer_vec.into_iter().next(); + + let txn_ctx = context + .extensions_mut() + .get_mut::(); + ensure_user_transaction_context(txn_ctx); + let prev = txn_ctx.user_transaction_context_opt.take().unwrap(); + txn_ctx.user_transaction_context_opt = Some(UserTransactionContext::new( + prev.senders().to_vec(), + fee_payer, + prev.entry_function_payload(), + )); + + Ok(smallvec![]) +} + /*************************************************************************************************** * module * @@ -264,6 +396,8 @@ pub fn make_all( "entry_function_payload_internal", native_entry_function_payload_internal, ), + ("senders", native_senders), + ("fee_payer_internal", native_fee_payer), ]); #[cfg(feature = "testing")] @@ -276,6 +410,8 @@ pub fn make_all( "set_transaction_hash_internal", native_test_only_set_transaction_hash, ), + ("set_senders_internal", native_test_only_set_senders), + ("set_fee_payer_internal", native_test_only_set_fee_payer), ]); builder.make_named_natives(natives) diff --git a/crates/types/src/env.rs b/crates/types/src/env.rs index f3d8114e..a04321a5 100644 --- a/crates/types/src/env.rs +++ b/crates/types/src/env.rs @@ -1,3 +1,4 @@ +use move_core_types::account_address::AccountAddress; use serde::{Deserialize, Serialize}; #[derive(Default, Debug, Clone, Serialize, Deserialize)] @@ -15,6 +16,9 @@ pub struct Env { /// SessionID is a seed for global unique ID of Table extension. /// Ex) transaction hash session_id: [u8; 32], + /// Optional fee payer for the current transaction. `None` means the + /// sender pays gas (or no fee payer concept applies). + fee_payer: Option, } impl Env { @@ -25,6 +29,7 @@ impl Env { next_account_number: u64, tx_hash: [u8; 32], session_id: [u8; 32], + fee_payer: Option, ) -> Self { Self { chain_id, @@ -33,6 +38,7 @@ impl Env { next_account_number, tx_hash, session_id, + fee_payer, } } @@ -61,4 +67,9 @@ impl Env { pub fn session_id(&self) -> &[u8] { &self.session_id } + + /// Return optional fee payer for the current transaction. + pub fn fee_payer(&self) -> Option { + self.fee_payer + } } diff --git a/crates/types/src/user_transaction_context.rs b/crates/types/src/user_transaction_context.rs index 6595c909..a9ac0996 100644 --- a/crates/types/src/user_transaction_context.rs +++ b/crates/types/src/user_transaction_context.rs @@ -6,23 +6,30 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserTransactionContext { - sender: AccountAddress, + senders: Vec, + fee_payer: Option, entry_function_payload: Option, } impl UserTransactionContext { pub fn new( - sender: AccountAddress, + senders: Vec, + fee_payer: Option, entry_function_payload: Option, ) -> Self { Self { - sender, + senders, + fee_payer, entry_function_payload, } } - pub fn sender(&self) -> AccountAddress { - self.sender + pub fn senders(&self) -> &[AccountAddress] { + &self.senders + } + + pub fn fee_payer(&self) -> Option { + self.fee_payer } pub fn entry_function_payload(&self) -> Option { diff --git a/crates/vm/src/initia_vm.rs b/crates/vm/src/initia_vm.rs index 0f9525cb..60c3070c 100644 --- a/crates/vm/src/initia_vm.rs +++ b/crates/vm/src/initia_vm.rs @@ -472,27 +472,29 @@ impl InitiaVM { traversal_context: &mut TraversalContext, ) -> Result { let move_resolver = code_storage.state_view_impl(); - let user_transaction_context_opt = match payload { - MessagePayload::Execute(entry_function) => Some(UserTransactionContext::new( - senders[0], - Some(EntryFunctionPayload::new( - entry_function.module().address, - entry_function.module().name.to_string(), - entry_function.function().to_string(), - entry_function - .ty_args() - .iter() - .map(|ty| ty.to_string()) - .collect(), - entry_function - .args() - .iter() - .map(|arg| arg.to_vec()) - .collect(), - )), + let entry_function_payload_opt = match payload { + MessagePayload::Execute(entry_function) => Some(EntryFunctionPayload::new( + entry_function.module().address, + entry_function.module().name.to_string(), + entry_function.function().to_string(), + entry_function + .ty_args() + .iter() + .map(|ty| ty.to_string()) + .collect(), + entry_function + .args() + .iter() + .map(|arg| arg.to_vec()) + .collect(), )), MessagePayload::Script(..) => None, }; + let user_transaction_context_opt = Some(UserTransactionContext::new( + senders.clone(), + env.fee_payer(), + entry_function_payload_opt, + )); let mut session = self.create_session( api, env, diff --git a/lib_test.go b/lib_test.go index 064c91a4..e27d1c1b 100644 --- a/lib_test.go +++ b/lib_test.go @@ -108,12 +108,14 @@ func publishModuleBundle( require.NoError(t, err) f4, err := os.ReadFile("./precompile/binaries/tests/TableTestData.mv") require.NoError(t, err) + f5, err := os.ReadFile("./precompile/binaries/tests/TxContextTests.mv") + require.NoError(t, err) - moduleIds, err := json.Marshal([]string{"0x2::TestCoin", "0x2::Bundle1", "0x2::Bundle2", "0x2::Bundle3", "0x2::TableTestData"}) + moduleIds, err := json.Marshal([]string{"0x2::TestCoin", "0x2::Bundle1", "0x2::Bundle2", "0x2::Bundle3", "0x2::TableTestData", "0x2::TxContextTests"}) require.NoError(t, err) modules, err := json.Marshal([]string{ - hex.EncodeToString(f0), hex.EncodeToString(f1), hex.EncodeToString(f2), hex.EncodeToString(f3), hex.EncodeToString(f4), + hex.EncodeToString(f0), hex.EncodeToString(f1), hex.EncodeToString(f2), hex.EncodeToString(f3), hex.EncodeToString(f4), hex.EncodeToString(f5), }) require.NoError(t, err) @@ -618,3 +620,135 @@ func Test_OracleAPI(t *testing.T) { require.NoError(t, err) require.Equal(t, fmt.Sprintf("[\"%d\",\"%d\",\"%d\"]", price, updatedAt, decimals), res.Ret) } + +// TestExecuteEntryFunctionWithFeePayer runs the TxContextTests::store_fee_payer +// entry function with Env.FeePayer set, then reads the stored value back via the +// view function read_stored_fee_payer to verify the round-trip from Env into Move. +func TestExecuteEntryFunctionWithFeePayer(t *testing.T) { + vm, kvStore := initializeVM(t, true) + defer vm.Destroy() + + publishModuleBundle(t, vm, kvStore) + + testAccount, err := types.NewAccountAddress("0x2") + require.NoError(t, err) + sender, err := types.NewAccountAddress("0x42") + require.NoError(t, err) + feePayer, err := types.NewAccountAddress("0xCAFE") + require.NoError(t, err) + + blockTimeNanos := uint64(time.Now().UnixNano()) + gasBalance := uint64(100000000) + _, err = vm.ExecuteEntryFunction( + &gasBalance, + kvStore, + api.NewEmptyMockAPI(nanosToSeconds(blockTimeNanos)), + types.Env{ + BlockHeight: 100, + BlockTimestampNanos: blockTimeNanos, + NextAccountNumber: 1, + TxHash: [32]uint8(generateRandomHash()), + SessionId: [32]uint8(generateRandomHash()), + FeePayer: &feePayer, + }, + []types.AccountAddress{sender}, + types.EntryFunction{ + Module: types.ModuleId{Address: testAccount, Name: "TxContextTests"}, + Function: "store_fee_payer", + TyArgs: []types.TypeTag{}, + Args: [][]byte{}, + IsJson: true, + }, + ) + require.NoError(t, err) + + senderArg, err := json.Marshal(sender.String()) + require.NoError(t, err) + viewGas := uint64(10000) + viewRes, err := vm.ExecuteViewFunction( + &viewGas, + kvStore, + api.NewEmptyMockAPI(nanosToSeconds(blockTimeNanos)), + types.Env{ + BlockHeight: 100, + BlockTimestampNanos: blockTimeNanos, + NextAccountNumber: 1, + TxHash: [32]uint8(generateRandomHash()), + SessionId: [32]uint8(generateRandomHash()), + }, + types.ViewFunction{ + Module: types.ModuleId{Address: testAccount, Name: "TxContextTests"}, + Function: "read_stored_fee_payer", + TyArgs: []types.TypeTag{}, + Args: [][]byte{senderArg}, + IsJson: true, + }, + ) + require.NoError(t, err) + // Option
serialized as JSON is "0xcafe" when Some. + require.Equal(t, fmt.Sprintf("\"%s\"", feePayer.String()), viewRes.Ret) +} + +// TestExecuteEntryFunctionSenders runs TxContextTests::store_senders with a sender +// vector and verifies via the view function that Move sees the full senders list. +func TestExecuteEntryFunctionSenders(t *testing.T) { + vm, kvStore := initializeVM(t, true) + defer vm.Destroy() + + publishModuleBundle(t, vm, kvStore) + + testAccount, err := types.NewAccountAddress("0x2") + require.NoError(t, err) + sender, err := types.NewAccountAddress("0x42") + require.NoError(t, err) + + blockTimeNanos := uint64(time.Now().UnixNano()) + gasBalance := uint64(100000000) + _, err = vm.ExecuteEntryFunction( + &gasBalance, + kvStore, + api.NewEmptyMockAPI(nanosToSeconds(blockTimeNanos)), + types.Env{ + BlockHeight: 100, + BlockTimestampNanos: blockTimeNanos, + NextAccountNumber: 1, + TxHash: [32]uint8(generateRandomHash()), + SessionId: [32]uint8(generateRandomHash()), + }, + []types.AccountAddress{sender}, + types.EntryFunction{ + Module: types.ModuleId{Address: testAccount, Name: "TxContextTests"}, + Function: "store_senders", + TyArgs: []types.TypeTag{}, + Args: [][]byte{}, + IsJson: true, + }, + ) + require.NoError(t, err) + + senderArg, err := json.Marshal(sender.String()) + require.NoError(t, err) + viewGas := uint64(10000) + viewRes, err := vm.ExecuteViewFunction( + &viewGas, + kvStore, + api.NewEmptyMockAPI(nanosToSeconds(blockTimeNanos)), + types.Env{ + BlockHeight: 100, + BlockTimestampNanos: blockTimeNanos, + NextAccountNumber: 1, + TxHash: [32]uint8(generateRandomHash()), + SessionId: [32]uint8(generateRandomHash()), + }, + types.ViewFunction{ + Module: types.ModuleId{Address: testAccount, Name: "TxContextTests"}, + Function: "read_stored_senders", + TyArgs: []types.TypeTag{}, + Args: [][]byte{senderArg}, + IsJson: true, + }, + ) + require.NoError(t, err) + // vector
with one element serialized as JSON is ["0x42"]. + require.Equal(t, fmt.Sprintf("[\"%s\"]", sender.String()), viewRes.Ret) +} diff --git a/precompile/binaries/minlib/transaction_context.mv b/precompile/binaries/minlib/transaction_context.mv index aba3700d..1e5e1b16 100644 Binary files a/precompile/binaries/minlib/transaction_context.mv and b/precompile/binaries/minlib/transaction_context.mv differ diff --git a/precompile/binaries/stdlib/transaction_context.mv b/precompile/binaries/stdlib/transaction_context.mv index aba3700d..1e5e1b16 100644 Binary files a/precompile/binaries/stdlib/transaction_context.mv and b/precompile/binaries/stdlib/transaction_context.mv differ diff --git a/precompile/modules/initia_stdlib/sources/transaction_context.move b/precompile/modules/initia_stdlib/sources/transaction_context.move index a2fe91f4..0833303f 100644 --- a/precompile/modules/initia_stdlib/sources/transaction_context.move +++ b/precompile/modules/initia_stdlib/sources/transaction_context.move @@ -1,4 +1,5 @@ module initia_std::transaction_context { + use std::option; use std::option::Option; use std::string::String; @@ -12,6 +13,18 @@ module initia_std::transaction_context { /// the sequence number and generates a new unique address. native public fun generate_unique_address(): address; + /// Returns all senders of the current transaction. + /// This function aborts if called outside of the transaction prologue, execution, or epilogue phases. + native public fun senders(): vector
; + + /// Returns the fee payer of the current transaction, or `None` if not set. + /// This function aborts if called outside of the transaction prologue, execution, or epilogue phases. + public fun fee_payer(): Option
{ + fee_payer_internal() + } + + native fun fee_payer_internal(): Option
; + /// Represents the entry function payload. struct EntryFunctionPayload has copy, drop { account_address: address, @@ -88,6 +101,28 @@ module initia_std::transaction_context { transaction_hash: vector ); + #[test_only] + public fun set_senders(senders: vector
) { + set_senders_internal(senders); + } + + #[test_only] + native fun set_senders_internal(senders: vector
); + + #[test_only] + public fun set_fee_payer(fee_payer: Option
) { + // Encode Option
as vector
of length 0 or 1 for the + // Rust-side pop convenience (see native_test_only_set_fee_payer). + let v: vector
= vector[]; + if (option::is_some(&fee_payer)) { + vector::push_back(&mut v, option::extract(&mut fee_payer)); + }; + set_fee_payer_internal(v); + } + + #[test_only] + native fun set_fee_payer_internal(fee_payer: vector
); + #[test] fun test_address_uniquess() { use std::vector; @@ -148,4 +183,47 @@ module initia_std::transaction_context { 0 ); } + + #[test] + fun test_set_senders_empty() { + set_senders(vector[]); + let s = senders(); + assert!(vector::length(&s) == 0, 0); + } + + #[test] + #[expected_failure(abort_code = 393216, location = Self)] + fun test_senders_aborts_without_context() { + senders(); + } + + #[test] + fun test_set_and_get_senders() { + set_senders(vector[@0x1, @0x2]); + let s = senders(); + assert!(vector::length(&s) == 2, 0); + assert!(*vector::borrow(&s, 0) == @0x1, 1); + assert!(*vector::borrow(&s, 1) == @0x2, 2); + } + + #[test] + fun test_set_fee_payer_none() { + set_fee_payer(option::none()); + let fp = fee_payer(); + assert!(option::is_none(&fp), 0); + } + + #[test] + #[expected_failure(abort_code = 393216, location = Self)] + fun test_fee_payer_aborts_without_context() { + fee_payer(); + } + + #[test] + fun test_set_and_get_fee_payer() { + set_fee_payer(option::some(@0x42)); + let fp = fee_payer(); + assert!(option::is_some(&fp), 0); + assert!(option::extract(&mut fp) == @0x42, 1); + } } diff --git a/precompile/modules/minitia_stdlib/sources/transaction_context.move b/precompile/modules/minitia_stdlib/sources/transaction_context.move index f672aa18..6419d436 100644 --- a/precompile/modules/minitia_stdlib/sources/transaction_context.move +++ b/precompile/modules/minitia_stdlib/sources/transaction_context.move @@ -1,4 +1,5 @@ module minitia_std::transaction_context { + use std::option; use std::option::Option; use std::string::String; @@ -12,6 +13,18 @@ module minitia_std::transaction_context { /// the sequence number and generates a new unique address. native public fun generate_unique_address(): address; + /// Returns all senders of the current transaction. + /// This function aborts if called outside of the transaction prologue, execution, or epilogue phases. + native public fun senders(): vector
; + + /// Returns the fee payer of the current transaction, or `None` if not set. + /// This function aborts if called outside of the transaction prologue, execution, or epilogue phases. + public fun fee_payer(): Option
{ + fee_payer_internal() + } + + native fun fee_payer_internal(): Option
; + /// Represents the entry function payload. struct EntryFunctionPayload has copy, drop { account_address: address, @@ -88,6 +101,28 @@ module minitia_std::transaction_context { transaction_hash: vector ); + #[test_only] + public fun set_senders(senders: vector
) { + set_senders_internal(senders); + } + + #[test_only] + native fun set_senders_internal(senders: vector
); + + #[test_only] + public fun set_fee_payer(fee_payer: Option
) { + // Encode Option
as vector
of length 0 or 1 for the + // Rust-side pop convenience (see native_test_only_set_fee_payer). + let v: vector
= vector[]; + if (option::is_some(&fee_payer)) { + vector::push_back(&mut v, option::extract(&mut fee_payer)); + }; + set_fee_payer_internal(v); + } + + #[test_only] + native fun set_fee_payer_internal(fee_payer: vector
); + #[test] fun test_address_uniquess() { use std::vector; @@ -148,4 +183,47 @@ module minitia_std::transaction_context { 0 ); } + + #[test] + fun test_set_senders_empty() { + set_senders(vector[]); + let s = senders(); + assert!(vector::length(&s) == 0, 0); + } + + #[test] + #[expected_failure(abort_code = 393216, location = Self)] + fun test_senders_aborts_without_context() { + senders(); + } + + #[test] + fun test_set_and_get_senders() { + set_senders(vector[@0x1, @0x2]); + let s = senders(); + assert!(vector::length(&s) == 2, 0); + assert!(*vector::borrow(&s, 0) == @0x1, 1); + assert!(*vector::borrow(&s, 1) == @0x2, 2); + } + + #[test] + fun test_set_fee_payer_none() { + set_fee_payer(option::none()); + let fp = fee_payer(); + assert!(option::is_none(&fp), 0); + } + + #[test] + #[expected_failure(abort_code = 393216, location = Self)] + fun test_fee_payer_aborts_without_context() { + fee_payer(); + } + + #[test] + fun test_set_and_get_fee_payer() { + set_fee_payer(option::some(@0x42)); + let fp = fee_payer(); + assert!(option::is_some(&fp), 0); + assert!(option::extract(&mut fp) == @0x42, 1); + } } diff --git a/precompile/modules/tests/sources/TxContextTests.move b/precompile/modules/tests/sources/TxContextTests.move new file mode 100644 index 00000000..21f80e54 --- /dev/null +++ b/precompile/modules/tests/sources/TxContextTests.move @@ -0,0 +1,38 @@ +module TestAccount::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
, + } + + /// Stores the senders observed during an entry function call. + struct SendersStore has key { + value: vector
, + } + + /// 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 }); + } + + #[view] + public fun read_stored_fee_payer(addr: address): Option
acquires FeePayerStore { + borrow_global(addr).value + } + + #[view] + public fun read_stored_senders(addr: address): vector
acquires SendersStore { + borrow_global(addr).value + } +} diff --git a/types/bcs.go b/types/bcs.go index 5fb4f44a..f7a8f02e 100644 --- a/types/bcs.go +++ b/types/bcs.go @@ -818,6 +818,7 @@ type Env struct { NextAccountNumber uint64 TxHash [32]uint8 SessionId [32]uint8 + FeePayer *AccountAddress } func (obj *Env) Serialize(serializer serde.Serializer) error { @@ -828,6 +829,7 @@ func (obj *Env) Serialize(serializer serde.Serializer) error { if err := serializer.SerializeU64(obj.NextAccountNumber); err != nil { return err } if err := serialize_array32_u8_array(obj.TxHash, serializer); err != nil { return err } if err := serialize_array32_u8_array(obj.SessionId, serializer); err != nil { return err } + if err := serialize_option_AccountAddress(obj.FeePayer, serializer); err != nil { return err } serializer.DecreaseContainerDepth() return nil } @@ -850,6 +852,7 @@ func DeserializeEnv(deserializer serde.Deserializer) (Env, error) { if val, err := deserializer.DeserializeU64(); err == nil { obj.NextAccountNumber = val } else { return obj, err } if val, err := deserialize_array32_u8_array(deserializer); err == nil { obj.TxHash = val } else { return obj, err } if val, err := deserialize_array32_u8_array(deserializer); err == nil { obj.SessionId = val } else { return obj, err } + if val, err := deserialize_option_AccountAddress(deserializer); err == nil { obj.FeePayer = val } else { return obj, err } deserializer.DecreaseContainerDepth() return obj, nil } @@ -2121,6 +2124,28 @@ func deserialize_array32_u8_array(deserializer serde.Deserializer) ([32]uint8, e return obj, nil } +func serialize_option_AccountAddress(value *AccountAddress, serializer serde.Serializer) error { + if value != nil { + if err := serializer.SerializeOptionTag(true); err != nil { return err } + if err := (*value).Serialize(serializer); err != nil { return err } + } else { + if err := serializer.SerializeOptionTag(false); err != nil { return err } + } + return nil +} + +func deserialize_option_AccountAddress(deserializer serde.Deserializer) (*AccountAddress, error) { + tag, err := deserializer.DeserializeOptionTag() + if err != nil { return nil, err } + if tag { + value := new(AccountAddress) + if val, err := DeserializeAccountAddress(deserializer); err == nil { *value = val } else { return nil, err } + return value, nil + } else { + return nil, nil + } +} + func serialize_option_CosmosCallback(value *CosmosCallback, serializer serde.Serializer) error { if value != nil { if err := serializer.SerializeOptionTag(true); err != nil { return err }