"context"
"encoding/json"
- log "github.com/sirupsen/logrus"
-
- "github.com/bytom/blockchain/signers"
- "github.com/bytom/blockchain/txbuilder"
- "github.com/bytom/common"
- "github.com/bytom/consensus"
- "github.com/bytom/crypto/ed25519/chainkd"
- "github.com/bytom/errors"
- "github.com/bytom/protocol/bc"
- "github.com/bytom/protocol/bc/types"
- "github.com/bytom/protocol/vm/vmutil"
+ "github.com/bytom/bytom/blockchain/signers"
+ "github.com/bytom/bytom/blockchain/txbuilder"
+ "github.com/bytom/bytom/common"
+ "github.com/bytom/bytom/consensus"
+ "github.com/bytom/bytom/crypto/ed25519/chainkd"
+ "github.com/bytom/bytom/errors"
+ "github.com/bytom/bytom/protocol/bc"
+ "github.com/bytom/bytom/protocol/bc/types"
+ "github.com/bytom/bytom/protocol/vm/vmutil"
)
//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"`
- ClientToken *string `json:"client_token"`
+ AccountID string `json:"account_id"`
+ UseUnconfirmed bool `json:"use_unconfirmed"`
+}
+
+func (a *spendAction) ActionType() string {
+ return "spend_account"
+}
+
+// MergeSpendAction merge common assetID and accountID spend action
+func MergeSpendAction(actions []txbuilder.Action) []txbuilder.Action {
+ resultActions := []txbuilder.Action{}
+ spendActionMap := make(map[string]*spendAction)
+
+ for _, act := range actions {
+ switch act := act.(type) {
+ case *spendAction:
+ 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)
+ }
+ default:
+ resultActions = append(resultActions, act)
+ }
+ }
+ return resultActions
+}
+
+//calcMergeGas calculate the gas required that n utxos are merged into one
+func calcMergeGas(num int) uint64 {
+ gas := uint64(0)
+ for num > 1 {
+ gas += txbuilder.ChainTxMergeGas
+ num -= txbuilder.ChainTxUtxoNum - 1
+ }
+ return gas
+}
+
+func (m *Manager) reserveBtmUtxoChain(builder *txbuilder.TemplateBuilder, accountID string, amount uint64, useUnconfirmed bool) ([]*UTXO, error) {
+ reservedAmount := uint64(0)
+ utxos := []*UTXO{}
+ for gasAmount := uint64(0); reservedAmount < gasAmount+amount; gasAmount = calcMergeGas(len(utxos)) {
+ reserveAmount := amount + gasAmount - reservedAmount
+ res, err := m.utxoKeeper.Reserve(accountID, consensus.BTMAssetID, reserveAmount, useUnconfirmed, builder.MaxTime())
+ if err != nil {
+ return nil, err
+ }
+
+ builder.OnRollback(func() { m.utxoKeeper.Cancel(res.id) })
+ reservedAmount += reserveAmount + res.change
+ utxos = append(utxos, res.utxos[:]...)
+ }
+ return utxos, nil
+}
+
+func (m *Manager) buildBtmTxChain(utxos []*UTXO, signer *signers.Signer) ([]*txbuilder.Template, *UTXO, error) {
+ if len(utxos) == 0 {
+ return nil, nil, errors.New("mergeSpendActionUTXO utxos num 0")
+ }
+
+ tpls := []*txbuilder.Template{}
+ if len(utxos) == 1 {
+ return tpls, utxos[len(utxos)-1], nil
+ }
+
+ acp, err := m.GetLocalCtrlProgramByAddress(utxos[0].Address)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ buildAmount := uint64(0)
+ builder := &txbuilder.TemplateBuilder{}
+ for index := 0; index < len(utxos); index++ {
+ input, sigInst, err := UtxoToInputs(signer, utxos[index])
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if err = builder.AddInput(input, sigInst); err != nil {
+ return nil, nil, err
+ }
+
+ buildAmount += input.Amount()
+ if builder.InputCount() != txbuilder.ChainTxUtxoNum && index != len(utxos)-1 {
+ continue
+ }
+
+ outAmount := buildAmount - txbuilder.ChainTxMergeGas
+ output := types.NewTxOutput(*consensus.BTMAssetID, outAmount, acp.ControlProgram)
+ if err := builder.AddOutput(output); err != nil {
+ return nil, nil, err
+ }
+
+ tpl, _, err := builder.Build()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ bcOut, err := tpl.Transaction.Output(*tpl.Transaction.ResultIds[0])
+ if err != nil {
+ return nil, nil, err
+ }
+
+ utxos = append(utxos, &UTXO{
+ OutputID: *tpl.Transaction.ResultIds[0],
+ AssetID: *consensus.BTMAssetID,
+ Amount: outAmount,
+ ControlProgram: acp.ControlProgram,
+ SourceID: *bcOut.Source.Ref,
+ SourcePos: bcOut.Source.Position,
+ ControlProgramIndex: acp.KeyIndex,
+ Address: acp.Address,
+ Change: acp.Change,
+ })
+
+ tpls = append(tpls, tpl)
+ buildAmount = 0
+ builder = &txbuilder.TemplateBuilder{}
+ if index == len(utxos)-2 {
+ break
+ }
+ }
+ return tpls, utxos[len(utxos)-1], nil
+}
+
+// SpendAccountChain build the spend action with auto merge utxo function
+func SpendAccountChain(ctx context.Context, builder *txbuilder.TemplateBuilder, action txbuilder.Action) ([]*txbuilder.Template, error) {
+ act, ok := action.(*spendAction)
+ if !ok {
+ return nil, errors.New("fail to convert the spend action")
+ }
+ if *act.AssetId != *consensus.BTMAssetID {
+ return nil, errors.New("spend chain action only support BTM")
+ }
+
+ utxos, err := act.accounts.reserveBtmUtxoChain(builder, act.AccountID, act.Amount, act.UseUnconfirmed)
+ if err != nil {
+ return nil, err
+ }
+
+ acct, err := act.accounts.FindByID(act.AccountID)
+ if err != nil {
+ return nil, err
+ }
+
+ tpls, utxo, err := act.accounts.buildBtmTxChain(utxos, acct.Signer)
+ if err != nil {
+ return nil, err
+ }
+
+ input, sigInst, err := UtxoToInputs(acct.Signer, utxo)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := builder.AddInput(input, sigInst); err != nil {
+ return nil, err
+ }
+
+ if utxo.Amount > act.Amount {
+ if err = builder.AddOutput(types.NewTxOutput(*consensus.BTMAssetID, utxo.Amount-act.Amount, utxo.ControlProgram)); err != nil {
+ return nil, errors.Wrap(err, "adding change output")
+ }
+ }
+ return tpls, nil
}
func (a *spendAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error {
if a.AssetId.IsZero() {
missing = append(missing, "asset_id")
}
+ if a.AssetAmount.Amount == 0 {
+ missing = append(missing, "amount")
+ }
if len(missing) > 0 {
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, a.ClientToken, 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.CreateCtrlProgramForChange(ctx, a.AccountID)
+ 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"`
+ accounts *Manager
+ OutputID *bc.Hash `json:"output_id"`
+ UseUnconfirmed bool `json:"use_unconfirmed"`
+ Arguments []txbuilder.ContractArgument `json:"arguments"`
+}
- ClientToken *string `json:"client_token"`
+func (a *spendUTXOAction) ActionType() string {
+ return "spend_account_unspent_output"
}
func (a *spendUTXOAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error {
return txbuilder.MissingFieldsError("output_id")
}
- res, err := a.accounts.utxoDB.ReserveUTXO(ctx, *a.OutputID, a.ClientToken, 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")
- }
+ if a.Arguments == nil {
+ return b.AddInput(txInput, sigInst)
+ }
+
+ sigInst = &txbuilder.SigningInstruction{}
+ if err := txbuilder.AddContractArgs(sigInst, a.Arguments); err != nil {
+ return err
}
+
+ return b.AddInput(txInput, sigInst)
}
// UtxoToInputs convert an utxo to the txinput
return txInput, sigInst, nil
}
- path := signers.Path(signer, signers.AccountKeySpace, u.ControlProgramIndex)
+ path, err := signers.Path(signer, signers.AccountKeySpace, u.Change, u.ControlProgramIndex)
+ if err != nil {
+ return nil, nil, err
+ }
if u.Address == "" {
sigInst.AddWitnessKeys(signer.XPubs, path, signer.Quorum)
return txInput, sigInst, nil
return nil, nil, err
}
+ sigInst.AddRawWitnessKeys(signer.XPubs, path, signer.Quorum)
+ derivedXPubs := chainkd.DeriveXPubs(signer.XPubs, path)
+
switch address.(type) {
case *common.AddressWitnessPubKeyHash:
- sigInst.AddRawWitnessKeys(signer.XPubs, path, signer.Quorum)
- derivedXPubs := chainkd.DeriveXPubs(signer.XPubs, path)
derivedPK := derivedXPubs[0].PublicKey()
sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness([]byte(derivedPK)))
case *common.AddressWitnessScriptHash:
- sigInst.AddRawWitnessKeys(signer.XPubs, path, signer.Quorum)
- path := signers.Path(signer, signers.AccountKeySpace, u.ControlProgramIndex)
- derivedXPubs := chainkd.DeriveXPubs(signer.XPubs, path)
derivedPKs := chainkd.XPubKeys(derivedXPubs)
script, err := vmutil.P2SPMultiSigProgram(derivedPKs, signer.Quorum)
if err != nil {
// 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.SaveControlPrograms(acps...)
})
}