-
Notifications
You must be signed in to change notification settings - Fork 2
/
exchange.go
1781 lines (1562 loc) · 66.2 KB
/
exchange.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
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package routes
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"sort"
"time"
"github.com/deso-smart/deso-core/v2/lib"
"github.com/pkg/errors"
"github.com/btcsuite/btcd/btcec"
"github.com/golang/glog"
bip39 "github.com/tyler-smith/go-bip39"
)
// This file implements basic functions required to manipulate
// DeSo programmatically. It is mainly useful to exchanges that
// list DeSo.
//
// We recommend using our Rosetta implementation instead of this API.
var (
IsGraylisted = []byte{1}
IsBlacklisted = []byte{1}
)
const (
// RoutePathAPIBase ...
RoutePathAPIBase = "/api/v1"
// RoutePathAPIKeyPair ...
RoutePathAPIKeyPair = "/api/v1/key-pair"
// RoutePathAPIBalance ...
RoutePathAPIBalance = "/api/v1/balance"
// RoutePathAPITransferDeSo ...
RoutePathAPITransferDeSo = "/api/v1/transfer-deso"
// RoutePathAPITransactionInfo ...
RoutePathAPITransactionInfo = "/api/v1/transaction-info"
// RoutePathAPINodeInfo ...
RoutePathAPINodeInfo = "/api/v1/node-info"
// RoutePathAPIBlock ...
RoutePathAPIBlock = "/api/v1/block"
)
// APIRoutes returns the routes for the public-facing API.
func (fes *APIServer) APIRoutes() []Route {
var APIRoutes = []Route{
{
"APIBase",
[]string{"GET"},
RoutePathAPIBase,
fes.APIBase,
PublicAccess, // CheckSecret
},
{
"APIKeyPair",
[]string{"POST", "OPTIONS"},
RoutePathAPIKeyPair,
fes.APIKeyPair,
PublicAccess, // CheckSecret
},
{
"APIBalance",
[]string{"POST", "OPTIONS"},
RoutePathAPIBalance,
fes.APIBalance,
PublicAccess, // CheckSecret
},
{
"APITransferDeSo",
[]string{"POST", "OPTIONS"},
RoutePathAPITransferDeSo,
fes.APITransferDeSo,
PublicAccess, // CheckSecret
},
{
"APITransactionInfo",
[]string{"POST", "OPTIONS"},
RoutePathAPITransactionInfo,
fes.APITransactionInfo,
PublicAccess, // CheckSecret
},
{
"APINodeInfo",
[]string{"POST", "OPTIONS"},
RoutePathAPINodeInfo,
fes.APINodeInfo,
AdminAccess, // CheckSecret
},
{
"APIBlock",
[]string{"POST", "OPTIONS"},
RoutePathAPIBlock,
fes.APIBlock,
PublicAccess, // CheckSecret
},
}
return APIRoutes
}
// APIAddError sets an error response on the ResponseWriter passed in.
func APIAddError(ww http.ResponseWriter, errorString string) {
//glog.Error(errorString)
ww.WriteHeader(http.StatusBadRequest)
json.NewEncoder(ww).Encode(struct {
Error string
}{Error: errorString})
}
// APIBaseResponse ...
type APIBaseResponse struct {
// Blank if successful. Otherwise, contains a description of the
// error that occurred.
Error string
// The information contained in the block’s header.
Header *HeaderResponse
Transactions []*TransactionResponse
}
func _headerToResponse(header *lib.MsgDeSoHeader, hash string) *HeaderResponse {
return &HeaderResponse{
BlockHashHex: hash,
Version: header.Version,
PrevBlockHashHex: header.PrevBlockHash.String(),
TransactionMerkleRootHex: header.TransactionMerkleRoot.String(),
TstampSecs: header.TstampSecs,
Height: header.Height,
Nonce: header.Nonce,
ExtraNonce: header.ExtraNonce,
}
}
// APIBase is an endpoint that simply confirms that the API is up and running.
func (fes *APIServer) APIBase(ww http.ResponseWriter, rr *http.Request) {
if fes.TXIndex == nil {
APIAddError(ww, fmt.Sprintf("APIBase: Cannot be called when TXIndexChain "+
"is nil. This error occurs when --txindex was not passed to the program on startup"))
return
}
blockNode := fes.blockchain.BlockTip()
// Take the hash computed from above and find the corresponding block.
blockMsg, err := lib.GetBlock(blockNode.Hash, fes.blockchain.DB(), fes.blockchain.Snapshot())
if err != nil {
APIAddError(ww, fmt.Sprintf("APIBase: Problem fetching block: %v", err))
return
}
if blockMsg == nil {
APIAddError(ww, fmt.Sprintf("APIBase: Block with hash %v not found", blockNode.Hash))
return
}
res := &APIBaseResponse{
Header: _headerToResponse(blockMsg.Header, blockNode.Hash.String()),
}
utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView()
if err != nil {
APIAddError(ww, fmt.Sprintf("APIBase: Problem fetching utxoView: %v", err))
return
}
for _, txn := range blockMsg.Txns {
// Look up the metadata for each transaction.
txnMeta := lib.DbGetTxindexTransactionRefByTxID(fes.TXIndex.TXIndexChain.DB(), nil, txn.Hash())
res.Transactions = append(
res.Transactions, APITransactionToResponse(
txn, txnMeta, utxoView, fes.Params))
}
if err := json.NewEncoder(ww).Encode(res); err != nil {
APIAddError(ww, fmt.Sprintf("APIBaseResponse: Problem encoding response "+
"as JSON: %v", err))
return
}
}
// APIKeyPairRequest specifies the params for a call to the
// APIKeyPair endpoint.
type APIKeyPairRequest struct {
// A BIP39 mnemonic and extra text. Mnemonic can be 12 words or
// 24 words. ExtraText is optional.
Mnemonic string
ExtraText string
// The index of the public/private key pair to generate
Index uint32
}
// APIKeyPairResponse specifies the response for a call to the
// APIKeyPair endpoint.
type APIKeyPairResponse struct {
// Blank if successful. Otherwise, contains a description of the
// error that occurred.
Error string
// The DeSo public key encoded using base58 check encoding with
// prefix = [3]byte{0x9, 0x7f, 0x0}
// This public key can be passed in subsequent API calls to check
// balance, among other things. All encoded DeSo public keys start
// with the characters “BC”
PublicKeyBase58Check string
// The DeSo public key encoded as a plain hex string. This should
// match the public key with the corresponding index generated by this tool.
// This should not be passed to subsequent API calls, it is only provided
// as a reference, mainly as a sanity-check.
PublicKeyHex string
// The DeSo private key encoded using base58 check encoding with
// prefix = [3]byte{0x50, 0xd5, 0x0}
// This private key can be passed in subsequent API calls to spend DeSo,
// among other things. All DeSo private keys start with
// the characters “bc”
PrivateKeyBase58Check string
// The DeSo private key encoded as a plain hex string. Note that
// this will not directly match what is produced by the tool because the
// tool shows the private key encoded using Bitcoin’s WIF format rather
// than as raw hex. To convert this raw hex into Bitcoin’s WIF format you can
// use this simple Python script. This should not be passed to subsequent
// API calls, it is only provided as a reference, mainly as a sanity-check.
PrivateKeyHex string
}
// APIKeyPair allows one to generate an arbitrary number of public/private
// DeSo keypairs.
//
// Each public/private key pair corresponds to
// a particular index associated. This means that index “5”, for example,
// will always generate the same public/private
// key pair. An infinite number of public/private key pairs can thus be generated
// by iterating an index for a seed.
//
// Note that all public/private keys are inter-operable as Bitcoin
// public/private keys. Meaning they represent a point on the secp256k1 curve
// (same as what is used by Bitcoin).
//
// Note also that, under the hood, DeSo takes the BIP39 mnemonic and
// generates the public/private key pairs using the BIP32 derivation path
// m/44’/0’/0’/0/i, where “i” is the “index” of the public/private key being
// generated. This means that the DeSo public/private key pair generated by
// the node will always line up with the public/private key pairs generated by
// this tool (https://iancoleman.io/bip39/). An engineer can therefore “sanity
// check” that things are working by generating a mnemonic using the tool,
// creating seed with that mnemonic, and then verifying that the
// public/private key pairs generated line up with what is shown by the tool.
func (fes *APIServer) APIKeyPair(ww http.ResponseWriter, rr *http.Request) {
// Decode the request data.
decoder := json.NewDecoder(io.LimitReader(rr.Body, MaxRequestBodySizeBytes))
apiKeyPairRequest := APIKeyPairRequest{}
if err := decoder.Decode(&apiKeyPairRequest); err != nil {
APIAddError(ww, fmt.Sprintf("APIKeyPair: Problem parsing request body: %v", err))
return
}
seedBytes, err := bip39.NewSeedWithErrorChecking(apiKeyPairRequest.Mnemonic, apiKeyPairRequest.ExtraText)
if err != nil {
APIAddError(ww, fmt.Sprintf("APIKeyPair: Error converting mnemonic and extra text to seed: %v", err))
return
}
pubKey, privKey, _, err := lib.ComputeKeysFromSeed(
seedBytes, apiKeyPairRequest.Index, fes.Params)
if err != nil {
APIAddError(ww, fmt.Sprintf("APIKeyPair: Problem generating key at "+
"index %d: %v", apiKeyPairRequest.Index, err))
return
}
res := APIKeyPairResponse{
PublicKeyBase58Check: lib.PkToString(pubKey.SerializeCompressed(), fes.Params),
PublicKeyHex: hex.EncodeToString(pubKey.SerializeCompressed()),
PrivateKeyBase58Check: lib.PrivToString(privKey.Serialize(), fes.Params),
PrivateKeyHex: hex.EncodeToString(privKey.Serialize()),
}
if err := json.NewEncoder(ww).Encode(res); err != nil {
APIAddError(ww, fmt.Sprintf("APIKeyPair: Problem encoding response as JSON: %v", err))
return
}
}
// APIBalanceRequest specifies the params for a call to the
// APIBalance endpoint.
type APIBalanceRequest struct {
PublicKeyBase58Check string
Confirmations uint32
}
// UTXOEntryResponse ...
// TODO: There is a slightly different but redundant definition of
// this in frontend_utils.go
type UTXOEntryResponse struct {
// A string that uniquely identifies a previous transaction. This is
// a sha256 hash of the transaction’s information encoded using
// base58 check encoding.
TransactionIDBase58Check string
// The index within this transaction that corresponds to an output
// spendable by the passed-in public key.
Index int64
// The amount that is spendable by this UTXO in “nanos”.
AmountNanos uint64
// The pulic key entitled to spend the amount stored in this UTXO.
PublicKeyBase58Check string
// The number of confirmations this UTXO has. Set to zero if the
// UTXO is unconfirmed.
Confirmations int64
// Whether or not this UTXO was a block reward.
UtxoType string
BlockHeight int64
}
// APIBalanceResponse specifies the response for a call to the
// APIBalance endpoint.
type APIBalanceResponse struct {
// Blank if successful. Otherwise, contains a description of the
// error that occurred.
Error string
// The balance of the public key queried in “nanos.” Note
// there are 1e9 “nanos” per DeSo, so if the balance were “1 DeSo” then
// this value would be set to 1e9.
ConfirmedBalanceNanos int64
// The unconfirmed balance of the public key queried in “nanos.” This field
// is set to zero if Confirmations is set to a value greater than zero.
UnconfirmedBalanceNanos int64
// DeSo uses a UTXO model similar to Bitcoin. As such, querying
// the balance returns all of the UTXOs for a particular public key for
// convenience. Note that a UTXO is simply a reference to a particular
// output index in a previous transaction
UTXOs []*UTXOEntryResponse
}
// APIBalance allows one to check the balance of a particular public key by
// passing the public key.
//
// Note that spent transaction outputs are not returned by this endpoint. To
// perform operations on spent transaction outputs, one must use the
// APITransactionInfo endpoint instead.
func (fes *APIServer) APIBalance(ww http.ResponseWriter, rr *http.Request) {
// Decode the request data.
decoder := json.NewDecoder(io.LimitReader(rr.Body, MaxRequestBodySizeBytes))
balanceRequest := APIBalanceRequest{}
if err := decoder.Decode(&balanceRequest); err != nil {
APIAddError(ww, fmt.Sprintf("APIBalanceRequest: Problem parsing request body: %v", err))
return
}
// A public key is required.
if balanceRequest.PublicKeyBase58Check == "" {
APIAddError(ww, "APIBalanceRequest: Missing PublicKeyBase58Check")
return
}
// Parse the public key into bytes.
publicKeyBytes, _, err := lib.Base58CheckDecode(balanceRequest.PublicKeyBase58Check)
if err != nil {
APIAddError(ww, fmt.Sprintf("APIBalanceRequest: Problem parsing request body: %v", err))
return
}
// Get all the UTXOs for the public key.
utxoView, err := fes.mempool.GetAugmentedUtxoViewForPublicKey(publicKeyBytes, nil)
if err != nil {
APIAddError(ww, fmt.Sprintf("APIBalanceRequest: Problem getting UTXOs for public key: %v", err))
return
}
utxoEntries, err := utxoView.GetUnspentUtxoEntrysForPublicKey(publicKeyBytes)
if err != nil {
APIAddError(ww, fmt.Sprintf("APIBalanceRequest: Problem getting UTXO entries for public key: %v", err))
return
}
// Get the height of the current block tip.
blockTipHeight := fes.blockchain.BlockTip().Height
// Populate the response by looping over the UTXOs we found.
balanceResponse := &APIBalanceResponse{}
balanceResponse.UTXOs = []*UTXOEntryResponse{}
for _, utxoEntry := range utxoEntries {
// The height of UTXOs in the mempool is tip+1 so confirmations will
// be (tip - (tip+1) + 1) = 0 for these, and >0 for anything that's
// confirmed.
confirmations := int64(blockTipHeight) - int64(utxoEntry.BlockHeight) + 1
// Ignore UTXOs that don't have enough confirmations on them.
if confirmations < int64(balanceRequest.Confirmations) {
continue
}
if confirmations > 0 {
balanceResponse.ConfirmedBalanceNanos += int64(utxoEntry.AmountNanos)
} else {
balanceResponse.UnconfirmedBalanceNanos += int64(utxoEntry.AmountNanos)
}
balanceResponse.UTXOs = append(balanceResponse.UTXOs, &UTXOEntryResponse{
TransactionIDBase58Check: lib.PkToString(utxoEntry.UtxoKey.TxID[:], fes.Params),
Index: int64(utxoEntry.UtxoKey.Index),
AmountNanos: utxoEntry.AmountNanos,
PublicKeyBase58Check: lib.PkToString(utxoEntry.PublicKey, fes.Params),
Confirmations: confirmations,
UtxoType: utxoEntry.UtxoType.String(),
BlockHeight: int64(utxoEntry.BlockHeight),
})
}
if err := json.NewEncoder(ww).Encode(balanceResponse); err != nil {
APIAddError(ww, fmt.Sprintf("APIBalance: Problem encoding response as JSON: %v", err))
return
}
}
// InputResponse ...
type InputResponse struct {
TransactionIDBase58Check string
Index int64
}
// OutputResponse ...
type OutputResponse struct {
PublicKeyBase58Check string
AmountNanos uint64
}
// TransactionResponse ...
// TODO: This is redundant with TransactionInfo in frontend_utils.
type TransactionResponse struct {
// A string that uniquely identifies this transaction. This is a sha256 hash
// of the transaction’s data encoded using base58 check encoding.
TransactionIDBase58Check string
// The raw hex of the transaction data. This can be fully-constructed from
// the human-readable portions of this object.
RawTransactionHex string `json:",omitempty"`
// The inputs and outputs for this transaction.
Inputs []*InputResponse `json:",omitempty"`
Outputs []*OutputResponse `json:",omitempty"`
// The signature of the transaction in hex format.
SignatureHex string `json:",omitempty"`
// Will always be “0” for basic transfers
TransactionType string `json:",omitempty"`
// TODO: Create a TransactionMeta portion for the response.
// The hash of the block in which this transaction was mined. If the
// transaction is unconfirmed, this field will be empty. To look up
// how many confirmations a transaction has, simply plug this value
// into the "block" endpoint.
BlockHashHex string `json:",omitempty"`
TransactionMetadata *lib.TransactionMetadata `json:",omitempty"`
// The ExtraData added to this transaction
ExtraData map[string]string `json:",omitempty"`
}
// TransactionInfoResponse contains information about the transaction
// that is computed for convenience.
type TransactionInfoResponse struct {
// The sum of the inputs
TotalInputNanos uint64
// The amount being sent to the “RecipientPublicKeyBase58Check”
SpendAmountNanos uint64
// The amount being returned to the “SenderPublicKeyBase58Check”
ChangeAmountNanos uint64
// The total fee and the fee rate (in nanos per KB) that was used for this
// transaction.
FeeNanos uint64
FeeRateNanosPerKB uint64
// Will match the public keys passed as params. Note that
// SenderPublicKeyBase58Check receives the change from this transaction.
SenderPublicKeyBase58Check string
RecipientPublicKeyBase58Check string
}
// APITransferDeSoRequest specifies the params for a call to the
// APITransferDeSo endpoint.
type APITransferDeSoRequest struct {
// An DeSo private key encoded using base58 check encoding (starts
// with "bc").
SenderPrivateKeyBase58Check string
// An DeSo public key encoded using base58 check encoding (starts
// with “BC”) that will receive the DeSo being sent. This field is required
// whether sending using an explicit public/private key pair.
RecipientPublicKeyBase58Check string
// The amount of DeSo to send in “nanos.” Note that “1 DeSo” is equal to
// 1e9 nanos, so to send 1 DeSo, this value would need to be set to 1e9.
AmountNanos int64
// The fee rate to use for this transaction. If left unset, a default fee rate
// will be used. This can be checked using the “DryRun” parameter below.
MinFeeRateNanosPerKB int64
// No need to specify ProfileEntryResponse in each TransactionFee
TransactionFees []TransactionFee `safeForLogging:"true"`
// When set to true, the transaction is returned in the response but not
// actually broadcast to the network. Useful for testing.
DryRun bool
}
// APITransferDeSoResponse specifies the response for a call to the
// APITransferDeSo endpoint.
type APITransferDeSoResponse struct {
// Blank if successful. Otherwise, contains a description of the
// error that occurred.
Error string
// The transaction we assembled.
Transaction *TransactionResponse
// Information about the transaction that we compute for
// convenience.
TransactionInfo *TransactionInfoResponse
}
// APITransactionToResponse converts a raw DeSo transaction message to
// an object that can be easily JSON serialized.
func APITransactionToResponse(
txnn *lib.MsgDeSoTxn,
txnMeta *lib.TransactionMetadata,
utxoView *lib.UtxoView,
params *lib.DeSoParams) *TransactionResponse {
signatureHex := ""
if txnn.Signature != nil {
signatureHex = hex.EncodeToString(txnn.Signature.Serialize())
}
// Remove UtxoOps from the response because it's massive and usually useless
// We do some funky pointer stuff here so that we don't change the original object
var txnMetaResponse lib.TransactionMetadata
if txnMeta != nil {
txnMetaResponse = *txnMeta
basicMetadata := *txnMeta.BasicTransferTxindexMetadata
basicMetadata.UtxoOps = nil
txnMetaResponse.BasicTransferTxindexMetadata = &basicMetadata
}
txnBytes, _ := txnn.ToBytes(false /*preSignature*/)
ret := &TransactionResponse{
TransactionIDBase58Check: lib.PkToString(txnn.Hash()[:], params),
RawTransactionHex: hex.EncodeToString(txnBytes),
SignatureHex: signatureHex,
TransactionType: txnn.TxnMeta.GetTxnType().String(),
TransactionMetadata: &txnMetaResponse,
// Inputs, Outputs, ExtraData, and some txnMeta fields set below.
}
for _, input := range txnn.TxInputs {
ret.Inputs = append(ret.Inputs, &InputResponse{
TransactionIDBase58Check: lib.PkToString(input.TxID[:], params),
Index: int64(input.Index),
})
}
for _, output := range txnn.TxOutputs {
ret.Outputs = append(ret.Outputs, &OutputResponse{
PublicKeyBase58Check: lib.PkToString(output.PublicKey, params),
AmountNanos: output.AmountNanos,
})
}
ret.ExtraData = DecodeExtraDataMap(params, utxoView, txnn.ExtraData)
if txnMeta != nil {
ret.BlockHashHex = txnMeta.BlockHashHex
}
return ret
}
// APITransferDeSo can be used to transfer DeSo from one public key to
// another programmatically. To transfer DeSo, one must provide a
// public/private key pair. DeSo uses a UTXO model like Bitcoin but
// DeSo transactions are generally simpler than Bitcoin transactions
// because DeSo always uses the “from public key”
// as the “change” public key (meaning that it does not “rotate” keys by
// default).
//
// For example, if a transaction sends 10 DeSo from PubA to PubB with 5 DeSo
// in “change” and 1 DeSo as a “miner fee,” then the transaction would look as
// follows:
// - Input: 16 DeSo (10 DeSo to send, 5 DeSo in change, and 1 DeSo as a fee)
// - PubB: 10 DeSo (the amount being sent from A to B)
// - PubA: 5 DeSo (change returned to A)
// - Implicit 1 DeSo is paid as a fee to the miner. The miner fee is implicitly
// computed as (total input – total output) just like in Bitcoin.
//
// TODO: This function is redundant with the APITransferDeSo function in frontend_utils
func (fes *APIServer) APITransferDeSo(ww http.ResponseWriter, rr *http.Request) {
decoder := json.NewDecoder(io.LimitReader(rr.Body, MaxRequestBodySizeBytes))
transferDeSoRequest := APITransferDeSoRequest{}
if err := decoder.Decode(&transferDeSoRequest); err != nil {
APIAddError(ww, fmt.Sprintf("APITransferDeSo: Problem parsing request body: %v", err))
return
}
senderPrivKeyString := transferDeSoRequest.SenderPrivateKeyBase58Check
if senderPrivKeyString == "" {
APIAddError(ww, "APITransferDeSo: SenderPrivateKeyBase58Check is required")
return
}
// Decode the sender public and private key
senderPrivBytes, _, err := lib.Base58CheckDecode(senderPrivKeyString)
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransferDeSo: Problem decoding sender "+
"base58 private key: %v", err))
return
}
senderPriv, senderPub := btcec.PrivKeyFromBytes(btcec.S256(), senderPrivBytes)
if senderPriv == nil {
APIAddError(ww, fmt.Sprintf("APITransferDeSo: Problem parsing sender "+
"base58 private key"))
return
}
// Decode the recipient's public key.
recipientPubBytes, _, err := lib.Base58CheckDecode(
transferDeSoRequest.RecipientPublicKeyBase58Check)
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransferDeSo: Problem decoding recipient "+
"base58 public key %s: %v", transferDeSoRequest.RecipientPublicKeyBase58Check, err))
return
}
recipientPub, err := btcec.ParsePubKey(recipientPubBytes, btcec.S256())
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransferDeSo: Problem encoding recipient "+
"base58 public key %s: %v", transferDeSoRequest.RecipientPublicKeyBase58Check, err))
return
}
// Compute the min fee.
minFeeRateNanosPerKB := transferDeSoRequest.MinFeeRateNanosPerKB
if minFeeRateNanosPerKB <= 0 {
minFeeRateNanosPerKB = int64(fes.MinFeeRateNanosPerKB)
}
senderPublicKeyBytes := senderPub.SerializeCompressed()
// Compute the additional transaction fees as specified by the request body and the node-level fees.
additionalOutputs, err := fes.getTransactionFee(lib.TxnTypeBasicTransfer, senderPublicKeyBytes, transferDeSoRequest.TransactionFees)
if err != nil {
_AddBadRequestError(ww, fmt.Sprintf("APITransferDeSo: TransactionFees specified in Request body are invalid: %v", err))
return
}
// If DryRun is set to false, which is the default, then we broadcast
// the transaction.
shouldBroadcast := !transferDeSoRequest.DryRun
// If the AmountNanos is less than zero then we have a special case where we create
// a transaction with the maximum spend.
var txnn *lib.MsgDeSoTxn
var totalInputt uint64
var spendAmountt uint64
var changeAmountt uint64
var feeNanoss uint64
if transferDeSoRequest.AmountNanos < 0 {
// Create a MAX transaction
txnn, totalInputt, spendAmountt, feeNanoss, err = fes.blockchain.CreateMaxSpend(
senderPublicKeyBytes, recipientPub.SerializeCompressed(),
uint64(minFeeRateNanosPerKB),
fes.backendServer.GetMempool(), additionalOutputs)
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransferDeSo: Error processing MAX transaction: %v", err))
return
}
// Sanity check that the input is equal to:
// (spend amount + change amount + fees)
if totalInputt != (spendAmountt + changeAmountt + feeNanoss) {
APIAddError(ww, fmt.Sprintf("APITransferDeSo: totalInput=%d is not equal "+
"to the sum of the (spend amount=%d, change=%d, and fees=%d) which sums "+
"to %d. This means there was likely a problem with CreateMaxSpend",
totalInputt, spendAmountt, changeAmountt, feeNanoss, (spendAmountt+changeAmountt+feeNanoss)))
return
}
// Process the transaction according to whether the user wants us to
// sign/validate/broadcast it.
err = fes._processTransactionWithKey(txnn, senderPriv, shouldBroadcast)
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransferDeSo: Problem processing transaction: %v", err))
return
}
} else {
// In this case, we are spending what the user asked us to spend as opposed to
// spending the maximum amount possible.
// Create the transaction outputs and add the recipient's public key and the
// amount we want to pay them
txnOutputs := append(additionalOutputs, &lib.DeSoOutput{
PublicKey: recipientPub.SerializeCompressed(),
// If we get here we know the amount is non-negative.
AmountNanos: uint64(transferDeSoRequest.AmountNanos),
})
// Assemble the transaction so that inputs can be found and fees can
// be computed.
txnn = &lib.MsgDeSoTxn{
// The inputs will be set below.
TxInputs: []*lib.DeSoInput{},
TxOutputs: txnOutputs,
PublicKey: senderPublicKeyBytes,
TxnMeta: &lib.BasicTransferMetadata{},
// We wait to compute the signature until we've added all the
// inputs and change.
}
// Add inputs to the transaction and do signing, validation, and broadcast
// depending on what the user requested.
totalInputt, spendAmountt, changeAmountt, feeNanoss, err = fes._augmentAndProcessTransactionWithSubsidyWithKey(
txnn, senderPriv,
uint64(minFeeRateNanosPerKB),
0, /*inputSubsidy*/
shouldBroadcast)
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransferDeSo: Error processing regular transaction: %v", err))
return
}
}
// Return the transaction in the response.
res := APITransferDeSoResponse{}
utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView()
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransferDeSo: Problem fetching utxoView: %v", err))
return
}
// The block hash param is empty because this transaction clearly hasn't been
// mined yet.
res.Transaction = APITransactionToResponse(txnn, nil, utxoView, fes.Params)
txnBytes, _ := txnn.ToBytes(false /*preSignature*/)
res.TransactionInfo = &TransactionInfoResponse{
TotalInputNanos: totalInputt,
SpendAmountNanos: spendAmountt,
ChangeAmountNanos: changeAmountt,
FeeNanos: feeNanoss,
FeeRateNanosPerKB: feeNanoss * 1000 / uint64(len(txnBytes)),
SenderPublicKeyBase58Check: lib.PkToString(senderPub.SerializeCompressed(), fes.Params),
RecipientPublicKeyBase58Check: lib.PkToString(recipientPubBytes, fes.Params),
}
if err := json.NewEncoder(ww).Encode(res); err != nil {
APIAddError(ww, fmt.Sprintf("APITransferDeSo: Problem encoding response as JSON: %v", err))
return
}
}
// APITransactionInfoRequest specifies the params for a call to the
// APITransactionInfo endpoint.
type APITransactionInfoRequest struct {
// When set to true, the response simply contains all transactions in the
// mempool with no filtering.
IsMempool bool
// A string that uniquely identifies this transaction. E.g. from a previous
// call to “transfer-deso”. Ignored when PublicKeyBase58Check is set.
TransactionIDBase58Check string
// An DeSo public key encoded using base58 check encoding (starts
// with “BC”) to get transaction IDs for. When set,
// TransactionIDBase58Check is ignored.
PublicKeyBase58Check string
// Only return transaction IDs
IDsOnly bool
// Offset from which a page should be fetched
LastTransactionIDBase58Check string
// The last index of a transaction for a public key seen. If less than 0, it means we are not looking at
// transactions in the database yet.
LastPublicKeyTransactionIndex int64
// Number of transactions to be returned
Limit uint64
}
// APITransactionInfoResponse specifies the response for a call to the
// APITransactionInfo endpoint.
type APITransactionInfoResponse struct {
// Blank if successful. Otherwise, contains a description of the
// error that occurred.
Error string
// The info for all transactions this public key is associated with from oldest
// to newest.
Transactions []*TransactionResponse
// The hash of the last transaction
LastTransactionIDBase58Check string
// The last index of a transaction for a public key seen.
LastPublicKeyTransactionIndex int64
BalanceNanos uint64
}
// APITransactionInfo allows one to get information about a particular transaction
// given its transaction ID (in base58check encoding) OR using a public key.
//
// If one has a TransactionIDBase58Check, e.g. from calling the
// “transfer-deso” endpoint, one can get the corresponding human-readable
// “TransactionInfo” by passing this transaction ID to a node. Note that
// DeSo nodes do not maintain a transaction index by default, so this
// endpoint will error if either --txindex is not passed when starting the node
// OR if the index is not yet up-to-date.
//
// If one has a PublicKeyBase58Check (starts with “BC”), one can get all of the
// TransactionIDs associated with that public key sorted by oldest to newest
// (this will include transactions where the address is a sender and a
// receiver). One can optionally get the full TransactionInfos for all of the
// transactions in the same call. Note that DeSo nodes do not maintain a
// transaction index by default, so this endpoint will error if either
// --txindex is not passed when starting the node OR if the index is not yet
// up-to-date.
func (fes *APIServer) APITransactionInfo(ww http.ResponseWriter, rr *http.Request) {
// If the --txindex flag hasn't been passed to the node, return an error outright.
if fes.TXIndex == nil {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: This function cannot be "+
"called without passing --txindex to the node on startup."))
return
}
// Decode the request
decoder := json.NewDecoder(io.LimitReader(rr.Body, MaxRequestBodySizeBytes))
transactionInfoRequest := APITransactionInfoRequest{}
if err := decoder.Decode(&transactionInfoRequest); err != nil {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: Problem parsing request body: %v", err))
return
}
var lastTxHash *lib.BlockHash
lastTxSeen := false
lastTxID := transactionInfoRequest.LastTransactionIDBase58Check
if lastTxID == "" {
lastTxSeen = true
} else {
txIDBytes, _, err := lib.Base58CheckDecode(lastTxID)
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: Error decoding last tx id (%v): %v", lastTxID, err))
return
}
lastTxHash = &lib.BlockHash{}
copy(lastTxHash[:], txIDBytes)
}
limit := transactionInfoRequest.Limit
if limit <= 0 {
// Legacy support for unpaginated requests
limit = 1000
}
utxoView, err := fes.backendServer.GetMempool().GetAugmentedUniversalView()
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: Problem fetching utxoView: %v", err))
return
}
// IsMempool means we should just return all of the transactions that are currently in the mempool.
if transactionInfoRequest.IsMempool {
// Get all the txns from the mempool.
poolTxns, _, err := fes.mempool.GetTransactionsOrderedByTimeAdded()
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: Error getting txns from mempool: %v", err))
return
}
res := &APITransactionInfoResponse{}
res.Transactions = []*TransactionResponse{}
for _, poolTx := range poolTxns {
// If we haven't seen the last transaction of the previous page, skip ahead until we find it.
if !lastTxSeen {
if reflect.DeepEqual(poolTx.Hash, lastTxHash) {
lastTxSeen = true
continue
}
}
if transactionInfoRequest.IDsOnly {
res.Transactions = append(res.Transactions,
&TransactionResponse{TransactionIDBase58Check: lib.PkToString(poolTx.Tx.Hash()[:], fes.Params)})
} else {
res.Transactions = append(res.Transactions, APITransactionToResponse(poolTx.Tx, poolTx.TxMeta, utxoView, fes.Params))
}
// If we've filled up the page, exit.
if uint64(len(res.Transactions)) == limit {
break
}
}
// Set the last transaction seen.
if len(res.Transactions) > 0 {
res.LastTransactionIDBase58Check = res.Transactions[len(res.Transactions)-1].TransactionIDBase58Check
}
// At this point, all the transactions should have been added to the request.
if err := json.NewEncoder(ww).Encode(res); err != nil {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: Problem encoding response as JSON: %v", err))
return
}
return
}
// If no public key is set, we're doing a simple transaction lookup using
// the passed-in TransactionIDBase58Check.
//
// Note: we do not apply pagination here as we are looking up a single value.
if transactionInfoRequest.PublicKeyBase58Check == "" {
// Parse the passed-in txID
txIDBytes, _, err := lib.Base58CheckDecode(transactionInfoRequest.TransactionIDBase58Check)
if err != nil {
// If not base58 try hex decode
txIDBytes, err = hex.DecodeString(transactionInfoRequest.TransactionIDBase58Check)
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: Problem parsing TransactionID: %v", err))
return
}
}
if len(txIDBytes) != 32 {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: TransactionID byte length is %d but should be 32", len(txIDBytes)))
return
}
txID := &lib.BlockHash{}
copy(txID[:], txIDBytes)
// Use the txID to lookup the requested transaction.
txn, txnMeta := lib.DbGetTxindexFullTransactionByTxID(fes.TXIndex.TXIndexChain.DB(), nil, fes.blockchain.DB(), txID)
if txn == nil {
// Try to look the transaction up in the mempool before giving up.
txnInPool := fes.mempool.GetTransaction(txID)
if txnInPool == nil {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: Could not find transaction with TransactionIDBase58Check = %s",
transactionInfoRequest.TransactionIDBase58Check))
return
}
txn = txnInPool.Tx
txnMeta = txnInPool.TxMeta
}
res := &APITransactionInfoResponse{}
res.Transactions = []*TransactionResponse{
APITransactionToResponse(txn, txnMeta, utxoView, fes.Params),
}
if err := json.NewEncoder(ww).Encode(res); err != nil {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: Problem encoding response as JSON: %v", err))
return
}
return
}
// At this point, we know we're looking up all the transactions for a particular public key
// Parse the public key
publicKeyBytes, _, err := lib.Base58CheckDecode(transactionInfoRequest.PublicKeyBase58Check)
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: Problem parsing PublicKeyBase58Check: %v", err))
return
}
totalBalanceNanos, err := utxoView.GetDeSoBalanceNanosForPublicKey(publicKeyBytes)
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: Problem getting utxos from view: %v", err))
return
}
res := &APITransactionInfoResponse{
BalanceNanos: totalBalanceNanos,
}
res.Transactions = []*TransactionResponse{}
validForPrefix := lib.DbTxindexPublicKeyPrefix(publicKeyBytes)
// If FetchStartIndex is specified then the startPrefix is the public key with FetchStartIndex appended.
// Otherwise, we leave off the index so that the seek will start from the end of the transaction list.
startPrefix := lib.DbTxindexPublicKeyPrefix(publicKeyBytes)
if transactionInfoRequest.LastPublicKeyTransactionIndex > 0 {
startPrefix = lib.DbTxindexPublicKeyIndexToTxnKey(publicKeyBytes, uint32(transactionInfoRequest.LastPublicKeyTransactionIndex))
}
// The maximum key length is the length of the DB key constructed from the public key plus the size of the uint64 appended to it.
maxKeyLen := len(lib.DbTxindexPublicKeyIndexToTxnKey(publicKeyBytes, uint32(0)))
keysFound, valsFound, err := lib.DBGetPaginatedKeysAndValuesForPrefix(
fes.TXIndex.TXIndexChain.DB(), startPrefix, validForPrefix,
maxKeyLen, int(limit), true, true)
if err != nil {
APIAddError(ww, fmt.Sprintf("APITransactionInfo: Error fetching paginated txns: %v", err))
return
}