package full import ( "bytes" "context" "fmt" "strconv" cid "github.com/ipfs/go-cid" "github.com/ipfs/go-hamt-ipld" cbor "github.com/ipfs/go-ipld-cbor" cbg "github.com/whyrusleeping/cbor-gen" "go.uber.org/fx" "golang.org/x/xerrors" "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-amt-ipld/v2" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/actors" "github.com/filecoin-project/lotus/chain/beacon" "github.com/filecoin-project/lotus/chain/gen" "github.com/filecoin-project/lotus/chain/state" "github.com/filecoin-project/lotus/chain/stmgr" "github.com/filecoin-project/lotus/chain/store" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/vm" "github.com/filecoin-project/lotus/chain/wallet" "github.com/filecoin-project/lotus/lib/bufbstore" "github.com/filecoin-project/lotus/node/modules/dtypes" "github.com/filecoin-project/sector-storage/ffiwrapper" "github.com/filecoin-project/specs-actors/actors/abi" "github.com/filecoin-project/specs-actors/actors/abi/big" "github.com/filecoin-project/specs-actors/actors/builtin" "github.com/filecoin-project/specs-actors/actors/builtin/market" "github.com/filecoin-project/specs-actors/actors/builtin/miner" samsig "github.com/filecoin-project/specs-actors/actors/builtin/multisig" "github.com/filecoin-project/specs-actors/actors/builtin/power" ) type StateAPI struct { fx.In // TODO: the wallet here is only needed because we have the MinerCreateBlock // API attached to the state API. It probably should live somewhere better Wallet *wallet.Wallet ProofVerifier ffiwrapper.Verifier StateManager *stmgr.StateManager Chain *store.ChainStore Beacon beacon.RandomBeacon } func (a *StateAPI) StateNetworkName(ctx context.Context) (dtypes.NetworkName, error) { return stmgr.GetNetworkName(ctx, a.StateManager, a.Chain.GetHeaviestTipSet().ParentState()) } func (a *StateAPI) StateMinerSectors(ctx context.Context, addr address.Address, filter *abi.BitField, filterOut bool, tsk types.TipSetKey) ([]*api.ChainSectorInfo, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return stmgr.GetMinerSectorSet(ctx, a.StateManager, ts, addr, filter, filterOut) } func (a *StateAPI) StateMinerProvingSet(ctx context.Context, addr address.Address, tsk types.TipSetKey) ([]*api.ChainSectorInfo, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } var mas miner.State _, err = a.StateManager.LoadActorState(ctx, addr, &mas, ts) if err != nil { return nil, xerrors.Errorf("(get sset) failed to load miner actor state: %w", err) } return stmgr.GetProvingSetRaw(ctx, a.StateManager, mas) } func (a *StateAPI) StateMinerInfo(ctx context.Context, actor address.Address, tsk types.TipSetKey) (miner.MinerInfo, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return miner.MinerInfo{}, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return stmgr.StateMinerInfo(ctx, a.StateManager, ts, actor) } func (a *StateAPI) StateMinerDeadlines(ctx context.Context, m address.Address, tsk types.TipSetKey) (*miner.Deadlines, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return stmgr.GetMinerDeadlines(ctx, a.StateManager, ts, m) } func (a *StateAPI) StateMinerProvingDeadline(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*miner.DeadlineInfo, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } var mas miner.State _, err = a.StateManager.LoadActorState(ctx, addr, &mas, ts) if err != nil { return nil, xerrors.Errorf("(get sset) failed to load miner actor state: %w", err) } return miner.ComputeProvingPeriodDeadline(mas.ProvingPeriodStart, ts.Height()), nil } func (a *StateAPI) StateMinerFaults(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*abi.BitField, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return stmgr.GetMinerFaults(ctx, a.StateManager, ts, addr) } func (a *StateAPI) StateMinerRecoveries(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*abi.BitField, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return stmgr.GetMinerRecoveries(ctx, a.StateManager, ts, addr) } func (a *StateAPI) StateMinerPower(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*api.MinerPower, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } m, net, err := stmgr.GetPower(ctx, a.StateManager, ts, addr) if err != nil { return nil, err } return &api.MinerPower{ MinerPower: m, TotalPower: net, }, nil } func (a *StateAPI) StatePledgeCollateral(ctx context.Context, tsk types.TipSetKey) (types.BigInt, error) { /*ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return types.EmptyInt, xerrors.Errorf("loading tipset %s: %w", tsk, err) } param, err := actors.SerializeParams(&actors.PledgeCollateralParams{Size: types.NewInt(0)}) if err != nil { return types.NewInt(0), err } ret, aerr := a.StateManager.Call(ctx, &types.Message{ From: actors.StoragePowerAddress, To: actors.StoragePowerAddress, Method: actors.SPAMethods.PledgeCollateralForSize, Params: param, }, ts) if aerr != nil { return types.NewInt(0), xerrors.Errorf("failed to get miner worker addr: %w", err) } if ret.MsgRct.ExitCode != 0 { return types.NewInt(0), xerrors.Errorf("failed to get miner worker addr (exit code %d)", ret.MsgRct.ExitCode) } return types.BigFromBytes(ret.Return), nil*/ log.Error("TODO StatePledgeCollateral") return big.Zero(), nil } func (a *StateAPI) StateCall(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (*api.InvocResult, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return a.StateManager.Call(ctx, msg, ts) } func (a *StateAPI) StateReplay(ctx context.Context, tsk types.TipSetKey, mc cid.Cid) (*api.InvocResult, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } m, r, err := a.StateManager.Replay(ctx, ts, mc) if err != nil { return nil, err } var errstr string if r.ActorErr != nil { errstr = r.ActorErr.Error() } return &api.InvocResult{ Msg: m, MsgRct: &r.MessageReceipt, InternalExecutions: r.InternalExecutions, Error: errstr, Duration: r.Duration, }, nil } func (a *StateAPI) stateForTs(ctx context.Context, ts *types.TipSet) (*state.StateTree, error) { if ts == nil { ts = a.Chain.GetHeaviestTipSet() } st, _, err := a.StateManager.TipSetState(ctx, ts) if err != nil { return nil, err } buf := bufbstore.NewBufferedBstore(a.Chain.Blockstore()) cst := cbor.NewCborStore(buf) return state.LoadStateTree(cst, st) } func (a *StateAPI) StateGetActor(ctx context.Context, actor address.Address, tsk types.TipSetKey) (*types.Actor, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } state, err := a.stateForTs(ctx, ts) if err != nil { return nil, xerrors.Errorf("computing tipset state failed: %w", err) } return state.GetActor(actor) } func (a *StateAPI) StateLookupID(ctx context.Context, addr address.Address, tsk types.TipSetKey) (address.Address, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return address.Undef, xerrors.Errorf("loading tipset %s: %w", tsk, err) } state, err := a.stateForTs(ctx, ts) if err != nil { return address.Undef, err } return state.LookupID(addr) } func (a *StateAPI) StateAccountKey(ctx context.Context, addr address.Address, tsk types.TipSetKey) (address.Address, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return address.Undef, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return a.StateManager.ResolveToKeyAddress(ctx, addr, ts) } func (a *StateAPI) StateReadState(ctx context.Context, act *types.Actor, tsk types.TipSetKey) (*api.ActorState, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } state, err := a.stateForTs(ctx, ts) if err != nil { return nil, err } blk, err := state.Store.(*cbor.BasicIpldStore).Blocks.Get(act.Head) if err != nil { return nil, err } oif, err := vm.DumpActorState(act.Code, blk.RawData()) if err != nil { return nil, err } return &api.ActorState{ Balance: act.Balance, State: oif, }, nil } // This is on StateAPI because miner.Miner requires this, and MinerAPI requires miner.Miner func (a *StateAPI) MinerGetBaseInfo(ctx context.Context, maddr address.Address, epoch abi.ChainEpoch, tsk types.TipSetKey) (*api.MiningBaseInfo, error) { return stmgr.MinerGetBaseInfo(ctx, a.StateManager, a.Beacon, tsk, epoch, maddr, a.ProofVerifier) } func (a *StateAPI) MinerCreateBlock(ctx context.Context, bt *api.BlockTemplate) (*types.BlockMsg, error) { fblk, err := gen.MinerCreateBlock(ctx, a.StateManager, a.Wallet, bt) if err != nil { return nil, err } var out types.BlockMsg out.Header = fblk.Header for _, msg := range fblk.BlsMessages { out.BlsMessages = append(out.BlsMessages, msg.Cid()) } for _, msg := range fblk.SecpkMessages { out.SecpkMessages = append(out.SecpkMessages, msg.Cid()) } return &out, nil } func (a *StateAPI) StateWaitMsg(ctx context.Context, msg cid.Cid) (*api.MsgLookup, error) { // TODO: consider using event system for this, expose confidence ts, recpt, err := a.StateManager.WaitForMessage(ctx, msg) if err != nil { return nil, err } return &api.MsgLookup{ Receipt: *recpt, TipSet: ts, }, nil } func (a *StateAPI) StateSearchMsg(ctx context.Context, msg cid.Cid) (*api.MsgLookup, error) { ts, recpt, err := a.StateManager.SearchForMessage(ctx, msg) if err != nil { return nil, err } if ts != nil { return &api.MsgLookup{ Receipt: *recpt, TipSet: ts, }, nil } else { return nil, nil } } func (a *StateAPI) StateGetReceipt(ctx context.Context, msg cid.Cid, tsk types.TipSetKey) (*types.MessageReceipt, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return a.StateManager.GetReceipt(ctx, msg, ts) } func (a *StateAPI) StateListMiners(ctx context.Context, tsk types.TipSetKey) ([]address.Address, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return stmgr.ListMinerActors(ctx, a.StateManager, ts) } func (a *StateAPI) StateListActors(ctx context.Context, tsk types.TipSetKey) ([]address.Address, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return a.StateManager.ListAllActors(ctx, ts) } func (a *StateAPI) StateMarketBalance(ctx context.Context, addr address.Address, tsk types.TipSetKey) (api.MarketBalance, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return api.MarketBalance{}, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return a.StateManager.MarketBalance(ctx, addr, ts) } func (a *StateAPI) StateMarketParticipants(ctx context.Context, tsk types.TipSetKey) (map[string]api.MarketBalance, error) { out := map[string]api.MarketBalance{} var state market.State ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } if _, err := a.StateManager.LoadActorState(ctx, builtin.StorageMarketActorAddr, &state, ts); err != nil { return nil, err } cst := cbor.NewCborStore(a.StateManager.ChainStore().Blockstore()) escrow, err := hamt.LoadNode(ctx, cst, state.EscrowTable, hamt.UseTreeBitWidth(5)) // todo: adt map if err != nil { return nil, err } locked, err := hamt.LoadNode(ctx, cst, state.EscrowTable, hamt.UseTreeBitWidth(5)) if err != nil { return nil, err } err = escrow.ForEach(ctx, func(k string, val interface{}) error { cv := val.(*cbg.Deferred) a, err := address.NewFromBytes([]byte(k)) if err != nil { return err } var es abi.TokenAmount if err := es.UnmarshalCBOR(bytes.NewReader(cv.Raw)); err != nil { return err } var lk abi.TokenAmount if err := locked.Find(ctx, k, &es); err != nil { return err } out[a.String()] = api.MarketBalance{ Escrow: es, Locked: lk, } return nil }) if err != nil { return nil, err } return out, nil } func (a *StateAPI) StateMarketDeals(ctx context.Context, tsk types.TipSetKey) (map[string]api.MarketDeal, error) { out := map[string]api.MarketDeal{} var state market.State ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } if _, err := a.StateManager.LoadActorState(ctx, builtin.StorageMarketActorAddr, &state, ts); err != nil { return nil, err } blks := cbor.NewCborStore(a.StateManager.ChainStore().Blockstore()) da, err := amt.LoadAMT(ctx, blks, state.Proposals) if err != nil { return nil, err } sa, err := amt.LoadAMT(ctx, blks, state.States) if err != nil { return nil, err } if err := da.ForEach(ctx, func(i uint64, v *cbg.Deferred) error { var d market.DealProposal if err := d.UnmarshalCBOR(bytes.NewReader(v.Raw)); err != nil { return err } var s market.DealState if err := sa.Get(ctx, i, &s); err != nil { if err != nil { if _, ok := err.(*amt.ErrNotFound); !ok { return xerrors.Errorf("failed to get state for deal in proposals array: %w", err) } s.SectorStartEpoch = -1 } } out[strconv.FormatInt(int64(i), 10)] = api.MarketDeal{ Proposal: d, State: s, } return nil }); err != nil { return nil, err } return out, nil } func (a *StateAPI) StateMarketStorageDeal(ctx context.Context, dealId abi.DealID, tsk types.TipSetKey) (*api.MarketDeal, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return stmgr.GetStorageDeal(ctx, a.StateManager, dealId, ts) } func (a *StateAPI) StateChangedActors(ctx context.Context, old cid.Cid, new cid.Cid) (map[string]types.Actor, error) { cst := cbor.NewCborStore(a.Chain.Blockstore()) nh, err := hamt.LoadNode(ctx, cst, new, hamt.UseTreeBitWidth(5)) if err != nil { return nil, err } oh, err := hamt.LoadNode(ctx, cst, old, hamt.UseTreeBitWidth(5)) if err != nil { return nil, err } out := map[string]types.Actor{} err = nh.ForEach(ctx, func(k string, nval interface{}) error { ncval := nval.(*cbg.Deferred) var act types.Actor var ocval cbg.Deferred switch err := oh.Find(ctx, k, &ocval); err { case nil: if bytes.Equal(ocval.Raw, ncval.Raw) { return nil // not changed } fallthrough case hamt.ErrNotFound: if err := act.UnmarshalCBOR(bytes.NewReader(ncval.Raw)); err != nil { return err } addr, err := address.NewFromBytes([]byte(k)) if err != nil { return xerrors.Errorf("address in state tree was not valid: %w", err) } out[addr.String()] = act default: return err } return nil }) if err != nil { return nil, err } return out, nil } func (a *StateAPI) StateMinerSectorCount(ctx context.Context, addr address.Address, tsk types.TipSetKey) (api.MinerSectors, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return api.MinerSectors{}, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return stmgr.SectorSetSizes(ctx, a.StateManager, addr, ts) } func (a *StateAPI) StateSectorPreCommitInfo(ctx context.Context, maddr address.Address, n abi.SectorNumber, tsk types.TipSetKey) (miner.SectorPreCommitOnChainInfo, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return miner.SectorPreCommitOnChainInfo{}, xerrors.Errorf("loading tipset %s: %w", tsk, err) } return stmgr.PreCommitInfo(ctx, a.StateManager, maddr, n, ts) } func (a *StateAPI) StateListMessages(ctx context.Context, match *types.Message, tsk types.TipSetKey, toheight abi.ChainEpoch) ([]cid.Cid, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } if ts == nil { ts = a.Chain.GetHeaviestTipSet() } if match.To == address.Undef && match.From == address.Undef { return nil, xerrors.Errorf("must specify at least To or From in message filter") } matchFunc := func(msg *types.Message) bool { if match.From != address.Undef && match.From != msg.From { return false } if match.To != address.Undef && match.To != msg.To { return false } return true } var out []cid.Cid for ts.Height() >= toheight { msgs, err := a.Chain.MessagesForTipset(ts) if err != nil { return nil, xerrors.Errorf("failed to get messages for tipset (%s): %w", ts.Key(), err) } for _, msg := range msgs { if matchFunc(msg.VMMessage()) { out = append(out, msg.Cid()) } } if ts.Height() == 0 { break } next, err := a.Chain.LoadTipSet(ts.Parents()) if err != nil { return nil, xerrors.Errorf("loading next tipset: %w", err) } ts = next } return out, nil } func (a *StateAPI) StateCompute(ctx context.Context, height abi.ChainEpoch, msgs []*types.Message, tsk types.TipSetKey) (*api.ComputeStateOutput, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return nil, xerrors.Errorf("loading tipset %s: %w", tsk, err) } st, t, err := stmgr.ComputeState(ctx, a.StateManager, height, msgs, ts) if err != nil { return nil, err } return &api.ComputeStateOutput{ Root: st, Trace: t, }, nil } func (a *StateAPI) MsigGetAvailableBalance(ctx context.Context, addr address.Address, tsk types.TipSetKey) (types.BigInt, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return types.EmptyInt, xerrors.Errorf("loading tipset %s: %w", tsk, err) } var st samsig.State act, err := a.StateManager.LoadActorState(ctx, addr, &st, ts) if err != nil { return types.EmptyInt, xerrors.Errorf("failed to load multisig actor state: %w", err) } if act.Code != builtin.MultisigActorCodeID { return types.EmptyInt, fmt.Errorf("given actor was not a multisig") } if st.UnlockDuration == 0 { return act.Balance, nil } offset := ts.Height() - st.StartEpoch if offset > st.UnlockDuration { return act.Balance, nil } minBalance := types.BigDiv(types.BigInt(st.InitialBalance), types.NewInt(uint64(st.UnlockDuration))) minBalance = types.BigMul(minBalance, types.NewInt(uint64(offset))) return types.BigSub(act.Balance, minBalance), nil } var initialPledgeNum = types.NewInt(103) var initialPledgeDen = types.NewInt(100) func (a *StateAPI) StateMinerInitialPledgeCollateral(ctx context.Context, maddr address.Address, snum abi.SectorNumber, tsk types.TipSetKey) (types.BigInt, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return types.EmptyInt, xerrors.Errorf("loading tipset %s: %w", tsk, err) } act, err := a.StateManager.GetActor(maddr, ts) if err != nil { return types.EmptyInt, err } as := store.ActorStore(ctx, a.Chain.Blockstore()) var st miner.State if err := as.Get(ctx, act.Head, &st); err != nil { return types.EmptyInt, err } precommit, found, err := st.GetPrecommittedSector(as, snum) if err != nil { return types.EmptyInt, err } if !found { return types.EmptyInt, xerrors.Errorf("no precommit found for sector %d", snum) } var dealWeights market.VerifyDealsOnSectorProveCommitReturn { var err error params, err := actors.SerializeParams(&market.VerifyDealsOnSectorProveCommitParams{ DealIDs: precommit.Info.DealIDs, SectorExpiry: precommit.Info.Expiration, }) if err != nil { return types.EmptyInt, err } ret, err := a.StateManager.Call(ctx, &types.Message{ From: maddr, To: builtin.StorageMarketActorAddr, Method: builtin.MethodsMarket.VerifyDealsOnSectorProveCommit, GasLimit: 100000000000, GasPrice: types.NewInt(0), Params: params, }, ts) if err != nil { return types.EmptyInt, err } if err := dealWeights.UnmarshalCBOR(bytes.NewReader(ret.MsgRct.Return)); err != nil { return types.BigInt{}, err } } initialPledge := big.Zero() { ssize, err := precommit.Info.RegisteredProof.SectorSize() if err != nil { return types.EmptyInt, err } params, err := actors.SerializeParams(&power.OnSectorProveCommitParams{ Weight: power.SectorStorageWeightDesc{ SectorSize: ssize, Duration: precommit.Info.Expiration - ts.Height(), // NB: not exactly accurate, but should always lead us to *over* estimate, not under DealWeight: dealWeights.DealWeight, VerifiedDealWeight: dealWeights.VerifiedDealWeight, }, }) if err != nil { return types.EmptyInt, err } ret, err := a.StateManager.Call(ctx, &types.Message{ From: maddr, To: builtin.StoragePowerActorAddr, Method: builtin.MethodsPower.OnSectorProveCommit, GasLimit: 10000000000, GasPrice: types.NewInt(0), Params: params, }, ts) if err != nil { return types.EmptyInt, err } if err := initialPledge.UnmarshalCBOR(bytes.NewReader(ret.MsgRct.Return)); err != nil { return types.BigInt{}, err } } return types.BigDiv(types.BigMul(initialPledge, initialPledgeNum), initialPledgeDen), nil } func (a *StateAPI) StateMinerAvailableBalance(ctx context.Context, maddr address.Address, tsk types.TipSetKey) (types.BigInt, error) { ts, err := a.Chain.GetTipSetFromKey(tsk) if err != nil { return types.EmptyInt, xerrors.Errorf("loading tipset %s: %w", tsk, err) } act, err := a.StateManager.GetActor(maddr, ts) if err != nil { return types.EmptyInt, err } as := store.ActorStore(ctx, a.Chain.Blockstore()) var st miner.State if err := as.Get(ctx, act.Head, &st); err != nil { return types.EmptyInt, err } // TODO: !!!! Use method that doesnt trigger additional state mutations, this is going to cause lots of objects to be created and written to disk log.Warnf("calling inefficient unlock vested funds method, fixme") vested, err := st.UnlockVestedFunds(as, ts.Height()) if err != nil { return types.EmptyInt, err } return types.BigAdd(st.GetAvailableBalance(act.Balance), vested), nil }