forked from tonicpow/go-minercraft
-
Notifications
You must be signed in to change notification settings - Fork 0
/
submit_transaction.go
293 lines (247 loc) · 8.88 KB
/
submit_transaction.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
package minercraft
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"github.com/tonicpow/go-minercraft/v2/apis/arc"
"github.com/tonicpow/go-minercraft/v2/apis/mapi"
)
const (
// MerkleFormatTSC can be set when calling SubmitTransaction to request a MerkleProof in TSC format.
MerkleFormatTSC = "TSC"
)
// SubmitTxModelAdapter is the interface for the adapter to get the submit tx response
type SubmitTxModelAdapter interface {
GetSubmitTxResponse() *UnifiedSubmissionPayload
}
// SubmitTxMapiAdapter is the adapter for the mAPI response
type SubmitTxMapiAdapter struct {
*mapi.SubmitTxModel
}
// SubmitTxArcAdapter is the adapter for the Arc response
type SubmitTxArcAdapter struct {
*arc.SubmitTxModel
}
// SubmitTransactionResponse is the raw response from the Merchant API request
//
// Specs: https://github.com/bitcoin-sv-specs/brfc-merchantapi#3-submit-transaction
type SubmitTransactionResponse struct {
JSONEnvelope
Results *UnifiedSubmissionPayload `json:"results"` // Custom field for unmarshalled payload data
}
/*
Example SubmitTransactionResponse.Payload (unmarshalled):
{
"apiVersion": "1.2.3",
"conflictedWith": ""
"currentHighestBlockHash": "71a7374389afaec80fcabbbf08dcd82d392cf68c9a13fe29da1a0c853facef01",
"currentHighestBlockHeight": 207,
"minerId": "03fcfcfcd0841b0a6ed2057fa8ed404788de47ceb3390c53e79c4ecd1e05819031",
"resultDescription": "",
"returnResult": "success",
"timestamp": "2020-01-15T11:40:29.826Z",
"txid": "6bdbcfab0526d30e8d68279f79dff61fb4026ace8b7b32789af016336e54f2f0",
"txSecondMempoolExpiry": 0,
}
*/
// UnifiedSubmissionPayload is the unmarshalled version of the payload envelope
type UnifiedSubmissionPayload struct {
// mAPI
APIVersion string `json:"apiVersion"`
ConflictedWith []*mapi.ConflictedWith `json:"conflictedWith"`
CurrentHighestBlockHash string `json:"currentHighestBlockHash"`
CurrentHighestBlockHeight int64 `json:"currentHighestBlockHeight"`
MinerID string `json:"minerId"`
ResultDescription string `json:"resultDescription"`
ReturnResult string `json:"returnResult"`
Timestamp string `json:"timestamp"`
TxID string `json:"txid"`
TxSecondMempoolExpiry int64 `json:"txSecondMempoolExpiry"`
// FailureRetryable if true indicates the tx can be resubmitted to mAPI.
FailureRetryable bool `json:"failureRetryable"`
// Arc
BlockHash string `json:"blockHash,omitempty"`
BlockHeight int64 `json:"blockHeight,omitempty"`
ExtraInfo string `json:"extraInfo,omitempty"`
Status int `json:"status,omitempty"`
Title string `json:"title,omitempty"`
TxStatus arc.TxStatus `json:"txStatus,omitempty"`
}
// Transaction is the body contents in the "submit transaction" request
type Transaction struct {
CallBackEncryption string `json:"callBackEncryption,omitempty"`
CallBackToken string `json:"callBackToken,omitempty"`
CallBackURL string `json:"callBackUrl,omitempty"`
DsCheck bool `json:"dsCheck,omitempty"`
MerkleFormat string `json:"merkleFormat,omitempty"`
MerkleProof bool `json:"merkleProof,omitempty"`
RawTx string `json:"rawtx"`
WaitForStatus arc.TxStatus `json:"waitForStatus,omitempty"`
}
// SubmitTransaction will fire a Merchant API request to submit a given transaction
//
// This endpoint is used to send a raw transaction to a miner for inclusion in the next block
// that the miner creates. It returns a JSONEnvelope with a payload that contains the response to the
// transaction submission. The purpose of the envelope is to ensure strict consistency in the
// message content for the purpose of signing responses.
//
// Specs: https://github.com/bitcoin-sv-specs/brfc-merchantapi#3-submit-transaction
func (c *Client) SubmitTransaction(ctx context.Context, miner *Miner, tx *Transaction) (*SubmitTransactionResponse, error) {
// Make sure we have a valid miner
if miner == nil {
return nil, errors.New("miner was nil")
}
// Make the HTTP request
result, err := submitTransaction(ctx, c, miner, tx)
if err != nil {
return nil, err
}
if result.Response.Error != nil {
return nil, result.Response.Error
}
submitResponse := &SubmitTransactionResponse{
JSONEnvelope: JSONEnvelope{
APIType: c.apiType,
Miner: result.Miner,
},
}
var modelAdapter SubmitTxModelAdapter
switch c.apiType {
case MAPI:
model := &SubmitTxMapiAdapter{}
err = submitResponse.process(result.Miner, result.Response.BodyContents)
if err != nil || len(submitResponse.Payload) <= 0 {
return nil, err
}
err = json.Unmarshal([]byte(submitResponse.Payload), model)
if err != nil {
return nil, err
}
if submitResponse.Payload == "" || submitResponse.Payload == "{}" {
return nil, errors.New("failed to unmarshal payload")
}
modelAdapter = &SubmitTxMapiAdapter{SubmitTxModel: model.SubmitTxModel}
case Arc:
model := &SubmitTxArcAdapter{}
err = json.Unmarshal(result.Response.BodyContents, model)
if err != nil {
return nil, err
}
modelAdapter = &SubmitTxArcAdapter{SubmitTxModel: model.SubmitTxModel}
default:
return nil, fmt.Errorf("unknown API type: %s", c.apiType)
}
submitResponse.Results = modelAdapter.GetSubmitTxResponse()
// Valid?
if submitResponse.Results == nil && (len(submitResponse.Payload) <= 0 && c.apiType == MAPI) {
return nil, errors.New("failed getting submit response from: " + miner.Name)
}
isValid, err := submitResponse.IsValid()
if err != nil {
return nil, err
}
submitResponse.Validated = isValid
// Return the fully parsed response
return submitResponse, nil
}
// submitTransaction will fire the HTTP request to submit a transaction
func submitTransaction(ctx context.Context, client *Client, miner *Miner, tx *Transaction) (*internalResult, error) {
result := &internalResult{Miner: miner}
api, err := client.MinerAPIByMinerID(miner.MinerID, client.apiType)
if err != nil {
result.Response = &RequestResponse{Error: err}
return nil, err
}
route, err := ActionRouteByAPIType(SubmitTx, client.apiType)
if err != nil {
result.Response = &RequestResponse{Error: err}
return nil, err
}
submitURL := api.URL + route
httpPayload := &httpPayload{
Method: http.MethodPost,
URL: submitURL,
Token: api.Token,
Headers: make(map[string]string),
}
switch client.apiType {
case MAPI:
err = proceedMapiSubmitTx(tx, httpPayload)
if err != nil {
return nil, err
}
case Arc:
err = proceedArcSubmitTx(tx, httpPayload)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown API type: %s", client.apiType)
}
result.Response = httpRequest(ctx, client, httpPayload)
return result, nil
}
// proceedArcSubmitTx will proceed with the Arc submit tx
func proceedArcSubmitTx(tx *Transaction, httpPayload *httpPayload) error {
body := map[string]string{
"rawTx": tx.RawTx,
}
data, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("failed to marshall JSON when submitting transaction: %w", err)
}
httpPayload.Data = data
if tx.MerkleProof {
httpPayload.Headers["X-MerkleProof"] = "true"
}
if tx.CallBackURL != "" {
httpPayload.Headers["X-CallbackUrl"] = tx.CallBackURL
}
if tx.CallBackToken != "" {
httpPayload.Headers["X-CallbackToken"] = tx.CallBackToken
}
if statusCode, ok := arc.MapTxStatusToInt(tx.WaitForStatus); ok {
httpPayload.Headers["X-WaitForStatus"] = strconv.Itoa(statusCode)
}
return nil
}
// proceedMapiSubmitTx will proceed with the mAPI submit tx
func proceedMapiSubmitTx(tx *Transaction, httpPayload *httpPayload) error {
data, err := json.Marshal(tx)
if err != nil {
return err
}
httpPayload.Data = data
return nil
}
// GetSubmitTxResponse will return the unified response for mAPI adapter
func (a *SubmitTxMapiAdapter) GetSubmitTxResponse() *UnifiedSubmissionPayload {
return &UnifiedSubmissionPayload{
APIVersion: a.APIVersion,
ConflictedWith: a.ConflictedWith,
CurrentHighestBlockHash: a.CurrentHighestBlockHash,
CurrentHighestBlockHeight: a.CurrentHighestBlockHeight,
MinerID: a.MinerID,
ResultDescription: a.ResultDescription,
ReturnResult: a.ReturnResult,
Timestamp: a.Timestamp,
TxID: a.TxID,
TxSecondMempoolExpiry: a.TxSecondMempoolExpiry,
FailureRetryable: a.FailureRetryable,
}
}
// GetSubmitTxResponse will return the unified response for Arc adapter
func (a *SubmitTxArcAdapter) GetSubmitTxResponse() *UnifiedSubmissionPayload {
return &UnifiedSubmissionPayload{
BlockHash: a.BlockHash,
BlockHeight: a.BlockHeight,
ExtraInfo: a.ExtraInfo,
Status: a.Status,
Title: a.Title,
TxStatus: a.TxStatus,
TxID: a.TxID,
}
}