OSDN Git Service

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