--- /dev/null
+package account
+
+import (
+ "context"
+ "encoding/json"
+
+ "github.com/vapor/blockchain/signers"
+ "github.com/vapor/blockchain/txbuilder"
+ "github.com/vapor/common"
+ "github.com/vapor/consensus"
+ "github.com/vapor/crypto/ed25519/chainkd"
+ "github.com/vapor/errors"
+ "github.com/vapor/protocol/bc"
+ "github.com/vapor/protocol/bc/types"
+ "github.com/vapor/protocol/vm/vmutil"
+)
+
+var (
+ //chainTxUtxoNum maximum utxo quantity in a tx
+ chainTxUtxoNum = 5
+ //chainTxMergeGas chain tx gas
+ chainTxMergeGas = uint64(10000000)
+)
+
+//DecodeSpendAction unmarshal JSON-encoded data of spend action
+func (m *Manager) DecodeSpendAction(data []byte) (txbuilder.Action, error) {
+ a := &spendAction{accounts: m}
+ return a, json.Unmarshal(data, a)
+}
+
+type spendAction struct {
+ accounts *Manager
+ bc.AssetAmount
+ 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 += chainTxMergeGas
+ num -= 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() != chainTxUtxoNum && index != len(utxos)-1 {
+ continue
+ }
+
+ outAmount := buildAmount - 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 {
+ var missing []string
+ if a.AccountID == "" {
+ missing = append(missing, "account_id")
+ }
+ if a.AssetId.IsZero() {
+ missing = append(missing, "asset_id")
+ }
+ if len(missing) > 0 {
+ return txbuilder.MissingFieldsError(missing...)
+ }
+
+ acct, err := a.accounts.FindByID(a.AccountID)
+ if err != nil {
+ return errors.Wrap(err, "get account info")
+ }
+
+ 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(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")
+ }
+
+ if err = b.AddInput(txInput, sigInst); err != nil {
+ return errors.Wrap(err, "adding inputs")
+ }
+ }
+
+ 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(b, acp)
+ if err = b.AddOutput(types.NewTxOutput(*a.AssetId, res.change, acp.ControlProgram)); err != nil {
+ return errors.Wrap(err, "adding change output")
+ }
+ }
+ return nil
+}
+
+//DecodeSpendUTXOAction unmarshal JSON-encoded data of spend utxo action
+func (m *Manager) DecodeSpendUTXOAction(data []byte) (txbuilder.Action, error) {
+ a := &spendUTXOAction{accounts: m}
+ return a, json.Unmarshal(data, a)
+}
+
+type spendUTXOAction struct {
+ accounts *Manager
+ OutputID *bc.Hash `json:"output_id"`
+ UseUnconfirmed bool `json:"use_unconfirmed"`
+ Arguments []txbuilder.ContractArgument `json:"arguments"`
+}
+
+func (a *spendUTXOAction) ActionType() string {
+ return "spend_account_unspent_output"
+}
+
+func (a *spendUTXOAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error {
+ if a.OutputID == nil {
+ return txbuilder.MissingFieldsError("output_id")
+ }
+
+ res, err := a.accounts.utxoKeeper.ReserveParticular(*a.OutputID, a.UseUnconfirmed, b.MaxTime())
+ if err != nil {
+ return err
+ }
+
+ b.OnRollback(func() { a.accounts.utxoKeeper.Cancel(res.id) })
+ var accountSigner *signers.Signer
+ 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])
+ if err != nil {
+ return err
+ }
+
+ 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
+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)
+ sigInst := &txbuilder.SigningInstruction{}
+ if signer == nil {
+ return txInput, sigInst, nil
+ }
+
+ 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
+ }
+
+ address, err := common.DecodeAddress(u.Address, &consensus.ActiveNetParams)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ sigInst.AddRawWitnessKeys(signer.XPubs, path, signer.Quorum)
+ derivedXPubs := chainkd.DeriveXPubs(signer.XPubs, path)
+
+ switch address.(type) {
+ case *common.AddressWitnessPubKeyHash:
+ derivedPK := derivedXPubs[0].PublicKey()
+ sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness([]byte(derivedPK)))
+
+ case *common.AddressWitnessScriptHash:
+ derivedPKs := chainkd.XPubKeys(derivedXPubs)
+ script, err := vmutil.P2SPMultiSigProgram(derivedPKs, signer.Quorum)
+ if err != nil {
+ return nil, nil, err
+ }
+ sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness(script))
+
+ default:
+ return nil, nil, errors.New("unsupport address type")
+ }
+
+ return txInput, sigInst, nil
+}
+
+// insertControlProgramDelayed takes a template builder and an account
+// control program that hasn't been inserted to the database yet. It
+// 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(b *txbuilder.TemplateBuilder, acp *CtrlProgram) {
+ m.delayedACPsMu.Lock()
+ m.delayedACPs[b] = append(m.delayedACPs[b], acp)
+ m.delayedACPsMu.Unlock()
+
+ b.OnRollback(func() {
+ m.delayedACPsMu.Lock()
+ delete(m.delayedACPs, b)
+ m.delayedACPsMu.Unlock()
+ })
+ b.OnBuild(func() error {
+ m.delayedACPsMu.Lock()
+ acps := m.delayedACPs[b]
+ delete(m.delayedACPs, b)
+ m.delayedACPsMu.Unlock()
+
+ // Insert all of the account control programs at once.
+ if len(acps) == 0 {
+ return nil
+ }
+ return m.SaveControlPrograms(acps...)
+ })
+}