diff --git a/x/programs/cmd/simulator/cmd/storage.go b/x/programs/cmd/simulator/cmd/storage.go index 3fdabeb005..51ab6a0fe6 100644 --- a/x/programs/cmd/simulator/cmd/storage.go +++ b/x/programs/cmd/simulator/cmd/storage.go @@ -6,6 +6,7 @@ package cmd import ( "context" "crypto/sha256" + "encoding/binary" "errors" "github.com/ava-labs/avalanchego/database" @@ -30,6 +31,25 @@ type programStateManager struct { state.Mutable } +func (s *programStateManager) GetBalance(ctx context.Context, address codec.Address) (uint64, error) { + return getAccountBalance(ctx, s, address) +} + +func (s *programStateManager) TransferBalance(ctx context.Context, from codec.Address, to codec.Address, amount uint64) error { + fromBalance, err := getAccountBalance(ctx, s, from) + if err != nil { + return err + } + if fromBalance < amount { + return errors.New("insufficient balance") + } + toBalance, err := getAccountBalance(ctx, s, to) + if err != nil { + return err + } + return setAccountBalance(ctx, s, to, toBalance+amount) +} + func (s *programStateManager) GetAccountProgram(ctx context.Context, account codec.Address) (ids.ID, error) { programID, exists, err := getAccountProgram(ctx, s, account) if err != nil { @@ -107,11 +127,12 @@ func accountStateKey(key []byte) (k []byte) { return } -func accountDataKey(key []byte) (k []byte) { - k = make([]byte, 2+len(key)) +func accountDataKey(account []byte, key []byte) (k []byte) { + k = make([]byte, 2+len(account)+len(key)) k[0] = accountPrefix - copy(k[1:], key) - k[len(k)-1] = accountDataPrefix + copy(k[1:], account) + k[1+len(account)] = accountDataPrefix + copy(k[2+len(account):], key) return } @@ -122,6 +143,33 @@ func programKey(key []byte) (k []byte) { return } +func getAccountBalance( + ctx context.Context, + db state.Immutable, + account codec.Address, +) ( + uint64, + error, +) { + v, err := db.GetValue(ctx, accountDataKey(account[:], []byte("balance"))) + if errors.Is(err, database.ErrNotFound) { + return 0, nil + } + if err != nil { + return 0, err + } + return binary.BigEndian.Uint64(v), nil +} + +func setAccountBalance( + ctx context.Context, + mu state.Mutable, + account codec.Address, + amount uint64, +) error { + return mu.Insert(ctx, accountDataKey(account[:], []byte("balance")), binary.BigEndian.AppendUint64(nil, amount)) +} + // [programID] -> [programBytes] func getAccountProgram( ctx context.Context, @@ -132,7 +180,7 @@ func getAccountProgram( bool, // exists error, ) { - v, err := db.GetValue(ctx, accountDataKey(account[:])) + v, err := db.GetValue(ctx, accountDataKey(account[:], []byte("program"))) if errors.Is(err, database.ErrNotFound) { return ids.Empty, false, nil } @@ -148,7 +196,7 @@ func setAccountProgram( account codec.Address, programID ids.ID, ) error { - return mu.Insert(ctx, accountDataKey(account[:]), programID[:]) + return mu.Insert(ctx, accountDataKey(account[:], []byte("program")), programID[:]) } // [programID] -> [programBytes] diff --git a/x/programs/cmd/simulator/cmd/storage_test.go b/x/programs/cmd/simulator/cmd/storage_test.go new file mode 100644 index 0000000000..9165a46f3f --- /dev/null +++ b/x/programs/cmd/simulator/cmd/storage_test.go @@ -0,0 +1,16 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPrefix(t *testing.T) { + require := require.New(t) + stateKey := accountDataKey([]byte{0}, []byte{1, 2, 3}) + require.Equal([]byte{accountPrefix, 0, accountDataPrefix, 1, 2, 3}, stateKey) +} diff --git a/x/programs/runtime/call_context.go b/x/programs/runtime/call_context.go index ed669dded6..e0e478542e 100644 --- a/x/programs/runtime/call_context.go +++ b/x/programs/runtime/call_context.go @@ -94,3 +94,8 @@ func (c CallContext) WithTimestamp(timestamp uint64) CallContext { c.defaultCallInfo.Timestamp = timestamp return c } + +func (c CallContext) WithValue(value uint64) CallContext { + c.defaultCallInfo.Value = value + return c +} diff --git a/x/programs/runtime/call_context_test.go b/x/programs/runtime/call_context_test.go index e884b397fa..8453676fd3 100644 --- a/x/programs/runtime/call_context_test.go +++ b/x/programs/runtime/call_context_test.go @@ -27,7 +27,7 @@ func TestCallContext(t *testing.T) { NewConfig(), logging.NoLog{}, ).WithDefaults( - &CallInfo{ + CallInfo{ State: &test.StateManager{ProgramsMap: map[ids.ID]string{programID: "call_program"}, AccountMap: map[codec.Address]ids.ID{programAccount: programID}}, Program: programAccount, Fuel: 1000000, @@ -74,7 +74,7 @@ func TestCallContextPreventOverwrite(t *testing.T) { NewConfig(), logging.NoLog{}, ).WithDefaults( - &CallInfo{ + CallInfo{ Program: program0Address, State: &test.StateManager{ProgramsMap: map[ids.ID]string{program0ID: "call_program"}, AccountMap: map[codec.Address]ids.ID{program0Address: program0ID}}, Fuel: 1000000, diff --git a/x/programs/runtime/import_balance.go b/x/programs/runtime/import_balance.go new file mode 100644 index 0000000000..eaa77b5518 --- /dev/null +++ b/x/programs/runtime/import_balance.go @@ -0,0 +1,45 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package runtime + +import ( + "context" + + "github.com/ava-labs/hypersdk/codec" +) + +const ( + sendBalanceCost = 10000 + getBalanceCost = 10000 +) + +type transferBalanceInput struct { + To codec.Address + Amount uint64 +} + +func NewBalanceModule() *ImportModule { + return &ImportModule{ + Name: "balance", + HostFunctions: map[string]HostFunction{ + "get": {FuelCost: getBalanceCost, Function: Function[codec.Address, uint64](func(callInfo *CallInfo, address codec.Address) (uint64, error) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + return callInfo.State.GetBalance(ctx, address) + })}, + "send": {FuelCost: sendBalanceCost, Function: Function[transferBalanceInput, Result[Unit, ProgramCallErrorCode]](func(callInfo *CallInfo, input transferBalanceInput) (Result[Unit, ProgramCallErrorCode], error) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + err := callInfo.State.TransferBalance(ctx, callInfo.Program, input.To, input.Amount) + if err != nil { + if extractedError, ok := ExtractProgramCallErrorCode(err); ok { + return Err[Unit, ProgramCallErrorCode](extractedError), nil + } + return Err[Unit, ProgramCallErrorCode](ExecutionFailure), err + } + return Ok[Unit, ProgramCallErrorCode](Unit{}), nil + })}, + }, + } +} diff --git a/x/programs/runtime/import_balance_test.go b/x/programs/runtime/import_balance_test.go new file mode 100644 index 0000000000..47c2fd2b1c --- /dev/null +++ b/x/programs/runtime/import_balance_test.go @@ -0,0 +1,49 @@ +// Copyright (C) 2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package runtime + +import ( + "context" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/x/programs/test" +) + +func TestImportBalanceGetBalance(t *testing.T) { + require := require.New(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + actor := codec.CreateAddress(0, ids.GenerateTestID()) + program := newTestProgram(ctx, "balance") + program.Runtime.StateManager.(test.StateManager).Balances[actor] = 3 + result, err := program.WithActor(actor).Call("balance") + require.NoError(err) + require.Equal(uint64(3), into[uint64](result)) +} + +func TestImportBalanceSend(t *testing.T) { + require := require.New(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + actor := codec.CreateAddress(0, ids.GenerateTestID()) + program := newTestProgram(ctx, "balance") + program.Runtime.StateManager.(test.StateManager).Balances[program.Address] = 3 + result, err := program.Call("send_balance", actor) + require.NoError(err) + require.True(into[bool](result)) + + result, err = program.WithActor(actor).Call("balance") + require.NoError(err) + require.Equal(uint64(1), into[uint64](result)) + + result, err = program.WithActor(program.Address).Call("balance") + require.NoError(err) + require.Equal(uint64(2), into[uint64](result)) +} diff --git a/x/programs/runtime/import_program.go b/x/programs/runtime/import_program.go index e98a30fac0..2b42b234cf 100644 --- a/x/programs/runtime/import_program.go +++ b/x/programs/runtime/import_program.go @@ -27,6 +27,7 @@ const ( ExecutionFailure ProgramCallErrorCode = iota CallPanicked OutOfFuel + InsufficientBalance ) type callProgramInput struct { @@ -34,6 +35,7 @@ type callProgramInput struct { FunctionName string Params []byte Fuel uint64 + Value uint64 } type deployProgramInput struct { @@ -72,6 +74,7 @@ func NewProgramModule(r *WasmRuntime) *ImportModule { newInfo.FunctionName = input.FunctionName newInfo.Params = input.Params newInfo.Fuel = input.Fuel + newInfo.Value = input.Value result, err := r.CallProgram( context.Background(), @@ -85,6 +88,7 @@ func NewProgramModule(r *WasmRuntime) *ImportModule { // return any remaining fuel to the calling program callInfo.AddFuel(newInfo.RemainingFuel()) + callInfo.Value += newInfo.Value return Ok[RawBytes, ProgramCallErrorCode](result), nil })}, diff --git a/x/programs/runtime/program.go b/x/programs/runtime/program.go index bfdd886020..ef5948f710 100644 --- a/x/programs/runtime/program.go +++ b/x/programs/runtime/program.go @@ -43,7 +43,7 @@ type CallInfo struct { // the serialized parameters that will be passed to the called function Params []byte - // the amount of fuel allowed to be consumed by wasm for this call + // the maximum amount of fuel allowed to be consumed by wasm for this call Fuel uint64 // the height of the chain that this call was made from @@ -55,6 +55,8 @@ type CallInfo struct { // the action id that triggered this call ActionID ids.ID + Value uint64 + inst *ProgramInstance } @@ -83,11 +85,17 @@ type ProgramInstance struct { result []byte } -func (p *ProgramInstance) call(_ context.Context, callInfo *CallInfo) ([]byte, error) { +func (p *ProgramInstance) call(ctx context.Context, callInfo *CallInfo) ([]byte, error) { if err := p.store.AddFuel(callInfo.Fuel); err != nil { return nil, err } + if callInfo.Value > 0 { + if err := callInfo.State.TransferBalance(ctx, callInfo.Actor, callInfo.Program, callInfo.Value); err != nil { + return nil, errors.New("insufficient balance") + } + } + // create the program context programCtx := Context{ Program: callInfo.Program, diff --git a/x/programs/runtime/runtime.go b/x/programs/runtime/runtime.go index 4b7b3e497a..7afb909c58 100644 --- a/x/programs/runtime/runtime.go +++ b/x/programs/runtime/runtime.go @@ -30,11 +30,17 @@ type WasmRuntime struct { } type StateManager interface { - GetProgramState(address codec.Address) state.Mutable - ProgramStore + BalanceManager + ProgramManager +} + +type BalanceManager interface { + GetBalance(ctx context.Context, address codec.Address) (uint64, error) + TransferBalance(ctx context.Context, from codec.Address, to codec.Address, amount uint64) error } -type ProgramStore interface { +type ProgramManager interface { + GetProgramState(address codec.Address) state.Mutable GetAccountProgram(ctx context.Context, account codec.Address) (ids.ID, error) GetProgramBytes(ctx context.Context, programID ids.ID) ([]byte, error) NewAccountWithProgram(ctx context.Context, programID ids.ID, accountCreationData []byte) (codec.Address, error) @@ -62,14 +68,15 @@ func NewRuntime( } runtime.AddImportModule(NewLogModule()) + runtime.AddImportModule(NewBalanceModule()) runtime.AddImportModule(NewStateAccessModule()) runtime.AddImportModule(NewProgramModule(runtime)) return runtime } -func (r *WasmRuntime) WithDefaults(callInfo *CallInfo) CallContext { - return CallContext{r: r, defaultCallInfo: *callInfo} +func (r *WasmRuntime) WithDefaults(callInfo CallInfo) CallContext { + return CallContext{r: r, defaultCallInfo: callInfo} } func (r *WasmRuntime) AddImportModule(mod *ImportModule) { diff --git a/x/programs/runtime/runtime_test.go b/x/programs/runtime/runtime_test.go index 2efed74185..193ca92267 100644 --- a/x/programs/runtime/runtime_test.go +++ b/x/programs/runtime/runtime_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ava-labs/hypersdk/codec" + "github.com/ava-labs/hypersdk/x/programs/test" ) func BenchmarkRuntimeCallProgramBasic(b *testing.B) { @@ -28,6 +29,34 @@ func BenchmarkRuntimeCallProgramBasic(b *testing.B) { } } +func TestRuntimeCallProgramBasicAttachValue(t *testing.T) { + require := require.New(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + program := newTestProgram(ctx, "simple") + actor := codec.CreateAddress(0, ids.GenerateTestID()) + program.Runtime.StateManager.(test.StateManager).Balances[actor] = 10 + + actorBalance, err := program.Runtime.StateManager.GetBalance(context.Background(), actor) + require.NoError(err) + require.Equal(uint64(10), actorBalance) + + // calling a program with a value transfers that amount from the caller to the program + result, err := program.WithActor(actor).WithValue(4).Call("get_value") + require.NoError(err) + require.Equal(uint64(0), into[uint64](result)) + + actorBalance, err = program.Runtime.StateManager.GetBalance(context.Background(), actor) + require.NoError(err) + require.Equal(uint64(6), actorBalance) + + programBalance, err := program.Runtime.StateManager.GetBalance(context.Background(), program.Address) + require.NoError(err) + require.Equal(uint64(4), programBalance) +} + func TestRuntimeCallProgramBasic(t *testing.T) { require := require.New(t) diff --git a/x/programs/runtime/serialization.go b/x/programs/runtime/serialization.go index 93d0e40f44..a9e4bfa3bb 100644 --- a/x/programs/runtime/serialization.go +++ b/x/programs/runtime/serialization.go @@ -206,3 +206,5 @@ func (o Option[T]) Some() (T, bool) { func (o Option[T]) None() bool { return o.isNone } + +type Unit struct{} diff --git a/x/programs/runtime/util_test.go b/x/programs/runtime/util_test.go index 972dc4aa0d..5024e4c5aa 100644 --- a/x/programs/runtime/util_test.go +++ b/x/programs/runtime/util_test.go @@ -64,6 +64,11 @@ func (t *testRuntime) WithTimestamp(ts uint64) *testRuntime { return t } +func (t *testRuntime) WithValue(value uint64) *testRuntime { + t.callContext = t.callContext.WithValue(value) + return t +} + func (t *testRuntime) AddProgram(programID ids.ID, programName string) { t.StateManager.(test.StateManager).ProgramsMap[programID] = programName } @@ -87,8 +92,13 @@ func newTestProgram(ctx context.Context, program string) *testProgram { Context: ctx, callContext: NewRuntime( NewConfig(), - logging.NoLog{}).WithDefaults(&CallInfo{Fuel: 10000000}), - StateManager: test.StateManager{ProgramsMap: map[ids.ID]string{id: program}, AccountMap: map[codec.Address]ids.ID{account: id}, Mu: test.NewTestDB()}, + logging.NoLog{}).WithDefaults(CallInfo{Fuel: 10000000}), + StateManager: test.StateManager{ + ProgramsMap: map[ids.ID]string{id: program}, + AccountMap: map[codec.Address]ids.ID{account: id}, + Balances: map[codec.Address]uint64{}, + Mu: test.NewTestDB(), + }, }, Address: account, } @@ -151,6 +161,11 @@ func (t *testProgram) WithTimestamp(ts uint64) *testProgram { return t } +func (t *testProgram) WithValue(value uint64) *testProgram { + t.Runtime = t.Runtime.WithValue(value) + return t +} + func into[T any](data []byte) T { result, err := Deserialize[T](data) if err != nil { diff --git a/x/programs/rust/examples/counter-external/src/lib.rs b/x/programs/rust/examples/counter-external/src/lib.rs index 5015e0b1aa..0e262dc8e3 100644 --- a/x/programs/rust/examples/counter-external/src/lib.rs +++ b/x/programs/rust/examples/counter-external/src/lib.rs @@ -2,13 +2,13 @@ use wasmlanche_sdk::{public, types::Address, Context, ExternalCallContext, Progr #[public] pub fn inc(_: Context, external: Program, address: Address) { - let ctx = ExternalCallContext::new(external, 1_000_000); + let ctx = ExternalCallContext::new(external, 1_000_000, 0); counter::inc(ctx, address, 1); } #[public] pub fn get_value(_: Context, external: Program, address: Address) -> u64 { - let ctx = ExternalCallContext::new(external, 1_000_000); + let ctx = ExternalCallContext::new(external, 1_000_000, 0); counter::get_value(ctx, address) } diff --git a/x/programs/rust/examples/counter/src/lib.rs b/x/programs/rust/examples/counter/src/lib.rs index 074c00685d..67f4023ec1 100644 --- a/x/programs/rust/examples/counter/src/lib.rs +++ b/x/programs/rust/examples/counter/src/lib.rs @@ -38,7 +38,7 @@ pub fn inc_external( amount: Count, ) -> bool { let args = borsh::to_vec(&(of, amount)).unwrap(); - target.call_function("inc", &args, max_units).unwrap() + target.call_function("inc", &args, max_units, 0).unwrap() } /// Gets the count at the address. @@ -61,7 +61,9 @@ fn get_value_internal(context: &Context, of: Address) -> Count { #[public] pub fn get_value_external(_: Context, target: Program, max_units: Gas, of: Address) -> Count { let args = borsh::to_vec(&of).unwrap(); - target.call_function("get_value", &args, max_units).unwrap() + target + .call_function("get_value", &args, max_units, 0) + .unwrap() } #[cfg(test)] diff --git a/x/programs/rust/sdk-macros/src/lib.rs b/x/programs/rust/sdk-macros/src/lib.rs index eaa0f94ec8..b68ccadb7f 100644 --- a/x/programs/rust/sdk-macros/src/lib.rs +++ b/x/programs/rust/sdk-macros/src/lib.rs @@ -200,7 +200,7 @@ pub fn public(_: TokenStream, item: TokenStream) -> TokenStream { let args = borsh::to_vec(&(#(#args),*)).expect("error serializing args"); param_0 .program() - .call_function::<#return_type>(#name, &args, param_0.max_units()) + .call_function::<#return_type>(#name, &args, param_0.max_units(), param_0.value()) .expect("calling the external program failed") }}); diff --git a/x/programs/rust/wasmlanche-sdk/src/balance.rs b/x/programs/rust/wasmlanche-sdk/src/balance.rs new file mode 100644 index 0000000000..b0bb8540a9 --- /dev/null +++ b/x/programs/rust/wasmlanche-sdk/src/balance.rs @@ -0,0 +1,35 @@ +use crate::{memory::HostPtr, types::Address, ExternalCallError}; + +/// Gets the balance for the specified address +/// # Panics +/// Panics if there was an issue deserializing the balance +#[must_use] +pub fn get(account: Address) -> u64 { + #[link(wasm_import_module = "balance")] + extern "C" { + #[link_name = "get"] + fn get(ptr: *const u8, len: usize) -> HostPtr; + } + let ptr = borsh::to_vec(&account).expect("failed to serialize args"); + let bytes = unsafe { get(ptr.as_ptr(), ptr.len()) }; + + borsh::from_slice(&bytes).expect("failed to deserialize the balance") +} + +/// Transfer currency from the calling program to the passed address +/// # Panics +/// Panics if there was an issue deserializing the result +/// # Errors +/// Errors if there are insufficient funds +pub fn send(to: Address, amount: u64) -> Result<(), ExternalCallError> { + #[link(wasm_import_module = "balance")] + extern "C" { + #[link_name = "send"] + fn send_value(ptr: *const u8, len: usize) -> HostPtr; + } + let ptr = borsh::to_vec(&(to, amount)).expect("failed to serialize args"); + + let bytes = unsafe { send_value(ptr.as_ptr(), ptr.len()) }; + + borsh::from_slice(&bytes).expect("failed to deserialize the result") +} diff --git a/x/programs/rust/wasmlanche-sdk/src/lib.rs b/x/programs/rust/wasmlanche-sdk/src/lib.rs index 7afd98dcd2..8afd574980 100644 --- a/x/programs/rust/wasmlanche-sdk/src/lib.rs +++ b/x/programs/rust/wasmlanche-sdk/src/lib.rs @@ -6,6 +6,7 @@ pub mod state; /// Program types. pub mod types; +pub mod balance; mod logging; mod memory; mod program; @@ -95,11 +96,16 @@ impl BorshDeserialize for Context { pub struct ExternalCallContext { program: Program, max_units: Gas, + value: u64, } impl ExternalCallContext { - pub fn new(program: Program, max_units: Gas) -> Self { - Self { program, max_units } + pub fn new(program: Program, max_units: Gas, value: u64) -> Self { + Self { + program, + max_units, + value, + } } pub fn program(&self) -> &Program { @@ -109,4 +115,8 @@ impl ExternalCallContext { pub fn max_units(&self) -> Gas { self.max_units } + + pub fn value(&self) -> u64 { + self.value + } } diff --git a/x/programs/rust/wasmlanche-sdk/src/program.rs b/x/programs/rust/wasmlanche-sdk/src/program.rs index 9129b6ecb9..ae2b6401e9 100644 --- a/x/programs/rust/wasmlanche-sdk/src/program.rs +++ b/x/programs/rust/wasmlanche-sdk/src/program.rs @@ -43,6 +43,8 @@ pub enum ExternalCallError { CallPanicked = 1, #[error("not enough fuel to cover the execution")] OutOfFuel = 2, + #[error("insufficient funds")] + InsufficientFunds = 3, } /// Represents the current Program in the context of the caller, or an external @@ -88,7 +90,8 @@ impl Program { /// let increment = 10; /// let params = borsh::to_vec(&increment).unwrap(); /// let max_units = 1000000; - /// let has_incremented: bool = target.call_function("increment", ¶ms, max_units)?; + /// let value = 0; + /// let has_incremented: bool = target.call_function("increment", ¶ms, max_units, value)?; /// assert!(has_incremented); /// # Ok::<(), wasmlanche_sdk::ExternalCallError>(()) /// ``` @@ -97,6 +100,7 @@ impl Program { function_name: &str, args: &[u8], max_units: Gas, + max_value: u64, ) -> Result { #[link(wasm_import_module = "program")] extern "C" { @@ -109,6 +113,7 @@ impl Program { function: function_name.as_bytes(), args, max_units, + max_value, }; let args_bytes = borsh::to_vec(&args).expect("failed to serialize args"); @@ -165,6 +170,7 @@ struct CallProgramArgs<'a, K> { function: &'a [u8], args: &'a [u8], max_units: Gas, + max_value: u64, } impl BorshSerialize for CallProgramArgs<'_, K> { @@ -174,13 +180,14 @@ impl BorshSerialize for CallProgramArgs<'_, K> { function, args, max_units, + max_value, } = self; target.serialize(writer)?; function.serialize(writer)?; args.serialize(writer)?; max_units.serialize(writer)?; - + max_value.serialize(writer)?; Ok(()) } } diff --git a/x/programs/test/programs/balance/Cargo.toml b/x/programs/test/programs/balance/Cargo.toml new file mode 100644 index 0000000000..760acf9f5d --- /dev/null +++ b/x/programs/test/programs/balance/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "balance" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasmlanche-sdk = { workspace = true } +borsh = { workspace = true } + +[build-dependencies] +wasmlanche-sdk = { workspace = true, features = ["build"] } diff --git a/x/programs/test/programs/balance/src/lib.rs b/x/programs/test/programs/balance/src/lib.rs new file mode 100644 index 0000000000..eddf71f292 --- /dev/null +++ b/x/programs/test/programs/balance/src/lib.rs @@ -0,0 +1,16 @@ +use wasmlanche_sdk::{ + balance::{get, send}, + public, + types::Address, + Context, +}; + +#[public] +pub fn balance(ctx: Context) -> u64 { + get(ctx.actor()) +} + +#[public] +pub fn send_balance(_: Context, recipient: Address) -> bool { + send(recipient, 1).is_ok() +} diff --git a/x/programs/test/programs/call_program/src/lib.rs b/x/programs/test/programs/call_program/src/lib.rs index e5534fb2b8..4d0e122061 100644 --- a/x/programs/test/programs/call_program/src/lib.rs +++ b/x/programs/test/programs/call_program/src/lib.rs @@ -11,7 +11,9 @@ pub fn simple_call(_: Context) -> i64 { #[public] pub fn simple_call_external(_: Context, target: Program, max_units: Gas) -> i64 { - target.call_function("simple_call", &[], max_units).unwrap() + target + .call_function("simple_call", &[], max_units, 0) + .unwrap() } #[public] @@ -22,7 +24,7 @@ pub fn actor_check(context: Context) -> Address { #[public] pub fn actor_check_external(_: Context, target: Program, max_units: Gas) -> Address { target - .call_function("actor_check", &[], max_units) + .call_function("actor_check", &[], max_units, 0) .expect("failure") } @@ -34,7 +36,7 @@ pub fn call_with_param(_: Context, value: i64) -> i64 { #[public] pub fn call_with_param_external(_: Context, target: Program, max_units: Gas, value: i64) -> i64 { target - .call_function("call_with_param", &value.to_le_bytes(), max_units) + .call_function("call_with_param", &value.to_le_bytes(), max_units, 0) .unwrap() } @@ -57,6 +59,6 @@ pub fn call_with_two_params_external( .chain(value2.to_le_bytes()) .collect(); target - .call_function("call_with_two_params", &args, max_units) + .call_function("call_with_two_params", &args, max_units, 0) .unwrap() } diff --git a/x/programs/test/programs/fuel/src/lib.rs b/x/programs/test/programs/fuel/src/lib.rs index 42d5776454..b5839938fa 100644 --- a/x/programs/test/programs/fuel/src/lib.rs +++ b/x/programs/test/programs/fuel/src/lib.rs @@ -7,5 +7,7 @@ pub fn get_fuel(ctx: Context) -> u64 { #[public] pub fn out_of_fuel(_: Context, target: Program) -> ExternalCallError { - target.call_function::("get_fuel", &[], 0).unwrap_err() + target + .call_function::("get_fuel", &[], 0, 0) + .unwrap_err() } diff --git a/x/programs/test/util.go b/x/programs/test/util.go index 4843001410..d27bbe72ce 100644 --- a/x/programs/test/util.go +++ b/x/programs/test/util.go @@ -36,6 +36,7 @@ func CompileTest(programName string) error { type StateManager struct { ProgramsMap map[ids.ID]string AccountMap map[codec.Address]ids.ID + Balances map[codec.Address]uint64 Mu state.Mutable } @@ -72,6 +73,26 @@ func (t StateManager) SetAccountProgram(_ context.Context, account codec.Address return nil } +func (t StateManager) GetBalance(_ context.Context, address codec.Address) (uint64, error) { + if balance, ok := t.Balances[address]; ok { + return balance, nil + } + return 0, nil +} + +func (t StateManager) TransferBalance(ctx context.Context, from codec.Address, to codec.Address, amount uint64) error { + balance, err := t.GetBalance(ctx, from) + if err != nil { + return err + } + if balance < amount { + return errors.New("insufficient balance") + } + t.Balances[from] -= amount + t.Balances[to] += amount + return nil +} + func (t StateManager) GetProgramState(address codec.Address) state.Mutable { return &prefixedState{address: address, inner: t.Mu} }