OSDN Git Service

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