OSDN Git Service

update
[bytom/shuttle.git] / swap / tradeoff.go
1 package swap
2
3 import (
4         "encoding/binary"
5         "encoding/hex"
6         "encoding/json"
7         "errors"
8         "fmt"
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         errTradeOffParametersInvalid = errors.New("Trade off parameters invalid")
17         errFailedSignTx              = errors.New("Failed to sign transaction")
18         errFailedGetPublicKey        = errors.New("Failed to get public key")
19 )
20
21 type compileLockContractResp struct {
22         Program string `json:"program"`
23 }
24
25 var compileLockContractReq = `{
26         "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}}",
27         "args":[
28                 {
29                         "string":"%s"
30                 },
31                 {
32                         "integer":%d
33                 },
34                 {
35                         "string":"%s"
36                 },
37                 {
38                         "string":"%s"
39                 }
40         ]
41 }`
42
43 // compileLockContract return contract control program
44 func compileLockContract(s *Server, contractArgs ContractArgs) (string, error) {
45         payload := []byte(fmt.Sprintf(compileLockContractReq,
46                 contractArgs.Asset,
47                 contractArgs.Amount,
48                 contractArgs.Seller,
49                 contractArgs.CancelKey,
50         ))
51         res := new(compileLockContractResp)
52         if err := s.request(compileURL, payload, res); err != nil {
53                 return "", err
54         }
55         return res.Program, nil
56 }
57
58 var buildLockTxReq = `{
59         "actions":[
60                 {
61                         "account_id":"%s",
62                         "amount":%d,
63                         "asset_id":"%s",
64                         "use_unconfirmed":true,
65                         "type":"spend_account"
66                 },
67                 {
68                         "amount":%d,
69                         "asset_id":"%s",
70                         "control_program":"%s",
71                         "type":"control_program"
72                 },
73                 {
74                         "account_id":"%s",
75                         "amount":%d,
76                         "asset_id":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
77                         "use_unconfirmed":true,
78                         "type":"spend_account"
79                 }
80         ],
81         "ttl":0,
82         "base_transaction":null
83 }`
84
85 // buildLockTransaction build locked contract transaction.
86 func buildLockTransaction(s *Server, accountInfo AccountInfo, contractValue AssetAmount, contractControlProgram string) (interface{}, error) {
87         payload := []byte(fmt.Sprintf(buildLockTxReq,
88                 accountInfo.AccountID,
89                 contractValue.Amount,
90                 contractValue.Asset,
91                 contractValue.Amount,
92                 contractValue.Asset,
93                 contractControlProgram,
94                 accountInfo.AccountID,
95                 accountInfo.TxFee,
96         ))
97         res := new(interface{})
98         if err := s.request(buildTransactionURL, payload, res); err != nil {
99                 return "", err
100         }
101         return res, nil
102 }
103
104 type signTxReq struct {
105         Password    string      `json:"password"`
106         Transaction interface{} `json:"transaction"`
107 }
108
109 type Transaction struct {
110         RawTransaction string `json:"raw_transaction"`
111 }
112
113 type signTxResp struct {
114         Tx           Transaction `json:"transaction"`
115         SignComplete bool        `json:"sign_complete"`
116 }
117
118 // signTransaction sign built contract transaction.
119 func signTransaction(s *Server, password string, transaction interface{}) (string, error) {
120         payload, err := json.Marshal(signTxReq{Password: password, Transaction: transaction})
121         if err != nil {
122                 return "", err
123         }
124
125         res := new(signTxResp)
126         if err := s.request(signTransactionURL, payload, res); err != nil {
127                 return "", err
128         }
129
130         if !res.SignComplete {
131                 return "", errFailedSignTx
132         }
133
134         return res.Tx.RawTransaction, nil
135 }
136
137 type submitTxReq struct {
138         RawTransaction string `json:"raw_transaction"`
139 }
140
141 type submitTxResp struct {
142         TransactionID string `json:"tx_id"`
143 }
144
145 // submitTransaction submit raw singed contract transaction.
146 func submitTransaction(s *Server, rawTransaction string) (string, error) {
147         payload, err := json.Marshal(submitTxReq{RawTransaction: rawTransaction})
148         if err != nil {
149                 return "", err
150         }
151
152         res := new(submitTxResp)
153         if err := s.request(submitTransactionURL, payload, res); err != nil {
154                 return "", err
155         }
156
157         return res.TransactionID, nil
158 }
159
160 type getContractUTXOIDReq struct {
161         TransactionID string `json:"tx_id"`
162 }
163
164 type TransactionOutput struct {
165         TransactionOutputID string `json:"id"`
166         ControlProgram      string `json:"control_program"`
167 }
168
169 type getContractUTXOIDResp struct {
170         TransactionOutputs []TransactionOutput `json:"outputs"`
171 }
172
173 // getContractUTXOID get contract UTXO ID by transaction ID and contract control program.
174 func getContractUTXOID(s *Server, transactionID, controlProgram string) (string, error) {
175         payload, err := json.Marshal(getContractUTXOIDReq{TransactionID: transactionID})
176         if err != nil {
177                 return "", err
178         }
179
180         res := new(getContractUTXOIDResp)
181         if err := s.request(getTransactionURL, payload, res); err != nil {
182                 return "", err
183         }
184
185         for _, v := range res.TransactionOutputs {
186                 if v.ControlProgram == controlProgram {
187                         return v.TransactionOutputID, nil
188                 }
189         }
190
191         return "", errFailedGetContractUTXOID
192 }
193
194 var buildUnlockContractTxReq = `{
195         "actions":[
196                 {
197                         "type":"spend_account_unspent_output",
198                         "arguments":[
199                                 {
200                                         "type":"integer",
201                                         "raw_data":{
202                                                 "value":0
203                                         }
204                                 }
205                         ],
206                         "use_unconfirmed":true,
207                         "output_id":"%s"
208                 },
209                 {
210                         "amount":%d,
211                         "asset_id":"%s",
212                         "control_program":"%s",
213                         "type":"control_program"
214                 },
215                 {
216                         "account_id":"%s",
217                         "amount":%d,
218                         "asset_id":"%s",
219                         "use_unconfirmed":true,
220                         "type":"spend_account"
221                 },
222                 {
223                         "account_id":"%s",
224                         "amount":%d,
225                         "asset_id":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
226                         "use_unconfirmed":true,
227                         "type":"spend_account"
228                 },
229                 {
230                         "amount":%d,
231                         "asset_id":"%s",
232                         "control_program":"%s",
233                         "type":"control_program"
234                 }
235         ],
236         "ttl":0,
237         "base_transaction":null
238 }`
239
240 // buildUnlockContractTransaction build unlocked contract transaction.
241 func buildUnlockContractTransaction(s *Server, accountInfo AccountInfo, contractUTXOID string) (interface{}, error) {
242         program, contractValue, err := ListUnspentOutputs(s, contractUTXOID)
243         if err != nil {
244                 return "", err
245         }
246
247         contractArgs, err := decodeProgram(s, program)
248         if err != nil {
249                 return "", err
250         }
251
252         payload := []byte(fmt.Sprintf(buildUnlockContractTxReq,
253                 contractUTXOID,
254                 contractArgs.Amount,
255                 contractArgs.Asset,
256                 contractArgs.Seller,
257                 accountInfo.AccountID,
258                 contractArgs.Amount,
259                 contractArgs.Asset,
260                 accountInfo.AccountID,
261                 accountInfo.TxFee,
262                 contractValue.Amount,
263                 contractValue.Asset,
264                 accountInfo.Receiver,
265         ))
266         res := new(interface{})
267         if err := s.request(buildTransactionURL, payload, res); err != nil {
268                 return "", err
269         }
270         return res, nil
271 }
272
273 type listUnspentOutputsResp struct {
274         AssetID     string `json:"asset_id"`
275         AssetAmount uint64 `json:"amount"`
276         Program     string `json:"program"`
277 }
278
279 type listUnspentOutputsReq struct {
280         UTXOID        string `json:"id"`
281         Unconfirmed   bool   `json:"unconfirmed"`
282         SmartContract bool   `json:"smart_contract"`
283 }
284
285 func ListUnspentOutputs(s *Server, contractUTXOID string) (string, *AssetAmount, error) {
286         payload, err := json.Marshal(listUnspentOutputsReq{
287                 UTXOID:        contractUTXOID,
288                 Unconfirmed:   true,
289                 SmartContract: true,
290         })
291         if err != nil {
292                 return "", nil, err
293         }
294
295         var res []listUnspentOutputsResp
296         if err := s.request(listUnspentOutputsURL, payload, &res); err != nil {
297                 return "", nil, err
298         }
299
300         if len(res) == 0 {
301                 return "", nil, errListUnspentOutputs
302         }
303
304         contractLockedValue := new(AssetAmount)
305         contractLockedValue.Asset = res[0].AssetID
306         contractLockedValue.Amount = res[0].AssetAmount
307         return res[0].Program, contractLockedValue, nil
308 }
309
310 type decodeProgramResp struct {
311         Instructions string `json:"instructions"`
312 }
313
314 type decodeProgramReq struct {
315         Program string `json:"program"`
316 }
317
318 func decodeProgram(s *Server, program string) (*ContractArgs, error) {
319         payload, err := json.Marshal(decodeProgramReq{Program: program})
320         if err != nil {
321                 return nil, err
322         }
323
324         res := new(decodeProgramResp)
325         if err := s.request(decodeProgramURL, payload, res); err != nil {
326                 return nil, err
327         }
328
329         instructions := strings.Fields(res.Instructions)
330         contractArgs := new(ContractArgs)
331         contractArgs.CancelKey = instructions[1]
332         contractArgs.Seller = instructions[3]
333         contractArgs.AssetAmount.Asset = instructions[7]
334         if len(contractArgs.CancelKey) != 64 || len(contractArgs.AssetAmount.Asset) != 64 {
335                 return nil, errTradeOffParametersInvalid
336         }
337
338         amount, err := parseUint64(instructions[5])
339         if err != nil {
340                 return nil, err
341         }
342
343         contractArgs.AssetAmount.Amount = amount
344         return contractArgs, nil
345 }
346
347 func parseUint64(s string) (uint64, error) {
348         data, err := hex.DecodeString(s)
349         if err != nil {
350                 return 0, err
351         }
352
353         var padded [8]byte
354         copy(padded[:], data)
355         num := binary.LittleEndian.Uint64(padded[:])
356
357         return num, nil
358 }
359
360 type listPublicKeysReq struct {
361         AccountID string `json:"account_id"`
362 }
363
364 type PubkeyInfo struct {
365         PublicKey      string   `json:"pubkey"`
366         DerivationPath []string `json:"derivation_path"`
367 }
368
369 type listPublicKeysResp struct {
370         RootXPub    string       `json:"root_xpub"`
371         PubkeyInfos []PubkeyInfo `json:"pubkey_infos"`
372 }
373
374 type XPubKeyInfo struct {
375         XPubKey        string   `json:"xpub"`
376         DerivationPath []string `json:"derivation_path"`
377 }
378
379 func getXPubKeyInfo(s *Server, accountID, publicKey string) (*XPubKeyInfo, error) {
380         payload, err := json.Marshal(listPublicKeysReq{AccountID: accountID})
381         if err != nil {
382                 return nil, err
383         }
384
385         res := new(listPublicKeysResp)
386         if err := s.request(listPubkeysURL, payload, res); err != nil {
387                 return nil, err
388         }
389
390         xpubKeyInfo := new(XPubKeyInfo)
391         xpubKeyInfo.XPubKey = res.RootXPub
392         for _, PubkeyInfo := range res.PubkeyInfos {
393                 if PubkeyInfo.PublicKey == publicKey {
394                         xpubKeyInfo.DerivationPath = PubkeyInfo.DerivationPath
395                         return xpubKeyInfo, nil
396                 }
397         }
398         return nil, errFailedGetPublicKey
399 }
400
401 var buildCancelContractTxReq = `{
402     "actions": [
403         {
404             "type": "spend_account_unspent_output",
405             "arguments": [
406                 {
407                     "type": "raw_tx_signature",
408                     "raw_data": %s
409                 },
410                 {
411                         "type":"integer",
412                         "raw_data":{
413                                 "value":1
414                         }
415                 }
416                         ],
417                         "use_unconfirmed":true,
418             "output_id": "%s"
419         },
420         {
421             "account_id": "%s",
422             "amount": %d,
423             "asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
424                         "use_unconfirmed":true,
425                         "type": "spend_account"
426         },
427         {
428             "amount": %d,
429             "asset_id": "%s",
430             "control_program": "%s",
431             "type": "control_program"
432         }
433     ],
434     "ttl": 0,
435     "base_transaction": null
436 }`
437
438 func buildCancelContractTransaction(s *Server, accountInfo AccountInfo, contractUTXOID string, xpubKeyInfo *XPubKeyInfo, contractValue *AssetAmount) (interface{}, error) {
439         xpubKeyInfoStr, err := json.Marshal(xpubKeyInfo)
440         if err != nil {
441                 return "", err
442         }
443         payload := []byte(fmt.Sprintf(buildCancelContractTxReq,
444                 xpubKeyInfoStr,
445                 contractUTXOID,
446                 accountInfo.AccountID,
447                 accountInfo.TxFee,
448                 contractValue.Amount,
449                 contractValue.Asset,
450                 accountInfo.Receiver,
451         ))
452         res := new(interface{})
453         if err := s.request(buildTransactionURL, payload, res); err != nil {
454                 return "", err
455         }
456         return res, nil
457 }
458
459 // DeployContract deploy contract.
460 func DeployContract(s *Server, accountInfo AccountInfo, contractArgs ContractArgs, contractValue AssetAmount) (string, error) {
461         // compile locked contract
462         contractControlProgram, err := compileLockContract(s, contractArgs)
463         if err != nil {
464                 return "", err
465         }
466
467         // build locked contract
468         txLocked, err := buildLockTransaction(s, accountInfo, contractValue, contractControlProgram)
469         if err != nil {
470                 return "", err
471         }
472
473         // sign locked contract transaction
474         signedTransaction, err := signTransaction(s, accountInfo.Password, txLocked)
475         if err != nil {
476                 return "", err
477         }
478
479         // submit signed transaction
480         txID, err := submitTransaction(s, signedTransaction)
481         if err != nil {
482                 return "", err
483         }
484
485         // get contract output ID
486         contractUTXOID, err := getContractUTXOID(s, txID, contractControlProgram)
487         if err != nil {
488                 return "", err
489         }
490         return contractUTXOID, nil
491 }
492
493 // CallContract call contract.
494 func CallContract(s *Server, accountInfo AccountInfo, contractUTXOID string) (string, error) {
495         // build unlocked contract transaction
496         txUnlocked, err := buildUnlockContractTransaction(s, accountInfo, contractUTXOID)
497         if err != nil {
498                 return "", err
499         }
500
501         // sign unlocked contract transaction
502         signedTransaction, err := signTransaction(s, accountInfo.Password, txUnlocked)
503         if err != nil {
504                 return "", err
505         }
506
507         // submit signed unlocked contract transaction
508         txID, err := submitTransaction(s, signedTransaction)
509         if err != nil {
510                 return "", err
511         }
512
513         return txID, nil
514 }
515
516 // CancelTradeoffContract cancel tradeoff contract.
517 func CancelTradeoffContract(s *Server, accountInfo AccountInfo, contractUTXOID string) (string, error) {
518         // get contract control program by contract UTXOID
519         contractControlProgram, contractValue, err := ListUnspentOutputs(s, contractUTXOID)
520         if err != nil {
521                 return "", err
522         }
523
524         // get public key by contract control program
525         contractArgs, err := decodeProgram(s, contractControlProgram)
526         if err != nil {
527                 return "", err
528         }
529
530         // get public key path and root xpub by contract args
531         xpubInfo, err := getXPubKeyInfo(s, accountInfo.AccountID, contractArgs.CancelKey)
532         if err != nil {
533                 return "", err
534         }
535
536         // build cancel contract transaction
537         builtTx, err := buildCancelContractTransaction(s, accountInfo, contractUTXOID, xpubInfo, contractValue)
538         if err != nil {
539                 return "", err
540         }
541
542         // sign cancel contract transaction
543         signedTx, err := signTransaction(s, accountInfo.Password, builtTx)
544         if err != nil {
545                 return "", err
546         }
547
548         // submit signed unlocked contract transaction
549         txID, err := submitTransaction(s, signedTx)
550         if err != nil {
551                 return "", err
552         }
553
554         return txID, nil
555 }