OSDN Git Service

modify dashboard directory (#1453)
[bytom/bytom.git] / api / transact.go
index c1b1ff8..3dcad4c 100644 (file)
@@ -3,15 +3,16 @@ package api
 import (
        "context"
        "encoding/json"
-       "fmt"
+       "math"
        "strings"
        "time"
 
        log "github.com/sirupsen/logrus"
 
-       "github.com/bytom/blockchain/pseudohsm"
+       "github.com/bytom/account"
        "github.com/bytom/blockchain/txbuilder"
        "github.com/bytom/consensus"
+       "github.com/bytom/consensus/segwit"
        "github.com/bytom/errors"
        "github.com/bytom/math/checked"
        "github.com/bytom/net/http/reqid"
@@ -19,89 +20,112 @@ import (
        "github.com/bytom/protocol/bc/types"
 )
 
-var defaultTxTTL = 5 * time.Minute
+var (
+       defaultTxTTL    = 5 * time.Minute
+       defaultBaseRate = float64(100000)
+       flexibleGas     = int64(1800)
+)
 
 func (a *API) actionDecoder(action string) (func([]byte) (txbuilder.Action, error), bool) {
-       var decoder func([]byte) (txbuilder.Action, error)
-       switch action {
-       case "control_address":
-               decoder = txbuilder.DecodeControlAddressAction
-       case "control_program":
-               decoder = txbuilder.DecodeControlProgramAction
-       case "control_receiver":
-               decoder = txbuilder.DecodeControlReceiverAction
-       case "issue":
-               decoder = a.wallet.AssetReg.DecodeIssueAction
-       case "retire":
-               decoder = txbuilder.DecodeRetireAction
-       case "spend_account":
-               decoder = a.wallet.AccountMgr.DecodeSpendAction
-       case "spend_account_unspent_output":
-               decoder = a.wallet.AccountMgr.DecodeSpendUTXOAction
-       default:
-               return nil, false
-       }
-       return decoder, true
-}
-
-func mergeActions(req *BuildRequest) []map[string]interface{} {
-       var actions []map[string]interface{}
-       actionMap := make(map[string]map[string]interface{})
-
-       for _, m := range req.Actions {
-               if actionType := m["type"].(string); actionType != "spend_account" {
-                       actions = append(actions, m)
-                       continue
-               }
-
-               actionKey := m["asset_id"].(string) + m["account_id"].(string)
-               amountNumber := m["amount"].(json.Number)
-               amount, _ := amountNumber.Int64()
-
-               if tmpM, ok := actionMap[actionKey]; ok {
-                       tmpNumber, _ := tmpM["amount"].(json.Number)
-                       tmpAmount, _ := tmpNumber.Int64()
-                       tmpM["amount"] = json.Number(fmt.Sprintf("%v", tmpAmount+amount))
-               } else {
-                       actionMap[actionKey] = m
-                       actions = append(actions, m)
-               }
+       decoders := map[string]func([]byte) (txbuilder.Action, error){
+               "control_address":              txbuilder.DecodeControlAddressAction,
+               "control_program":              txbuilder.DecodeControlProgramAction,
+               "issue":                        a.wallet.AssetReg.DecodeIssueAction,
+               "retire":                       txbuilder.DecodeRetireAction,
+               "spend_account":                a.wallet.AccountMgr.DecodeSpendAction,
+               "spend_account_unspent_output": a.wallet.AccountMgr.DecodeSpendUTXOAction,
        }
-
-       return actions
+       decoder, ok := decoders[action]
+       return decoder, ok
 }
 
-func onlyHaveSpendActions(req *BuildRequest) bool {
+func onlyHaveInputActions(req *BuildRequest) (bool, error) {
        count := 0
-       for _, m := range req.Actions {
-               if actionType := m["type"].(string); strings.HasPrefix(actionType, "spend") {
+       for i, act := range req.Actions {
+               actionType, ok := act["type"].(string)
+               if !ok {
+                       return false, errors.WithDetailf(ErrBadActionType, "no action type provided on action %d", i)
+               }
+
+               if strings.HasPrefix(actionType, "spend") || actionType == "issue" {
                        count++
                }
        }
 
-       return count == len(req.Actions)
+       return count == len(req.Actions), nil
 }
 
 func (a *API) buildSingle(ctx context.Context, req *BuildRequest) (*txbuilder.Template, error) {
-       err := a.filterAliases(ctx, req)
+       if err := a.checkRequestValidity(ctx, req); err != nil {
+               return nil, err
+       }
+       actions, err := a.mergeSpendActions(req)
+       if err != nil {
+               return nil, err
+       }
+
+       maxTime := time.Now().Add(req.TTL.Duration)
+       tpl, err := txbuilder.Build(ctx, req.Tx, actions, maxTime, req.TimeRange)
+       if errors.Root(err) == txbuilder.ErrAction {
+               // append each of the inner errors contained in the data.
+               var Errs string
+               var rootErr error
+               for i, innerErr := range errors.Data(err)["actions"].([]error) {
+                       if i == 0 {
+                               rootErr = errors.Root(innerErr)
+                       }
+                       Errs = Errs + innerErr.Error()
+               }
+               err = errors.WithDetail(rootErr, Errs)
+       }
        if err != nil {
                return nil, err
        }
 
-       if onlyHaveSpendActions(req) {
-               return nil, errors.New("transaction only contain spend actions, didn't have output actions")
+       // ensure null is never returned for signing instructions
+       if tpl.SigningInstructions == nil {
+               tpl.SigningInstructions = []*txbuilder.SigningInstruction{}
        }
+       return tpl, nil
+}
 
-       reqActions := mergeActions(req)
-       actions := make([]txbuilder.Action, 0, len(reqActions))
-       for i, act := range reqActions {
+// POST /build-transaction
+func (a *API) build(ctx context.Context, buildReqs *BuildRequest) Response {
+       subctx := reqid.NewSubContext(ctx, reqid.New())
+       tmpl, err := a.buildSingle(subctx, buildReqs)
+       if err != nil {
+               return NewErrorResponse(err)
+       }
+
+       return NewSuccessResponse(tmpl)
+}
+func (a *API) checkRequestValidity(ctx context.Context, req *BuildRequest) error {
+       if err := a.completeMissingIDs(ctx, req); err != nil {
+               return err
+       }
+
+       if req.TTL.Duration == 0 {
+               req.TTL.Duration = defaultTxTTL
+       }
+
+       if ok, err := onlyHaveInputActions(req); err != nil {
+               return err
+       } else if ok {
+               return errors.WithDetail(ErrBadActionConstruction, "transaction contains only input actions and no output actions")
+       }
+       return nil
+}
+
+func (a *API) mergeSpendActions(req *BuildRequest) ([]txbuilder.Action, error) {
+       actions := make([]txbuilder.Action, 0, len(req.Actions))
+       for i, act := range req.Actions {
                typ, ok := act["type"].(string)
                if !ok {
-                       return nil, errors.WithDetailf(errBadActionType, "no action type provided on action %d", i)
+                       return nil, errors.WithDetailf(ErrBadActionType, "no action type provided on action %d", i)
                }
                decoder, ok := a.actionDecoder(typ)
                if !ok {
-                       return nil, errors.WithDetailf(errBadActionType, "unknown action type %q on action %d", typ, i)
+                       return nil, errors.WithDetailf(ErrBadActionType, "unknown action type %q on action %d", typ, i)
                }
 
                // Remarshal to JSON, the action may have been modified when we
@@ -112,59 +136,56 @@ func (a *API) buildSingle(ctx context.Context, req *BuildRequest) (*txbuilder.Te
                }
                action, err := decoder(b)
                if err != nil {
-                       return nil, errors.WithDetailf(errBadAction, "%s on action %d", err.Error(), i)
+                       return nil, errors.WithDetailf(ErrBadAction, "%s on action %d", err.Error(), i)
                }
                actions = append(actions, action)
        }
+       actions = account.MergeSpendAction(actions)
+       return actions, nil
+}
 
-       ttl := req.TTL.Duration
-       if ttl == 0 {
-               ttl = defaultTxTTL
+func (a *API) buildTxs(ctx context.Context, req *BuildRequest) ([]*txbuilder.Template, error) {
+       if err := a.checkRequestValidity(ctx, req); err != nil {
+               return nil, err
+       }
+       actions, err := a.mergeSpendActions(req)
+       if err != nil {
+               return nil, err
        }
-       maxTime := time.Now().Add(ttl)
 
-       tpl, err := txbuilder.Build(ctx, req.Tx, actions, maxTime, req.TimeRange)
-       if errors.Root(err) == txbuilder.ErrAction {
-               // append each of the inner errors contained in the data.
-               var Errs string
-               for _, innerErr := range errors.Data(err)["actions"].([]error) {
-                       Errs = Errs + "<" + innerErr.Error() + ">"
+       builder := txbuilder.NewBuilder(time.Now().Add(req.TTL.Duration))
+       tpls := []*txbuilder.Template{}
+       for _, action := range actions {
+               if action.ActionType() == "spend_account" {
+                       tpls, err = account.SpendAccountChain(ctx, builder, action)
+               } else {
+                       err = action.Build(ctx, builder)
+               }
+
+               if err != nil {
+                       builder.Rollback()
+                       return nil, err
                }
-               err = errors.New(err.Error() + "-" + Errs)
        }
+
+       tpl, _, err := builder.Build()
        if err != nil {
+               builder.Rollback()
                return nil, err
        }
 
-       // ensure null is never returned for signing instructions
-       if tpl.SigningInstructions == nil {
-               tpl.SigningInstructions = []*txbuilder.SigningInstruction{}
-       }
-       return tpl, nil
+       tpls = append(tpls, tpl)
+       return tpls, nil
 }
 
-// POST /build-transaction
-func (a *API) build(ctx context.Context, buildReqs *BuildRequest) Response {
+// POST /build-chain-transactions
+func (a *API) buildChainTxs(ctx context.Context, buildReqs *BuildRequest) Response {
        subctx := reqid.NewSubContext(ctx, reqid.New())
-
-       tmpl, err := a.buildSingle(subctx, buildReqs)
+       tmpls, err := a.buildTxs(subctx, buildReqs)
        if err != nil {
                return NewErrorResponse(err)
        }
-
-       return NewSuccessResponse(tmpl)
-}
-
-func (a *API) submitSingle(ctx context.Context, tpl *txbuilder.Template) (map[string]string, error) {
-       if tpl.Transaction == nil {
-               return nil, errors.Wrap(txbuilder.ErrMissingRawTx)
-       }
-
-       if err := txbuilder.FinalizeTx(ctx, a.chain, tpl.Transaction); err != nil {
-               return nil, errors.Wrapf(err, "tx %s", tpl.Transaction.ID.String())
-       }
-
-       return map[string]string{"tx_id": tpl.Transaction.ID.String()}, nil
+       return NewSuccessResponse(tmpls)
 }
 
 type submitTxResp struct {
@@ -179,66 +200,140 @@ func (a *API) submit(ctx context.Context, ins struct {
                return NewErrorResponse(err)
        }
 
-       log.WithField("tx_id", ins.Tx.ID).Info("submit single tx")
+       log.WithField("tx_id", ins.Tx.ID.String()).Info("submit single tx")
        return NewSuccessResponse(&submitTxResp{TxID: &ins.Tx.ID})
 }
 
-// POST /sign-submit-transaction
-func (a *API) signSubmit(ctx context.Context, x struct {
-       Password string             `json:"password"`
-       Txs      txbuilder.Template `json:"transaction"`
+type submitTxsResp struct {
+       TxID []*bc.Hash `json:"tx_id"`
+}
+
+// POST /submit-transactions
+func (a *API) submitTxs(ctx context.Context, ins struct {
+       Tx []types.Tx `json:"raw_transactions"`
 }) Response {
-       if err := txbuilder.Sign(ctx, &x.Txs, nil, x.Password, a.pseudohsmSignTemplate); err != nil {
-               log.WithField("build err", err).Error("fail on sign transaction.")
-               return NewErrorResponse(err)
+       txHashs := []*bc.Hash{}
+       for i := range ins.Tx {
+               if err := txbuilder.FinalizeTx(ctx, a.chain, &ins.Tx[i]); err != nil {
+                       return NewErrorResponse(err)
+               }
+               log.WithField("tx_id", ins.Tx[i].ID.String()).Info("submit single tx")
+               txHashs = append(txHashs, &ins.Tx[i].ID)
        }
+       return NewSuccessResponse(&submitTxsResp{TxID: txHashs})
+}
 
-       if signCount, complete := txbuilder.SignInfo(&x.Txs); !complete && signCount == 0 {
-               return NewErrorResponse(pseudohsm.ErrLoadKey)
-       }
-       log.Info("Sign Transaction complete.")
+// EstimateTxGasResp estimate transaction consumed gas
+type EstimateTxGasResp struct {
+       TotalNeu   int64 `json:"total_neu"`
+       StorageNeu int64 `json:"storage_neu"`
+       VMNeu      int64 `json:"vm_neu"`
+}
 
-       txID, err := a.submitSingle(nil, &x.Txs)
+// EstimateTxGas estimate consumed neu for transaction
+func EstimateTxGas(template txbuilder.Template) (*EstimateTxGasResp, error) {
+       // base tx size and not include sign
+       data, err := template.Transaction.TxData.MarshalText()
        if err != nil {
-               log.WithField("err", err).Error("submit single tx")
-               return NewErrorResponse(err)
+               return nil, err
        }
+       baseTxSize := int64(len(data))
 
-       log.WithField("tx_id", txID["tx_id"]).Info("submit single tx")
-       return NewSuccessResponse(txID)
-}
+       // extra tx size for sign witness parts
+       signSize := estimateSignSize(template.SigningInstructions)
 
-type calculateTxGasResp struct {
-       LeftBTM     int64 `json:"left_btm"`
-       ConsumedBTM int64 `json:"consumed_btm"`
-       LeftGas     int64 `json:"left_gas"`
-       ConsumedGas int64 `json:"consumed_gas"`
-       StorageGas  int64 `json:"storage_gas"`
-       VMGas       int64 `json:"vm_gas"`
-}
+       // total gas for tx storage
+       totalTxSizeGas, ok := checked.MulInt64(baseTxSize+signSize, consensus.StorageGasRate)
+       if !ok {
+               return nil, errors.New("calculate txsize gas got a math error")
+       }
 
-// POST /calculate-transaction-gas
-func (a *API) calculateGas(ctx context.Context, ins struct {
-       Tx types.Tx `json:"raw_transaction"`
-}) Response {
-       gasState, err := txbuilder.CalculateTxGas(a.chain, &ins.Tx)
-       if err != nil {
-               return NewErrorResponse(err)
+       // consume gas for run VM
+       totalP2WPKHGas := int64(0)
+       totalP2WSHGas := int64(0)
+       baseP2WPKHGas := int64(1419)
+       // flexible Gas is used for handle need extra utxo situation
+
+       for pos, inpID := range template.Transaction.Tx.InputIDs {
+               sp, err := template.Transaction.Spend(inpID)
+               if err != nil {
+                       continue
+               }
+
+               resOut, err := template.Transaction.Output(*sp.SpentOutputId)
+               if err != nil {
+                       continue
+               }
+
+               if segwit.IsP2WPKHScript(resOut.ControlProgram.Code) {
+                       totalP2WPKHGas += baseP2WPKHGas
+               } else if segwit.IsP2WSHScript(resOut.ControlProgram.Code) {
+                       sigInst := template.SigningInstructions[pos]
+                       totalP2WSHGas += estimateP2WSHGas(sigInst)
+               }
        }
 
-       btmLeft, ok := checked.MulInt64(gasState.GasLeft, consensus.VMGasRate)
-       if !ok {
-               return NewErrorResponse(errors.New("calculate btmleft got a math error"))
+       // total estimate gas
+       totalGas := totalTxSizeGas + totalP2WPKHGas + totalP2WSHGas + flexibleGas
+
+       // rounding totalNeu with base rate 100000
+       totalNeu := float64(totalGas*consensus.VMGasRate) / defaultBaseRate
+       roundingNeu := math.Ceil(totalNeu)
+       estimateNeu := int64(roundingNeu) * int64(defaultBaseRate)
+
+       // TODO add priority
+
+       return &EstimateTxGasResp{
+               TotalNeu:   estimateNeu,
+               StorageNeu: totalTxSizeGas * consensus.VMGasRate,
+               VMNeu:      (totalP2WPKHGas + totalP2WSHGas) * consensus.VMGasRate,
+       }, nil
+}
+
+// estimate p2wsh gas.
+// OP_CHECKMULTISIG consume (984 * a - 72 * b - 63) gas,
+// where a represent the num of public keys, and b represent the num of quorum.
+func estimateP2WSHGas(sigInst *txbuilder.SigningInstruction) int64 {
+       P2WSHGas := int64(0)
+       baseP2WSHGas := int64(738)
+
+       for _, witness := range sigInst.WitnessComponents {
+               switch t := witness.(type) {
+               case *txbuilder.SignatureWitness:
+                       P2WSHGas += baseP2WSHGas + (984*int64(len(t.Keys)) - 72*int64(t.Quorum) - 63)
+               case *txbuilder.RawTxSigWitness:
+                       P2WSHGas += baseP2WSHGas + (984*int64(len(t.Keys)) - 72*int64(t.Quorum) - 63)
+               }
        }
+       return P2WSHGas
+}
 
-       txGasResp := &calculateTxGasResp{
-               LeftBTM:     btmLeft,
-               ConsumedBTM: int64(gasState.BTMValue) - btmLeft,
-               LeftGas:     gasState.GasLeft,
-               ConsumedGas: gasState.GasUsed,
-               StorageGas:  gasState.StorageGas,
-               VMGas:       gasState.GasUsed - gasState.StorageGas,
+// estimate signature part size.
+// if need multi-sign, calculate the size according to the length of keys.
+func estimateSignSize(signingInstructions []*txbuilder.SigningInstruction) int64 {
+       signSize := int64(0)
+       baseWitnessSize := int64(300)
+
+       for _, sigInst := range signingInstructions {
+               for _, witness := range sigInst.WitnessComponents {
+                       switch t := witness.(type) {
+                       case *txbuilder.SignatureWitness:
+                               signSize += int64(t.Quorum) * baseWitnessSize
+                       case *txbuilder.RawTxSigWitness:
+                               signSize += int64(t.Quorum) * baseWitnessSize
+                       }
+               }
        }
+       return signSize
+}
 
+// POST /estimate-transaction-gas
+func (a *API) estimateTxGas(ctx context.Context, in struct {
+       TxTemplate txbuilder.Template `json:"transaction_template"`
+}) Response {
+       txGasResp, err := EstimateTxGas(in.TxTemplate)
+       if err != nil {
+               return NewErrorResponse(err)
+       }
        return NewSuccessResponse(txGasResp)
 }