OSDN Git Service

add TestDeployHTLCContract
[bytom/shuttle.git] / swap / trade_offer.go
1 package swap
2
3 import (
4         "encoding/hex"
5         "encoding/json"
6         "errors"
7         "fmt"
8         "strconv"
9         "strings"
10 )
11
12 var (
13         errFailedGetContractUTXOID = errors.New("Failed to get contract UTXO ID")
14         errMarshal                 = errors.New("Failed to marshal")
15         errListUnspentOutputs      = errors.New("Failed to list unspent outputs")
16 )
17
18 type AccountInfo struct {
19         AccountID string
20         Password  string
21         Receiver  string
22         TxFee     uint64
23 }
24
25 type AssetAmount struct {
26         Asset  string
27         Amount uint64
28 }
29
30 type ContractArgs struct {
31         AssetAmount
32         Seller    string
33         CancelKey string
34 }
35
36 type compileLockContractResponse struct {
37         Program string `json:"program"`
38 }
39
40 var compileLockContractPayload = `{
41         "contract":"contract TradeOffer(assetRequested: Asset, amountRequested: Amount, seller: Program, cancelKey: PublicKey) locks valueAmount of valueAsset { clause trade() { lock amountRequested of assetRequested with seller unlock valueAmount of valueAsset } clause cancel(sellerSig: Signature) { verify checkTxSig(cancelKey, sellerSig) unlock valueAmount of valueAsset}}",
42         "args":[
43                 {
44                         "string":"%s"
45                 },
46                 {
47                         "integer":%s
48                 },
49                 {
50                         "string":"%s"
51                 },
52                 {
53                         "string":"%s"
54                 }
55         ]
56 }`
57
58 // compileLockContract return contract control program
59 func compileLockContract(contractArgs ContractArgs) (string, error) {
60         payload := []byte(fmt.Sprintf(
61                 compileLockContractPayload,
62                 contractArgs.Asset,
63                 strconv.FormatUint(contractArgs.Amount, 10),
64                 contractArgs.Seller,
65                 contractArgs.CancelKey,
66         ))
67         res := new(compileLockContractResponse)
68         if err := request(compileURL, payload, res); err != nil {
69                 return "", err
70         }
71         return res.Program, nil
72 }
73
74 var buildLockTransactionPayload = `{
75         "actions":[
76                 {
77                         "account_id":"%s",
78                         "amount":%s,
79                         "asset_id":"%s",
80                         "use_unconfirmed":true,
81                         "type":"spend_account"
82                 },
83                 {
84                         "amount":%s,
85                         "asset_id":"%s",
86                         "control_program":"%s",
87                         "type":"control_program"
88                 },
89                 {
90                         "account_id":"%s",
91                         "amount":%s,
92                         "asset_id":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
93                         "use_unconfirmed":true,
94                         "type":"spend_account"
95                 }
96         ],
97         "ttl":0,
98         "base_transaction":null
99 }`
100
101 // buildLockTransaction build locked contract transaction.
102 func buildLockTransaction(accountInfo AccountInfo, contractValue AssetAmount, contractControlProgram string) (interface{}, error) {
103         payload := []byte(fmt.Sprintf(
104                 buildLockTransactionPayload,
105                 accountInfo.AccountID,
106                 strconv.FormatUint(contractValue.Amount, 10),
107                 contractValue.Asset,
108                 strconv.FormatUint(contractValue.Amount, 10),
109                 contractValue.Asset,
110                 contractControlProgram,
111                 accountInfo.AccountID,
112                 strconv.FormatUint(accountInfo.TxFee, 10),
113         ))
114         res := new(interface{})
115         if err := request(buildTransactionURL, payload, res); err != nil {
116                 return "", err
117         }
118         return res, nil
119 }
120
121 type signTransactionRequest struct {
122         Password    string      `json:"password"`
123         Transaction interface{} `json:"transaction"`
124 }
125
126 type Transaction struct {
127         RawTransaction string `json:"raw_transaction"`
128 }
129
130 type signTransactionResponse struct {
131         Tx Transaction `json:"transaction"`
132 }
133
134 // signTransaction sign built contract transaction.
135 func signTransaction(password string, transaction interface{}) (string, error) {
136         payload, err := json.Marshal(signTransactionRequest{Password: password, Transaction: transaction})
137         if err != nil {
138                 return "", err
139         }
140
141         res := new(signTransactionResponse)
142         if err := request(signTransactionURL, payload, res); err != nil {
143                 return "", err
144         }
145
146         return res.Tx.RawTransaction, nil
147 }
148
149 type submitTransactionRequest struct {
150         RawTransaction string `json:"raw_transaction"`
151 }
152
153 type submitTransactionResponse struct {
154         TransactionID string `json:"tx_id"`
155 }
156
157 // submitTransaction submit raw singed contract transaction.
158 func submitTransaction(rawTransaction string) (string, error) {
159         payload, err := json.Marshal(submitTransactionRequest{RawTransaction: rawTransaction})
160         if err != nil {
161                 return "", err
162         }
163
164         res := new(submitTransactionResponse)
165         if err := request(submitTransactionURL, payload, res); err != nil {
166                 return "", err
167         }
168
169         return res.TransactionID, nil
170 }
171
172 type getContractUTXOIDRequest struct {
173         TransactionID string `json:"tx_id"`
174 }
175
176 type TransactionOutput struct {
177         TransactionOutputID string `json:"id"`
178         ControlProgram      string `json:"control_program"`
179 }
180
181 type getContractUTXOIDResponse struct {
182         TransactionOutputs []TransactionOutput `json:"outputs"`
183 }
184
185 // getContractUTXOID get contract UTXO ID by transaction ID and contract control program.
186 func getContractUTXOID(transactionID, controlProgram string) (string, error) {
187         payload, err := json.Marshal(getContractUTXOIDRequest{TransactionID: transactionID})
188         if err != nil {
189                 return "", err
190         }
191
192         res := new(getContractUTXOIDResponse)
193         if err := request(getTransactionURL, payload, res); err != nil {
194                 return "", err
195         }
196
197         for _, v := range res.TransactionOutputs {
198                 if v.ControlProgram == controlProgram {
199                         return v.TransactionOutputID, nil
200                 }
201         }
202
203         return "", errFailedGetContractUTXOID
204 }
205
206 var buildUnlockContractTransactionPayload = `{
207         "actions":[
208                 {
209                         "type":"spend_account_unspent_output",
210                         "arguments":[
211                                 {
212                                         "type":"integer",
213                                         "raw_data":{
214                                                 "value":0
215                                         }
216                                 }
217                         ],
218                         "use_unconfirmed":true,
219                         "output_id":"%s"
220                 },
221                 {
222                         "amount":%s,
223                         "asset_id":"%s",
224                         "control_program":"%s",
225                         "type":"control_program"
226                 },
227                 {
228                         "account_id":"%s",
229                         "amount":%s,
230                         "asset_id":"%s",
231                         "use_unconfirmed":true,
232                         "type":"spend_account"
233                 },
234                 {
235                         "account_id":"%s",
236                         "amount":%s,
237                         "asset_id":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
238                         "use_unconfirmed":true,
239                         "type":"spend_account"
240                 },
241                 {
242                         "amount":%s,
243                         "asset_id":"%s",
244                         "control_program":"%s",
245                         "type":"control_program"
246                 }
247         ],
248         "ttl":0,
249         "base_transaction":null
250 }`
251
252 // buildUnlockContractTransaction build unlocked contract transaction.
253 func buildUnlockContractTransaction(accountInfo AccountInfo, contractUTXOID string, contractArgs ContractArgs, contractValue AssetAmount) (interface{}, error) {
254         payload := []byte(fmt.Sprintf(
255                 buildUnlockContractTransactionPayload,
256                 contractUTXOID,
257                 strconv.FormatUint(contractArgs.Amount, 10),
258                 contractArgs.Asset,
259                 contractArgs.Seller,
260                 accountInfo.AccountID,
261                 strconv.FormatUint(contractArgs.Amount, 10),
262                 contractArgs.Asset,
263                 accountInfo.AccountID,
264                 strconv.FormatUint(accountInfo.TxFee, 10),
265                 strconv.FormatUint(contractValue.Amount, 10),
266                 contractValue.Asset,
267                 accountInfo.Receiver,
268         ))
269         res := new(interface{})
270         if err := request(buildTransactionURL, payload, res); err != nil {
271                 return "", err
272         }
273         return res, nil
274 }
275
276 type listUnspentOutputsResponse struct {
277         AssetID     string `json:"asset_id"`
278         AssetAmount uint64 `json:"amount"`
279         Program     string `json:"program"`
280 }
281
282 var listUnspentOutputsPayload = `{
283         "id": "%s",
284         "unconfirmed": true,
285         "smart_contract": true
286 }`
287
288 func ListUnspentOutputs(contractUTXOID string) (string, *AssetAmount, error) {
289         payload := []byte(fmt.Sprintf(
290                 listUnspentOutputsPayload,
291                 contractUTXOID,
292         ))
293         var res []listUnspentOutputsResponse
294         if err := request(listUnspentOutputsURL, payload, &res); err != nil {
295                 return "", nil, err
296         }
297
298         contractLockedValue := new(AssetAmount)
299         if len(res) != 0 {
300                 contractLockedValue.Asset = res[0].AssetID
301                 contractLockedValue.Amount = res[0].AssetAmount
302                 return res[0].Program, contractLockedValue, nil
303         }
304         return "", nil, errListUnspentOutputs
305 }
306
307 type decodeProgramResponse struct {
308         Instructions string `json:"instructions"`
309 }
310
311 var decodeProgramPayload = `{
312         "program": "%s"
313 }`
314
315 func DecodeProgram(program string) (*ContractArgs, error) {
316         payload := []byte(fmt.Sprintf(
317                 decodeProgramPayload,
318                 program,
319         ))
320         res := new(decodeProgramResponse)
321         if err := request(decodeProgramURL, payload, res); err != nil {
322                 return nil, err
323         }
324
325         instructions := strings.Fields(res.Instructions)
326         contractArgs := new(ContractArgs)
327         contractArgs.CancelKey = instructions[1]
328         contractArgs.Seller = instructions[3]
329         contractArgs.AssetAmount.Asset = instructions[7]
330
331         amount, err := parseUint64(instructions[5])
332         if err != nil {
333                 return nil, err
334         }
335
336         contractArgs.AssetAmount.Amount = amount
337         return contractArgs, nil
338 }
339
340 func parseUint64(s string) (uint64, error) {
341         data, err := hex.DecodeString(s)
342         if err != nil {
343                 return 0, err
344         }
345
346         for i := 0; i < len(data)/2; i++ {
347                 data[i], data[len(data)-1-i] = data[len(data)-1-i], data[i]
348         }
349         s = hex.EncodeToString(data)
350         num, err := strconv.ParseUint(s, 16, 64)
351         if err != nil {
352                 return 0, err
353         }
354
355         return num, nil
356 }
357
358 // DeployContract deploy contract.
359 func DeployContract(accountInfo AccountInfo, contractArgs ContractArgs, contractValue AssetAmount) (string, error) {
360         // compile locked contract
361         contractControlProgram, err := compileLockContract(contractArgs)
362         if err != nil {
363                 return "", err
364         }
365
366         // build locked contract
367         txLocked, err := buildLockTransaction(accountInfo, contractValue, contractControlProgram)
368         if err != nil {
369                 return "", err
370         }
371
372         // sign locked contract transaction
373         signedTransaction, err := signTransaction(accountInfo.Password, txLocked)
374         if err != nil {
375                 return "", err
376         }
377
378         // submit signed transaction
379         txID, err := submitTransaction(signedTransaction)
380         if err != nil {
381                 return "", err
382         }
383
384         // get contract output ID
385         contractUTXOID, err := getContractUTXOID(txID, contractControlProgram)
386         if err != nil {
387                 return "", err
388         }
389         return contractUTXOID, nil
390 }
391
392 // CallContract call contract.
393 func CallContract(accountInfo AccountInfo, contractUTXOID string, contractArgs ContractArgs, contractValue AssetAmount) (string, error) {
394         // build unlocked contract transaction
395         txUnlocked, err := buildUnlockContractTransaction(accountInfo, contractUTXOID, contractArgs, contractValue)
396         if err != nil {
397                 return "", err
398         }
399
400         // sign unlocked contract transaction
401         signedTransaction, err := signTransaction(accountInfo.Password, txUnlocked)
402         if err != nil {
403                 return "", err
404         }
405
406         // submit signed unlocked contract transaction
407         txID, err := submitTransaction(signedTransaction)
408         if err != nil {
409                 return "", err
410         }
411
412         return txID, nil
413 }