summaryrefslogtreecommitdiff
path: root/data/transactions/logic/ledger_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'data/transactions/logic/ledger_test.go')
-rw-r--r--data/transactions/logic/ledger_test.go803
1 files changed, 803 insertions, 0 deletions
diff --git a/data/transactions/logic/ledger_test.go b/data/transactions/logic/ledger_test.go
new file mode 100644
index 000000000..54929b3b8
--- /dev/null
+++ b/data/transactions/logic/ledger_test.go
@@ -0,0 +1,803 @@
+// Copyright (C) 2019-2022 Algorand, Inc.
+// This file is part of go-algorand
+//
+// go-algorand is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// go-algorand is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with go-algorand. If not, see <https://www.gnu.org/licenses/>.
+
+package logic
+
+import (
+ "errors"
+ "fmt"
+ "math/rand"
+
+ "github.com/algorand/go-algorand/config"
+ "github.com/algorand/go-algorand/data/basics"
+ "github.com/algorand/go-algorand/data/transactions"
+ "github.com/algorand/go-algorand/protocol"
+)
+
+type balanceRecord struct {
+ addr basics.Address
+ auth basics.Address
+ balance uint64
+ locals map[basics.AppIndex]basics.TealKeyValue
+ holdings map[basics.AssetIndex]basics.AssetHolding
+ mods map[basics.AppIndex]map[string]basics.ValueDelta
+}
+
+func makeBalanceRecord(addr basics.Address, balance uint64) balanceRecord {
+ br := balanceRecord{
+ addr: addr,
+ balance: balance,
+ locals: make(map[basics.AppIndex]basics.TealKeyValue),
+ holdings: make(map[basics.AssetIndex]basics.AssetHolding),
+ mods: make(map[basics.AppIndex]map[string]basics.ValueDelta),
+ }
+ return br
+}
+
+// In our test ledger, we don't store the creatables with their
+// creators, so we need to carry the creator around with them.
+type appParams struct {
+ basics.AppParams
+ Creator basics.Address
+}
+
+type asaParams struct {
+ basics.AssetParams
+ Creator basics.Address
+}
+
+// Ledger is a fake ledger that is "good enough" to reasonably test AVM programs.
+type Ledger struct {
+ balances map[basics.Address]balanceRecord
+ applications map[basics.AppIndex]appParams
+ assets map[basics.AssetIndex]asaParams
+ mods map[basics.AppIndex]map[string]basics.ValueDelta
+ rnd basics.Round
+}
+
+// MakeLedger constructs a Ledger with the given balances.
+func MakeLedger(balances map[basics.Address]uint64) *Ledger {
+ l := new(Ledger)
+ l.balances = make(map[basics.Address]balanceRecord)
+ for addr, balance := range balances {
+ l.NewAccount(addr, balance)
+ }
+ l.applications = make(map[basics.AppIndex]appParams)
+ l.assets = make(map[basics.AssetIndex]asaParams)
+ l.mods = make(map[basics.AppIndex]map[string]basics.ValueDelta)
+ return l
+}
+
+// Reset removes all of the mods created by previous AVM execution
+func (l *Ledger) Reset() {
+ l.mods = make(map[basics.AppIndex]map[string]basics.ValueDelta)
+ for addr, br := range l.balances {
+ br.mods = make(map[basics.AppIndex]map[string]basics.ValueDelta)
+ l.balances[addr] = br
+ }
+}
+
+// NewAccount adds a new account with a given balance to the Ledger.
+func (l *Ledger) NewAccount(addr basics.Address, balance uint64) {
+ l.balances[addr] = makeBalanceRecord(addr, balance)
+}
+
+// NewApp add a new AVM app to the Ledger, and arranges so that future
+// executions will act as though they are that app. In most uses, it only sets
+// up the id and schema but no code, as testing will want to try many different
+// code sequences.
+func (l *Ledger) NewApp(creator basics.Address, appID basics.AppIndex, params basics.AppParams) {
+ params = params.Clone()
+ if params.GlobalState == nil {
+ params.GlobalState = make(basics.TealKeyValue)
+ }
+ l.applications[appID] = appParams{
+ Creator: creator,
+ AppParams: params,
+ }
+}
+
+// NewAsset adds an asset with the given id and params to the ledger.
+func (l *Ledger) NewAsset(creator basics.Address, assetID basics.AssetIndex, params basics.AssetParams) {
+ l.assets[assetID] = asaParams{
+ Creator: creator,
+ AssetParams: params,
+ }
+ br, ok := l.balances[creator]
+ if !ok {
+ br = makeBalanceRecord(creator, 0)
+ }
+ br.holdings[assetID] = basics.AssetHolding{Amount: params.Total, Frozen: params.DefaultFrozen}
+ l.balances[creator] = br
+}
+
+const firstTestID = 5000
+
+// Counter implements LedgerForLogic, but it not really a txn counter, but is
+// sufficient for the logic package.
+func (l *Ledger) Counter() uint64 {
+ for try := firstTestID; true; try++ {
+ if _, ok := l.assets[basics.AssetIndex(try)]; ok {
+ continue
+ }
+ if _, ok := l.applications[basics.AppIndex(try)]; ok {
+ continue
+ }
+ return uint64(try)
+ }
+ panic("wow")
+}
+
+// NewHolding sets the ASA balance of a given account.
+func (l *Ledger) NewHolding(addr basics.Address, assetID uint64, amount uint64, frozen bool) {
+ br, ok := l.balances[addr]
+ if !ok {
+ br = makeBalanceRecord(addr, 0)
+ }
+ br.holdings[basics.AssetIndex(assetID)] = basics.AssetHolding{Amount: amount, Frozen: frozen}
+ l.balances[addr] = br
+}
+
+// NewLocals essentially "opts in" an address to an app id.
+func (l *Ledger) NewLocals(addr basics.Address, appID uint64) {
+ if _, ok := l.balances[addr]; !ok {
+ l.balances[addr] = makeBalanceRecord(addr, 0)
+ }
+ l.balances[addr].locals[basics.AppIndex(appID)] = basics.TealKeyValue{}
+}
+
+// NewLocal sets a local value of an app on an address
+func (l *Ledger) NewLocal(addr basics.Address, appID uint64, key string, value basics.TealValue) {
+ l.balances[addr].locals[basics.AppIndex(appID)][key] = value
+}
+
+// NoLocal removes a key from an address locals for an app.
+func (l *Ledger) NoLocal(addr basics.Address, appID uint64, key string) {
+ delete(l.balances[addr].locals[basics.AppIndex(appID)], key)
+}
+
+// NewGlobal sets a global value for an app
+func (l *Ledger) NewGlobal(appID uint64, key string, value basics.TealValue) {
+ l.applications[basics.AppIndex(appID)].GlobalState[key] = value
+}
+
+// NoGlobal removes a global key for an app
+func (l *Ledger) NoGlobal(appID uint64, key string) {
+ delete(l.applications[basics.AppIndex(appID)].GlobalState, key)
+}
+
+// Rekey sets the authAddr for an address.
+func (l *Ledger) Rekey(addr basics.Address, auth basics.Address) {
+ if br, ok := l.balances[addr]; ok {
+ br.auth = auth
+ l.balances[addr] = br
+ }
+}
+
+// Round gives the current Round of the test ledger, which is random but consistent
+func (l *Ledger) Round() basics.Round {
+ return l.round()
+}
+
+// LatestTimestamp gives a uint64, chosen randomly. It should
+// probably increase monotonically, but no tests care yet.
+func (l *Ledger) LatestTimestamp() int64 {
+ return int64(rand.Uint32() + 1)
+}
+
+// Balance returns the value in an account, as MicroAlgos
+func (l *Ledger) Balance(addr basics.Address) (amount basics.MicroAlgos, err error) {
+ br, ok := l.balances[addr]
+ if !ok {
+ return basics.MicroAlgos{Raw: 0}, nil
+ }
+ return basics.MicroAlgos{Raw: br.balance}, nil
+}
+
+// MinBalance computes the MinBalance requirement for an account,
+// under the given consensus parameters.
+func (l *Ledger) MinBalance(addr basics.Address, proto *config.ConsensusParams) (amount basics.MicroAlgos, err error) {
+ br, ok := l.balances[addr]
+ if !ok {
+ br = makeBalanceRecord(addr, 0)
+ }
+
+ var min uint64
+
+ // First, base MinBalance
+ min = proto.MinBalance
+
+ // MinBalance for each Asset
+ assetCost := basics.MulSaturate(proto.MinBalance, uint64(len(br.holdings)))
+ min = basics.AddSaturate(min, assetCost)
+
+ // Base MinBalance + GlobalStateSchema.MinBalance + ExtraProgramPages MinBalance for each created application
+ for _, params := range l.applications {
+ if params.Creator == addr {
+ min = basics.AddSaturate(min, proto.AppFlatParamsMinBalance)
+ min = basics.AddSaturate(min, params.GlobalStateSchema.MinBalance(proto).Raw)
+ min = basics.AddSaturate(min, basics.MulSaturate(proto.AppFlatParamsMinBalance, uint64(params.ExtraProgramPages)))
+ }
+ }
+
+ // Base MinBalance + LocalStateSchema.MinBalance for each opted in application
+ for idx := range br.locals {
+ min = basics.AddSaturate(min, proto.AppFlatParamsMinBalance)
+ min = basics.AddSaturate(min, l.applications[idx].LocalStateSchema.MinBalance(proto).Raw)
+ }
+
+ return basics.MicroAlgos{Raw: min}, nil
+}
+
+// Authorizer returns the address that must authorize txns from a
+// given address. It's either the address itself, or the value it has
+// been rekeyed to.
+func (l *Ledger) Authorizer(addr basics.Address) (basics.Address, error) {
+ br, ok := l.balances[addr]
+ if !ok {
+ return addr, nil // Not rekeyed if not present
+ }
+ if !br.auth.IsZero() {
+ return br.auth, nil
+ }
+ return br.addr, nil
+}
+
+// GetGlobal returns the current value of a global in an app, taking
+// into account the mods created by earlier teal execution.
+func (l *Ledger) GetGlobal(appIdx basics.AppIndex, key string) (basics.TealValue, bool, error) {
+ params, ok := l.applications[appIdx]
+ if !ok {
+ return basics.TealValue{}, false, fmt.Errorf("no such app %d", appIdx)
+ }
+
+ // return most recent value if available
+ tkvm, ok := l.mods[appIdx]
+ if ok {
+ val, ok := tkvm[key]
+ if ok {
+ tv, ok := val.ToTealValue()
+ return tv, ok, nil
+ }
+ }
+
+ // otherwise return original one
+ val, ok := params.GlobalState[key]
+ return val, ok, nil
+}
+
+// SetGlobal "sets" a global, but only through the mods mechanism, so
+// it can be removed with Reset()
+func (l *Ledger) SetGlobal(appIdx basics.AppIndex, key string, value basics.TealValue) error {
+ params, ok := l.applications[appIdx]
+ if !ok {
+ return fmt.Errorf("no such app %d", appIdx)
+ }
+
+ // if writing the same value, return
+ // this simulates real ledger behavior for tests
+ val, ok := params.GlobalState[key]
+ if ok && val == value {
+ return nil
+ }
+
+ // write to deltas
+ _, ok = l.mods[appIdx]
+ if !ok {
+ l.mods[appIdx] = make(map[string]basics.ValueDelta)
+ }
+ l.mods[appIdx][key] = value.ToValueDelta()
+ return nil
+}
+
+// DelGlobal "deletes" a global, but only through the mods mechanism, so
+// the deletion can be Reset()
+func (l *Ledger) DelGlobal(appIdx basics.AppIndex, key string) error {
+ params, ok := l.applications[appIdx]
+ if !ok {
+ return fmt.Errorf("no such app %d", appIdx)
+ }
+
+ exist := false
+ if _, ok := params.GlobalState[key]; ok {
+ exist = true
+ }
+
+ _, ok = l.mods[appIdx]
+ if !ok && !exist {
+ // nothing to delete
+ return nil
+ }
+ if !ok {
+ l.mods[appIdx] = make(map[string]basics.ValueDelta)
+ }
+ _, ok = l.mods[appIdx][key]
+ if ok || exist {
+ l.mods[appIdx][key] = basics.ValueDelta{Action: basics.DeleteAction}
+ }
+ return nil
+}
+
+// GetLocal returns the current value bound to a local key, taking
+// into account mods caused by earlier executions.
+func (l *Ledger) GetLocal(addr basics.Address, appIdx basics.AppIndex, key string, accountIdx uint64) (basics.TealValue, bool, error) {
+ br, ok := l.balances[addr]
+ if !ok {
+ return basics.TealValue{}, false, fmt.Errorf("no such address")
+ }
+ tkvd, ok := br.locals[appIdx]
+ if !ok {
+ return basics.TealValue{}, false, fmt.Errorf("no app for account")
+ }
+
+ // check deltas first
+ tkvm, ok := br.mods[appIdx]
+ if ok {
+ val, ok := tkvm[key]
+ if ok {
+ tv, ok := val.ToTealValue()
+ return tv, ok, nil
+ }
+ }
+
+ val, ok := tkvd[key]
+ return val, ok, nil
+}
+
+// SetLocal "sets" the current value bound to a local key using the
+// mods mechanism, so it can be Reset()
+func (l *Ledger) SetLocal(addr basics.Address, appIdx basics.AppIndex, key string, value basics.TealValue, accountIdx uint64) error {
+ br, ok := l.balances[addr]
+ if !ok {
+ return fmt.Errorf("no such address")
+ }
+ tkv, ok := br.locals[appIdx]
+ if !ok {
+ return fmt.Errorf("no app for account")
+ }
+
+ // if writing the same value, return
+ // this simulates real ledger behavior for tests
+ val, ok := tkv[key]
+ if ok && val == value {
+ return nil
+ }
+
+ // write to deltas
+ _, ok = br.mods[appIdx]
+ if !ok {
+ br.mods[appIdx] = make(map[string]basics.ValueDelta)
+ }
+ br.mods[appIdx][key] = value.ToValueDelta()
+ return nil
+}
+
+// DelLocal "deletes" the current value bound to a local key using the
+// mods mechanism, so it can be Reset()
+func (l *Ledger) DelLocal(addr basics.Address, appIdx basics.AppIndex, key string, accountIdx uint64) error {
+ br, ok := l.balances[addr]
+ if !ok {
+ return fmt.Errorf("no such address")
+ }
+ tkv, ok := br.locals[appIdx]
+ if !ok {
+ return fmt.Errorf("no app for account")
+ }
+ exist := false
+ if _, ok := tkv[key]; ok {
+ exist = true
+ }
+
+ _, ok = br.mods[appIdx]
+ if !ok && !exist {
+ // nothing to delete
+ return nil
+ }
+ if !ok {
+ br.mods[appIdx] = make(map[string]basics.ValueDelta)
+ }
+ _, ok = br.mods[appIdx][key]
+ if ok || exist {
+ br.mods[appIdx][key] = basics.ValueDelta{Action: basics.DeleteAction}
+ }
+ return nil
+}
+
+// OptedIn returns whether an Address has opted into the app (usually
+// from NewLocals, but potentially from executing AVM inner
+// transactions.
+func (l *Ledger) OptedIn(addr basics.Address, appIdx basics.AppIndex) (bool, error) {
+ br, ok := l.balances[addr]
+ if !ok {
+ return false, fmt.Errorf("no such address")
+ }
+ _, ok = br.locals[appIdx]
+ return ok, nil
+}
+
+// AssetHolding gives the amount of an ASA held by an account, or
+// error if the account is not opted into the asset.
+func (l *Ledger) AssetHolding(addr basics.Address, assetID basics.AssetIndex) (basics.AssetHolding, error) {
+ if br, ok := l.balances[addr]; ok {
+ if asset, ok := br.holdings[assetID]; ok {
+ return asset, nil
+ }
+ return basics.AssetHolding{}, fmt.Errorf("No asset for account")
+ }
+ return basics.AssetHolding{}, fmt.Errorf("no such address")
+}
+
+// AssetParams gives the parameters of an ASA if it exists
+func (l *Ledger) AssetParams(assetID basics.AssetIndex) (basics.AssetParams, basics.Address, error) {
+ if asset, ok := l.assets[assetID]; ok {
+ return asset.AssetParams, asset.Creator, nil
+ }
+ return basics.AssetParams{}, basics.Address{}, fmt.Errorf("no such asset")
+}
+
+// AppParams gives the parameters of an App if it exists
+func (l *Ledger) AppParams(appID basics.AppIndex) (basics.AppParams, basics.Address, error) {
+ if app, ok := l.applications[appID]; ok {
+ return app.AppParams, app.Creator, nil
+ }
+ return basics.AppParams{}, basics.Address{}, fmt.Errorf("no such app %d", appID)
+}
+
+func (l *Ledger) move(from basics.Address, to basics.Address, amount uint64) error {
+ fbr, ok := l.balances[from]
+ if !ok {
+ fbr = makeBalanceRecord(from, 0)
+ }
+ tbr, ok := l.balances[to]
+ if !ok {
+ tbr = makeBalanceRecord(to, 0)
+ }
+ if fbr.balance < amount {
+ return fmt.Errorf("insufficient balance")
+ }
+ fbr.balance -= amount
+ tbr.balance += amount
+ // We do not check min balances yet. They are checked when txn is complete.
+ l.balances[from] = fbr
+ l.balances[to] = tbr
+ return nil
+}
+
+func (l *Ledger) rekey(tx *transactions.Transaction) error {
+ // rekeying: update br.auth to tx.RekeyTo if provided
+ if (tx.RekeyTo != basics.Address{}) {
+ br, ok := l.balances[tx.Sender]
+ if !ok {
+ return fmt.Errorf("no account")
+ }
+ if tx.RekeyTo == tx.Sender {
+ br.auth = basics.Address{}
+ } else {
+ br.auth = tx.RekeyTo
+ }
+ l.balances[tx.Sender] = br
+ }
+ return nil
+}
+
+func (l *Ledger) pay(from basics.Address, pay transactions.PaymentTxnFields) error {
+ err := l.move(from, pay.Receiver, pay.Amount.Raw)
+ if err != nil {
+ return err
+ }
+ if !pay.CloseRemainderTo.IsZero() {
+ sbr := l.balances[from]
+ if len(sbr.holdings) > 0 {
+ return fmt.Errorf("unable to close, Sender (%s) has holdings", from)
+ }
+ if len(sbr.locals) > 0 {
+ return fmt.Errorf("unable to close, Sender (%s) is opted in to apps", from)
+ }
+ // Should also check app creations.
+ // Need not check asa creations, as you can't opt out if you created.
+ // (though this test ledger doesn't know that)
+ remainder := sbr.balance
+ if remainder > 0 {
+ return l.move(from, pay.CloseRemainderTo, remainder)
+ }
+ }
+ return nil
+}
+
+func (l *Ledger) axfer(from basics.Address, xfer transactions.AssetTransferTxnFields) error {
+ to := xfer.AssetReceiver
+ aid := xfer.XferAsset
+ amount := xfer.AssetAmount
+ close := xfer.AssetCloseTo
+
+ fbr, ok := l.balances[from]
+ if !ok {
+ fbr = makeBalanceRecord(from, 0)
+ }
+ fholding, ok := fbr.holdings[aid]
+ if !ok {
+ if from == to && amount == 0 {
+ // opt in
+ if params, exists := l.assets[aid]; exists {
+ fbr.holdings[aid] = basics.AssetHolding{
+ Frozen: params.DefaultFrozen,
+ }
+ return nil
+ }
+ return fmt.Errorf("Asset (%d) does not exist", aid)
+ }
+ return fmt.Errorf("Sender (%s) not opted in to %d", from, aid)
+ }
+ if fholding.Frozen {
+ return fmt.Errorf("Sender (%s) is frozen for %d", from, aid)
+ }
+ tbr, ok := l.balances[to]
+ if !ok {
+ tbr = makeBalanceRecord(to, 0)
+ }
+ tholding, ok := tbr.holdings[aid]
+ if !ok && amount > 0 {
+ return fmt.Errorf("AssetReceiver (%s) not opted in to %d", to, aid)
+ }
+ if fholding.Amount < amount {
+ return fmt.Errorf("insufficient balance")
+ }
+
+ // Not just an optimization.
+ // amount >0 : allows axfer to not opted in account
+ // from != to : prevents overwriting the same balance record with only
+ // the second change, and ensures fholding remains correct
+ // for closeTo handling.
+ if amount > 0 && from != to {
+ fholding.Amount -= amount
+ fbr.holdings[aid] = fholding
+ l.balances[from] = fbr
+
+ tholding.Amount += amount
+ tbr.holdings[aid] = tholding
+ l.balances[to] = tbr
+ }
+
+ if !close.IsZero() && fholding.Amount > 0 {
+ cbr, ok := l.balances[close]
+ if !ok {
+ cbr = makeBalanceRecord(close, 0)
+ }
+ cholding, ok := cbr.holdings[aid]
+ if !ok {
+ return fmt.Errorf("AssetCloseTo (%s) not opted in to %d", to, aid)
+ }
+
+ // Opt out
+ delete(fbr.holdings, aid)
+ l.balances[from] = fbr
+
+ cholding.Amount += fholding.Amount
+ cbr.holdings[aid] = cholding
+ l.balances[close] = cbr
+ }
+
+ return nil
+}
+
+func (l *Ledger) acfg(from basics.Address, cfg transactions.AssetConfigTxnFields, ad *transactions.ApplyData) error {
+ if cfg.ConfigAsset == 0 {
+ aid := basics.AssetIndex(l.Counter())
+ l.NewAsset(from, aid, cfg.AssetParams)
+ ad.ConfigAsset = aid
+ return nil
+ }
+ // This is just a mock. We don't check all the rules about
+ // not setting fields that have been zeroed. Nor do we keep
+ // anything from before.
+ l.assets[cfg.ConfigAsset] = asaParams{
+ Creator: from,
+ AssetParams: cfg.AssetParams,
+ }
+ return nil
+}
+
+func (l *Ledger) afrz(from basics.Address, frz transactions.AssetFreezeTxnFields) error {
+ aid := frz.FreezeAsset
+ params, ok := l.assets[aid]
+ if !ok {
+ return fmt.Errorf("Asset (%d) does not exist", aid)
+ }
+ if params.Freeze != from {
+ return fmt.Errorf("Asset (%d) can not be frozen by %s", aid, from)
+ }
+ br, ok := l.balances[frz.FreezeAccount]
+ if !ok {
+ return fmt.Errorf("%s does not hold Asset (%d)", frz.FreezeAccount, aid)
+ }
+ holding, ok := br.holdings[aid]
+ if !ok {
+ return fmt.Errorf("%s does not hold Asset (%d)", frz.FreezeAccount, aid)
+ }
+ holding.Frozen = frz.AssetFrozen
+ br.holdings[aid] = holding
+ return nil
+}
+
+func (l *Ledger) appl(from basics.Address, appl transactions.ApplicationCallTxnFields, ad *transactions.ApplyData, gi int, ep *EvalParams) error {
+ aid := appl.ApplicationID
+ if aid == 0 {
+ aid = basics.AppIndex(l.Counter())
+ params := basics.AppParams{
+ ApprovalProgram: appl.ApprovalProgram,
+ ClearStateProgram: appl.ClearStateProgram,
+ GlobalState: map[string]basics.TealValue{},
+ StateSchemas: basics.StateSchemas{
+ LocalStateSchema: basics.StateSchema{
+ NumUint: appl.LocalStateSchema.NumUint,
+ NumByteSlice: appl.LocalStateSchema.NumByteSlice,
+ },
+ GlobalStateSchema: basics.StateSchema{
+ NumUint: appl.GlobalStateSchema.NumUint,
+ NumByteSlice: appl.GlobalStateSchema.NumByteSlice,
+ },
+ },
+ ExtraProgramPages: appl.ExtraProgramPages,
+ }
+ l.NewApp(from, aid, params)
+ ad.ApplicationID = aid
+ }
+
+ if appl.OnCompletion == transactions.OptInOC {
+ br, ok := l.balances[from]
+ if !ok {
+ return errors.New("no account")
+ }
+ br.locals[aid] = make(map[string]basics.TealValue)
+ }
+
+ // Execute the Approval program
+ params, ok := l.applications[aid]
+ if !ok {
+ return errors.New("No application")
+ }
+ pass, cx, err := EvalContract(params.ApprovalProgram, gi, aid, ep)
+ if err != nil {
+ return err
+ }
+ if !pass {
+ return errors.New("Approval program failed")
+ }
+ ad.EvalDelta = cx.Txn.EvalDelta
+
+ switch appl.OnCompletion {
+ case transactions.NoOpOC:
+ case transactions.OptInOC:
+ // done earlier so locals could be changed
+ case transactions.CloseOutOC:
+ // get the local state, error if not exists, delete it
+ br, ok := l.balances[from]
+ if !ok {
+ return errors.New("no account")
+ }
+ _, ok = br.locals[aid]
+ if !ok {
+ return errors.New("not opted in")
+ }
+ delete(br.locals, aid)
+ case transactions.DeleteApplicationOC:
+ // get the global object, delete it
+ _, ok := l.applications[aid]
+ if !ok {
+ return errors.New("no app")
+ }
+ delete(l.applications, aid)
+ case transactions.UpdateApplicationOC:
+ app, ok := l.applications[aid]
+ if !ok {
+ return errors.New("no app")
+ }
+ app.ApprovalProgram = appl.ApprovalProgram
+ app.ClearStateProgram = appl.ClearStateProgram
+ l.applications[aid] = app
+ }
+ return nil
+}
+
+// Perform causes txn to "occur" against the ledger.
+func (l *Ledger) Perform(gi int, ep *EvalParams) error {
+ txn := &ep.TxnGroup[gi]
+ err := l.move(txn.Txn.Sender, ep.Specials.FeeSink, txn.Txn.Fee.Raw)
+ if err != nil {
+ return err
+ }
+
+ err = l.rekey(&txn.Txn)
+ if err != nil {
+ return err
+ }
+
+ switch txn.Txn.Type {
+ case protocol.PaymentTx:
+ return l.pay(txn.Txn.Sender, txn.Txn.PaymentTxnFields)
+ case protocol.AssetTransferTx:
+ return l.axfer(txn.Txn.Sender, txn.Txn.AssetTransferTxnFields)
+ case protocol.AssetConfigTx:
+ return l.acfg(txn.Txn.Sender, txn.Txn.AssetConfigTxnFields, &txn.ApplyData)
+ case protocol.AssetFreezeTx:
+ return l.afrz(txn.Txn.Sender, txn.Txn.AssetFreezeTxnFields)
+ case protocol.ApplicationCallTx:
+ return l.appl(txn.Txn.Sender, txn.Txn.ApplicationCallTxnFields, &txn.ApplyData, gi, ep)
+ default:
+ return fmt.Errorf("%s txn in AVM", txn.Txn.Type)
+ }
+}
+
+// Get() through allocated() implement cowForLogicLedger, so we should
+// be able to make logicLedger with this inside. That let's us to
+// write tests and then poke around and see how the balance table
+// inside is affected.
+
+// Get returns the AccountData of an address. This test ledger does
+// not handle rewards, so the pening rewards flag is ignored.
+func (l *Ledger) Get(addr basics.Address, withPendingRewards bool) (basics.AccountData, error) {
+ br, ok := l.balances[addr]
+ if !ok {
+ return basics.AccountData{}, fmt.Errorf("addr %s not in test.Ledger", addr.String())
+ }
+ return basics.AccountData{
+ MicroAlgos: basics.MicroAlgos{Raw: br.balance},
+ AssetParams: map[basics.AssetIndex]basics.AssetParams{},
+ Assets: map[basics.AssetIndex]basics.AssetHolding{},
+ AppLocalStates: map[basics.AppIndex]basics.AppLocalState{},
+ AppParams: map[basics.AppIndex]basics.AppParams{},
+ }, nil
+}
+
+// GetCreator returns the creator of the given creatable, an app or asa.
+func (l *Ledger) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) {
+ if ctype == basics.AssetCreatable {
+ params, found := l.assets[basics.AssetIndex(cidx)]
+ return params.Creator, found, nil
+ }
+ if ctype == basics.AppCreatable {
+ params, found := l.applications[basics.AppIndex(cidx)]
+ return params.Creator, found, nil
+ }
+ return basics.Address{}, false, fmt.Errorf("%v %d is not in test.Ledger", ctype, cidx)
+}
+
+// SetKey creates a new key-value in {addr, aidx, global} storage
+func (l *Ledger) SetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, value basics.TealValue, accountIdx uint64) error {
+ if global {
+ l.NewGlobal(uint64(aidx), key, value)
+ } else {
+ l.NewLocal(addr, uint64(aidx), key, value)
+ }
+ return nil
+}
+
+// DelKey removes a key from {addr, aidx, global} storage
+func (l *Ledger) DelKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) error {
+ if global {
+ l.NoGlobal(uint64(aidx), key)
+ } else {
+ l.NoLocal(addr, uint64(aidx), key)
+ }
+ return nil
+}
+
+func (l *Ledger) round() basics.Round {
+ if l.rnd == basics.Round(0) {
+ l.rnd = basics.Round(rand.Uint32() + 1)
+ }
+ return l.rnd
+}