OSDN Git Service

optimise mergeSpendAction
[bytom/bytom.git] / api / transact.go
1 package api
2
3 import (
4         "context"
5         "encoding/json"
6         "fmt"
7         "math"
8         "strings"
9         "time"
10
11         log "github.com/sirupsen/logrus"
12
13         "github.com/bytom/account"
14         "github.com/bytom/blockchain/txbuilder"
15         "github.com/bytom/consensus"
16         "github.com/bytom/consensus/segwit"
17         "github.com/bytom/errors"
18         "github.com/bytom/math/checked"
19         "github.com/bytom/net/http/reqid"
20         "github.com/bytom/protocol/bc"
21         "github.com/bytom/protocol/bc/types"
22 )
23
24 var (
25         defaultTxTTL    = 5 * time.Minute
26         defaultBaseRate = float64(100000)
27 )
28
29 func (a *API) actionDecoder(action string) (func([]byte) (txbuilder.Action, error), bool) {
30         var decoder func([]byte) (txbuilder.Action, error)
31         switch action {
32         case "control_address":
33                 decoder = txbuilder.DecodeControlAddressAction
34         case "control_program":
35                 decoder = txbuilder.DecodeControlProgramAction
36         case "control_receiver":
37                 decoder = txbuilder.DecodeControlReceiverAction
38         case "issue":
39                 decoder = a.wallet.AssetReg.DecodeIssueAction
40         case "retire":
41                 decoder = txbuilder.DecodeRetireAction
42         case "spend_account":
43                 decoder = a.wallet.AccountMgr.DecodeSpendAction
44         case "spend_account_unspent_output":
45                 decoder = a.wallet.AccountMgr.DecodeSpendUTXOAction
46         default:
47                 return nil, false
48         }
49         return decoder, true
50 }
51
52 // TODO modify mergeActions to loadSpendAction
53 func mergeActions(req *BuildRequest) ([]map[string]interface{}, error) {
54         var actions []map[string]interface{}
55         actionMap := make(map[string]map[string]interface{})
56
57         for _, m := range req.Actions {
58                 if actionType := m["type"].(string); actionType != "spend_account" {
59                         actions = append(actions, m)
60                         continue
61                 }
62
63                 if m["amount"] == nil {
64                         return nil, errEmptyAmount
65                 }
66
67                 amountNumber := m["amount"].(json.Number)
68                 amount, err := amountNumber.Int64()
69                 if err != nil || amount == 0 {
70                         return nil, errBadAmount
71                 }
72
73                 actionKey := m["asset_id"].(string) + m["account_id"].(string)
74                 if tmpM, ok := actionMap[actionKey]; ok {
75                         if tmpM["amount"] == nil {
76                                 return nil, errEmptyAmount
77                         }
78
79                         tmpNumber := tmpM["amount"].(json.Number)
80                         tmpAmount, err := tmpNumber.Int64()
81                         if err != nil || tmpAmount == 0 {
82                                 return nil, errBadAmount
83                         }
84
85                         tmpM["amount"] = json.Number(fmt.Sprintf("%v", tmpAmount+amount))
86                 } else {
87                         actionMap[actionKey] = m
88                         actions = append(actions, m)
89                 }
90         }
91
92         return actions, nil
93 }
94
95 func onlyHaveSpendActions(req *BuildRequest) bool {
96         count := 0
97         for _, m := range req.Actions {
98                 if actionType := m["type"].(string); strings.HasPrefix(actionType, "spend") {
99                         count++
100                 }
101         }
102
103         return count == len(req.Actions)
104 }
105
106 func (a *API) buildSingle(ctx context.Context, req *BuildRequest) (*txbuilder.Template, error) {
107         err := a.filterAliases(ctx, req)
108         if err != nil {
109                 return nil, err
110         }
111
112         if onlyHaveSpendActions(req) {
113                 return nil, errors.New("transaction only contain spend actions, didn't have output actions")
114         }
115
116         spendActions := []txbuilder.Action{}
117         actions := make([]txbuilder.Action, 0, len(req.Actions))
118         for i, act := range req.Actions {
119                 typ, ok := act["type"].(string)
120                 if !ok {
121                         return nil, errors.WithDetailf(errBadActionType, "no action type provided on action %d", i)
122                 }
123                 decoder, ok := a.actionDecoder(typ)
124                 if !ok {
125                         return nil, errors.WithDetailf(errBadActionType, "unknown action type %q on action %d", typ, i)
126                 }
127
128                 // Remarshal to JSON, the action may have been modified when we
129                 // filtered aliases.
130                 b, err := json.Marshal(act)
131                 if err != nil {
132                         return nil, err
133                 }
134                 action, err := decoder(b)
135                 if err != nil {
136                         return nil, errors.WithDetailf(errBadAction, "%s on action %d", err.Error(), i)
137                 }
138
139                 if typ == "spend_account" {
140                         spendActions = append(spendActions, action)
141                 } else {
142                         actions = append(actions, action)
143                 }
144         }
145         actions = append(account.MergeSpendAction(spendActions), actions...)
146
147         ttl := req.TTL.Duration
148         if ttl == 0 {
149                 ttl = defaultTxTTL
150         }
151         maxTime := time.Now().Add(ttl)
152
153         tpl, err := txbuilder.Build(ctx, req.Tx, actions, maxTime, req.TimeRange)
154         if errors.Root(err) == txbuilder.ErrAction {
155                 // append each of the inner errors contained in the data.
156                 var Errs string
157                 for _, innerErr := range errors.Data(err)["actions"].([]error) {
158                         Errs = Errs + "<" + innerErr.Error() + ">"
159                 }
160                 err = errors.New(err.Error() + "-" + Errs)
161         }
162         if err != nil {
163                 return nil, err
164         }
165
166         // ensure null is never returned for signing instructions
167         if tpl.SigningInstructions == nil {
168                 tpl.SigningInstructions = []*txbuilder.SigningInstruction{}
169         }
170         return tpl, nil
171 }
172
173 // POST /build-transaction
174 func (a *API) build(ctx context.Context, buildReqs *BuildRequest) Response {
175         subctx := reqid.NewSubContext(ctx, reqid.New())
176
177         tmpl, err := a.buildSingle(subctx, buildReqs)
178         if err != nil {
179                 return NewErrorResponse(err)
180         }
181
182         return NewSuccessResponse(tmpl)
183 }
184
185 type submitTxResp struct {
186         TxID *bc.Hash `json:"tx_id"`
187 }
188
189 // POST /submit-transaction
190 func (a *API) submit(ctx context.Context, ins struct {
191         Tx types.Tx `json:"raw_transaction"`
192 }) Response {
193         if err := txbuilder.FinalizeTx(ctx, a.chain, &ins.Tx); err != nil {
194                 return NewErrorResponse(err)
195         }
196
197         log.WithField("tx_id", ins.Tx.ID).Info("submit single tx")
198         return NewSuccessResponse(&submitTxResp{TxID: &ins.Tx.ID})
199 }
200
201 // EstimateTxGasResp estimate transaction consumed gas
202 type EstimateTxGasResp struct {
203         TotalNeu   int64 `json:"total_neu"`
204         StorageNeu int64 `json:"storage_neu"`
205         VMNeu      int64 `json:"vm_neu"`
206 }
207
208 // EstimateTxGas estimate consumed neu for transaction
209 func EstimateTxGas(template txbuilder.Template) (*EstimateTxGasResp, error) {
210         // base tx size and not include sign
211         data, err := template.Transaction.TxData.MarshalText()
212         if err != nil {
213                 return nil, err
214         }
215         baseTxSize := int64(len(data))
216
217         // extra tx size for sign witness parts
218         baseWitnessSize := int64(300)
219         lenSignInst := int64(len(template.SigningInstructions))
220         signSize := baseWitnessSize * lenSignInst
221
222         // total gas for tx storage
223         totalTxSizeGas, ok := checked.MulInt64(baseTxSize+signSize, consensus.StorageGasRate)
224         if !ok {
225                 return nil, errors.New("calculate txsize gas got a math error")
226         }
227
228         // consume gas for run VM
229         totalP2WPKHGas := int64(0)
230         totalP2WSHGas := int64(0)
231         baseP2WPKHGas := int64(1419)
232         baseP2WSHGas := int64(2499)
233
234         for _, inpID := range template.Transaction.Tx.InputIDs {
235                 sp, err := template.Transaction.Spend(inpID)
236                 if err != nil {
237                         continue
238                 }
239
240                 resOut, err := template.Transaction.Output(*sp.SpentOutputId)
241                 if err != nil {
242                         continue
243                 }
244
245                 if segwit.IsP2WPKHScript(resOut.ControlProgram.Code) {
246                         totalP2WPKHGas += baseP2WPKHGas
247                 } else if segwit.IsP2WSHScript(resOut.ControlProgram.Code) {
248                         totalP2WSHGas += baseP2WSHGas
249                 }
250         }
251
252         // total estimate gas
253         totalGas := totalTxSizeGas + totalP2WPKHGas + totalP2WSHGas
254
255         // rounding totalNeu with base rate 100000
256         totalNeu := float64(totalGas*consensus.VMGasRate) / defaultBaseRate
257         roundingNeu := math.Ceil(totalNeu)
258         estimateNeu := int64(roundingNeu) * int64(defaultBaseRate)
259
260         return &EstimateTxGasResp{
261                 TotalNeu:   estimateNeu,
262                 StorageNeu: totalTxSizeGas * consensus.VMGasRate,
263                 VMNeu:      (totalP2WPKHGas + totalP2WSHGas) * consensus.VMGasRate,
264         }, nil
265 }
266
267 // POST /estimate-transaction-gas
268 func (a *API) estimateTxGas(ctx context.Context, in struct {
269         TxTemplate txbuilder.Template `json:"transaction_template"`
270 }) Response {
271         txGasResp, err := EstimateTxGas(in.TxTemplate)
272         if err != nil {
273                 return NewErrorResponse(err)
274         }
275         return NewSuccessResponse(txGasResp)
276 }