package account
import (
- "context"
"encoding/json"
"strings"
"sync"
- "time"
"github.com/golang/groupcache/lru"
log "github.com/sirupsen/logrus"
"github.com/bytom/crypto/sha3pool"
"github.com/bytom/errors"
"github.com/bytom/protocol"
+ "github.com/bytom/protocol/bc"
"github.com/bytom/protocol/vm/vmutil"
)
return append(contractIndexPrefix, []byte(accountID)...)
}
-// NewManager creates a new account manager
-func NewManager(walletDB dbm.DB, chain *protocol.Chain) *Manager {
- return &Manager{
- db: walletDB,
- chain: chain,
- utxoDB: newReserver(chain, walletDB),
- cache: lru.New(maxAccountCache),
- aliasCache: lru.New(maxAccountCache),
- delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram),
- }
+// Account is structure of Bytom account
+type Account struct {
+ *signers.Signer
+ ID string `json:"id"`
+ Alias string `json:"alias"`
+}
+
+//CtrlProgram is structure of account control program
+type CtrlProgram struct {
+ AccountID string
+ Address string
+ KeyIndex uint64
+ ControlProgram []byte
+ Change bool // Mark whether this control program is for UTXO change
}
// Manager stores accounts and their associated control programs.
type Manager struct {
- db dbm.DB
- chain *protocol.Chain
- utxoDB *reserver
+ db dbm.DB
+ chain *protocol.Chain
+ utxoKeeper *utxoKeeper
cacheMu sync.Mutex
cache *lru.Cache
accountMu sync.Mutex
}
-// ExpireReservations removes reservations that have expired periodically.
-// It blocks until the context is canceled.
-func (m *Manager) ExpireReservations(ctx context.Context, period time.Duration) {
- ticks := time.Tick(period)
- for {
- select {
- case <-ctx.Done():
- log.Info("Deposed, ExpireReservations exiting")
- return
- case <-ticks:
- err := m.utxoDB.ExpireReservations(ctx)
- if err != nil {
- log.WithField("error", err).Error("Expire reservations")
- }
- }
- }
-}
-
-// Account is structure of Bytom account
-type Account struct {
- *signers.Signer
- ID string `json:"id"`
- Alias string `json:"alias"`
-}
-
-func (m *Manager) getNextAccountIndex() uint64 {
- m.accIndexMu.Lock()
- defer m.accIndexMu.Unlock()
-
- var nextIndex uint64 = 1
- if rawIndexBytes := m.db.Get(accountIndexKey); rawIndexBytes != nil {
- nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
- }
-
- m.db.Set(accountIndexKey, common.Unit64ToBytes(nextIndex))
- return nextIndex
-}
-
-func (m *Manager) getNextContractIndex(accountID string) uint64 {
- m.accIndexMu.Lock()
- defer m.accIndexMu.Unlock()
-
- nextIndex := uint64(1)
- if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
- nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
+// NewManager creates a new account manager
+func NewManager(walletDB dbm.DB, chain *protocol.Chain) *Manager {
+ return &Manager{
+ db: walletDB,
+ chain: chain,
+ utxoKeeper: newUtxoKeeper(chain.BestBlockHeight, walletDB),
+ cache: lru.New(maxAccountCache),
+ aliasCache: lru.New(maxAccountCache),
+ delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram),
}
-
- m.db.Set(contractIndexKey(accountID), common.Unit64ToBytes(nextIndex))
- return nextIndex
}
-// GetContractIndex return the current index
-func (m *Manager) GetContractIndex(accountID string) uint64 {
- m.accIndexMu.Lock()
- defer m.accIndexMu.Unlock()
-
- index := uint64(1)
- if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
- index = common.BytesToUnit64(rawIndexBytes)
- }
- return index
+// AddUnconfirmedUtxo add untxo list to utxoKeeper
+func (m *Manager) AddUnconfirmedUtxo(utxos []*UTXO) {
+ m.utxoKeeper.AddUnconfirmedUtxo(utxos)
}
// Create creates a new Account.
-func (m *Manager) Create(ctx context.Context, xpubs []chainkd.XPub, quorum int, alias string) (*Account, error) {
+func (m *Manager) Create(xpubs []chainkd.XPub, quorum int, alias string) (*Account, error) {
m.accountMu.Lock()
defer m.accountMu.Unlock()
if err != nil {
return nil, ErrMarshalAccount
}
- storeBatch := m.db.NewBatch()
accountID := Key(id)
+ storeBatch := m.db.NewBatch()
storeBatch.Set(accountID, rawAccount)
storeBatch.Set(aliasKey(normalizedAlias), []byte(id))
storeBatch.Write()
-
return account, nil
}
+// CreateAddress generate an address for the select account
+func (m *Manager) CreateAddress(accountID string, change bool) (cp *CtrlProgram, err error) {
+ account, err := m.FindByID(accountID)
+ if err != nil {
+ return nil, err
+ }
+ return m.createAddress(account, change)
+}
+
+// DeleteAccount deletes the account's ID or alias matching accountInfo.
+func (m *Manager) DeleteAccount(aliasOrID string) (err error) {
+ account := &Account{}
+ if account, err = m.FindByAlias(aliasOrID); err != nil {
+ if account, err = m.FindByID(aliasOrID); err != nil {
+ return err
+ }
+ }
+
+ m.cacheMu.Lock()
+ m.aliasCache.Remove(account.Alias)
+ m.cacheMu.Unlock()
+
+ storeBatch := m.db.NewBatch()
+ storeBatch.Delete(aliasKey(account.Alias))
+ storeBatch.Delete(Key(account.ID))
+ storeBatch.Write()
+ return nil
+}
+
// FindByAlias retrieves an account's Signer record by its alias
-func (m *Manager) FindByAlias(ctx context.Context, alias string) (*Account, error) {
+func (m *Manager) FindByAlias(alias string) (*Account, error) {
m.cacheMu.Lock()
cachedID, ok := m.aliasCache.Get(alias)
m.cacheMu.Unlock()
if ok {
- return m.FindByID(ctx, cachedID.(string))
+ return m.FindByID(cachedID.(string))
}
rawID := m.db.Get(aliasKey(alias))
m.cacheMu.Lock()
m.aliasCache.Add(alias, accountID)
m.cacheMu.Unlock()
- return m.FindByID(ctx, accountID)
+ return m.FindByID(accountID)
}
// FindByID returns an account's Signer record by its ID.
-func (m *Manager) FindByID(ctx context.Context, id string) (*Account, error) {
+func (m *Manager) FindByID(id string) (*Account, error) {
m.cacheMu.Lock()
cachedAccount, ok := m.cache.Get(id)
m.cacheMu.Unlock()
return account, nil
}
-// GetAliasByID return the account alias by given ID
-func (m *Manager) GetAliasByID(id string) string {
+// GetAccountByProgram return Account by given CtrlProgram
+func (m *Manager) GetAccountByProgram(program *CtrlProgram) (*Account, error) {
+ rawAccount := m.db.Get(Key(program.AccountID))
+ if rawAccount == nil {
+ return nil, ErrFindAccount
+ }
+
account := &Account{}
+ return account, json.Unmarshal(rawAccount, account)
+}
+// GetAliasByID return the account alias by given ID
+func (m *Manager) GetAliasByID(id string) string {
rawAccount := m.db.Get(Key(id))
if rawAccount == nil {
- log.Warn("fail to find account")
+ log.Warn("GetAliasByID fail to find account")
return ""
}
+ account := &Account{}
if err := json.Unmarshal(rawAccount, account); err != nil {
log.Warn(err)
- return ""
}
return account.Alias
}
-// CreateAddress generate an address for the select account
-func (m *Manager) CreateAddress(ctx context.Context, accountID string, change bool) (cp *CtrlProgram, err error) {
- account, err := m.FindByID(ctx, accountID)
- if err != nil {
- return nil, err
- }
- return m.createAddress(ctx, account, change)
-}
-
-// CreateAddress generate an address for the select account
-func (m *Manager) createAddress(ctx context.Context, account *Account, change bool) (cp *CtrlProgram, err error) {
- if len(account.XPubs) == 1 {
- cp, err = m.createP2PKH(ctx, account, change)
- } else {
- cp, err = m.createP2SH(ctx, account, change)
- }
- if err != nil {
- return nil, err
- }
-
- if err = m.insertAccountControlProgram(ctx, cp); err != nil {
- return nil, err
- }
- return cp, nil
-}
-
-func (m *Manager) createP2PKH(ctx context.Context, account *Account, change bool) (*CtrlProgram, error) {
- idx := m.getNextContractIndex(account.ID)
- path := signers.Path(account.Signer, signers.AccountKeySpace, idx)
- derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
- derivedPK := derivedXPubs[0].PublicKey()
- pubHash := crypto.Ripemd160(derivedPK)
-
- // TODO: pass different params due to config
- address, err := common.NewAddressWitnessPubKeyHash(pubHash, &consensus.ActiveNetParams)
- if err != nil {
- return nil, err
- }
-
- control, err := vmutil.P2WPKHProgram([]byte(pubHash))
- if err != nil {
- return nil, err
- }
-
- return &CtrlProgram{
- AccountID: account.ID,
- Address: address.EncodeAddress(),
- KeyIndex: idx,
- ControlProgram: control,
- Change: change,
- }, nil
-}
-
-func (m *Manager) createP2SH(ctx context.Context, account *Account, change bool) (*CtrlProgram, error) {
- idx := m.getNextContractIndex(account.ID)
- path := signers.Path(account.Signer, signers.AccountKeySpace, idx)
- derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
- derivedPKs := chainkd.XPubKeys(derivedXPubs)
- signScript, err := vmutil.P2SPMultiSigProgram(derivedPKs, account.Quorum)
- if err != nil {
- return nil, err
- }
- scriptHash := crypto.Sha256(signScript)
-
- // TODO: pass different params due to config
- address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.ActiveNetParams)
- if err != nil {
- return nil, err
- }
-
- control, err := vmutil.P2WSHProgram(scriptHash)
- if err != nil {
- return nil, err
- }
-
- return &CtrlProgram{
- AccountID: account.ID,
- Address: address.EncodeAddress(),
- KeyIndex: idx,
- ControlProgram: control,
- Change: change,
- }, nil
-}
-
-//CtrlProgram is structure of account control program
-type CtrlProgram struct {
- AccountID string
- Address string
- KeyIndex uint64
- ControlProgram []byte
- Change bool // Mark whether this control program is for UTXO change
-}
-
-func (m *Manager) insertAccountControlProgram(ctx context.Context, progs ...*CtrlProgram) error {
- var hash common.Hash
- for _, prog := range progs {
- accountCP, err := json.Marshal(prog)
- if err != nil {
- return err
- }
-
- sha3pool.Sum256(hash[:], prog.ControlProgram)
- m.db.Set(ContractKey(hash), accountCP)
- }
- return nil
-}
-
-// IsLocalControlProgram check is the input control program belong to local
-func (m *Manager) IsLocalControlProgram(prog []byte) bool {
- var hash common.Hash
- sha3pool.Sum256(hash[:], prog)
- bytes := m.db.Get(ContractKey(hash))
- return bytes != nil
-}
-
// GetCoinbaseControlProgram will return a coinbase script
func (m *Manager) GetCoinbaseControlProgram() ([]byte, error) {
if data := m.db.Get(miningAddressKey); data != nil {
return nil, err
}
- program, err := m.createAddress(nil, account, false)
+ program, err := m.createAddress(account, false)
if err != nil {
return nil, err
}
return program.ControlProgram, nil
}
-// DeleteAccount deletes the account's ID or alias matching accountInfo.
-func (m *Manager) DeleteAccount(aliasOrID string) (err error) {
- account := &Account{}
- if account, err = m.FindByAlias(nil, aliasOrID); err != nil {
- if account, err = m.FindByID(nil, aliasOrID); err != nil {
- return err
- }
+// GetContractIndex return the current index
+func (m *Manager) GetContractIndex(accountID string) uint64 {
+ m.accIndexMu.Lock()
+ defer m.accIndexMu.Unlock()
+
+ index := uint64(1)
+ if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
+ index = common.BytesToUnit64(rawIndexBytes)
}
+ return index
+}
- storeBatch := m.db.NewBatch()
+// GetProgramByAddress return CtrlProgram by given address
+func (m *Manager) GetProgramByAddress(address string) (*CtrlProgram, error) {
+ addr, err := common.DecodeAddress(address, &consensus.ActiveNetParams)
+ if err != nil {
+ return nil, err
+ }
- m.cacheMu.Lock()
- m.aliasCache.Remove(account.Alias)
- m.cacheMu.Unlock()
+ redeemContract := addr.ScriptAddress()
+ program := []byte{}
+ switch addr.(type) {
+ case *common.AddressWitnessPubKeyHash:
+ program, err = vmutil.P2WPKHProgram(redeemContract)
+ case *common.AddressWitnessScriptHash:
+ program, err = vmutil.P2WSHProgram(redeemContract)
+ default:
+ return nil, ErrInvalidAddress
+ }
+ if err != nil {
+ return nil, err
+ }
- storeBatch.Delete(aliasKey(account.Alias))
- storeBatch.Delete(Key(account.ID))
- storeBatch.Write()
+ var hash [32]byte
+ sha3pool.Sum256(hash[:], program)
+ rawProgram := m.db.Get(ContractKey(hash))
+ if rawProgram == nil {
+ return nil, ErrFindCtrlProgram
+ }
- return nil
+ cp := &CtrlProgram{}
+ return cp, json.Unmarshal(rawProgram, cp)
+}
+
+// IsLocalControlProgram check is the input control program belong to local
+func (m *Manager) IsLocalControlProgram(prog []byte) bool {
+ var hash common.Hash
+ sha3pool.Sum256(hash[:], prog)
+ bytes := m.db.Get(ContractKey(hash))
+ return bytes != nil
}
// ListAccounts will return the accounts in the db
}
accounts = append(accounts, account)
}
-
return accounts, nil
}
// ListControlProgram return all the local control program
func (m *Manager) ListControlProgram() ([]*CtrlProgram, error) {
- var cps []*CtrlProgram
+ cps := []*CtrlProgram{}
cpIter := m.db.IteratorPrefix(contractPrefix)
defer cpIter.Release()
}
cps = append(cps, cp)
}
-
return cps, nil
}
-// GetProgramByAddress return CtrlProgram by given address
-func (m *Manager) GetProgramByAddress(address string) (*CtrlProgram, error) {
- addr, err := common.DecodeAddress(address, &consensus.ActiveNetParams)
+// RemoveUnconfirmedUtxo remove utxos from the utxoKeeper
+func (m *Manager) RemoveUnconfirmedUtxo(hashes []*bc.Hash) {
+ m.utxoKeeper.RemoveUnconfirmedUtxo(hashes)
+}
+
+// CreateAddress generate an address for the select account
+func (m *Manager) createAddress(account *Account, change bool) (cp *CtrlProgram, err error) {
+ if len(account.XPubs) == 1 {
+ cp, err = m.createP2PKH(account, change)
+ } else {
+ cp, err = m.createP2SH(account, change)
+ }
if err != nil {
return nil, err
}
+ return cp, m.insertControlPrograms(cp)
+}
- redeemContract := addr.ScriptAddress()
- program := []byte{}
- switch addr.(type) {
- case *common.AddressWitnessPubKeyHash:
- program, err = vmutil.P2WPKHProgram(redeemContract)
- case *common.AddressWitnessScriptHash:
- program, err = vmutil.P2WSHProgram(redeemContract)
- default:
- return nil, ErrInvalidAddress
+func (m *Manager) createP2PKH(account *Account, change bool) (*CtrlProgram, error) {
+ idx := m.getNextContractIndex(account.ID)
+ path := signers.Path(account.Signer, signers.AccountKeySpace, idx)
+ derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
+ derivedPK := derivedXPubs[0].PublicKey()
+ pubHash := crypto.Ripemd160(derivedPK)
+
+ address, err := common.NewAddressWitnessPubKeyHash(pubHash, &consensus.ActiveNetParams)
+ if err != nil {
+ return nil, err
}
+
+ control, err := vmutil.P2WPKHProgram([]byte(pubHash))
if err != nil {
return nil, err
}
- var hash [32]byte
- cp := &CtrlProgram{}
- sha3pool.Sum256(hash[:], program)
- rawProgram := m.db.Get(ContractKey(hash))
- if rawProgram == nil {
- return nil, ErrFindCtrlProgram
+ return &CtrlProgram{
+ AccountID: account.ID,
+ Address: address.EncodeAddress(),
+ KeyIndex: idx,
+ ControlProgram: control,
+ Change: change,
+ }, nil
+}
+
+func (m *Manager) createP2SH(account *Account, change bool) (*CtrlProgram, error) {
+ idx := m.getNextContractIndex(account.ID)
+ path := signers.Path(account.Signer, signers.AccountKeySpace, idx)
+ derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
+ derivedPKs := chainkd.XPubKeys(derivedXPubs)
+ signScript, err := vmutil.P2SPMultiSigProgram(derivedPKs, account.Quorum)
+ if err != nil {
+ return nil, err
+ }
+ scriptHash := crypto.Sha256(signScript)
+
+ address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.ActiveNetParams)
+ if err != nil {
+ return nil, err
}
- if err := json.Unmarshal(rawProgram, cp); err != nil {
+ control, err := vmutil.P2WSHProgram(scriptHash)
+ if err != nil {
return nil, err
}
- return cp, nil
+ return &CtrlProgram{
+ AccountID: account.ID,
+ Address: address.EncodeAddress(),
+ KeyIndex: idx,
+ ControlProgram: control,
+ Change: change,
+ }, nil
}
-// GetAccountByProgram return Account by given CtrlProgram
-func (m *Manager) GetAccountByProgram(program *CtrlProgram) (*Account, error) {
- rawAccount := m.db.Get(Key(program.AccountID))
- if rawAccount == nil {
- return nil, ErrFindAccount
+func (m *Manager) getNextAccountIndex() uint64 {
+ m.accIndexMu.Lock()
+ defer m.accIndexMu.Unlock()
+
+ var nextIndex uint64 = 1
+ if rawIndexBytes := m.db.Get(accountIndexKey); rawIndexBytes != nil {
+ nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
}
+ m.db.Set(accountIndexKey, common.Unit64ToBytes(nextIndex))
+ return nextIndex
+}
- account := &Account{}
- if err := json.Unmarshal(rawAccount, account); err != nil {
- return nil, err
+func (m *Manager) getNextContractIndex(accountID string) uint64 {
+ m.accIndexMu.Lock()
+ defer m.accIndexMu.Unlock()
+
+ nextIndex := uint64(1)
+ if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
+ nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
}
+ m.db.Set(contractIndexKey(accountID), common.Unit64ToBytes(nextIndex))
+ return nextIndex
+}
- return account, nil
+func (m *Manager) insertControlPrograms(progs ...*CtrlProgram) error {
+ var hash common.Hash
+ for _, prog := range progs {
+ accountCP, err := json.Marshal(prog)
+ if err != nil {
+ return err
+ }
+
+ sha3pool.Sum256(hash[:], prog.ControlProgram)
+ m.db.Set(ContractKey(hash), accountCP)
+ }
+ return nil
}
package account
import (
- "context"
"io/ioutil"
"os"
"strings"
func TestCreateAccountWithUppercase(t *testing.T) {
m := mockAccountManager(t)
alias := "UPPER"
- account, err := m.Create(nil, []chainkd.XPub{testutil.TestXPub}, 1, alias)
+ account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias)
if err != nil {
t.Fatal(err)
func TestCreateAccountWithSpaceTrimed(t *testing.T) {
m := mockAccountManager(t)
alias := " with space "
- account, err := m.Create(nil, []chainkd.XPub{testutil.TestXPub}, 1, alias)
+ account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias)
if err != nil {
t.Fatal(err)
t.Fatal("created account alias should be lowercase")
}
- nilAccount, err := m.FindByAlias(nil, alias)
+ nilAccount, err := m.FindByAlias(alias)
if nilAccount != nil {
t.Fatal("expected nil")
}
- target, err := m.FindByAlias(nil, strings.ToLower(strings.TrimSpace(alias)))
+ target, err := m.FindByAlias(strings.ToLower(strings.TrimSpace(alias)))
if target == nil {
t.Fatal("expected Account, but got nil")
}
func TestCreateAccount(t *testing.T) {
m := mockAccountManager(t)
- ctx := context.Background()
-
- account, err := m.Create(ctx, []chainkd.XPub{testutil.TestXPub}, 1, "test-alias")
+ account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias")
if err != nil {
testutil.FatalErr(t, err)
}
- found, err := m.FindByID(ctx, account.ID)
+ found, err := m.FindByID(account.ID)
if err != nil {
t.Errorf("unexpected error %v", err)
}
func TestCreateAccountReusedAlias(t *testing.T) {
m := mockAccountManager(t)
- ctx := context.Background()
- m.createTestAccount(ctx, t, "test-alias", nil)
+ m.createTestAccount(t, "test-alias", nil)
- _, err := m.Create(ctx, []chainkd.XPub{testutil.TestXPub}, 1, "test-alias")
+ _, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias")
if errors.Root(err) != ErrDuplicateAlias {
t.Errorf("expected %s when reusing an alias, got %v", ErrDuplicateAlias, err)
}
func TestDeleteAccount(t *testing.T) {
m := mockAccountManager(t)
- ctx := context.Background()
- account1, err := m.Create(ctx, []chainkd.XPub{testutil.TestXPub}, 1, "test-alias1")
+ account1, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias1")
if err != nil {
testutil.FatalErr(t, err)
}
- account2, err := m.Create(ctx, []chainkd.XPub{testutil.TestXPub}, 1, "test-alias2")
+ account2, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias2")
if err != nil {
testutil.FatalErr(t, err)
}
testutil.FatalErr(t, err)
}
- found, err := m.FindByID(ctx, account1.ID)
+ found, err := m.FindByID(account1.ID)
if err != nil {
t.Errorf("expected account %v should be deleted", found)
}
testutil.FatalErr(t, err)
}
- found, err = m.FindByID(ctx, account2.ID)
+ found, err = m.FindByID(account2.ID)
if err != nil {
t.Errorf("expected account %v should be deleted", found)
}
func TestFindByID(t *testing.T) {
m := mockAccountManager(t)
- ctx := context.Background()
- account := m.createTestAccount(ctx, t, "", nil)
+ account := m.createTestAccount(t, "", nil)
- found, err := m.FindByID(ctx, account.ID)
+ found, err := m.FindByID(account.ID)
if err != nil {
testutil.FatalErr(t, err)
}
func TestFindByAlias(t *testing.T) {
m := mockAccountManager(t)
- ctx := context.Background()
- account := m.createTestAccount(ctx, t, "some-alias", nil)
+ account := m.createTestAccount(t, "some-alias", nil)
- found, err := m.FindByAlias(ctx, "some-alias")
+ found, err := m.FindByAlias("some-alias")
if err != nil {
testutil.FatalErr(t, err)
}
return NewManager(testDB, chain)
}
-func (m *Manager) createTestAccount(ctx context.Context, t testing.TB, alias string, tags map[string]interface{}) *Account {
- account, err := m.Create(ctx, []chainkd.XPub{testutil.TestXPub}, 1, alias)
+func (m *Manager) createTestAccount(t testing.TB, alias string, tags map[string]interface{}) *Account {
+ account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias)
if err != nil {
testutil.FatalErr(t, err)
}
"encoding/hex"
"encoding/json"
- log "github.com/sirupsen/logrus"
-
"github.com/bytom/blockchain/signers"
"github.com/bytom/blockchain/txbuilder"
"github.com/bytom/common"
//DecodeSpendAction unmarshal JSON-encoded data of spend action
func (m *Manager) DecodeSpendAction(data []byte) (txbuilder.Action, error) {
a := &spendAction{accounts: m}
- err := json.Unmarshal(data, a)
- return a, err
+ return a, json.Unmarshal(data, a)
}
type spendAction struct {
accounts *Manager
bc.AssetAmount
- AccountID string `json:"account_id"`
+ AccountID string `json:"account_id"`
+ UseUnconfirmed bool `json:"use_unconfirmed"`
}
// MergeSpendAction merge common assetID and accountID spend action
actionKey := act.AssetId.String() + act.AccountID
if tmpAct, ok := spendActionMap[actionKey]; ok {
tmpAct.Amount += act.Amount
+ tmpAct.UseUnconfirmed = tmpAct.UseUnconfirmed || act.UseUnconfirmed
} else {
spendActionMap[actionKey] = act
resultActions = append(resultActions, act)
return txbuilder.MissingFieldsError(missing...)
}
- acct, err := a.accounts.FindByID(ctx, a.AccountID)
+ acct, err := a.accounts.FindByID(a.AccountID)
if err != nil {
return errors.Wrap(err, "get account info")
}
- src := source{
- AssetID: *a.AssetId,
- AccountID: a.AccountID,
- }
- res, err := a.accounts.utxoDB.Reserve(src, a.Amount, b.MaxTime())
+ res, err := a.accounts.utxoKeeper.Reserve(a.AccountID, a.AssetId, a.Amount, a.UseUnconfirmed, b.MaxTime())
if err != nil {
return errors.Wrap(err, "reserving utxos")
}
// Cancel the reservation if the build gets rolled back.
- b.OnRollback(canceler(ctx, a.accounts, res.ID))
-
- for _, r := range res.UTXOs {
+ b.OnRollback(func() { a.accounts.utxoKeeper.Cancel(res.id) })
+ for _, r := range res.utxos {
txInput, sigInst, err := UtxoToInputs(acct.Signer, r)
if err != nil {
return errors.Wrap(err, "creating inputs")
}
- err = b.AddInput(txInput, sigInst)
- if err != nil {
+
+ if err = b.AddInput(txInput, sigInst); err != nil {
return errors.Wrap(err, "adding inputs")
}
}
- if res.Change > 0 {
- acp, err := a.accounts.CreateAddress(ctx, a.AccountID, true)
+ if res.change > 0 {
+ acp, err := a.accounts.CreateAddress(a.AccountID, true)
if err != nil {
return errors.Wrap(err, "creating control program")
}
// Don't insert the control program until callbacks are executed.
- a.accounts.insertControlProgramDelayed(ctx, b, acp)
-
- err = b.AddOutput(types.NewTxOutput(*a.AssetId, res.Change, acp.ControlProgram))
- if err != nil {
+ a.accounts.insertControlProgramDelayed(b, acp)
+ if err = b.AddOutput(types.NewTxOutput(*a.AssetId, res.change, acp.ControlProgram)); err != nil {
return errors.Wrap(err, "adding change output")
}
}
//DecodeSpendUTXOAction unmarshal JSON-encoded data of spend utxo action
func (m *Manager) DecodeSpendUTXOAction(data []byte) (txbuilder.Action, error) {
a := &spendUTXOAction{accounts: m}
- err := json.Unmarshal(data, a)
- return a, err
+ return a, json.Unmarshal(data, a)
}
type spendUTXOAction struct {
- accounts *Manager
- OutputID *bc.Hash `json:"output_id"`
- Arguments []contractArgument `json:"arguments"`
+ accounts *Manager
+ OutputID *bc.Hash `json:"output_id"`
+ UseUnconfirmed bool `json:"use_unconfirmed"`
+ Arguments []contractArgument `json:"arguments"`
}
// contractArgument for smart contract
return txbuilder.MissingFieldsError("output_id")
}
- res, err := a.accounts.utxoDB.ReserveUTXO(ctx, *a.OutputID, b.MaxTime())
+ res, err := a.accounts.utxoKeeper.ReserveParticular(*a.OutputID, a.UseUnconfirmed, b.MaxTime())
if err != nil {
return err
}
- b.OnRollback(canceler(ctx, a.accounts, res.ID))
+ b.OnRollback(func() { a.accounts.utxoKeeper.Cancel(res.id) })
var accountSigner *signers.Signer
- if len(res.Source.AccountID) != 0 {
- account, err := a.accounts.FindByID(ctx, res.Source.AccountID)
+ if len(res.utxos[0].AccountID) != 0 {
+ account, err := a.accounts.FindByID(res.utxos[0].AccountID)
if err != nil {
return err
}
+
accountSigner = account.Signer
}
- txInput, sigInst, err := UtxoToInputs(accountSigner, res.UTXOs[0])
+ txInput, sigInst, err := UtxoToInputs(accountSigner, res.utxos[0])
if err != nil {
return err
}
return b.AddInput(txInput, sigInst)
}
-// Best-effort cancellation attempt to put in txbuilder.BuildResult.Rollback.
-func canceler(ctx context.Context, m *Manager, rid uint64) func() {
- return func() {
- if err := m.utxoDB.Cancel(ctx, rid); err != nil {
- log.WithField("error", err).Error("Best-effort cancellation attempt to put in txbuilder.BuildResult.Rollback")
- }
- }
-}
-
// UtxoToInputs convert an utxo to the txinput
func UtxoToInputs(signer *signers.Signer, u *UTXO) (*types.TxInput, *txbuilder.SigningInstruction, error) {
txInput := types.NewSpendInput(nil, u.SourceID, u.AssetID, u.Amount, u.SourcePos, u.ControlProgram)
// registers callbacks on the TemplateBuilder so that all of the template's
// account control programs are batch inserted if building the rest of
// the template is successful.
-func (m *Manager) insertControlProgramDelayed(ctx context.Context, b *txbuilder.TemplateBuilder, acp *CtrlProgram) {
+func (m *Manager) insertControlProgramDelayed(b *txbuilder.TemplateBuilder, acp *CtrlProgram) {
m.delayedACPsMu.Lock()
m.delayedACPs[b] = append(m.delayedACPs[b], acp)
m.delayedACPsMu.Unlock()
if len(acps) == 0 {
return nil
}
- return m.insertAccountControlProgram(ctx, acps...)
+ return m.insertControlPrograms(acps...)
})
}
for _, slice := range image.Slice {
for i := uint64(1); i <= slice.ContractIndex; i++ {
- if _, err := m.createAddress(nil, slice.Account, false); err != nil {
+ if _, err := m.createAddress(slice.Account, false); err != nil {
return err
}
}
+++ /dev/null
-package account
-
-import (
- "context"
- "encoding/json"
- "sync"
- "sync/atomic"
- "time"
-
- dbm "github.com/tendermint/tmlibs/db"
-
- "github.com/bytom/errors"
- "github.com/bytom/protocol"
- "github.com/bytom/protocol/bc"
-)
-
-var (
- // ErrInsufficient indicates the account doesn't contain enough
- // units of the requested asset to satisfy the reservation.
- // New units must be deposited into the account in order to
- // satisfy the request; change will not be sufficient.
- ErrInsufficient = errors.New("reservation found insufficient funds")
-
- // ErrReserved indicates that a reservation could not be
- // satisfied because some of the outputs were already reserved.
- // When those reservations are finalized into a transaction
- // (and no other transaction spends funds from the account),
- // new change outputs will be created
- // in sufficient amounts to satisfy the request.
- ErrReserved = errors.New("reservation found outputs already reserved")
- // ErrMatchUTXO indicates the account doesn't contain enough utxo to satisfy the reservation.
- ErrMatchUTXO = errors.New("can't match enough valid utxos")
- // ErrReservation indicates the reserver doesn't found the reservation with the provided ID.
- ErrReservation = errors.New("couldn't find reservation")
-)
-
-// UTXO describes an individual account utxo.
-type UTXO struct {
- OutputID bc.Hash
- SourceID bc.Hash
- AssetID bc.AssetID
- Amount uint64
- SourcePos uint64
- ControlProgram []byte
- AccountID string
- Address string
- ControlProgramIndex uint64
- ValidHeight uint64
- Change bool
-}
-
-func (u *UTXO) source() source {
- return source{AssetID: u.AssetID, AccountID: u.AccountID}
-}
-
-// source describes the criteria to use when selecting UTXOs.
-type source struct {
- AssetID bc.AssetID
- AccountID string
-}
-
-// reservation describes a reservation of a set of UTXOs belonging
-// to a particular account. Reservations are immutable.
-type reservation struct {
- ID uint64
- Source source
- UTXOs []*UTXO
- Change uint64
- Expiry time.Time
-}
-
-func newReserver(c *protocol.Chain, walletdb dbm.DB) *reserver {
- return &reserver{
- c: c,
- db: walletdb,
- reservations: make(map[uint64]*reservation),
- sources: make(map[source]*sourceReserver),
- }
-}
-
-// reserver implements a utxo reserver that stores reservations
-// in-memory. It relies on the account_utxos table for the source of
-// truth of valid UTXOs but tracks which of those UTXOs are reserved
-// in-memory.
-//
-// To reduce latency and prevent deadlock, no two mutexes (either on
-// reserver or sourceReserver) should be held at the same time
-type reserver struct {
- // `sync/atomic` expects the first word in an allocated struct to be 64-bit
- // aligned on both ARM and x86-32. See https://goo.gl/zW7dgq for more details.
- nextReservationID uint64
- c *protocol.Chain
- db dbm.DB
-
- reservationsMu sync.Mutex
- reservations map[uint64]*reservation
-
- sourcesMu sync.Mutex
- sources map[source]*sourceReserver
-}
-
-// Reserve selects and reserves UTXOs according to the criteria provided
-// in source. The resulting reservation expires at exp.
-func (re *reserver) Reserve(src source, amount uint64, exp time.Time) (res *reservation, err error) {
- sourceReserver := re.source(src)
-
- // Try to reserve the right amount.
- rid := atomic.AddUint64(&re.nextReservationID, 1)
- reserved, total, isImmature, err := sourceReserver.reserve(rid, amount)
- if err != nil {
- if isImmature {
- return nil, errors.WithDetail(err, "some coinbase utxos are immature")
- }
- return nil, err
- }
-
- res = &reservation{
- ID: rid,
- Source: src,
- UTXOs: reserved,
- Expiry: exp,
- }
-
- // Save the successful reservation.
- re.reservationsMu.Lock()
- defer re.reservationsMu.Unlock()
- re.reservations[rid] = res
-
- // Make change if necessary
- if total > amount {
- res.Change = total - amount
- }
- return res, nil
-}
-
-// ReserveUTXO reserves a specific utxo for spending. The resulting
-// reservation expires at exp.
-func (re *reserver) ReserveUTXO(ctx context.Context, out bc.Hash, exp time.Time) (*reservation, error) {
- u, err := findSpecificUTXO(re.db, out)
- if err != nil {
- return nil, err
- }
-
- //u.ValidHeight > 0 means coinbase utxo
- if u.ValidHeight > 0 && u.ValidHeight > re.c.BestBlockHeight() {
- return nil, errors.WithDetail(ErrMatchUTXO, "this coinbase utxo is immature")
- }
-
- rid := atomic.AddUint64(&re.nextReservationID, 1)
- err = re.source(u.source()).reserveUTXO(rid, u)
- if err != nil {
- return nil, err
- }
-
- res := &reservation{
- ID: rid,
- Source: u.source(),
- UTXOs: []*UTXO{u},
- Expiry: exp,
- }
- re.reservationsMu.Lock()
- re.reservations[rid] = res
- re.reservationsMu.Unlock()
- return res, nil
-}
-
-// Cancel makes a best-effort attempt at canceling the reservation with
-// the provided ID.
-func (re *reserver) Cancel(ctx context.Context, rid uint64) error {
- re.reservationsMu.Lock()
- res, ok := re.reservations[rid]
- delete(re.reservations, rid)
- re.reservationsMu.Unlock()
- if !ok {
- return errors.Wrapf(ErrReservation, "rid=%d", rid)
- }
- re.source(res.Source).cancel(res)
- /*if res.ClientToken != nil {
- re.idempotency.Forget(*res.ClientToken)
- }*/
- return nil
-}
-
-// ExpireReservations cleans up all reservations that have expired,
-// making their UTXOs available for reservation again.
-func (re *reserver) ExpireReservations(ctx context.Context) error {
- // Remove records of any reservations that have expired.
- now := time.Now()
- var canceled []*reservation
- re.reservationsMu.Lock()
- for rid, res := range re.reservations {
- if res.Expiry.Before(now) {
- canceled = append(canceled, res)
- delete(re.reservations, rid)
- }
- }
- re.reservationsMu.Unlock()
-
- for _, res := range canceled {
- re.source(res.Source).cancel(res)
- }
- return nil
-}
-
-func (re *reserver) source(src source) *sourceReserver {
- re.sourcesMu.Lock()
- defer re.sourcesMu.Unlock()
-
- sr, ok := re.sources[src]
- if ok {
- return sr
- }
-
- sr = &sourceReserver{
- db: re.db,
- src: src,
- reserved: make(map[bc.Hash]uint64),
- currentHeight: re.c.BestBlockHeight,
- }
- re.sources[src] = sr
- return sr
-}
-
-type sourceReserver struct {
- db dbm.DB
- src source
- currentHeight func() uint64
- mu sync.Mutex
- reserved map[bc.Hash]uint64
-}
-
-func (sr *sourceReserver) reserve(rid uint64, amount uint64) ([]*UTXO, uint64, bool, error) {
- var (
- reserved, unavailable uint64
- reservedUTXOs []*UTXO
- )
-
- utxos, isImmature, err := findMatchingUTXOs(sr.db, sr.src, sr.currentHeight)
- if err != nil {
- return nil, 0, isImmature, errors.Wrap(err)
- }
-
- sr.mu.Lock()
- defer sr.mu.Unlock()
- for _, u := range utxos {
- // If the UTXO is already reserved, skip it.
- if _, ok := sr.reserved[u.OutputID]; ok {
- unavailable += u.Amount
- continue
- }
-
- reserved += u.Amount
- reservedUTXOs = append(reservedUTXOs, u)
- if reserved >= amount {
- break
- }
- }
- if reserved+unavailable < amount {
- // Even if everything was available, this account wouldn't have
- // enough to satisfy the request.
- return nil, 0, isImmature, ErrInsufficient
- }
- if reserved < amount {
- // The account has enough for the request, but some is tied up in
- // other reservations.
- return nil, 0, isImmature, ErrReserved
- }
-
- // We've found enough to satisfy the request.
- for _, u := range reservedUTXOs {
- sr.reserved[u.OutputID] = rid
- }
-
- return reservedUTXOs, reserved, isImmature, nil
-}
-
-func (sr *sourceReserver) reserveUTXO(rid uint64, utxo *UTXO) error {
- sr.mu.Lock()
- defer sr.mu.Unlock()
-
- _, isReserved := sr.reserved[utxo.OutputID]
- if isReserved {
- return ErrReserved
- }
-
- sr.reserved[utxo.OutputID] = rid
- return nil
-}
-
-func (sr *sourceReserver) cancel(res *reservation) {
- sr.mu.Lock()
- defer sr.mu.Unlock()
- for _, utxo := range res.UTXOs {
- delete(sr.reserved, utxo.OutputID)
- }
-}
-
-func findMatchingUTXOs(db dbm.DB, src source, currentHeight func() uint64) ([]*UTXO, bool, error) {
- utxos := []*UTXO{}
- isImmature := false
- utxoIter := db.IteratorPrefix([]byte(UTXOPreFix))
- defer utxoIter.Release()
-
- for utxoIter.Next() {
- u := &UTXO{}
- if err := json.Unmarshal(utxoIter.Value(), u); err != nil {
- return nil, false, errors.Wrap(err)
- }
-
- //u.ValidHeight > 0 means coinbase utxo
- if u.ValidHeight > 0 && u.ValidHeight > currentHeight() {
- isImmature = true
- continue
- }
-
- if u.AccountID == src.AccountID && u.AssetID == src.AssetID {
- utxos = append(utxos, u)
- }
- }
-
- if len(utxos) == 0 {
- return nil, isImmature, ErrMatchUTXO
- }
- return utxos, isImmature, nil
-}
-
-func findSpecificUTXO(db dbm.DB, outHash bc.Hash) (*UTXO, error) {
- u := &UTXO{}
-
- data := db.Get(StandardUTXOKey(outHash))
- if data == nil {
- if data = db.Get(ContractUTXOKey(outHash)); data == nil {
- return nil, errors.Wrapf(ErrMatchUTXO, "output_id = %s", outHash.String())
- }
- }
- return u, json.Unmarshal(data, u)
-}
+++ /dev/null
-package account
-
-import (
- "context"
- "encoding/json"
- "io/ioutil"
- "os"
- "testing"
- "time"
-
- dbm "github.com/tendermint/tmlibs/db"
-
- "github.com/bytom/blockchain/pseudohsm"
- "github.com/bytom/consensus"
- "github.com/bytom/crypto/ed25519/chainkd"
- "github.com/bytom/database/leveldb"
- "github.com/bytom/database/storage"
- "github.com/bytom/protocol"
- "github.com/bytom/protocol/bc"
- "github.com/bytom/protocol/state"
-)
-
-func TestCancelReservation(t *testing.T) {
- dirPath, err := ioutil.TempDir(".", "")
- if err != nil {
- t.Fatal(err)
- }
- defer os.RemoveAll(dirPath)
-
- testDB := dbm.NewDB("testdb", "leveldb", "temp")
- defer os.RemoveAll("temp")
-
- chain, err := mockChain(testDB)
- if err != nil {
- t.Fatal(err)
- }
-
- accountManager := NewManager(testDB, chain)
- hsm, err := pseudohsm.New(dirPath)
- if err != nil {
- t.Fatal(err)
- }
-
- xpub1, err := hsm.XCreate("test_pub", "password")
- if err != nil {
- t.Fatal(err)
- }
-
- testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub}, 1, "testAccount")
- if err != nil {
- t.Fatal(err)
- }
-
- controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
- if err != nil {
- t.Fatal(err)
- }
-
- utxo := mockUTXO(controlProg)
-
- batch := testDB.NewBatch()
-
- utxoE := struct {
- hash bc.Hash
- utxoEntry *storage.UtxoEntry
- exist bool
- }{
- hash: utxo.OutputID,
- utxoEntry: storage.NewUtxoEntry(true, 0, false),
- exist: true,
- }
-
- view := state.NewUtxoViewpoint()
- view.Entries[utxoE.hash] = utxoE.utxoEntry
-
- leveldb.SaveUtxoView(batch, view)
- batch.Write()
-
- utxoDB := newReserver(chain, testDB)
-
- batch = utxoDB.db.NewBatch()
-
- data, err := json.Marshal(utxo)
- if err != nil {
- t.Fatal(err)
- }
-
- batch.Set(StandardUTXOKey(utxo.OutputID), data)
- batch.Write()
-
- outid := utxo.OutputID
-
- ctx := context.Background()
- res, err := utxoDB.ReserveUTXO(ctx, outid, time.Now())
- if err != nil {
- t.Fatal(err)
- }
-
- // Verify that the UTXO is reserved.
- _, err = utxoDB.ReserveUTXO(ctx, outid, time.Now())
- if err != ErrReserved {
- t.Fatalf("got=%s want=%s", err, ErrReserved)
- }
-
- // Cancel the reservation.
- err = utxoDB.Cancel(ctx, res.ID)
- if err != nil {
- t.Fatal(err)
- }
-
- // Reserving again should succeed.
- _, err = utxoDB.ReserveUTXO(ctx, outid, time.Now())
- if err != nil {
- t.Fatal(err)
- }
-}
-
-func mockChain(testDB dbm.DB) (*protocol.Chain, error) {
- store := leveldb.NewStore(testDB)
- txPool := protocol.NewTxPool()
- chain, err := protocol.NewChain(store, txPool)
- if err != nil {
- return nil, err
- }
- return chain, nil
-}
-
-func mockUTXO(controlProg *CtrlProgram) *UTXO {
- utxo := &UTXO{}
- utxo.OutputID = bc.Hash{V0: 1}
- utxo.SourceID = bc.Hash{V0: 2}
- utxo.AssetID = *consensus.BTMAssetID
- utxo.Amount = 1000000000
- utxo.SourcePos = 0
- utxo.ControlProgram = controlProg.ControlProgram
- utxo.AccountID = controlProg.AccountID
- utxo.Address = controlProg.Address
- utxo.ControlProgramIndex = controlProg.KeyIndex
- return utxo
-}
--- /dev/null
+package account
+
+import (
+ "encoding/json"
+ "sort"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ dbm "github.com/tendermint/tmlibs/db"
+
+ "github.com/bytom/errors"
+ "github.com/bytom/protocol/bc"
+)
+
+// pre-define error types
+var (
+ ErrInsufficient = errors.New("reservation found insufficient funds")
+ ErrImmature = errors.New("reservation found immature funds")
+ ErrReserved = errors.New("reservation found outputs already reserved")
+ ErrMatchUTXO = errors.New("can't find utxo with given hash")
+ ErrReservation = errors.New("couldn't find reservation")
+)
+
+// UTXO describes an individual account utxo.
+type UTXO struct {
+ OutputID bc.Hash
+ SourceID bc.Hash
+ AssetID bc.AssetID
+ Amount uint64
+ SourcePos uint64
+ ControlProgram []byte
+ AccountID string
+ Address string
+ ControlProgramIndex uint64
+ ValidHeight uint64
+ Change bool
+}
+
+// reservation describes a reservation of a set of UTXOs
+type reservation struct {
+ id uint64
+ utxos []*UTXO
+ change uint64
+ expiry time.Time
+}
+
+type utxoKeeper struct {
+ // `sync/atomic` expects the first word in an allocated struct to be 64-bit
+ // aligned on both ARM and x86-32. See https://goo.gl/zW7dgq for more details.
+ nextIndex uint64
+ db dbm.DB
+ mtx sync.RWMutex
+ currentHeight func() uint64
+
+ unconfirmed map[bc.Hash]*UTXO
+ reserved map[bc.Hash]uint64
+ reservations map[uint64]*reservation
+}
+
+func newUtxoKeeper(f func() uint64, walletdb dbm.DB) *utxoKeeper {
+ uk := &utxoKeeper{
+ db: walletdb,
+ currentHeight: f,
+ unconfirmed: make(map[bc.Hash]*UTXO),
+ reserved: make(map[bc.Hash]uint64),
+ reservations: make(map[uint64]*reservation),
+ }
+ go uk.expireWorker()
+ return uk
+}
+
+func (uk *utxoKeeper) AddUnconfirmedUtxo(utxos []*UTXO) {
+ uk.mtx.Lock()
+ defer uk.mtx.Unlock()
+
+ for _, utxo := range utxos {
+ uk.unconfirmed[utxo.OutputID] = utxo
+ }
+}
+
+// Cancel canceling the reservation with the provided ID.
+func (uk *utxoKeeper) Cancel(rid uint64) {
+ uk.mtx.Lock()
+ uk.cancel(rid)
+ uk.mtx.Unlock()
+}
+
+func (uk *utxoKeeper) RemoveUnconfirmedUtxo(hashes []*bc.Hash) {
+ uk.mtx.Lock()
+ defer uk.mtx.Unlock()
+
+ for _, hash := range hashes {
+ delete(uk.unconfirmed, *hash)
+ }
+}
+
+func (uk *utxoKeeper) Reserve(accountID string, assetID *bc.AssetID, amount uint64, useUnconfirmed bool, exp time.Time) (*reservation, error) {
+ uk.mtx.Lock()
+ defer uk.mtx.Unlock()
+
+ utxos, immatureAmount := uk.findUtxos(accountID, assetID, useUnconfirmed)
+ optUtxos, optAmount, reservedAmount := uk.optUTXOs(utxos, amount)
+ if optAmount+reservedAmount+immatureAmount < amount {
+ return nil, ErrInsufficient
+ }
+
+ if optAmount+reservedAmount < amount {
+ return nil, ErrImmature
+ }
+
+ if optAmount < amount {
+ return nil, ErrReserved
+ }
+
+ result := &reservation{
+ id: atomic.AddUint64(&uk.nextIndex, 1),
+ utxos: optUtxos,
+ change: optAmount - amount,
+ expiry: exp,
+ }
+
+ uk.reservations[result.id] = result
+ for _, u := range optUtxos {
+ uk.reserved[u.OutputID] = result.id
+ }
+ return result, nil
+}
+
+func (uk *utxoKeeper) ReserveParticular(outHash bc.Hash, useUnconfirmed bool, exp time.Time) (*reservation, error) {
+ uk.mtx.Lock()
+ defer uk.mtx.Unlock()
+
+ if _, ok := uk.reserved[outHash]; ok {
+ return nil, ErrReserved
+ }
+
+ u, err := uk.findUtxo(outHash, useUnconfirmed)
+ if err != nil {
+ return nil, err
+ }
+
+ if u.ValidHeight > uk.currentHeight() {
+ return nil, ErrImmature
+ }
+
+ result := &reservation{
+ id: atomic.AddUint64(&uk.nextIndex, 1),
+ utxos: []*UTXO{u},
+ expiry: exp,
+ }
+ uk.reservations[result.id] = result
+ uk.reserved[u.OutputID] = result.id
+ return result, nil
+}
+
+func (uk *utxoKeeper) cancel(rid uint64) {
+ res, ok := uk.reservations[rid]
+ if !ok {
+ return
+ }
+
+ delete(uk.reservations, rid)
+ for _, utxo := range res.utxos {
+ delete(uk.reserved, utxo.OutputID)
+ }
+}
+
+func (uk *utxoKeeper) expireWorker() {
+ ticker := time.NewTicker(1000 * time.Millisecond)
+ for now := range ticker.C {
+ uk.expireReservation(now)
+ }
+}
+func (uk *utxoKeeper) expireReservation(t time.Time) {
+ uk.mtx.Lock()
+ defer uk.mtx.Unlock()
+
+ for rid, res := range uk.reservations {
+ if res.expiry.Before(t) {
+ uk.cancel(rid)
+ }
+ }
+}
+
+func (uk *utxoKeeper) findUtxos(accountID string, assetID *bc.AssetID, useUnconfirmed bool) ([]*UTXO, uint64) {
+ immatureAmount := uint64(0)
+ currentHeight := uk.currentHeight()
+ utxos := []*UTXO{}
+ appendUtxo := func(u *UTXO) {
+ if u.AccountID != accountID || u.AssetID != *assetID {
+ return
+ }
+ if u.ValidHeight > currentHeight {
+ immatureAmount += u.Amount
+ } else {
+ utxos = append(utxos, u)
+ }
+ }
+
+ utxoIter := uk.db.IteratorPrefix([]byte(UTXOPreFix))
+ defer utxoIter.Release()
+ for utxoIter.Next() {
+ u := &UTXO{}
+ if err := json.Unmarshal(utxoIter.Value(), u); err != nil {
+ log.WithField("err", err).Error("utxoKeeper findUtxos fail on unmarshal utxo")
+ continue
+ }
+ appendUtxo(u)
+ }
+ if !useUnconfirmed {
+ return utxos, immatureAmount
+ }
+
+ for _, u := range uk.unconfirmed {
+ appendUtxo(u)
+ }
+ return utxos, immatureAmount
+}
+
+func (uk *utxoKeeper) findUtxo(outHash bc.Hash, useUnconfirmed bool) (*UTXO, error) {
+ if u, ok := uk.unconfirmed[outHash]; useUnconfirmed && ok {
+ return u, nil
+ }
+
+ u := &UTXO{}
+ if data := uk.db.Get(StandardUTXOKey(outHash)); data != nil {
+ return u, json.Unmarshal(data, u)
+ }
+ if data := uk.db.Get(ContractUTXOKey(outHash)); data != nil {
+ return u, json.Unmarshal(data, u)
+ }
+ return nil, ErrMatchUTXO
+}
+
+func (uk *utxoKeeper) optUTXOs(utxos []*UTXO, amount uint64) ([]*UTXO, uint64, uint64) {
+ sort.Slice(utxos, func(i, j int) bool {
+ return utxos[i].Amount < utxos[j].Amount
+ })
+
+ var optAmount, reservedAmount uint64
+ optUtxos := []*UTXO{}
+ for _, u := range utxos {
+ if _, ok := uk.reserved[u.OutputID]; ok {
+ reservedAmount += u.Amount
+ continue
+ }
+
+ optAmount += u.Amount
+ optUtxos = append(optUtxos, u)
+ if optAmount >= amount {
+ break
+ }
+ }
+ return optUtxos, optAmount, reservedAmount
+}
--- /dev/null
+package account
+
+import (
+ "encoding/json"
+ "os"
+ "testing"
+ "time"
+
+ dbm "github.com/tendermint/tmlibs/db"
+
+ "github.com/bytom/protocol/bc"
+ "github.com/bytom/testutil"
+)
+
+func TestAddUnconfirmedUtxo(t *testing.T) {
+ cases := []struct {
+ before utxoKeeper
+ after utxoKeeper
+ addUtxos []*UTXO
+ }{
+ {
+ before: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{},
+ },
+ after: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{},
+ },
+ addUtxos: []*UTXO{},
+ },
+ {
+ before: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{},
+ },
+ after: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ },
+ addUtxos: []*UTXO{
+ &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ },
+ {
+ before: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ },
+ after: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ },
+ addUtxos: []*UTXO{
+ &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ },
+ {
+ before: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ },
+ after: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ bc.NewHash([32]byte{0x02}): &UTXO{OutputID: bc.NewHash([32]byte{0x02})},
+ bc.NewHash([32]byte{0x03}): &UTXO{OutputID: bc.NewHash([32]byte{0x03})},
+ },
+ },
+ addUtxos: []*UTXO{
+ &UTXO{OutputID: bc.NewHash([32]byte{0x02})},
+ &UTXO{OutputID: bc.NewHash([32]byte{0x03})},
+ },
+ },
+ }
+
+ for i, c := range cases {
+ c.before.AddUnconfirmedUtxo(c.addUtxos)
+ if !testutil.DeepEqual(c.before, c.after) {
+ t.Errorf("case %d: got %v want %v", i, c.before, c.after)
+ }
+ }
+}
+
+func TestCancel(t *testing.T) {
+ cases := []struct {
+ before utxoKeeper
+ after utxoKeeper
+ cancelRid uint64
+ }{
+ {
+ before: utxoKeeper{
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ after: utxoKeeper{
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ cancelRid: 0,
+ },
+ {
+ before: utxoKeeper{
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 1,
+ },
+ reservations: map[uint64]*reservation{
+ 1: &reservation{
+ id: 1,
+ utxos: []*UTXO{
+ &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ change: 9527,
+ expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ },
+ after: utxoKeeper{
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ cancelRid: 1,
+ },
+ {
+ before: utxoKeeper{
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 1,
+ },
+ reservations: map[uint64]*reservation{
+ 1: &reservation{
+ id: 1,
+ utxos: []*UTXO{
+ &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ change: 9527,
+ expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ },
+ after: utxoKeeper{
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 1,
+ },
+ reservations: map[uint64]*reservation{
+ 1: &reservation{
+ id: 1,
+ utxos: []*UTXO{
+ &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ change: 9527,
+ expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ },
+ cancelRid: 2,
+ },
+ {
+ before: utxoKeeper{
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 1,
+ bc.NewHash([32]byte{0x02}): 3,
+ bc.NewHash([32]byte{0x03}): 3,
+ bc.NewHash([32]byte{0x04}): 3,
+ },
+ reservations: map[uint64]*reservation{
+ 1: &reservation{
+ id: 1,
+ utxos: []*UTXO{
+ &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ change: 9527,
+ expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+ },
+ 3: &reservation{
+ id: 3,
+ utxos: []*UTXO{
+ &UTXO{OutputID: bc.NewHash([32]byte{0x02})},
+ &UTXO{OutputID: bc.NewHash([32]byte{0x03})},
+ &UTXO{OutputID: bc.NewHash([32]byte{0x04})},
+ },
+ change: 9528,
+ expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ },
+ after: utxoKeeper{
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 1,
+ },
+ reservations: map[uint64]*reservation{
+ 1: &reservation{
+ id: 1,
+ utxos: []*UTXO{
+ &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ change: 9527,
+ expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ },
+ cancelRid: 3,
+ },
+ }
+
+ for i, c := range cases {
+ c.before.cancel(c.cancelRid)
+ if !testutil.DeepEqual(c.before, c.after) {
+ t.Errorf("case %d: got %v want %v", i, c.before, c.after)
+ }
+ }
+}
+
+func TestRemoveUnconfirmedUtxo(t *testing.T) {
+ cases := []struct {
+ before utxoKeeper
+ after utxoKeeper
+ removeUtxos []*bc.Hash
+ }{
+ {
+ before: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{},
+ },
+ after: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{},
+ },
+ removeUtxos: []*bc.Hash{},
+ },
+ {
+ before: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.Hash{V0: 1}: &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ },
+ after: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{},
+ },
+ removeUtxos: []*bc.Hash{
+ &bc.Hash{V0: 1},
+ },
+ },
+ {
+ before: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.Hash{V0: 1}: &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ bc.Hash{V0: 2}: &UTXO{OutputID: bc.NewHash([32]byte{0x02})},
+ bc.Hash{V0: 3}: &UTXO{OutputID: bc.NewHash([32]byte{0x03})},
+ bc.Hash{V0: 4}: &UTXO{OutputID: bc.NewHash([32]byte{0x04})},
+ bc.Hash{V0: 5}: &UTXO{OutputID: bc.NewHash([32]byte{0x05})},
+ },
+ },
+ after: utxoKeeper{
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.Hash{V0: 2}: &UTXO{OutputID: bc.NewHash([32]byte{0x02})},
+ bc.Hash{V0: 4}: &UTXO{OutputID: bc.NewHash([32]byte{0x04})},
+ },
+ },
+ removeUtxos: []*bc.Hash{
+ &bc.Hash{V0: 1},
+ &bc.Hash{V0: 3},
+ &bc.Hash{V0: 5},
+ },
+ },
+ }
+
+ for i, c := range cases {
+ c.before.RemoveUnconfirmedUtxo(c.removeUtxos)
+ if !testutil.DeepEqual(c.before, c.after) {
+ t.Errorf("case %d: got %v want %v", i, c.before, c.after)
+ }
+ }
+}
+
+func TestReserve(t *testing.T) {
+ currentHeight := func() uint64 { return 9527 }
+ testDB := dbm.NewDB("testdb", "leveldb", "temp")
+ defer os.RemoveAll("temp")
+
+ cases := []struct {
+ before utxoKeeper
+ after utxoKeeper
+ err error
+ reserveAmount uint64
+ exp time.Time
+ }{
+ {
+ before: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ after: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ reserveAmount: 1,
+ err: ErrInsufficient,
+ },
+ {
+ before: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ after: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ reserveAmount: 4,
+ err: ErrInsufficient,
+ },
+ {
+ before: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ ValidHeight: 9528,
+ },
+ },
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ after: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ ValidHeight: 9528,
+ },
+ },
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ reserveAmount: 3,
+ err: ErrImmature,
+ },
+ {
+ before: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 0,
+ },
+ reservations: map[uint64]*reservation{},
+ },
+ after: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 0,
+ },
+ reservations: map[uint64]*reservation{},
+ },
+ reserveAmount: 3,
+ err: ErrReserved,
+ },
+ {
+ before: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ after: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 1,
+ },
+ reservations: map[uint64]*reservation{
+ 1: &reservation{
+ id: 1,
+ utxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ change: 1,
+ expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ },
+ reserveAmount: 2,
+ err: nil,
+ exp: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+ },
+ {
+ before: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ nextIndex: 1,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ bc.NewHash([32]byte{0x02}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x02}),
+ AccountID: "testAccount",
+ Amount: 5,
+ },
+ bc.NewHash([32]byte{0x03}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x03}),
+ AccountID: "testAccount",
+ Amount: 7,
+ },
+ },
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 1,
+ },
+ reservations: map[uint64]*reservation{},
+ },
+ after: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ bc.NewHash([32]byte{0x02}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x02}),
+ AccountID: "testAccount",
+ Amount: 5,
+ },
+ bc.NewHash([32]byte{0x03}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x03}),
+ AccountID: "testAccount",
+ Amount: 7,
+ },
+ },
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 1,
+ bc.NewHash([32]byte{0x02}): 2,
+ bc.NewHash([32]byte{0x03}): 2,
+ },
+ reservations: map[uint64]*reservation{
+ 2: &reservation{
+ id: 2,
+ utxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x02}),
+ AccountID: "testAccount",
+ Amount: 5,
+ },
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x03}),
+ AccountID: "testAccount",
+ Amount: 7,
+ },
+ },
+ change: 4,
+ expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ },
+ reserveAmount: 8,
+ err: nil,
+ exp: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+ },
+ }
+
+ for i, c := range cases {
+ if _, err := c.before.Reserve("testAccount", &bc.AssetID{}, c.reserveAmount, true, c.exp); err != c.err {
+ t.Errorf("case %d: got error %v want error %v", i, err, c.err)
+ }
+ checkUtxoKeeperEqual(t, i, &c.before, &c.after)
+ }
+}
+
+func TestReserveParticular(t *testing.T) {
+ currentHeight := func() uint64 { return 9527 }
+ testDB := dbm.NewDB("testdb", "leveldb", "temp")
+ defer os.RemoveAll("temp")
+
+ cases := []struct {
+ before utxoKeeper
+ after utxoKeeper
+ err error
+ reserveHash bc.Hash
+ exp time.Time
+ }{
+ {
+ before: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 0,
+ },
+ reservations: map[uint64]*reservation{},
+ },
+ after: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 0,
+ },
+ reservations: map[uint64]*reservation{},
+ },
+ reserveHash: bc.NewHash([32]byte{0x01}),
+ err: ErrReserved,
+ },
+ {
+ before: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ ValidHeight: 9528,
+ },
+ },
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ after: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ ValidHeight: 9528,
+ },
+ },
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ reserveHash: bc.NewHash([32]byte{0x01}),
+ err: ErrImmature,
+ },
+ {
+ before: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ reserved: map[bc.Hash]uint64{},
+ reservations: map[uint64]*reservation{},
+ },
+ after: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 1,
+ },
+ reservations: map[uint64]*reservation{
+ 1: &reservation{
+ id: 1,
+ utxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ change: 0,
+ expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+ },
+ },
+ },
+ reserveHash: bc.NewHash([32]byte{0x01}),
+ err: nil,
+ exp: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+ },
+ }
+
+ for i, c := range cases {
+ if _, err := c.before.ReserveParticular(c.reserveHash, true, c.exp); err != c.err {
+ t.Errorf("case %d: got error %v want error %v", i, err, c.err)
+ }
+ checkUtxoKeeperEqual(t, i, &c.before, &c.after)
+ }
+}
+
+func TestExpireReservation(t *testing.T) {
+ before := &utxoKeeper{
+ reservations: map[uint64]*reservation{
+ 1: &reservation{expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC)},
+ 2: &reservation{expiry: time.Date(3016, 8, 10, 0, 0, 0, 0, time.UTC)},
+ 3: &reservation{expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC)},
+ 4: &reservation{expiry: time.Date(3016, 8, 10, 0, 0, 0, 0, time.UTC)},
+ 5: &reservation{expiry: time.Date(3016, 8, 10, 0, 0, 0, 0, time.UTC)},
+ },
+ }
+ after := &utxoKeeper{
+ reservations: map[uint64]*reservation{
+ 2: &reservation{expiry: time.Date(3016, 8, 10, 0, 0, 0, 0, time.UTC)},
+ 4: &reservation{expiry: time.Date(3016, 8, 10, 0, 0, 0, 0, time.UTC)},
+ 5: &reservation{expiry: time.Date(3016, 8, 10, 0, 0, 0, 0, time.UTC)},
+ },
+ }
+ before.expireReservation(time.Date(2017, 8, 10, 0, 0, 0, 0, time.UTC))
+ checkUtxoKeeperEqual(t, 0, before, after)
+}
+
+func TestFindUtxos(t *testing.T) {
+ currentHeight := func() uint64 { return 9527 }
+ testDB := dbm.NewDB("testdb", "leveldb", "temp")
+ defer os.RemoveAll("temp")
+
+ cases := []struct {
+ uk utxoKeeper
+ dbUtxos []*UTXO
+ useUnconfirmed bool
+ wantUtxos []*UTXO
+ immatureAmount uint64
+ }{
+ {
+ uk: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{},
+ },
+ dbUtxos: []*UTXO{},
+ useUnconfirmed: true,
+ wantUtxos: []*UTXO{},
+ immatureAmount: 0,
+ },
+ {
+ uk: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{},
+ },
+ dbUtxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x02}),
+ AccountID: "testAccount",
+ AssetID: bc.AssetID{V0: 6},
+ Amount: 3,
+ },
+ },
+ useUnconfirmed: false,
+ wantUtxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ immatureAmount: 0,
+ },
+ {
+ uk: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{},
+ },
+ dbUtxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x02}),
+ AccountID: "testAccount",
+ Amount: 3,
+ ValidHeight: 9528,
+ },
+ },
+ useUnconfirmed: false,
+ wantUtxos: []*UTXO{},
+ immatureAmount: 3,
+ },
+ {
+ uk: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ },
+ dbUtxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x02}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ useUnconfirmed: false,
+ wantUtxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x02}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ immatureAmount: 0,
+ },
+ {
+ uk: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x11}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ },
+ dbUtxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x02}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ useUnconfirmed: true,
+ wantUtxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x02}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ },
+ immatureAmount: 0,
+ },
+ {
+ uk: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 1,
+ },
+ bc.NewHash([32]byte{0x02}): &UTXO{
+ OutputID: bc.NewHash([32]byte{0x02}),
+ AccountID: "notMe",
+ Amount: 2,
+ },
+ },
+ },
+ dbUtxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x03}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x04}),
+ AccountID: "notMe",
+ Amount: 4,
+ },
+ },
+ useUnconfirmed: true,
+ wantUtxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x03}),
+ AccountID: "testAccount",
+ Amount: 3,
+ },
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ AccountID: "testAccount",
+ Amount: 1,
+ },
+ },
+ immatureAmount: 0,
+ },
+ }
+
+ for i, c := range cases {
+ for _, u := range c.dbUtxos {
+ data, err := json.Marshal(u)
+ if err != nil {
+ t.Error(err)
+ }
+ testDB.Set(StandardUTXOKey(u.OutputID), data)
+ }
+
+ gotUtxos, immatureAmount := c.uk.findUtxos("testAccount", &bc.AssetID{}, c.useUnconfirmed)
+ if !testutil.DeepEqual(gotUtxos, c.wantUtxos) {
+ t.Errorf("case %d: got %v want %v", i, gotUtxos, c.wantUtxos)
+ }
+ if immatureAmount != c.immatureAmount {
+ t.Errorf("case %d: got %v want %v", i, immatureAmount, c.immatureAmount)
+ }
+
+ for _, u := range c.dbUtxos {
+ testDB.Delete(StandardUTXOKey(u.OutputID))
+ }
+ }
+}
+
+func TestFindUtxo(t *testing.T) {
+ currentHeight := func() uint64 { return 9527 }
+ testDB := dbm.NewDB("testdb", "leveldb", "temp")
+ defer os.RemoveAll("temp")
+
+ cases := []struct {
+ uk utxoKeeper
+ dbUtxos map[string]*UTXO
+ outHash bc.Hash
+ useUnconfirmed bool
+ wantUtxo *UTXO
+ err error
+ }{
+ {
+ uk: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{},
+ },
+ dbUtxos: map[string]*UTXO{},
+ outHash: bc.NewHash([32]byte{0x01}),
+ wantUtxo: nil,
+ err: ErrMatchUTXO,
+ },
+ {
+ uk: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ },
+ dbUtxos: map[string]*UTXO{},
+ outHash: bc.NewHash([32]byte{0x01}),
+ wantUtxo: nil,
+ useUnconfirmed: false,
+ err: ErrMatchUTXO,
+ },
+ {
+ uk: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{
+ bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ },
+ dbUtxos: map[string]*UTXO{},
+ outHash: bc.NewHash([32]byte{0x01}),
+ wantUtxo: &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ useUnconfirmed: true,
+ err: nil,
+ },
+ {
+ uk: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{},
+ },
+ dbUtxos: map[string]*UTXO{
+ string(StandardUTXOKey(bc.NewHash([32]byte{0x01}))): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ outHash: bc.NewHash([32]byte{0x01}),
+ wantUtxo: &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ useUnconfirmed: false,
+ err: nil,
+ },
+ {
+ uk: utxoKeeper{
+ db: testDB,
+ currentHeight: currentHeight,
+ unconfirmed: map[bc.Hash]*UTXO{},
+ },
+ dbUtxos: map[string]*UTXO{
+ string(ContractUTXOKey(bc.NewHash([32]byte{0x01}))): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ },
+ outHash: bc.NewHash([32]byte{0x01}),
+ wantUtxo: &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+ useUnconfirmed: false,
+ err: nil,
+ },
+ }
+
+ for i, c := range cases {
+ for k, u := range c.dbUtxos {
+ data, err := json.Marshal(u)
+ if err != nil {
+ t.Error(err)
+ }
+ testDB.Set([]byte(k), data)
+ }
+
+ gotUtxo, err := c.uk.findUtxo(c.outHash, c.useUnconfirmed)
+ if !testutil.DeepEqual(gotUtxo, c.wantUtxo) {
+ t.Errorf("case %d: got %v want %v", i, gotUtxo, c.wantUtxo)
+ }
+ if err != c.err {
+ t.Errorf("case %d: got %v want %v", i, err, c.err)
+ }
+
+ for _, u := range c.dbUtxos {
+ testDB.Delete(StandardUTXOKey(u.OutputID))
+ }
+ }
+}
+
+func TestOptUTXOs(t *testing.T) {
+ cases := []struct {
+ uk utxoKeeper
+ input []*UTXO
+ inputAmount uint64
+ wantUtxos []*UTXO
+ optAmount uint64
+ reservedAmount uint64
+ }{
+ {
+ uk: utxoKeeper{
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 1,
+ },
+ },
+ input: []*UTXO{},
+ inputAmount: 13,
+ wantUtxos: []*UTXO{},
+ optAmount: 0,
+ reservedAmount: 0,
+ },
+ {
+ uk: utxoKeeper{
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 1,
+ },
+ },
+ input: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ Amount: 1,
+ },
+ },
+ inputAmount: 13,
+ wantUtxos: []*UTXO{},
+ optAmount: 0,
+ reservedAmount: 1,
+ },
+ {
+ uk: utxoKeeper{
+ reserved: map[bc.Hash]uint64{
+ bc.NewHash([32]byte{0x01}): 1,
+ },
+ },
+ input: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x01}),
+ Amount: 1,
+ },
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x02}),
+ Amount: 3,
+ },
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x03}),
+ Amount: 5,
+ },
+ },
+ inputAmount: 13,
+ wantUtxos: []*UTXO{
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x02}),
+ Amount: 3,
+ },
+ &UTXO{
+ OutputID: bc.NewHash([32]byte{0x03}),
+ Amount: 5,
+ },
+ },
+ optAmount: 8,
+ reservedAmount: 1,
+ },
+ }
+
+ for i, c := range cases {
+ got, optAmount, reservedAmount := c.uk.optUTXOs(c.input, c.inputAmount)
+ if !testutil.DeepEqual(got, c.wantUtxos) {
+ t.Errorf("case %d: utxos got %v want %v", i, got, c.wantUtxos)
+ }
+ if optAmount != c.optAmount {
+ t.Errorf("case %d: utxos got %v want %v", i, optAmount, c.optAmount)
+ }
+ if reservedAmount != c.reservedAmount {
+ t.Errorf("case %d: reservedAmount got %v want %v", i, reservedAmount, c.reservedAmount)
+ }
+ }
+}
+
+func checkUtxoKeeperEqual(t *testing.T, i int, a, b *utxoKeeper) {
+ if !testutil.DeepEqual(a.unconfirmed, b.unconfirmed) {
+ t.Errorf("case %d: unconfirmed got %v want %v", i, a.unconfirmed, b.unconfirmed)
+ }
+ if !testutil.DeepEqual(a.reserved, b.reserved) {
+ t.Errorf("case %d: reserved got %v want %v", i, a.reserved, b.reserved)
+ }
+ if !testutil.DeepEqual(a.reservations, b.reservations) {
+ t.Errorf("case %d: reservations got %v want %v", i, a.reservations, b.reservations)
+ }
+}
Quorum int `json:"quorum"`
Alias string `json:"alias"`
}) Response {
- acc, err := a.wallet.AccountMgr.Create(ctx, ins.RootXPubs, ins.Quorum, ins.Alias)
+ acc, err := a.wallet.AccountMgr.Create(ins.RootXPubs, ins.Quorum, ins.Alias)
if err != nil {
return NewErrorResponse(err)
}
accountID := ins.AccountID
var target *account.Account
if ins.AccountAlias != "" {
- acc, err := a.wallet.AccountMgr.FindByAlias(ctx, ins.AccountAlias)
+ acc, err := a.wallet.AccountMgr.FindByAlias(ins.AccountAlias)
if err != nil {
return NewErrorResponse(err)
}
target = acc
} else {
- acc, err := a.wallet.AccountMgr.FindByID(ctx, accountID)
+ acc, err := a.wallet.AccountMgr.FindByID(accountID)
if err != nil {
return NewErrorResponse(err)
}
ID string `json:"id"`
SmartContract bool `json:"smart_contract"`
}) Response {
- accountUTXOs := a.wallet.GetAccountUTXOs(filter.ID, filter.SmartContract)
+ accountUTXOs := a.wallet.GetAccountUtxos(filter.ID, filter.SmartContract)
UTXOs := []query.AnnotatedUTXO{}
for _, utxo := range accountUTXOs {
func (a *API) listPubKeys(ctx context.Context, ins struct {
AccountID string `json:"account_id"`
}) Response {
- account, err := a.wallet.AccountMgr.FindByID(ctx, ins.AccountID)
+ account, err := a.wallet.AccountMgr.FindByID(ins.AccountID)
if err != nil {
return NewErrorResponse(err)
}
}) Response {
accountID := ins.AccountID
if ins.AccountAlias != "" {
- account, err := a.wallet.AccountMgr.FindByAlias(ctx, ins.AccountAlias)
+ account, err := a.wallet.AccountMgr.FindByAlias(ins.AccountAlias)
if err != nil {
return NewErrorResponse(err)
}
accountID = account.ID
}
- program, err := a.wallet.AccountMgr.CreateAddress(ctx, accountID, false)
+ program, err := a.wallet.AccountMgr.CreateAddress(accountID, false)
if err != nil {
return NewErrorResponse(err)
}
id, _ := m["account_id"].(string)
alias, _ := m["account_alias"].(string)
if id == "" && alias != "" {
- acc, err := a.wallet.AccountMgr.FindByAlias(ctx, alias)
+ acc, err := a.wallet.AccountMgr.FindByAlias(alias)
if err != nil {
return errors.WithDetailf(err, "invalid account alias %s on action %d", alias, index)
}
decoders := map[string]func([]byte) (txbuilder.Action, error){
"control_address": txbuilder.DecodeControlAddressAction,
"control_program": txbuilder.DecodeControlProgramAction,
- "control_receiver": txbuilder.DecodeControlReceiverAction,
"issue": a.wallet.AssetReg.DecodeIssueAction,
"retire": txbuilder.DecodeRetireAction,
"spend_account": a.wallet.AccountMgr.DecodeSpendAction,
var retirementProgram = []byte{byte(vm.OP_FAIL)}
-// DecodeControlReceiverAction convert input data to action struct
-func DecodeControlReceiverAction(data []byte) (Action, error) {
- a := new(controlReceiverAction)
- err := stdjson.Unmarshal(data, a)
- return a, err
-}
-
-type controlReceiverAction struct {
- bc.AssetAmount
- Receiver *Receiver `json:"receiver"`
-}
-
-func (a *controlReceiverAction) Build(ctx context.Context, b *TemplateBuilder) error {
- var missing []string
- if a.Receiver == nil {
- missing = append(missing, "receiver")
- } else {
- if len(a.Receiver.ControlProgram) == 0 {
- missing = append(missing, "receiver.control_program")
- }
- }
- if a.AssetId.IsZero() {
- missing = append(missing, "asset_id")
- }
- if a.Amount == 0 {
- missing = append(missing, "amount")
- }
- if len(missing) > 0 {
- return MissingFieldsError(missing...)
- }
-
- out := types.NewTxOutput(*a.AssetId, a.Amount, a.Receiver.ControlProgram)
- return b.AddOutput(out)
-}
-
// DecodeControlAddressAction convert input data to action struct
func DecodeControlAddressAction(data []byte) (Action, error) {
a := new(controlAddressAction)
_ "net/http/pprof"
"os"
"path/filepath"
- "time"
"github.com/prometheus/prometheus/util/flock"
log "github.com/sirupsen/logrus"
)
const (
- webAddress = "http://127.0.0.1:9888"
- expireReservationsPeriod = time.Second
- maxNewBlockChSize = 1024
+ webAddress = "http://127.0.0.1:9888"
+ maxNewBlockChSize = 1024
)
type Node struct {
if config.Wallet.Rescan {
wallet.RescanBlocks()
}
-
- // Clean up expired UTXO reservations periodically.
- go accounts.ExpireReservations(ctx, expireReservationsPeriod)
}
newBlockCh := make(chan *bc.Hash, maxNewBlockChSize)
// newPoolTxListener listener transaction from txPool, and send it to syncManager and wallet
func newPoolTxListener(txPool *protocol.TxPool, syncManager *netsync.SyncManager, wallet *w.Wallet) {
- newTxCh := txPool.GetNewTxCh()
+ txMsgCh := txPool.GetMsgCh()
syncManagerTxCh := syncManager.GetNewTxCh()
for {
- newTx := <-newTxCh
- syncManagerTxCh <- newTx
- if wallet != nil {
- wallet.GetNewTxCh() <- newTx
+ msg := <-txMsgCh
+ switch msg.MsgType {
+ case protocol.MsgNewTx:
+ syncManagerTxCh <- msg.Tx
+ if wallet != nil {
+ wallet.AddUnconfirmedTx(msg.TxDesc)
+ }
+ case protocol.MsgRemoveTx:
+ if wallet != nil {
+ wallet.RemoveUnconfirmedTx(msg.TxDesc)
+ }
+ default:
+ log.Warn("got unknow message type from the txPool channel")
}
}
}
log "github.com/sirupsen/logrus"
)
+const (
+ MsgNewTx = iota
+ MsgRemoveTx
+)
+
var (
maxCachedErrTxs = 1000
- maxNewTxChSize = 1000
+ maxMsgChSize = 1000
maxNewTxNum = 10000
// ErrTransactionNotExist is the pre-defined error message
// TxDesc store tx and related info for mining strategy
type TxDesc struct {
- Tx *types.Tx
- Added time.Time
- Height uint64
- Weight uint64
- Fee uint64
- FeePerKB uint64
+ Tx *types.Tx
+ Added time.Time
+ StatusFail bool
+ Height uint64
+ Weight uint64
+ Fee uint64
+ FeePerKB uint64
+}
+
+type TxPoolMsg struct {
+ *TxDesc
+ MsgType int
}
// TxPool is use for store the unconfirmed transaction
pool map[bc.Hash]*TxDesc
utxo map[bc.Hash]bc.Hash
errCache *lru.Cache
- newTxCh chan *types.Tx
+ msgCh chan *TxPoolMsg
}
// NewTxPool init a new TxPool
pool: make(map[bc.Hash]*TxDesc),
utxo: make(map[bc.Hash]bc.Hash),
errCache: lru.New(maxCachedErrTxs),
- newTxCh: make(chan *types.Tx, maxNewTxChSize),
+ msgCh: make(chan *TxPoolMsg, maxMsgChSize),
}
}
// GetNewTxCh return a unconfirmed transaction feed channel
-func (tp *TxPool) GetNewTxCh() chan *types.Tx {
- return tp.newTxCh
+func (tp *TxPool) GetMsgCh() <-chan *TxPoolMsg {
+ return tp.msgCh
}
// AddTransaction add a verified transaction to pool
-func (tp *TxPool) AddTransaction(tx *types.Tx, gasOnlyTx bool, height, fee uint64) (*TxDesc, error) {
+func (tp *TxPool) AddTransaction(tx *types.Tx, statusFail bool, height, fee uint64) (*TxDesc, error) {
tp.mtx.Lock()
defer tp.mtx.Unlock()
}
txD := &TxDesc{
- Tx: tx,
- Added: time.Now(),
- Weight: tx.TxData.SerializedSize,
- Height: height,
- Fee: fee,
- FeePerKB: fee * 1000 / tx.TxHeader.SerializedSize,
+ Tx: tx,
+ Added: time.Now(),
+ StatusFail: statusFail,
+ Weight: tx.TxData.SerializedSize,
+ Height: height,
+ Fee: fee,
+ FeePerKB: fee * 1000 / tx.TxHeader.SerializedSize,
}
tp.pool[tx.Tx.ID] = txD
// error due to it's a retirement, utxo doesn't care this output type so skip it
continue
}
- if !gasOnlyTx || *output.Source.Value.AssetId == *consensus.BTMAssetID {
+ if !statusFail || *output.Source.Value.AssetId == *consensus.BTMAssetID {
tp.utxo[*id] = tx.Tx.ID
}
}
- tp.newTxCh <- tx
+ tp.msgCh <- &TxPoolMsg{TxDesc: txD, MsgType: MsgNewTx}
log.WithField("tx_id", tx.Tx.ID.String()).Debug("Add tx to mempool")
return txD, nil
}
delete(tp.pool, *txHash)
atomic.StoreInt64(&tp.lastUpdated, time.Now().Unix())
+ tp.msgCh <- &TxPoolMsg{TxDesc: txD, MsgType: MsgRemoveTx}
log.WithField("tx_id", txHash).Debug("remove tx from mempool")
}
}
func processNewTxch(txPool *protocol.TxPool) {
- newTxCh := txPool.GetNewTxCh()
+ newTxCh := txPool.GetMsgCh()
for tx := range newTxCh {
if tx == nil {
}
txs := []*types.Tx{}
for i := 0; i < txNumber; i++ {
testAccountAlias := fmt.Sprintf("testAccount%d", i)
- testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub.XPub}, 1, testAccountAlias)
+ testAccount, err := accountManager.Create([]chainkd.XPub{xpub.XPub}, 1, testAccountAlias)
if err != nil {
return nil, err
}
- controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+ controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
if err != nil {
return nil, err
}
txs := []*types.Tx{}
for i := 0; i < txNumber; i++ {
testAccountAlias := fmt.Sprintf("testAccount%d", i)
- testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, testAccountAlias)
+ testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, testAccountAlias)
if err != nil {
return nil, err
}
- controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+ controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
if err != nil {
return nil, err
}
txs := []*types.Tx{}
for i := 0; i < txNumber; i++ {
testAccountAlias := fmt.Sprintf("testAccount%d", i)
- testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, testAccountAlias)
+ testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, testAccountAlias)
if err != nil {
return nil, err
}
- controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+ controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
if err != nil {
return nil, err
}
t.Fatal(err)
}
- testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub.XPub}, 1, "testAccount")
+ testAccount, err := accountManager.Create([]chainkd.XPub{xpub.XPub}, 1, "testAccount")
if err != nil {
t.Fatal(err)
}
- controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+ controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
if err != nil {
t.Fatal(err)
}
t.Fatal(err)
}
- testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, "testAccount")
+ testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, "testAccount")
if err != nil {
t.Fatal(err)
}
- controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+ controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
if err != nil {
t.Fatal(err)
}
t.Fatal(err)
}
- testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, "testAccount")
+ testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, "testAccount")
if err != nil {
t.Fatal(err)
}
- controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+ controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
if err != nil {
t.Fatal(err)
}
}
xpubs = append(xpubs, *xpub)
}
- _, err := g.AccountManager.Create(nil, xpubs, quorum, name)
+ _, err := g.AccountManager.Create(xpubs, quorum, name)
return err
}
func (g *TxGenerator) createAsset(accountAlias string, assetAlias string) (*asset.Asset, error) {
- acc, err := g.AccountManager.FindByAlias(nil, accountAlias)
+ acc, err := g.AccountManager.FindByAlias(accountAlias)
if err != nil {
return nil, err
}
}
func (g *TxGenerator) createControlProgram(accountAlias string, change bool) (*account.CtrlProgram, error) {
- acc, err := g.AccountManager.FindByAlias(nil, accountAlias)
+ acc, err := g.AccountManager.FindByAlias(accountAlias)
if err != nil {
return nil, err
}
- return g.AccountManager.CreateAddress(nil, acc.ID, change)
+ return g.AccountManager.CreateAddress(acc.ID, change)
}
// AddSpendInput add a spend input
return err
}
- acc, err := g.AccountManager.FindByAlias(nil, accountAlias)
+ acc, err := g.AccountManager.FindByAlias(accountAlias)
if err != nil {
return err
}
// AddTxInputFromUtxo add a tx input which spent the utxo
func (g *TxGenerator) AddTxInputFromUtxo(utxo *account.UTXO, accountAlias string) error {
- acc, err := g.AccountManager.FindByAlias(nil, accountAlias)
+ acc, err := g.AccountManager.FindByAlias(accountAlias)
if err != nil {
return err
}
}
func (ctx *walletTestContext) createControlProgram(accountName string, change bool) (*account.CtrlProgram, error) {
- acc, err := ctx.Wallet.AccountMgr.FindByAlias(nil, accountName)
+ acc, err := ctx.Wallet.AccountMgr.FindByAlias(accountName)
if err != nil {
return nil, err
}
- return ctx.Wallet.AccountMgr.CreateAddress(nil, acc.ID, change)
+ return ctx.Wallet.AccountMgr.CreateAddress(acc.ID, change)
}
func (ctx *walletTestContext) getPubkey(keyAlias string) *chainkd.XPub {
}
func (ctx *walletTestContext) createAsset(accountAlias string, assetAlias string) (*asset.Asset, error) {
- acc, err := ctx.Wallet.AccountMgr.FindByAlias(nil, accountAlias)
+ acc, err := ctx.Wallet.AccountMgr.FindByAlias(accountAlias)
if err != nil {
return nil, err
}
}
xpubs = append(xpubs, *xpub)
}
- _, err := ctx.Wallet.AccountMgr.Create(nil, xpubs, quorum, name)
+ _, err := ctx.Wallet.AccountMgr.Create(xpubs, quorum, name)
return err
}
"github.com/bytom/account"
"github.com/bytom/asset"
"github.com/bytom/blockchain/query"
- "github.com/bytom/consensus"
- "github.com/bytom/consensus/segwit"
"github.com/bytom/crypto/sha3pool"
chainjson "github.com/bytom/encoding/json"
- "github.com/bytom/errors"
"github.com/bytom/protocol/bc"
"github.com/bytom/protocol/bc/types"
)
-type rawOutput struct {
- OutputID bc.Hash
- bc.AssetAmount
- ControlProgram []byte
- txHash bc.Hash
- outputIndex uint32
- sourceID bc.Hash
- sourcePos uint64
- ValidHeight uint64
-}
-
-type accountOutput struct {
- rawOutput
- AccountID string
- Address string
- keyIndex uint64
- change bool
-}
-
const (
//TxPrefix is wallet database transactions prefix
TxPrefix = "TXS:"
// deleteTransaction delete transactions when orphan block rollback
func (w *Wallet) deleteTransactions(batch db.Batch, height uint64) {
tmpTx := query.AnnotatedTx{}
-
txIter := w.DB.IteratorPrefix(calcDeleteKey(height))
defer txIter.Release()
for txIter.Next() {
if err := json.Unmarshal(txIter.Value(), &tmpTx); err == nil {
- // delete index
batch.Delete(calcTxIndexKey(tmpTx.ID.String()))
}
-
batch.Delete(txIter.Key())
}
}
-// ReverseAccountUTXOs process the invalid blocks when orphan block rollback
-func (w *Wallet) reverseAccountUTXOs(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
- var err error
-
- // unknow how many spent and retire outputs
- reverseOuts := []*rawOutput{}
-
- // handle spent UTXOs
- for txIndex, tx := range b.Transactions {
- for _, inpID := range tx.Tx.InputIDs {
- // spend and retire
- sp, err := tx.Spend(inpID)
- if err != nil {
- continue
- }
-
- resOut, ok := tx.Entries[*sp.SpentOutputId].(*bc.Output)
- if !ok {
- continue
- }
-
- statusFail, _ := txStatus.GetStatus(txIndex)
- if statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
- continue
- }
-
- out := &rawOutput{
- OutputID: *sp.SpentOutputId,
- AssetAmount: *resOut.Source.Value,
- ControlProgram: resOut.ControlProgram.Code,
- txHash: tx.ID,
- sourceID: *resOut.Source.Ref,
- sourcePos: resOut.Source.Position,
- }
- reverseOuts = append(reverseOuts, out)
- }
- }
-
- accOuts := loadAccountInfo(reverseOuts, w)
- if err = upsertConfirmedAccountOutputs(accOuts, batch); err != nil {
- log.WithField("err", err).Error("reversing account spent and retire outputs")
- return
- }
-
- // handle new UTXOs
- for _, tx := range b.Transactions {
- for j := range tx.Outputs {
- resOutID := tx.ResultIds[j]
- resOut, ok := tx.Entries[*resOutID].(*bc.Output)
- if !ok {
- // retirement
- continue
- }
-
- if segwit.IsP2WScript(resOut.ControlProgram.Code) {
- // delete standard UTXOs
- batch.Delete(account.StandardUTXOKey(*resOutID))
- } else {
- // delete contract UTXOs
- batch.Delete(account.ContractUTXOKey(*resOutID))
- }
- }
- }
-}
-
// saveExternalAssetDefinition save external and local assets definition,
// when query ,query local first and if have no then query external
// details see getAliasDefinition
return nil
}
-// buildAccountUTXOs process valid blocks to build account unspent outputs db
-func (w *Wallet) buildAccountUTXOs(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
- // get the spent UTXOs and delete the UTXOs from DB
- prevoutDBKeys(batch, b, txStatus)
-
- // handle new UTXOs
- var outs []*rawOutput
- for txIndex, tx := range b.Transactions {
- for i, out := range tx.Outputs {
- resOutID := tx.ResultIds[i]
- resOut, ok := tx.Entries[*resOutID].(*bc.Output)
- if !ok {
- continue
- }
-
- if statusFail, _ := txStatus.GetStatus(txIndex); statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
- continue
- }
-
- out := &rawOutput{
- OutputID: *tx.OutputID(i),
- AssetAmount: out.AssetAmount,
- ControlProgram: out.ControlProgram,
- txHash: tx.ID,
- outputIndex: uint32(i),
- sourceID: *resOut.Source.Ref,
- sourcePos: resOut.Source.Position,
- }
-
- // coinbase utxo valid height
- if txIndex == 0 {
- out.ValidHeight = b.Height + consensus.CoinbasePendingBlockNumber
- }
- outs = append(outs, out)
- }
- }
- accOuts := loadAccountInfo(outs, w)
-
- if err := upsertConfirmedAccountOutputs(accOuts, batch); err != nil {
- log.WithField("err", err).Error("building new account outputs")
- }
-}
-
-func prevoutDBKeys(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
- for txIndex, tx := range b.Transactions {
- for _, inpID := range tx.Tx.InputIDs {
- sp, err := tx.Spend(inpID)
- if err != nil {
- continue
- }
-
- statusFail, _ := txStatus.GetStatus(txIndex)
- if statusFail && *sp.WitnessDestination.Value.AssetId != *consensus.BTMAssetID {
- continue
- }
-
- resOut, ok := tx.Entries[*sp.SpentOutputId].(*bc.Output)
- if !ok {
- // retirement
- log.WithField("SpentOutputId", *sp.SpentOutputId).Info("the OutputId is retirement")
- continue
- }
-
- if segwit.IsP2WScript(resOut.ControlProgram.Code) {
- // delete standard UTXOs
- batch.Delete(account.StandardUTXOKey(*sp.SpentOutputId))
- } else {
- // delete contract UTXOs
- batch.Delete(account.ContractUTXOKey(*sp.SpentOutputId))
- }
- }
- }
- return
-}
-
-// loadAccountInfo turns a set of output IDs into a set of
-// outputs by adding account annotations. Outputs that can't be
-// annotated are excluded from the result.
-func loadAccountInfo(outs []*rawOutput, w *Wallet) []*accountOutput {
- outsByScript := make(map[string][]*rawOutput, len(outs))
- for _, out := range outs {
- scriptStr := string(out.ControlProgram)
- outsByScript[scriptStr] = append(outsByScript[scriptStr], out)
- }
-
- result := make([]*accountOutput, 0, len(outs))
- cp := account.CtrlProgram{}
-
- var hash [32]byte
- for s := range outsByScript {
- // smart contract UTXO
- if !segwit.IsP2WScript([]byte(s)) {
- for _, out := range outsByScript[s] {
- newOut := &accountOutput{
- rawOutput: *out,
- change: false,
- }
- result = append(result, newOut)
- }
-
- continue
- }
-
- sha3pool.Sum256(hash[:], []byte(s))
- bytes := w.DB.Get(account.ContractKey(hash))
- if bytes == nil {
- continue
- }
-
- err := json.Unmarshal(bytes, &cp)
- if err != nil {
- continue
- }
-
- isExist := w.DB.Get(account.Key(cp.AccountID))
- if isExist == nil {
- continue
- }
-
- for _, out := range outsByScript[s] {
- newOut := &accountOutput{
- rawOutput: *out,
- AccountID: cp.AccountID,
- Address: cp.Address,
- keyIndex: cp.KeyIndex,
- change: cp.Change,
- }
- result = append(result, newOut)
- }
- }
-
- return result
-}
-
-// upsertConfirmedAccountOutputs records the account data for confirmed utxos.
-// If the account utxo already exists (because it's from a local tx), the
-// block confirmation data will in the row will be updated.
-func upsertConfirmedAccountOutputs(outs []*accountOutput, batch db.Batch) error {
- var u *account.UTXO
-
- for _, out := range outs {
- u = &account.UTXO{
- OutputID: out.OutputID,
- SourceID: out.sourceID,
- AssetID: *out.AssetId,
- Amount: out.Amount,
- SourcePos: out.sourcePos,
- ControlProgram: out.ControlProgram,
- ControlProgramIndex: out.keyIndex,
- AccountID: out.AccountID,
- Address: out.Address,
- ValidHeight: out.ValidHeight,
- Change: out.change,
- }
-
- data, err := json.Marshal(u)
- if err != nil {
- return errors.Wrap(err, "failed marshal accountutxo")
- }
-
- if segwit.IsP2WScript(out.ControlProgram) {
- // standard UTXOs
- batch.Set(account.StandardUTXOKey(out.OutputID), data)
- } else {
- // contract UTXOs
- batch.Set(account.ContractUTXOKey(out.OutputID), data)
- }
-
- }
- return nil
-}
-
// filterAccountTxs related and build the fully annotated transactions.
func (w *Wallet) filterAccountTxs(b *types.Block, txStatus *bc.TransactionStatus) []*query.AnnotatedTx {
annotatedTxs := make([]*query.AnnotatedTx, 0, len(b.Transactions))
return annotatedTxs, nil
}
-// GetAccountUTXOs return all account unspent outputs
-func (w *Wallet) GetAccountUTXOs(id string, isSmartContract bool) []account.UTXO {
- accountUTXO := account.UTXO{}
- accountUTXOs := []account.UTXO{}
-
- prefix := account.UTXOPreFix
- if isSmartContract {
- prefix = account.SUTXOPrefix
- }
- accountUTXOIter := w.DB.IteratorPrefix([]byte(prefix + id))
- defer accountUTXOIter.Release()
- for accountUTXOIter.Next() {
- if err := json.Unmarshal(accountUTXOIter.Value(), &accountUTXO); err != nil {
- hashKey := accountUTXOIter.Key()[len(prefix):]
- log.WithField("UTXO hash", string(hashKey)).Warn("get account UTXO")
- continue
- }
-
- accountUTXOs = append(accountUTXOs, accountUTXO)
- }
-
- return accountUTXOs
-}
-
// GetAccountBalances return all account balances
func (w *Wallet) GetAccountBalances(id string) ([]AccountBalance, error) {
- return w.indexBalances(w.GetAccountUTXOs("", false))
+ return w.indexBalances(w.GetAccountUtxos("", false))
}
// AccountBalance account balance
AssetDefinition map[string]interface{} `json:"asset_definition"`
}
-func (w *Wallet) indexBalances(accountUTXOs []account.UTXO) ([]AccountBalance, error) {
+func (w *Wallet) indexBalances(accountUTXOs []*account.UTXO) ([]AccountBalance, error) {
accBalance := make(map[string]map[string]uint64)
balances := []AccountBalance{}
"sort"
"time"
+ log "github.com/sirupsen/logrus"
+
"github.com/bytom/account"
"github.com/bytom/blockchain/query"
"github.com/bytom/crypto/sha3pool"
+ "github.com/bytom/protocol"
"github.com/bytom/protocol/bc/types"
)
return []byte(UnconfirmedTxPrefix + formatKey)
}
+// SortByTimestamp implements sort.Interface for AnnotatedTx slices
+type SortByTimestamp []*query.AnnotatedTx
+
+func (a SortByTimestamp) Len() int { return len(a) }
+func (a SortByTimestamp) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a SortByTimestamp) Less(i, j int) bool { return a[i].Timestamp > a[j].Timestamp }
+
+// AddUnconfirmedTx handle wallet status update when tx add into txpool
+func (w *Wallet) AddUnconfirmedTx(txD *protocol.TxDesc) {
+ if err := w.saveUnconfirmedTx(txD.Tx); err != nil {
+ log.WithField("err", err).Error("wallet fail on saveUnconfirmedTx")
+ }
+
+ utxos := txOutToUtxos(txD.Tx, txD.StatusFail, 0)
+ utxos = w.filterAccountUtxo(utxos)
+ w.AccountMgr.AddUnconfirmedUtxo(utxos)
+}
+
+// GetUnconfirmedTxs get account unconfirmed transactions, filter transactions by accountID when accountID is not empty
+func (w *Wallet) GetUnconfirmedTxs(accountID string) ([]*query.AnnotatedTx, error) {
+ annotatedTxs := []*query.AnnotatedTx{}
+ txIter := w.DB.IteratorPrefix([]byte(UnconfirmedTxPrefix))
+ defer txIter.Release()
+
+ for txIter.Next() {
+ annotatedTx := &query.AnnotatedTx{}
+ if err := json.Unmarshal(txIter.Value(), &annotatedTx); err != nil {
+ return nil, err
+ }
+
+ if accountID == "" || findTransactionsByAccount(annotatedTx, accountID) {
+ annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
+ annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...)
+ }
+ }
+
+ sort.Sort(SortByTimestamp(annotatedTxs))
+ return annotatedTxs, nil
+}
+
+// GetUnconfirmedTxByTxID get unconfirmed transaction by txID
+func (w *Wallet) GetUnconfirmedTxByTxID(txID string) (*query.AnnotatedTx, error) {
+ annotatedTx := &query.AnnotatedTx{}
+ txInfo := w.DB.Get(calcUnconfirmedTxKey(txID))
+ if txInfo == nil {
+ return nil, fmt.Errorf("No transaction(tx_id=%s) from txpool", txID)
+ }
+
+ if err := json.Unmarshal(txInfo, annotatedTx); err != nil {
+ return nil, err
+ }
+
+ annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
+ return annotatedTx, nil
+}
+
+// RemoveUnconfirmedTx handle wallet status update when tx removed from txpool
+func (w *Wallet) RemoveUnconfirmedTx(txD *protocol.TxDesc) {
+ w.AccountMgr.RemoveUnconfirmedUtxo(txD.Tx.ResultIds)
+}
+
func (w *Wallet) buildAnnotatedUnconfirmedTx(tx *types.Tx) *query.AnnotatedTx {
annotatedTx := &query.AnnotatedTx{
ID: tx.ID,
for i := range tx.Outputs {
annotatedTx.Outputs = append(annotatedTx.Outputs, w.BuildAnnotatedOutput(tx, i))
}
-
return annotatedTx
}
return true
}
}
-
return false
}
// SaveUnconfirmedTx save unconfirmed annotated transaction to the database
-func (w *Wallet) SaveUnconfirmedTx(tx *types.Tx) error {
+func (w *Wallet) saveUnconfirmedTx(tx *types.Tx) error {
if !w.checkRelatedTransaction(tx) {
return nil
}
w.DB.Set(calcUnconfirmedTxKey(tx.ID.String()), rawTx)
return nil
}
-
-// GetUnconfirmedTxByTxID get unconfirmed transaction by txID
-func (w *Wallet) GetUnconfirmedTxByTxID(txID string) (*query.AnnotatedTx, error) {
- annotatedTx := &query.AnnotatedTx{}
- txInfo := w.DB.Get(calcUnconfirmedTxKey(txID))
- if txInfo == nil {
- return nil, fmt.Errorf("No transaction(tx_id=%s) from txpool", txID)
- }
-
- if err := json.Unmarshal(txInfo, annotatedTx); err != nil {
- return nil, err
- }
- annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
-
- return annotatedTx, nil
-}
-
-// SortByTimestamp implements sort.Interface for AnnotatedTx slices
-type SortByTimestamp []*query.AnnotatedTx
-
-func (a SortByTimestamp) Len() int { return len(a) }
-func (a SortByTimestamp) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
-func (a SortByTimestamp) Less(i, j int) bool { return a[i].Timestamp > a[j].Timestamp }
-
-// GetUnconfirmedTxs get account unconfirmed transactions, filter transactions by accountID when accountID is not empty
-func (w *Wallet) GetUnconfirmedTxs(accountID string) ([]*query.AnnotatedTx, error) {
- annotatedTxs := []*query.AnnotatedTx{}
-
- txIter := w.DB.IteratorPrefix([]byte(UnconfirmedTxPrefix))
- defer txIter.Release()
- for txIter.Next() {
- annotatedTx := &query.AnnotatedTx{}
- if err := json.Unmarshal(txIter.Value(), &annotatedTx); err != nil {
- return nil, err
- }
-
- if accountID == "" || findTransactionsByAccount(annotatedTx, accountID) {
- annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
- annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...)
- }
- }
-
- // sort SortByTimestamp by timestamp
- sort.Sort(SortByTimestamp(annotatedTxs))
- return annotatedTxs, nil
-}
t.Fatal(err)
}
- testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub}, 1, "testAccount")
+ testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub}, 1, "testAccount")
if err != nil {
t.Fatal(err)
}
- controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+ controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
if err != nil {
t.Fatal(err)
}
t.Fatal(err)
}
testTx := types.NewTx(*txData)
- w.SaveUnconfirmedTx(testTx)
+ w.saveUnconfirmedTx(testTx)
txs := AnnotatedTxs([]*types.Tx{testTx}, w)
wantTx := txs[0]
--- /dev/null
+package wallet
+
+import (
+ "encoding/json"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/tendermint/tmlibs/db"
+
+ "github.com/bytom/account"
+ "github.com/bytom/consensus"
+ "github.com/bytom/consensus/segwit"
+ "github.com/bytom/crypto/sha3pool"
+ "github.com/bytom/errors"
+ "github.com/bytom/protocol/bc"
+ "github.com/bytom/protocol/bc/types"
+)
+
+// GetAccountUtxos return all account unspent outputs
+func (w *Wallet) GetAccountUtxos(id string, isSmartContract bool) []*account.UTXO {
+ prefix := account.UTXOPreFix
+ if isSmartContract {
+ prefix = account.SUTXOPrefix
+ }
+ accountUtxos := []*account.UTXO{}
+ accountUtxoIter := w.DB.IteratorPrefix([]byte(prefix + id))
+ defer accountUtxoIter.Release()
+
+ for accountUtxoIter.Next() {
+ accountUtxo := &account.UTXO{}
+ if err := json.Unmarshal(accountUtxoIter.Value(), accountUtxo); err != nil {
+ log.WithField("err", err).Warn("GetAccountUtxos fail on unmarshal utxo")
+ continue
+ }
+
+ accountUtxos = append(accountUtxos, accountUtxo)
+ }
+ return accountUtxos
+}
+
+func (w *Wallet) attachUtxos(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
+ for txIndex, tx := range b.Transactions {
+ statusFail, err := txStatus.GetStatus(txIndex)
+ if err != nil {
+ log.WithField("err", err).Error("attachUtxos fail on get tx status")
+ continue
+ }
+
+ inputUtxos := txInToUtxos(tx, statusFail)
+ for _, inputUtxo := range inputUtxos {
+ if segwit.IsP2WScript(inputUtxo.ControlProgram) {
+ batch.Delete(account.StandardUTXOKey(inputUtxo.OutputID))
+ } else {
+ batch.Delete(account.ContractUTXOKey(inputUtxo.OutputID))
+ }
+ }
+ }
+
+ utxos := []*account.UTXO{}
+ for txIndex, tx := range b.Transactions {
+ statusFail, err := txStatus.GetStatus(txIndex)
+ if err != nil {
+ log.WithField("err", err).Error("attachUtxos fail on get txStatus")
+ continue
+ }
+
+ validHeight := uint64(0)
+ if txIndex == 0 {
+ validHeight = b.Height + consensus.CoinbasePendingBlockNumber
+ }
+ outputUtxos := txOutToUtxos(tx, statusFail, validHeight)
+ utxos = append(utxos, outputUtxos...)
+ }
+
+ utxos = w.filterAccountUtxo(utxos)
+ if err := batchSaveUtxos(utxos, batch); err != nil {
+ log.WithField("err", err).Error("attachUtxos fail on batchSaveUtxos")
+ }
+}
+
+func (w *Wallet) detachUtxos(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
+ utxos := []*account.UTXO{}
+ for txIndex, tx := range b.Transactions {
+ statusFail, err := txStatus.GetStatus(txIndex)
+ if err != nil {
+ log.WithField("err", err).Error("detachUtxos fail on get tx status")
+ continue
+ }
+
+ inputUtxos := txInToUtxos(tx, statusFail)
+ utxos = append(utxos, inputUtxos...)
+ }
+
+ utxos = w.filterAccountUtxo(utxos)
+ if err := batchSaveUtxos(utxos, batch); err != nil {
+ log.WithField("err", err).Error("detachUtxos fail on batchSaveUtxos")
+ return
+ }
+
+ for _, tx := range b.Transactions {
+ for j := range tx.Outputs {
+ resOut, err := tx.Output(*tx.ResultIds[j])
+ if err != nil {
+ continue
+ }
+
+ if segwit.IsP2WScript(resOut.ControlProgram.Code) {
+ batch.Delete(account.StandardUTXOKey(*tx.ResultIds[j]))
+ } else {
+ batch.Delete(account.ContractUTXOKey(*tx.ResultIds[j]))
+ }
+ }
+ }
+}
+
+func (w *Wallet) filterAccountUtxo(utxos []*account.UTXO) []*account.UTXO {
+ outsByScript := make(map[string][]*account.UTXO, len(utxos))
+ for _, utxo := range utxos {
+ scriptStr := string(utxo.ControlProgram)
+ outsByScript[scriptStr] = append(outsByScript[scriptStr], utxo)
+ }
+
+ result := make([]*account.UTXO, 0, len(utxos))
+ for s := range outsByScript {
+ if !segwit.IsP2WScript([]byte(s)) {
+ for _, utxo := range outsByScript[s] {
+ result = append(result, utxo)
+ }
+ continue
+ }
+
+ var hash [32]byte
+ sha3pool.Sum256(hash[:], []byte(s))
+ data := w.DB.Get(account.ContractKey(hash))
+ if data == nil {
+ continue
+ }
+
+ cp := &account.CtrlProgram{}
+ if err := json.Unmarshal(data, cp); err != nil {
+ log.WithField("err", err).Error("filterAccountUtxo fail on unmarshal control program")
+ continue
+ }
+
+ for _, utxo := range outsByScript[s] {
+ utxo.AccountID = cp.AccountID
+ utxo.Address = cp.Address
+ utxo.ControlProgramIndex = cp.KeyIndex
+ utxo.Change = cp.Change
+ result = append(result, utxo)
+ }
+ }
+ return result
+}
+
+func batchSaveUtxos(utxos []*account.UTXO, batch db.Batch) error {
+ for _, utxo := range utxos {
+ data, err := json.Marshal(utxo)
+ if err != nil {
+ return errors.Wrap(err, "failed marshal accountutxo")
+ }
+
+ if segwit.IsP2WScript(utxo.ControlProgram) {
+ batch.Set(account.StandardUTXOKey(utxo.OutputID), data)
+ } else {
+ batch.Set(account.ContractUTXOKey(utxo.OutputID), data)
+ }
+ }
+ return nil
+}
+
+func txInToUtxos(tx *types.Tx, statusFail bool) []*account.UTXO {
+ utxos := []*account.UTXO{}
+ for _, inpID := range tx.Tx.InputIDs {
+ sp, err := tx.Spend(inpID)
+ if err != nil {
+ continue
+ }
+
+ resOut, err := tx.Output(*sp.SpentOutputId)
+ if err != nil {
+ log.WithField("err", err).Error("txInToUtxos fail on get resOut")
+ continue
+ }
+
+ if statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
+ continue
+ }
+
+ utxos = append(utxos, &account.UTXO{
+ OutputID: *sp.SpentOutputId,
+ AssetID: *resOut.Source.Value.AssetId,
+ Amount: resOut.Source.Value.Amount,
+ ControlProgram: resOut.ControlProgram.Code,
+ SourceID: *resOut.Source.Ref,
+ SourcePos: resOut.Source.Position,
+ })
+ }
+ return utxos
+}
+
+func txOutToUtxos(tx *types.Tx, statusFail bool, vaildHeight uint64) []*account.UTXO {
+ utxos := []*account.UTXO{}
+ for i, out := range tx.Outputs {
+ bcOut, err := tx.Output(*tx.ResultIds[i])
+ if err != nil {
+ continue
+ }
+
+ if statusFail && *out.AssetAmount.AssetId != *consensus.BTMAssetID {
+ continue
+ }
+
+ utxos = append(utxos, &account.UTXO{
+ OutputID: *tx.OutputID(i),
+ AssetID: *out.AssetAmount.AssetId,
+ Amount: out.Amount,
+ ControlProgram: out.ControlProgram,
+ SourceID: *bcOut.Source.Ref,
+ SourcePos: bcOut.Source.Position,
+ ValidHeight: vaildHeight,
+ })
+ }
+ return utxos
+}
--- /dev/null
+package wallet
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "os"
+ "sort"
+ "testing"
+
+ dbm "github.com/tendermint/tmlibs/db"
+
+ "github.com/bytom/account"
+ "github.com/bytom/consensus"
+ "github.com/bytom/protocol/bc"
+ "github.com/bytom/protocol/bc/types"
+ "github.com/bytom/testutil"
+)
+
+func TestGetAccountUtxos(t *testing.T) {
+ testDB := dbm.NewDB("testdb", "leveldb", "temp")
+ defer os.RemoveAll("temp")
+
+ cases := []struct {
+ dbUtxos map[string]*account.UTXO
+ id string
+ isSmartContract bool
+ wantUtxos []*account.UTXO
+ }{
+ {
+ dbUtxos: map[string]*account.UTXO{},
+ id: "",
+ isSmartContract: false,
+ wantUtxos: []*account.UTXO{},
+ },
+ {
+ dbUtxos: map[string]*account.UTXO{
+ string(account.StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{
+ OutputID: bc.Hash{V0: 1},
+ },
+ string(account.StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{
+ OutputID: bc.Hash{V0: 2},
+ },
+ string(account.StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{
+ OutputID: bc.Hash{V0: 3},
+ },
+ string(account.ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{
+ OutputID: bc.Hash{V0: 4},
+ },
+ },
+ id: "",
+ isSmartContract: false,
+ wantUtxos: []*account.UTXO{
+ &account.UTXO{OutputID: bc.Hash{V0: 1}},
+ &account.UTXO{OutputID: bc.Hash{V0: 2}},
+ &account.UTXO{OutputID: bc.Hash{V0: 3}},
+ },
+ },
+ {
+ dbUtxos: map[string]*account.UTXO{
+ string(account.StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{
+ OutputID: bc.Hash{V0: 1},
+ },
+ string(account.StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{
+ OutputID: bc.Hash{V0: 2},
+ },
+ string(account.StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{
+ OutputID: bc.Hash{V0: 3},
+ },
+ string(account.ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{
+ OutputID: bc.Hash{V0: 4},
+ },
+ },
+ id: "",
+ isSmartContract: true,
+ wantUtxos: []*account.UTXO{
+ &account.UTXO{OutputID: bc.Hash{V0: 4}},
+ },
+ },
+ {
+ dbUtxos: map[string]*account.UTXO{
+ string(account.StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{
+ OutputID: bc.Hash{V0: 1},
+ },
+ string(account.StandardUTXOKey(bc.Hash{V0: 1, V1: 2})): &account.UTXO{
+ OutputID: bc.Hash{V0: 1, V1: 2},
+ },
+ string(account.StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{
+ OutputID: bc.Hash{V0: 2},
+ },
+ string(account.StandardUTXOKey(bc.Hash{V0: 2, V1: 2})): &account.UTXO{
+ OutputID: bc.Hash{V0: 2, V1: 2},
+ },
+ },
+ id: "0000000000000002",
+ isSmartContract: false,
+ wantUtxos: []*account.UTXO{
+ &account.UTXO{OutputID: bc.Hash{V0: 2}},
+ &account.UTXO{OutputID: bc.Hash{V0: 2, V1: 2}},
+ },
+ },
+ }
+
+ w := &Wallet{DB: testDB}
+ for i, c := range cases {
+ for k, u := range c.dbUtxos {
+ data, err := json.Marshal(u)
+ if err != nil {
+ t.Error(err)
+ }
+ testDB.Set([]byte(k), data)
+ }
+
+ gotUtxos := w.GetAccountUtxos(c.id, c.isSmartContract)
+ if !testutil.DeepEqual(gotUtxos, c.wantUtxos) {
+ t.Errorf("case %d: got %v want %v", i, gotUtxos, c.wantUtxos)
+ }
+
+ for k := range c.dbUtxos {
+ testDB.Delete([]byte(k))
+ }
+ }
+}
+
+func TestFilterAccountUtxo(t *testing.T) {
+ testDB := dbm.NewDB("testdb", "leveldb", "temp")
+ defer os.RemoveAll("temp")
+
+ cases := []struct {
+ dbPrograms map[string]*account.CtrlProgram
+ input []*account.UTXO
+ wantUtxos []*account.UTXO
+ }{
+ {
+ dbPrograms: map[string]*account.CtrlProgram{},
+ input: []*account.UTXO{},
+ wantUtxos: []*account.UTXO{},
+ },
+ {
+ dbPrograms: map[string]*account.CtrlProgram{
+ "436f6e74726163743a2a37a64a4e15a772ab43bf3f5956d0d1f353946496788e7f40d0ff1796286a6f": &account.CtrlProgram{
+ AccountID: "testAccount",
+ Address: "testAddress",
+ KeyIndex: 53,
+ Change: true,
+ },
+ },
+ input: []*account.UTXO{
+ &account.UTXO{
+ ControlProgram: []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 3,
+ },
+ &account.UTXO{
+ ControlProgram: []byte{0x91},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 4,
+ },
+ },
+ wantUtxos: []*account.UTXO{
+ &account.UTXO{
+ ControlProgram: []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 3,
+ AccountID: "testAccount",
+ Address: "testAddress",
+ ControlProgramIndex: 53,
+ Change: true,
+ },
+ &account.UTXO{
+ ControlProgram: []byte{0x91},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 4,
+ },
+ },
+ },
+ {
+ dbPrograms: map[string]*account.CtrlProgram{},
+ input: []*account.UTXO{
+ &account.UTXO{
+ ControlProgram: []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 3,
+ },
+ &account.UTXO{
+ ControlProgram: []byte{0x91},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 3,
+ },
+ },
+ wantUtxos: []*account.UTXO{
+ &account.UTXO{
+ ControlProgram: []byte{0x91},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 3,
+ },
+ },
+ },
+ {
+ dbPrograms: map[string]*account.CtrlProgram{
+ "436f6e74726163743a2a37a64a4e15a772ab43bf3f5956d0d1f353946496788e7f40d0ff1796286a6f": &account.CtrlProgram{
+ AccountID: "testAccount",
+ Address: "testAddress",
+ KeyIndex: 53,
+ Change: true,
+ },
+ "436f6e74726163743adb4d86262c12ba70d50b3ca3ae102d5682436243bd1e8c79569603f75675036a": &account.CtrlProgram{
+ AccountID: "testAccount2",
+ Address: "testAddress2",
+ KeyIndex: 72,
+ Change: false,
+ },
+ },
+ input: []*account.UTXO{
+ &account.UTXO{
+ ControlProgram: []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 3,
+ },
+ &account.UTXO{
+ ControlProgram: []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 5,
+ },
+ &account.UTXO{
+ ControlProgram: []byte{0x00, 0x14, 0xc6, 0xbf, 0x22, 0x19, 0x64, 0x2a, 0xc5, 0x9e, 0x5b, 0xe4, 0xeb, 0xdf, 0x5b, 0x22, 0x49, 0x56, 0xa7, 0x98, 0xa4, 0xdf},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 7,
+ },
+ },
+ wantUtxos: []*account.UTXO{
+ &account.UTXO{
+ ControlProgram: []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 3,
+ AccountID: "testAccount",
+ Address: "testAddress",
+ ControlProgramIndex: 53,
+ Change: true,
+ },
+ &account.UTXO{
+ ControlProgram: []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 5,
+ AccountID: "testAccount",
+ Address: "testAddress",
+ ControlProgramIndex: 53,
+ Change: true,
+ },
+ &account.UTXO{
+ ControlProgram: []byte{0x00, 0x14, 0xc6, 0xbf, 0x22, 0x19, 0x64, 0x2a, 0xc5, 0x9e, 0x5b, 0xe4, 0xeb, 0xdf, 0x5b, 0x22, 0x49, 0x56, 0xa7, 0x98, 0xa4, 0xdf},
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 7,
+ AccountID: "testAccount2",
+ Address: "testAddress2",
+ ControlProgramIndex: 72,
+ Change: false,
+ },
+ },
+ },
+ }
+
+ w := &Wallet{DB: testDB}
+ for i, c := range cases {
+ for s, p := range c.dbPrograms {
+ data, err := json.Marshal(p)
+ if err != nil {
+ t.Error(err)
+ }
+ key, err := hex.DecodeString(s)
+ if err != nil {
+ t.Error(err)
+ }
+ testDB.Set(key, data)
+ }
+
+ gotUtxos := w.filterAccountUtxo(c.input)
+ sort.Slice(gotUtxos[:], func(i, j int) bool {
+ return gotUtxos[i].Amount < gotUtxos[j].Amount
+ })
+
+ if !testutil.DeepEqual(gotUtxos, c.wantUtxos) {
+ t.Errorf("case %d: got %v want %v", i, gotUtxos, c.wantUtxos)
+ }
+ for s := range c.dbPrograms {
+ key, err := hex.DecodeString(s)
+ if err != nil {
+ t.Error(err)
+ }
+ testDB.Delete(key)
+ }
+ }
+}
+
+func TestTxInToUtxos(t *testing.T) {
+ cases := []struct {
+ tx *types.Tx
+ statusFail bool
+ wantUtxos []*account.UTXO
+ }{
+ {
+ tx: types.NewTx(types.TxData{
+ Inputs: []*types.TxInput{
+ types.NewCoinbaseInput([]byte{0x51}),
+ },
+ Outputs: []*types.TxOutput{
+ types.NewTxOutput(*consensus.BTMAssetID, 41250000000, []byte{0x51}),
+ },
+ }),
+ statusFail: false,
+ wantUtxos: []*account.UTXO{},
+ },
+ {
+ tx: types.NewTx(types.TxData{
+ Inputs: []*types.TxInput{
+ types.NewIssuanceInput([]byte{}, 4125, []byte{0x51}, [][]byte{}, []byte{}),
+ },
+ Outputs: []*types.TxOutput{
+ types.NewTxOutput(*consensus.BTMAssetID, 4125, []byte{0x51}),
+ },
+ }),
+ statusFail: false,
+ wantUtxos: []*account.UTXO{},
+ },
+ {
+ tx: types.NewTx(types.TxData{
+ Inputs: []*types.TxInput{
+ types.NewSpendInput([][]byte{}, bc.Hash{V0: 1}, bc.AssetID{V0: 1}, 1, 1, []byte{0x51}),
+ types.NewSpendInput([][]byte{}, bc.Hash{V0: 2}, bc.AssetID{V0: 1}, 3, 2, []byte{0x52}),
+ types.NewSpendInput([][]byte{}, bc.Hash{V0: 3}, *consensus.BTMAssetID, 5, 3, []byte{0x53}),
+ types.NewSpendInput([][]byte{}, bc.Hash{V0: 4}, *consensus.BTMAssetID, 7, 4, []byte{0x54}),
+ },
+ Outputs: []*types.TxOutput{
+ types.NewTxOutput(bc.AssetID{V0: 1}, 4, []byte{0x51}),
+ types.NewTxOutput(*consensus.BTMAssetID, 12, []byte{0x53}),
+ },
+ }),
+ statusFail: false,
+ wantUtxos: []*account.UTXO{
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0x9c, 0x0d, 0xfd, 0x17, 0x4a, 0x63, 0x02, 0x0f, 0xf1, 0xda, 0xe9, 0x2d, 0xc8, 0xdb, 0x3c, 0x1c, 0x46, 0xda, 0xf2, 0x8f, 0xef, 0xe9, 0x5c, 0xef, 0x38, 0x82, 0xe0, 0xaf, 0xc5, 0x28, 0x39, 0x3c}),
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 1,
+ ControlProgram: []byte{0x51},
+ SourceID: bc.Hash{V0: 1},
+ SourcePos: 1,
+ },
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0xc1, 0xf6, 0xa3, 0xb8, 0x32, 0x20, 0xbc, 0xde, 0x95, 0x23, 0x23, 0xf8, 0xe4, 0xe9, 0x82, 0x45, 0x86, 0xfb, 0xf9, 0x43, 0x0d, 0xfb, 0xfc, 0x7c, 0xcf, 0xf9, 0x6c, 0x2a, 0xbf, 0x35, 0xb7, 0xe1}),
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 3,
+ ControlProgram: []byte{0x52},
+ SourceID: bc.Hash{V0: 2},
+ SourcePos: 2,
+ },
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0x3d, 0x0c, 0x52, 0xb0, 0x59, 0xe6, 0xdd, 0xcd, 0x31, 0xe1, 0x25, 0xc4, 0x33, 0xfc, 0x2c, 0x9c, 0xbe, 0xe3, 0x0c, 0x3e, 0x72, 0xb4, 0xac, 0x36, 0x5b, 0xbb, 0x8d, 0x44, 0xa0, 0x82, 0x27, 0xe4}),
+ AssetID: *consensus.BTMAssetID,
+ Amount: 5,
+ ControlProgram: []byte{0x53},
+ SourceID: bc.Hash{V0: 3},
+ SourcePos: 3,
+ },
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0x88, 0x9e, 0x88, 0xd6, 0x21, 0x74, 0x7f, 0xa0, 0x01, 0x79, 0x29, 0x9a, 0xd8, 0xa3, 0xe9, 0xc7, 0x67, 0xac, 0x39, 0x1c, 0x2b, 0x3a, 0xd1, 0x56, 0x50, 0x42, 0xe1, 0x6b, 0x43, 0x2c, 0x05, 0x91}),
+ AssetID: *consensus.BTMAssetID,
+ Amount: 7,
+ ControlProgram: []byte{0x54},
+ SourceID: bc.Hash{V0: 4},
+ SourcePos: 4,
+ },
+ },
+ },
+ {
+ tx: types.NewTx(types.TxData{
+ Inputs: []*types.TxInput{
+ types.NewSpendInput([][]byte{}, bc.Hash{V0: 1}, bc.AssetID{V0: 1}, 1, 1, []byte{0x51}),
+ types.NewSpendInput([][]byte{}, bc.Hash{V0: 2}, bc.AssetID{V0: 1}, 3, 2, []byte{0x52}),
+ types.NewSpendInput([][]byte{}, bc.Hash{V0: 3}, *consensus.BTMAssetID, 5, 3, []byte{0x53}),
+ types.NewSpendInput([][]byte{}, bc.Hash{V0: 4}, *consensus.BTMAssetID, 7, 4, []byte{0x54}),
+ },
+ Outputs: []*types.TxOutput{
+ types.NewTxOutput(bc.AssetID{V0: 1}, 4, []byte{0x51}),
+ types.NewTxOutput(*consensus.BTMAssetID, 12, []byte{0x53}),
+ },
+ }),
+ statusFail: true,
+ wantUtxos: []*account.UTXO{
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0x3d, 0x0c, 0x52, 0xb0, 0x59, 0xe6, 0xdd, 0xcd, 0x31, 0xe1, 0x25, 0xc4, 0x33, 0xfc, 0x2c, 0x9c, 0xbe, 0xe3, 0x0c, 0x3e, 0x72, 0xb4, 0xac, 0x36, 0x5b, 0xbb, 0x8d, 0x44, 0xa0, 0x82, 0x27, 0xe4}),
+ AssetID: *consensus.BTMAssetID,
+ Amount: 5,
+ ControlProgram: []byte{0x53},
+ SourceID: bc.Hash{V0: 3},
+ SourcePos: 3,
+ },
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0x88, 0x9e, 0x88, 0xd6, 0x21, 0x74, 0x7f, 0xa0, 0x01, 0x79, 0x29, 0x9a, 0xd8, 0xa3, 0xe9, 0xc7, 0x67, 0xac, 0x39, 0x1c, 0x2b, 0x3a, 0xd1, 0x56, 0x50, 0x42, 0xe1, 0x6b, 0x43, 0x2c, 0x05, 0x91}),
+ AssetID: *consensus.BTMAssetID,
+ Amount: 7,
+ ControlProgram: []byte{0x54},
+ SourceID: bc.Hash{V0: 4},
+ SourcePos: 4,
+ },
+ },
+ },
+ }
+
+ for i, c := range cases {
+ if gotUtxos := txInToUtxos(c.tx, c.statusFail); !testutil.DeepEqual(gotUtxos, c.wantUtxos) {
+ for k, v := range gotUtxos {
+ data, _ := json.Marshal(v)
+ fmt.Println(k, string(data))
+ }
+ for k, v := range c.wantUtxos {
+ data, _ := json.Marshal(v)
+ fmt.Println(k, string(data))
+ }
+ t.Errorf("case %d: got %v want %v", i, gotUtxos, c.wantUtxos)
+ }
+ }
+}
+
+func TestTxOutToUtxos(t *testing.T) {
+ cases := []struct {
+ tx *types.Tx
+ statusFail bool
+ vaildHeight uint64
+ wantUtxos []*account.UTXO
+ }{
+ {
+ tx: types.NewTx(types.TxData{
+ Inputs: []*types.TxInput{
+ types.NewCoinbaseInput([]byte{0x51}),
+ },
+ Outputs: []*types.TxOutput{
+ types.NewTxOutput(*consensus.BTMAssetID, 41250000000, []byte{0x51}),
+ },
+ }),
+ statusFail: false,
+ vaildHeight: 98,
+ wantUtxos: []*account.UTXO{
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0x67, 0x13, 0xec, 0x34, 0x57, 0xf8, 0xdc, 0xf1, 0xb2, 0x95, 0x71, 0x4c, 0xe0, 0xca, 0x94, 0xef, 0x81, 0x84, 0x6b, 0xa4, 0xc0, 0x8b, 0xb4, 0x40, 0xd3, 0xc0, 0x55, 0xc2, 0x7a, 0x9c, 0x04, 0x0a}),
+ AssetID: *consensus.BTMAssetID,
+ Amount: 41250000000,
+ ControlProgram: []byte{0x51},
+ SourceID: bc.NewHash([32]byte{0xb4, 0x7e, 0x94, 0x31, 0x88, 0xfe, 0xd3, 0xe9, 0xac, 0x99, 0x7c, 0xfc, 0x99, 0x6d, 0xd7, 0x4d, 0x04, 0x10, 0x77, 0xcb, 0x1c, 0xf8, 0x95, 0x14, 0x00, 0xe3, 0x42, 0x00, 0x8d, 0x05, 0xec, 0xdc}),
+ SourcePos: 0,
+ ValidHeight: 98,
+ },
+ },
+ },
+ {
+ tx: types.NewTx(types.TxData{
+ Inputs: []*types.TxInput{
+ types.NewSpendInput([][]byte{}, bc.Hash{V0: 1}, bc.AssetID{V0: 1}, 5, 1, []byte{0x51}),
+ types.NewSpendInput([][]byte{}, bc.Hash{V0: 2}, *consensus.BTMAssetID, 7, 1, []byte{0x51}),
+ },
+ Outputs: []*types.TxOutput{
+ types.NewTxOutput(bc.AssetID{V0: 1}, 2, []byte{0x51}),
+ types.NewTxOutput(bc.AssetID{V0: 1}, 3, []byte{0x52}),
+ types.NewTxOutput(*consensus.BTMAssetID, 2, []byte{0x53}),
+ types.NewTxOutput(*consensus.BTMAssetID, 5, []byte{0x54}),
+ },
+ }),
+ statusFail: false,
+ vaildHeight: 0,
+ wantUtxos: []*account.UTXO{
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0xff, 0xcd, 0xc4, 0xdc, 0xe8, 0x7e, 0xce, 0x93, 0x4b, 0x14, 0x2b, 0x2b, 0x84, 0xf2, 0x4d, 0x08, 0xca, 0x9f, 0x0b, 0x97, 0xa3, 0x0e, 0x38, 0x5a, 0xb0, 0xa7, 0x1e, 0x8f, 0x22, 0x55, 0xa6, 0x19}),
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 2,
+ ControlProgram: []byte{0x51},
+ SourceID: bc.NewHash([32]byte{0x39, 0x4f, 0x89, 0xd4, 0xdc, 0x26, 0xb9, 0x57, 0x91, 0x2f, 0xe9, 0x7f, 0xba, 0x51, 0x68, 0xcf, 0xe4, 0xae, 0x0c, 0xef, 0x79, 0x56, 0xa0, 0x45, 0xda, 0x27, 0xdc, 0x69, 0xd8, 0xef, 0x32, 0x61}),
+ SourcePos: 0,
+ },
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0x89, 0xcd, 0x38, 0x92, 0x6f, 0xee, 0xc6, 0x10, 0xae, 0x61, 0xef, 0x62, 0x70, 0x88, 0x94, 0x7c, 0x26, 0xaa, 0xfb, 0x05, 0xa2, 0x0a, 0x63, 0x9d, 0x21, 0x22, 0x0c, 0xe3, 0xc2, 0xe5, 0xf9, 0xbf}),
+ AssetID: bc.AssetID{V0: 1},
+ Amount: 3,
+ ControlProgram: []byte{0x52},
+ SourceID: bc.NewHash([32]byte{0x39, 0x4f, 0x89, 0xd4, 0xdc, 0x26, 0xb9, 0x57, 0x91, 0x2f, 0xe9, 0x7f, 0xba, 0x51, 0x68, 0xcf, 0xe4, 0xae, 0x0c, 0xef, 0x79, 0x56, 0xa0, 0x45, 0xda, 0x27, 0xdc, 0x69, 0xd8, 0xef, 0x32, 0x61}),
+ SourcePos: 1,
+ },
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0xcf, 0xb9, 0xeb, 0xa3, 0xc8, 0xe8, 0xf1, 0x5a, 0x5c, 0x70, 0xf8, 0x9e, 0x7d, 0x9e, 0xf7, 0xb2, 0x66, 0x42, 0x8c, 0x97, 0x8e, 0xc2, 0x4d, 0x4b, 0x28, 0x57, 0xa7, 0x61, 0x1c, 0xf1, 0xea, 0x9d}),
+ AssetID: *consensus.BTMAssetID,
+ Amount: 2,
+ ControlProgram: []byte{0x53},
+ SourceID: bc.NewHash([32]byte{0x39, 0x4f, 0x89, 0xd4, 0xdc, 0x26, 0xb9, 0x57, 0x91, 0x2f, 0xe9, 0x7f, 0xba, 0x51, 0x68, 0xcf, 0xe4, 0xae, 0x0c, 0xef, 0x79, 0x56, 0xa0, 0x45, 0xda, 0x27, 0xdc, 0x69, 0xd8, 0xef, 0x32, 0x61}),
+ SourcePos: 2,
+ },
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0x21, 0xf2, 0xe4, 0xee, 0xec, 0x1f, 0x82, 0xd8, 0xf2, 0xe1, 0x2b, 0x9e, 0x72, 0xfa, 0x91, 0x2b, 0x8c, 0xce, 0xbd, 0x18, 0x6d, 0x16, 0xf8, 0xc4, 0xf1, 0x71, 0x9d, 0x6b, 0x44, 0x41, 0xde, 0xb9}),
+ AssetID: *consensus.BTMAssetID,
+ Amount: 5,
+ ControlProgram: []byte{0x54},
+ SourceID: bc.NewHash([32]byte{0x39, 0x4f, 0x89, 0xd4, 0xdc, 0x26, 0xb9, 0x57, 0x91, 0x2f, 0xe9, 0x7f, 0xba, 0x51, 0x68, 0xcf, 0xe4, 0xae, 0x0c, 0xef, 0x79, 0x56, 0xa0, 0x45, 0xda, 0x27, 0xdc, 0x69, 0xd8, 0xef, 0x32, 0x61}),
+ SourcePos: 3,
+ },
+ },
+ },
+ {
+ tx: types.NewTx(types.TxData{
+ Inputs: []*types.TxInput{
+ types.NewSpendInput([][]byte{}, bc.Hash{V0: 1}, bc.AssetID{V0: 1}, 5, 1, []byte{0x51}),
+ types.NewSpendInput([][]byte{}, bc.Hash{V0: 2}, *consensus.BTMAssetID, 7, 1, []byte{0x51}),
+ },
+ Outputs: []*types.TxOutput{
+ types.NewTxOutput(bc.AssetID{V0: 1}, 2, []byte{0x51}),
+ types.NewTxOutput(bc.AssetID{V0: 1}, 3, []byte{0x52}),
+ types.NewTxOutput(*consensus.BTMAssetID, 2, []byte{0x53}),
+ types.NewTxOutput(*consensus.BTMAssetID, 5, []byte{0x54}),
+ },
+ }),
+ statusFail: true,
+ vaildHeight: 0,
+ wantUtxos: []*account.UTXO{
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0xcf, 0xb9, 0xeb, 0xa3, 0xc8, 0xe8, 0xf1, 0x5a, 0x5c, 0x70, 0xf8, 0x9e, 0x7d, 0x9e, 0xf7, 0xb2, 0x66, 0x42, 0x8c, 0x97, 0x8e, 0xc2, 0x4d, 0x4b, 0x28, 0x57, 0xa7, 0x61, 0x1c, 0xf1, 0xea, 0x9d}),
+ AssetID: *consensus.BTMAssetID,
+ Amount: 2,
+ ControlProgram: []byte{0x53},
+ SourceID: bc.NewHash([32]byte{0x39, 0x4f, 0x89, 0xd4, 0xdc, 0x26, 0xb9, 0x57, 0x91, 0x2f, 0xe9, 0x7f, 0xba, 0x51, 0x68, 0xcf, 0xe4, 0xae, 0x0c, 0xef, 0x79, 0x56, 0xa0, 0x45, 0xda, 0x27, 0xdc, 0x69, 0xd8, 0xef, 0x32, 0x61}),
+ SourcePos: 2,
+ },
+ &account.UTXO{
+ OutputID: bc.NewHash([32]byte{0x21, 0xf2, 0xe4, 0xee, 0xec, 0x1f, 0x82, 0xd8, 0xf2, 0xe1, 0x2b, 0x9e, 0x72, 0xfa, 0x91, 0x2b, 0x8c, 0xce, 0xbd, 0x18, 0x6d, 0x16, 0xf8, 0xc4, 0xf1, 0x71, 0x9d, 0x6b, 0x44, 0x41, 0xde, 0xb9}),
+ AssetID: *consensus.BTMAssetID,
+ Amount: 5,
+ ControlProgram: []byte{0x54},
+ SourceID: bc.NewHash([32]byte{0x39, 0x4f, 0x89, 0xd4, 0xdc, 0x26, 0xb9, 0x57, 0x91, 0x2f, 0xe9, 0x7f, 0xba, 0x51, 0x68, 0xcf, 0xe4, 0xae, 0x0c, 0xef, 0x79, 0x56, 0xa0, 0x45, 0xda, 0x27, 0xdc, 0x69, 0xd8, 0xef, 0x32, 0x61}),
+ SourcePos: 3,
+ },
+ },
+ },
+ }
+
+ for i, c := range cases {
+ if gotUtxos := txOutToUtxos(c.tx, c.statusFail, c.vaildHeight); !testutil.DeepEqual(gotUtxos, c.wantUtxos) {
+ t.Errorf("case %d: got %v want %v", i, gotUtxos, c.wantUtxos)
+ }
+ }
+}
const (
//SINGLE single sign
SINGLE = 1
-
- maxTxChanSize = 10000 // txChanSize is the size of channel listening to Txpool newTxCh
)
var walletKey = []byte("walletInfo")
Hsm *pseudohsm.HSM
chain *protocol.Chain
rescanCh chan struct{}
- newTxCh chan *types.Tx
}
//NewWallet return a new wallet instance
chain: chain,
Hsm: hsm,
rescanCh: make(chan struct{}, 1),
- newTxCh: make(chan *types.Tx, maxTxChanSize),
}
if err := w.loadWalletInfo(); err != nil {
}
go w.walletUpdater()
- go w.UnconfirmedTxCollector()
-
return w, nil
}
storeBatch := w.DB.NewBatch()
w.indexTransactions(storeBatch, block, txStatus)
- w.buildAccountUTXOs(storeBatch, block, txStatus)
+ w.attachUtxos(storeBatch, block, txStatus)
w.status.WorkHeight = block.Height
w.status.WorkHash = block.Hash()
}
storeBatch := w.DB.NewBatch()
- w.reverseAccountUTXOs(storeBatch, block, txStatus)
+ w.detachUtxos(storeBatch, block, txStatus)
w.deleteTransactions(storeBatch, w.status.BestHeight)
w.status.BestHeight = block.Height - 1
}
}
-// GetNewTxCh return a unconfirmed transaction feed channel
-func (w *Wallet) GetNewTxCh() chan *types.Tx {
- return w.newTxCh
-}
-
-// UnconfirmedTxCollector collector unconfirmed transaction
-func (w *Wallet) UnconfirmedTxCollector() {
- for {
- w.SaveUnconfirmedTx(<-w.newTxCh)
- }
-}
-
// GetWalletStatusInfo return current wallet StatusInfo
func (w *Wallet) GetWalletStatusInfo() StatusInfo {
w.rw.RLock()
return w.status
}
-
-func (w *Wallet) createProgram(account *account.Account, XPub *pseudohsm.XPub, index uint64) error {
- for i := uint64(0); i < index; i++ {
- if _, err := w.AccountMgr.CreateAddress(nil, account.ID, false); err != nil {
- return err
- }
- }
- return nil
-}
t.Fatal(err)
}
- testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub}, 1, "testAccount")
+ testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub}, 1, "testAccount")
if err != nil {
t.Fatal(err)
}
- controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+ controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
if err != nil {
t.Fatal(err)
}
tx := types.NewTx(*txData)
block := mockSingleBlock(tx)
txStatus := bc.NewTransactionStatus()
+ txStatus.SetStatus(0, false)
store.SaveBlock(block, txStatus)
w := mockWallet(testDB, accountManager, reg, chain)