Browse Source

initial release

Pierluigi Maori 1 year ago
commit
9921429e48
100 changed files with 14036 additions and 0 deletions
  1. 21
    0
      LICENSE
  2. 54
    0
      Makefile
  3. 157
    0
      README.md
  4. 921
    0
      api/bitsharesapi.go
  5. 142
    0
      api/provider.go
  6. 94
    0
      api/rpcclient.go
  7. 349
    0
      api/tester.go
  8. 139
    0
      api/types.go
  9. 268
    0
      api/wallet.go
  10. 324
    0
      api/wsclient.go
  11. 102
    0
      config/chaincconfig.go
  12. 144
    0
      crypto/keybag.go
  13. 102
    0
      crypto/transactionsigner.go
  14. 22
    0
      gen/data/data.go
  15. 328
    0
      gen/generator.go
  16. 95
    0
      gen/opdatastore.go
  17. 93
    0
      gen/samples/accountcreateoperation.go
  18. 88
    0
      gen/samples/accountupdateoperation.go
  19. 26
    0
      gen/samples/accountupgradeoperation.go
  20. 27
    0
      gen/samples/accountwhitelistoperation.go
  21. 29
    0
      gen/samples/assetclaimfeesoperation.go
  22. 60
    0
      gen/samples/assetcreateoperation.go
  23. 27
    0
      gen/samples/assetfundfeepooloperation.go
  24. 36
    0
      gen/samples/assetglobalsettleoperation.go
  25. 36
    0
      gen/samples/assetissueoperation.go
  26. 50
    0
      gen/samples/assetpublishfeedoperation.go
  27. 29
    0
      gen/samples/assetreserveoperation.go
  28. 29
    0
      gen/samples/assetsettleoperation.go
  29. 35
    0
      gen/samples/assetupdatebitassetoperation.go
  30. 29
    0
      gen/samples/assetupdatefeedproducersoperation.go
  31. 50
    0
      gen/samples/assetupdateoperation.go
  32. 30
    0
      gen/samples/balanceclaimoperation.go
  33. 33
    0
      gen/samples/bidcolatteraloperation.go
  34. 155
    0
      gen/samples/blindtransferoperation.go
  35. 33
    0
      gen/samples/callorderupdateoperation.go
  36. 25
    0
      gen/samples/committeemembercreateoperation.go
  37. 27
    0
      gen/samples/customoperation.go
  38. 44
    0
      gen/samples/fillorderoperation.go
  39. 26
    0
      gen/samples/limitordercanceloperation.go
  40. 35
    0
      gen/samples/limitordercreateoperation.go
  41. 37
    0
      gen/samples/overridetransferoperation.go
  42. 442
    0
      gen/samples/proposalcreateoperation.go
  43. 27
    0
      gen/samples/proposaldeleteoperation.go
  44. 60
    0
      gen/samples/proposalupdateoperation.go
  45. 40
    0
      gen/samples/transferfromblindoperation.go
  46. 36
    0
      gen/samples/transferoperation.go
  47. 91
    0
      gen/samples/transfertoblindoperation.go
  48. 37
    0
      gen/samples/vestingbalancecreateoperation.go
  49. 29
    0
      gen/samples/vestingbalancewithdrawoperation.go
  50. 32
    0
      gen/samples/withdrawpermissioncreateoperation.go
  51. 26
    0
      gen/samples/withdrawpermissiondeleteoperation.go
  52. 26
    0
      gen/samples/witnesscreateoperation.go
  53. 27
    0
      gen/samples/witnessupdateoperation.go
  54. 35
    0
      gen/samples/workercreateoperation.go
  55. 18
    0
      gen/templates/opsampledata.go.tmpl
  56. 84
    0
      operations/accountcreateoperation.go
  57. 600
    0
      operations/accountcreateoperation_ffjson.go
  58. 25
    0
      operations/accountcreateoperation_test.go
  59. 79
    0
      operations/accountupdateoperation.go
  60. 468
    0
      operations/accountupdateoperation_ffjson.go
  61. 25
    0
      operations/accountupdateoperation_test.go
  62. 60
    0
      operations/accountupgradeoperation.go
  63. 435
    0
      operations/accountupgradeoperation_ffjson.go
  64. 27
    0
      operations/accountupgradeoperation_test.go
  65. 65
    0
      operations/accountwhitelistoperation.go
  66. 475
    0
      operations/accountwhitelistoperation_ffjson.go
  67. 27
    0
      operations/accountwhitelistoperation_test.go
  68. 81
    0
      operations/assetcreateoperation.go
  69. 627
    0
      operations/assetcreateoperation_ffjson.go
  70. 27
    0
      operations/assetcreateoperation_test.go
  71. 64
    0
      operations/assetfundfeepooloperation.go
  72. 472
    0
      operations/assetfundfeepooloperation_ffjson.go
  73. 27
    0
      operations/assetfundfeepooloperation_test.go
  74. 73
    0
      operations/assetissueoperation.go
  75. 525
    0
      operations/assetissueoperation_ffjson.go
  76. 27
    0
      operations/assetissueoperation_test.go
  77. 64
    0
      operations/assetpublishfeedoperation.go
  78. 474
    0
      operations/assetpublishfeedoperation_ffjson.go
  79. 27
    0
      operations/assetpublishfeedoperation_test.go
  80. 59
    0
      operations/assetreserveoperation.go
  81. 420
    0
      operations/assetreserveoperation_ffjson.go
  82. 27
    0
      operations/assetreserveoperation_test.go
  83. 59
    0
      operations/assetsettleoperation.go
  84. 417
    0
      operations/assetsettleoperation_ffjson.go
  85. 27
    0
      operations/assetsettleoperation_test.go
  86. 65
    0
      operations/assetupdatebitassetoperation.go
  87. 477
    0
      operations/assetupdatebitassetoperation_ffjson.go
  88. 27
    0
      operations/assetupdatebitassetoperation_test.go
  89. 64
    0
      operations/assetupdatefeedproduceroperation.go
  90. 547
    0
      operations/assetupdatefeedproduceroperation_ffjson.go
  91. 27
    0
      operations/assetupdatefeedproduceroperation_test.go
  92. 73
    0
      operations/assetupdateoperation.go
  93. 542
    0
      operations/assetupdateoperation_ffjson.go
  94. 27
    0
      operations/assetupdateoperation_test.go
  95. 62
    0
      operations/balanceclaimoperation.go
  96. 425
    0
      operations/balanceclaimoperation_ffjson.go
  97. 25
    0
      operations/balanceclaimoperation_test.go
  98. 64
    0
      operations/bidcolatteraloperation.go
  99. 467
    0
      operations/bidcolatteraloperation_ffjson.go
  100. 0
    0
      operations/bidcolatteraloperation_test.go

+ 21
- 0
LICENSE View File

@@ -0,0 +1,21 @@
1
+MIT License
2
+
3
+Copyright (c) 2017 denkhaus
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy
6
+of this software and associated documentation files (the "Software"), to deal
7
+in the Software without restriction, including without limitation the rights
8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+copies of the Software, and to permit persons to whom the Software is
10
+furnished to do so, subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in all
13
+copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+SOFTWARE.

+ 54
- 0
Makefile View File

@@ -0,0 +1,54 @@
1
+
2
+clean_ffjson_base: 
3
+	@rm -rf types/ffjson-inception* ||:
4
+	@rm -f types/*_ffjson_expose.go ||:
5
+	@rm -rf operations/ffjson-inception* ||:
6
+	@rm -f operations/*_ffjson_expose.go ||:
7
+
8
+clean_ffjson_gen:
9
+	@rm -f types/*_ffjson.go ||: 
10
+	@rm -rf operations/*_ffjson.go ||: 
11
+
12
+generate: clean_ffjson_base	
13
+	-go generate ./...
14
+
15
+generate_new: clean_ffjson_base clean_ffjson_gen		
16
+	-go generate ./...
17
+
18
+init: 
19
+	@echo "######################## -> install dependencies"
20
+	@go get -u golang.org/x/tools/cmd/stringer
21
+	@go get -u github.com/mitchellh/reflectwalk
22
+	@go get -u github.com/stretchr/objx
23
+	@go get -u github.com/cespare/reflex
24
+	@go get -u github.com/bradhe/stopwatch
25
+
26
+test_api: 
27
+	go test -v ./tests -run ^TestCommon$
28
+	go test -v ./tests -run ^TestSubscribe$
29
+	go test -v ./tests -run ^TestWalletAPI$
30
+	go test -v ./tests -run ^TestWebsocketAPI$
31
+	go test -v ./types 
32
+
33
+test_operations:
34
+	go test -v ./operations -run ^TestOperations$
35
+
36
+test_blocks:
37
+	@echo "this is a long running test, abort with Ctrl + C"
38
+	go test -v ./tests -timeout 10m -run ^TestBlockRange$
39
+
40
+buildgen:
41
+	@echo "build btsgen"
42
+	@go get -u -d ./gen 
43
+	@go build -o /tmp/btsgen ./gen 
44
+	@cp /tmp/btsgen $(GOPATH)/bin
45
+
46
+opsamples: buildgen
47
+	@echo "exec btsgen"
48
+	@cd gen && btsgen
49
+
50
+build: generate
51
+	go build -o /tmp/go-tmpbuild ./operations 
52
+
53
+watch:
54
+	reflex -g 'operations/*.go' make test_operations

+ 157
- 0
README.md View File

@@ -0,0 +1,157 @@
1
+# bitshares
2
+
3
+A Bitshares API consuming a websocket connection to an active full node. If you need wallet features, specify an optional RPC connection to your local `cli_wallet`. 
4
+Look for several examples in tests. This is work in progress and may have breaking changes. 
5
+No additional cgo dependencies for transaction signing required. 
6
+Use it at your own risk. 
7
+
8
+Freezed version for prosume-ecp
9
+
10
+## install
11
+
12
+```
13
+go get -u github.com/denkhaus/bitshares
14
+```
15
+
16
+Install dev-dependencies with
17
+```
18
+make init
19
+```
20
+
21
+This API uses [ffjson](https://github.com/pquerna/ffjson). 
22
+If you change this code you have to regenerate the required static `MarshalJSON` and `UnmarshalJSON` functions for all API-structures with
23
+
24
+```
25
+make generate
26
+```
27
+
28
+## testing
29
+
30
+To test this stuff I use a combined Docker based MainNet/TestNet wallet, you can find [here](https://github.com/denkhaus/bitshares-docker).
31
+Operations testing uses generated real blockchain sample code by gen package. To test run:
32
+
33
+```
34
+make test_operations
35
+make test_api
36
+```
37
+
38
+or a long running block (deserialize/serialize/compare) range test.
39
+
40
+```
41
+make test_blocks
42
+```
43
+
44
+## code
45
+
46
+```
47
+rpcApiUrl    := "http://localhost:8095" 
48
+wsFullApiUrl := "wss://bitshares.openledger.info/ws"
49
+
50
+//Note: The RPC endpoint is optional. If you do not need wallet functions
51
+//pass an empty string as second parameter.
52
+
53
+api := api.New(wsFullApiUrl, rpcApiUrl)
54
+if err := api.Connect(); err != nil {
55
+	log.Fatal(err)
56
+}
57
+
58
+api.OnError(func(err error) {
59
+	log.Fatal(err)
60
+})
61
+
62
+UserID   := types.NewGrapheneID("1.2.253") 
63
+AssetBTS := types.NewGrapheneID("1.3.0") 
64
+
65
+res, api.GetAccountBalances(UserID, AssetBTS)
66
+if err != nil {
67
+	log.Fatal(err)
68
+}
69
+
70
+log.Printf("balances: %v", res)
71
+
72
+```
73
+
74
+For a long application lifecycle, you can use an API instance with latency tester that connects to the most reliable node.
75
+Note: Because the tester takes time to unleash its magic, use the above-mentioned constructor for quick in and out.
76
+
77
+```
78
+rpcApiUrl    := "http://localhost:8095" 
79
+wsFullApiUrl := "wss://bitshares.openledger.info/ws"
80
+
81
+//Note: The RPC endpoint is optional. If you do not need wallet functions
82
+//pass an empty string as second parameter.
83
+
84
+//wsFullApiUrl serves as "quick startup" fallback endpoint here, until the latency tester provides the first results.
85
+api, err := api.NewWithAutoEndpoint(wsFullApiUrl, rpcApiUrl)
86
+if err != nil {
87
+	log.Fatal(err)
88
+}
89
+
90
+if err := api.Connect(); err != nil {
91
+	log.Fatal(err)
92
+}
93
+
94
+api.OnError(func(err error) {
95
+	log.Fatal(err)
96
+})
97
+
98
+...
99
+
100
+```
101
+
102
+## implemented and tested (serialize/unserialize) operations
103
+
104
+- [x] OperationTypeTransfer OperationType
105
+- [x] OperationTypeLimitOrderCreate
106
+- [x] OperationTypeLimitOrderCancel
107
+- [x] OperationTypeCallOrderUpdate
108
+- [x] OperationTypeFillOrder (test failing)
109
+- [x] OperationTypeAccountCreate
110
+- [x] OperationTypeAccountUpdate
111
+- [x] OperationTypeAccountWhitelist
112
+- [x] OperationTypeAccountUpgrade
113
+- [ ] OperationTypeAccountTransfer 
114
+- [x] OperationTypeAssetCreate
115
+- [x] OperationTypeAssetUpdate
116
+- [x] OperationTypeAssetUpdateBitasset
117
+- [x] OperationTypeAssetUpdateFeedProducers
118
+- [x] OperationTypeAssetIssue
119
+- [x] OperationTypeAssetReserve
120
+- [x] OperationTypeAssetFundFeePool
121
+- [x] OperationTypeAssetSettle
122
+- [ ] OperationTypeAssetGlobalSettle 
123
+- [x] OperationTypeAssetPublishFeed
124
+- [x] OperationTypeWitnessCreate
125
+- [x] OperationTypeWitnessUpdate
126
+- [x] OperationTypeProposalCreate
127
+- [x] OperationTypeProposalUpdate
128
+- [x] OperationTypeProposalDelete
129
+- [ ] OperationTypeWithdrawPermissionCreate              
130
+- [ ] OperationTypeWithdrawPermissionUpdate              
131
+- [ ] OperationTypeWithdrawPermissionClaim               
132
+- [ ] OperationTypeWithdrawPermissionDelete              
133
+- [ ] OperationTypeCommitteeMemberCreate                 
134
+- [ ] OperationTypeCommitteeMemberUpdate                 
135
+- [ ] OperationTypeCommitteeMemberUpdateGlobalParameters 
136
+- [x] OperationTypeVestingBalanceCreate
137
+- [x] OperationTypeVestingBalanceWithdraw
138
+- [x] OperationTypeWorkerCreate
139
+- [ ] OperationTypeCustom 
140
+- [ ] OperationTypeAssert 
141
+- [x] OperationTypeBalanceClaim
142
+- [x] OperationTypeOverrideTransfer
143
+- [ ] OperationTypeTransferToBlind   
144
+- [ ] OperationTypeBlindTransfer     
145
+- [ ] OperationTypeTransferFromBlind 
146
+- [ ] OperationTypeAssetSettleCancel 
147
+- [ ] OperationTypeAssetClaimFees    
148
+- [ ] OperationTypeFBADistribute     
149
+- [x] OperationTypeBidColatteral
150
+- [ ] OperationTypeExecuteBid 
151
+
152
+## todo
153
+- add missing operations
154
+- add convenience functions 
155
+
156
+
157
+Have fun and feel free to contribute needed operations and tests.

+ 921
- 0
api/bitsharesapi.go View File

@@ -0,0 +1,921 @@
1
+package api
2
+
3
+import (
4
+	"time"
5
+
6
+	"eprosume/bitshares/config"
7
+	"eprosume/bitshares/crypto"
8
+	"eprosume/bitshares/types"
9
+	"eprosume/bitshares/util"
10
+	"github.com/denkhaus/logging"
11
+	"github.com/juju/errors"
12
+	"github.com/pquerna/ffjson/ffjson"
13
+)
14
+
15
+const (
16
+	InvalidApiID           = -1
17
+	AssetsListAll          = -1
18
+	AssetsMaxBatchSize     = 100
19
+	GetCallOrdersLimit     = 100
20
+	GetLimitOrdersLimit    = 100
21
+	GetSettleOrdersLimit   = 100
22
+	GetTradeHistoryLimit   = 100
23
+	GetAccountHistoryLimit = 100
24
+)
25
+
26
+type BitsharesAPI interface {
27
+	//Common functions
28
+	CallWsAPI(apiID int, method string, args ...interface{}) (interface{}, error)
29
+	Close() error
30
+	Connect() error
31
+	DatabaseAPIID() int
32
+	HistoryAPIID() int
33
+	BroadcastAPIID() int
34
+	SetCredentials(username, password string)
35
+	OnError(ErrorFunc)
36
+	OnNotify(subscriberID int, notifyFn func(msg interface{}) error) error
37
+	BuildSignedTransaction(keyBag *crypto.KeyBag, feeAsset types.GrapheneObject, ops ...types.Operation) (*types.SignedTransaction, error)
38
+	VerifySignedTransaction(keyBag *crypto.KeyBag, tx *types.SignedTransaction) (bool, error)
39
+	SignTransaction(keyBag *crypto.KeyBag, trx *types.SignedTransaction) error
40
+	SignWithKeys(types.PrivateKeys, *types.SignedTransaction) error
41
+
42
+	//Websocket API functions
43
+	BroadcastTransaction(tx *types.SignedTransaction) error
44
+	CancelAllSubscriptions() error
45
+	CancelOrder(orderID types.GrapheneObject, broadcast bool) (*types.SignedTransaction, error)
46
+	GetAccountBalances(account types.GrapheneObject, assets ...types.GrapheneObject) (types.AssetAmounts, error)
47
+	GetAccountByName(name string) (*types.Account, error)
48
+	GetAccountHistory(account types.GrapheneObject, stop types.GrapheneObject, limit int, start types.GrapheneObject) (types.OperationHistories, error)
49
+	GetAccounts(accountIDs ...types.GrapheneObject) (types.Accounts, error)
50
+	GetFullAccounts(accountIDs ...types.GrapheneObject) (types.FullAccountInfos, error)
51
+	GetBlock(number uint64) (*types.Block, error)
52
+	GetCallOrders(assetID types.GrapheneObject, limit int) (types.CallOrders, error)
53
+	GetChainID() (string, error)
54
+	GetDynamicGlobalProperties() (*types.DynamicGlobalProperties, error)
55
+	GetLimitOrders(base, quote types.GrapheneObject, limit int) (types.LimitOrders, error)
56
+	GetOrderBook(base, quote types.GrapheneObject, depth int) (types.OrderBook, error)
57
+	GetMarginPositions(accountID types.GrapheneObject) (types.CallOrders, error)
58
+	GetObjects(objectIDs ...types.GrapheneObject) ([]interface{}, error)
59
+	GetPotentialSignatures(tx *types.SignedTransaction) (types.PublicKeys, error)
60
+	GetRequiredSignatures(tx *types.SignedTransaction, keys types.PublicKeys) (types.PublicKeys, error)
61
+	GetRequiredFees(ops types.Operations, feeAsset types.GrapheneObject) (types.AssetAmounts, error)
62
+	GetSettleOrders(assetID types.GrapheneObject, limit int) (types.SettleOrders, error)
63
+	GetTradeHistory(base, quote types.GrapheneObject, toTime, fromTime time.Time, limit int) (types.MarketTrades, error)
64
+	ListAssets(lowerBoundSymbol string, limit int) (types.Assets, error)
65
+	SetSubscribeCallback(notifyID int, clearFilter bool) error
66
+	SubscribeToMarket(notifyID int, base types.GrapheneObject, quote types.GrapheneObject) error
67
+	UnsubscribeFromMarket(base types.GrapheneObject, quote types.GrapheneObject) error
68
+	Get24Volume(base types.GrapheneObject, quote types.GrapheneObject) (types.Volume24, error)
69
+
70
+	//Wallet API functions
71
+	WalletListAccountBalances(account types.GrapheneObject) (types.AssetAmounts, error)
72
+	WalletLock() error
73
+	WalletUnlock(password string) error
74
+	WalletIsLocked() (bool, error)
75
+	WalletBorrowAsset(account types.GrapheneObject, amountToBorrow string, symbolToBorrow types.GrapheneObject, amountOfCollateral string, broadcast bool) (*types.SignedTransaction, error)
76
+	WalletBuy(account types.GrapheneObject, base, quote types.GrapheneObject, rate string, amount string, broadcast bool) (*types.SignedTransaction, error)
77
+	WalletBuyEx(account types.GrapheneObject, base, quote types.GrapheneObject, rate float64, amount float64, broadcast bool) (*types.SignedTransaction, error)
78
+	WalletSell(account types.GrapheneObject, base, quote types.GrapheneObject, rate string, amount string, broadcast bool) (*types.SignedTransaction, error)
79
+	WalletSellEx(account types.GrapheneObject, base, quote types.GrapheneObject, rate float64, amount float64, broadcast bool) (*types.SignedTransaction, error)
80
+	WalletSellAsset(account types.GrapheneObject, amountToSell string, symbolToSell types.GrapheneObject, minToReceive string, symbolToReceive types.GrapheneObject, timeout uint32, fillOrKill bool, broadcast bool) (*types.SignedTransaction, error)
81
+	WalletSignTransaction(tx *types.SignedTransaction, broadcast bool) (*types.SignedTransaction, error)
82
+	WalletSerializeTransaction(tx *types.SignedTransaction) (string, error)
83
+}
84
+
85
+type bitsharesAPI struct {
86
+	wsClient       ClientProvider
87
+	rpcClient      RPCClient
88
+	username       string
89
+	password       string
90
+	databaseAPIID  int
91
+	historyAPIID   int
92
+	broadcastAPIID int
93
+}
94
+
95
+func (p *bitsharesAPI) getAPIID(identifier string) (int, error) {
96
+	resp, err := p.wsClient.CallAPI(1, identifier, types.EmptyParams)
97
+	if err != nil {
98
+		return InvalidApiID, errors.Annotate(err, identifier)
99
+	}
100
+
101
+	logging.DDumpJSON("getApiID <", resp)
102
+
103
+	return int(resp.(float64)), nil
104
+}
105
+
106
+// login
107
+func (p *bitsharesAPI) login() (bool, error) {
108
+	resp, err := p.wsClient.CallAPI(1, "login", p.username, p.password)
109
+	if err != nil {
110
+		return false, err
111
+	}
112
+
113
+	logging.DDumpJSON("login <", resp)
114
+
115
+	return resp.(bool), nil
116
+}
117
+
118
+// SetSubscribeCallback
119
+func (p *bitsharesAPI) SetSubscribeCallback(notifyID int, clearFilter bool) error {
120
+	// returns nil if successfull
121
+	_, err := p.wsClient.CallAPI(p.databaseAPIID, "set_subscribe_callback", notifyID, clearFilter)
122
+	if err != nil {
123
+		return err
124
+	}
125
+
126
+	return nil
127
+}
128
+
129
+// SubscribeToMarket
130
+func (p *bitsharesAPI) SubscribeToMarket(notifyID int, base types.GrapheneObject, quote types.GrapheneObject) error {
131
+	// returns nil if successfull
132
+	_, err := p.wsClient.CallAPI(p.databaseAPIID, "subscribe_to_market", notifyID, base.ID(), quote.ID())
133
+	if err != nil {
134
+		return err
135
+	}
136
+
137
+	return nil
138
+}
139
+
140
+// UnsubscribeFromMarket
141
+func (p *bitsharesAPI) UnsubscribeFromMarket(base types.GrapheneObject, quote types.GrapheneObject) error {
142
+	// returns nil if successfull
143
+	_, err := p.wsClient.CallAPI(p.databaseAPIID, "unsubscribe_from_market", base.ID(), quote.ID())
144
+	if err != nil {
145
+		return err
146
+	}
147
+
148
+	return nil
149
+}
150
+
151
+// CancelAllSubscriptions
152
+func (p *bitsharesAPI) CancelAllSubscriptions() error {
153
+	// returns nil
154
+	_, err := p.wsClient.CallAPI(p.databaseAPIID, "cancel_all_subscriptions", types.EmptyParams)
155
+	if err != nil {
156
+		return err
157
+	}
158
+
159
+	return nil
160
+}
161
+
162
+//Broadcast a transaction to the network.
163
+//The transaction will be checked for validity prior to broadcasting.
164
+//If it fails to apply at the connected node, an error will be thrown and the transaction will not be broadcast.
165
+func (p *bitsharesAPI) BroadcastTransaction(tx *types.SignedTransaction) error {
166
+	_, err := p.wsClient.CallAPI(p.broadcastAPIID, "broadcast_transaction", tx)
167
+	if err != nil {
168
+		return err
169
+	}
170
+
171
+	return nil
172
+}
173
+
174
+//SignTransaction signs a given transaction.
175
+//Required signing keys get selected by API and have to be in keyBag.
176
+func (p *bitsharesAPI) SignTransaction(keyBag *crypto.KeyBag, tx *types.SignedTransaction) error {
177
+	reqPk, err := p.RequiredSigningKeys(tx)
178
+	if err != nil {
179
+		return errors.Annotate(err, "RequiredSigningKeys")
180
+	}
181
+
182
+	signer := crypto.NewTransactionSigner(tx)
183
+
184
+	privKeys := keyBag.PrivatesByPublics(reqPk)
185
+	if len(privKeys) == 0 {
186
+		return types.ErrNoSigningKeyFound
187
+	}
188
+
189
+	if err := signer.Sign(privKeys, config.CurrentConfig()); err != nil {
190
+		return errors.Annotate(err, "Sign")
191
+	}
192
+
193
+	return nil
194
+}
195
+
196
+//SignWithKeys signs a given transaction with given private keys.
197
+func (p *bitsharesAPI) SignWithKeys(keys types.PrivateKeys, tx *types.SignedTransaction) error {
198
+	signer := crypto.NewTransactionSigner(tx)
199
+	if err := signer.Sign(keys, config.CurrentConfig()); err != nil {
200
+		return errors.Annotate(err, "Sign")
201
+	}
202
+
203
+	return nil
204
+}
205
+
206
+//VerifySignedTransaction verifies a signed transaction against all available keys in keyBag.
207
+//If all required key are found the function returns true, otherwise false.
208
+func (p *bitsharesAPI) VerifySignedTransaction(keyBag *crypto.KeyBag, tx *types.SignedTransaction) (bool, error) {
209
+	signer := crypto.NewTransactionSigner(tx)
210
+	verified, err := signer.Verify(keyBag, config.CurrentConfig())
211
+	if err != nil {
212
+		return false, errors.Annotate(err, "Verify")
213
+	}
214
+
215
+	return verified, nil
216
+}
217
+
218
+//BuildSignedTransaction builds a new transaction by given operation(s),
219
+//applies fees, current block data and signs the transaction.
220
+func (p *bitsharesAPI) BuildSignedTransaction(keyBag *crypto.KeyBag, feeAsset types.GrapheneObject, ops ...types.Operation) (*types.SignedTransaction, error) {
221
+	operations := types.Operations(ops)
222
+	fees, err := p.GetRequiredFees(operations, feeAsset)
223
+	if err != nil {
224
+		return nil, errors.Annotate(err, "GetRequiredFees")
225
+	}
226
+
227
+	if err := operations.ApplyFees(fees); err != nil {
228
+		return nil, errors.Annotate(err, "ApplyFees")
229
+	}
230
+
231
+	props, err := p.GetDynamicGlobalProperties()
232
+	if err != nil {
233
+		return nil, errors.Annotate(err, "GetDynamicGlobalProperties")
234
+	}
235
+
236
+	tx, err := types.NewSignedTransactionWithBlockData(props)
237
+	if err != nil {
238
+		return nil, errors.Annotate(err, "NewTransaction")
239
+	}
240
+
241
+	tx.Operations = operations
242
+
243
+	reqPk, err := p.RequiredSigningKeys(tx)
244
+	if err != nil {
245
+		return nil, errors.Annotate(err, "RequiredSigningKeys")
246
+	}
247
+
248
+	signer := crypto.NewTransactionSigner(tx)
249
+
250
+	privKeys := keyBag.PrivatesByPublics(reqPk)
251
+	if len(privKeys) == 0 {
252
+		return nil, types.ErrNoSigningKeyFound
253
+	}
254
+
255
+	if err := signer.Sign(privKeys, config.CurrentConfig()); err != nil {
256
+		return nil, errors.Annotate(err, "Sign")
257
+	}
258
+
259
+	return tx, nil
260
+}
261
+
262
+//RequiredSigningKeys is a convenience call to retrieve the minimum subset of public keys to sign a transaction.
263
+//If the transaction is already signed, the result is empty.
264
+func (p *bitsharesAPI) RequiredSigningKeys(tx *types.SignedTransaction) (types.PublicKeys, error) {
265
+	potPk, err := p.GetPotentialSignatures(tx)
266
+	if err != nil {
267
+		return nil, errors.Annotate(err, "GetPotentialSignatures")
268
+	}
269
+
270
+	logging.DDumpJSON("potential pubkeys <", potPk)
271
+
272
+	reqPk, err := p.GetRequiredSignatures(tx, potPk)
273
+	if err != nil {
274
+		return nil, errors.Annotate(err, "GetRequiredSignatures")
275
+	}
276
+
277
+	logging.DDumpJSON("required pubkeys <", reqPk)
278
+
279
+	return reqPk, nil
280
+}
281
+
282
+//GetPotentialSignatures will return the set of all public keys that could possibly sign for a given transaction.
283
+//This call can be used by wallets to filter their set of public keys to just the relevant subset prior to calling
284
+//GetRequiredSignatures to get the minimum subset.
285
+func (p *bitsharesAPI) GetPotentialSignatures(tx *types.SignedTransaction) (types.PublicKeys, error) {
286
+	resp, err := p.wsClient.CallAPI(p.databaseAPIID, "get_potential_signatures", tx)
287
+	if err != nil {
288
+		return nil, err
289
+	}
290
+
291
+	logging.DDumpJSON("get_potential_signatures <", resp)
292
+
293
+	ret := types.PublicKeys{}
294
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
295
+		return nil, errors.Annotate(err, "unmarshal PublicKeys")
296
+	}
297
+
298
+	return ret, nil
299
+}
300
+
301
+//GetRequiredSignatures returns the minimum subset of public keys to sign a transaction.
302
+func (p *bitsharesAPI) GetRequiredSignatures(tx *types.SignedTransaction, potKeys types.PublicKeys) (types.PublicKeys, error) {
303
+	resp, err := p.wsClient.CallAPI(p.databaseAPIID, "get_required_signatures", tx, potKeys)
304
+	if err != nil {
305
+		return nil, err
306
+	}
307
+
308
+	logging.DDumpJSON("get_required_signatures <", resp)
309
+
310
+	ret := types.PublicKeys{}
311
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
312
+		return nil, errors.Annotate(err, "unmarshal PublicKeys")
313
+	}
314
+
315
+	return ret, nil
316
+}
317
+
318
+//GetBlock returns a Block by block number.
319
+func (p *bitsharesAPI) GetBlock(number uint64) (*types.Block, error) {
320
+	resp, err := p.wsClient.CallAPI(0, "get_block", number)
321
+	if err != nil {
322
+		return nil, err
323
+	}
324
+
325
+	logging.DDumpJSON("get_block <", resp)
326
+
327
+	ret := types.Block{}
328
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
329
+		return nil, errors.Annotate(err, "unmarshal Block")
330
+	}
331
+
332
+	return &ret, nil
333
+}
334
+
335
+//GetAccountByName returns a Account object by username
336
+func (p *bitsharesAPI) GetAccountByName(name string) (*types.Account, error) {
337
+	resp, err := p.wsClient.CallAPI(0, "get_account_by_name", name)
338
+	if err != nil {
339
+		return nil, err
340
+	}
341
+
342
+	logging.DDumpJSON("get_account_by_name <", resp)
343
+
344
+	ret := types.Account{}
345
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
346
+		return nil, errors.Annotate(err, "unmarshal Account")
347
+	}
348
+
349
+	return &ret, nil
350
+}
351
+
352
+// GetAccountHistory returns OperationHistory object(s).
353
+// account: The account whose history should be queried
354
+// stop: ID of the earliest operation to retrieve
355
+// limit: Maximum number of operations to retrieve (must not exceed 100)
356
+// start: ID of the most recent operation to retrieve
357
+func (p *bitsharesAPI) GetAccountHistory(account types.GrapheneObject, stop types.GrapheneObject, limit int, start types.GrapheneObject) (types.OperationHistories, error) {
358
+	if limit > GetAccountHistoryLimit {
359
+		limit = GetAccountHistoryLimit
360
+	}
361
+
362
+	resp, err := p.wsClient.CallAPI(p.historyAPIID, "get_account_history", account.ID(), stop.ID(), limit, start.ID())
363
+	if err != nil {
364
+		return nil, err
365
+	}
366
+
367
+	logging.DDumpJSON("get_account_history <", resp)
368
+
369
+	ret := types.OperationHistories{}
370
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
371
+		return nil, errors.Annotate(err, "unmarshal Histories")
372
+	}
373
+
374
+	return ret, nil
375
+}
376
+
377
+//GetAccounts returns a list of accounts by accountID(s).
378
+func (p *bitsharesAPI) GetAccounts(accounts ...types.GrapheneObject) (types.Accounts, error) {
379
+	ids := types.GrapheneObjects(accounts).ToStrings()
380
+	resp, err := p.wsClient.CallAPI(0, "get_accounts", ids)
381
+	if err != nil {
382
+		return nil, err
383
+	}
384
+
385
+	logging.DDumpJSON("get_accounts <", resp)
386
+
387
+	ret := types.Accounts{}
388
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
389
+		return nil, errors.Annotate(err, "unmarshal Accounts")
390
+	}
391
+
392
+	return ret, nil
393
+}
394
+
395
+//GetDynamicGlobalProperties returns essential runtime properties of bitshares network
396
+func (p *bitsharesAPI) GetDynamicGlobalProperties() (*types.DynamicGlobalProperties, error) {
397
+	resp, err := p.wsClient.CallAPI(0, "get_dynamic_global_properties", types.EmptyParams)
398
+	if err != nil {
399
+		return nil, err
400
+	}
401
+
402
+	logging.DDumpJSON("get_dynamic_global_properties <", resp)
403
+
404
+	var ret types.DynamicGlobalProperties
405
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
406
+		return nil, errors.Annotate(err, "unmarshal DynamicGlobalProperties")
407
+	}
408
+
409
+	return &ret, nil
410
+}
411
+
412
+//GetAccountBalances retrieves AssetAmounts by given AccountID
413
+func (p *bitsharesAPI) GetAccountBalances(account types.GrapheneObject, assets ...types.GrapheneObject) (types.AssetAmounts, error) {
414
+
415
+	ids := types.GrapheneObjects(assets).ToStrings()
416
+	resp, err := p.wsClient.CallAPI(0, "get_account_balances", account.ID(), ids)
417
+	if err != nil {
418
+		return nil, err
419
+	}
420
+
421
+	logging.DDumpJSON("get_account_balances <", resp)
422
+
423
+	ret := types.AssetAmounts{}
424
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
425
+		return nil, errors.Annotate(err, "unmarshal AssetAmounts")
426
+	}
427
+
428
+	return ret, nil
429
+}
430
+
431
+//GetFullAccounts retrieves full account information by given AccountIDs
432
+func (p *bitsharesAPI) GetFullAccounts(accounts ...types.GrapheneObject) (types.FullAccountInfos, error) {
433
+	ids := types.GrapheneObjects(accounts).ToStrings()
434
+	resp, err := p.wsClient.CallAPI(0, "get_full_accounts", ids, false) //do not subscribe for now
435
+	if err != nil {
436
+		return nil, err
437
+	}
438
+
439
+	logging.DDumpJSON("get_full_accounts <", resp)
440
+
441
+	ret := types.FullAccountInfos{}
442
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
443
+		return nil, errors.Annotate(err, "unmarshal FullAccountInfos: "+string(util.ToBytes(resp)))
444
+	}
445
+
446
+	return ret, nil
447
+}
448
+
449
+// Get24Volume returns the base:quote assets 24h volume
450
+func (p *bitsharesAPI) Get24Volume(base, quote types.GrapheneObject) (ret types.Volume24, err error) {
451
+	resp, err := p.wsClient.CallAPI(p.databaseAPIID, "get_24_volume", base.ID(), quote.ID())
452
+	if err != nil {
453
+		return
454
+	}
455
+
456
+	logging.DDumpJSON("get_24_volume <", resp)
457
+
458
+	if err = ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
459
+		err = errors.Annotate(err, "unmarshal Volume24")
460
+		return
461
+	}
462
+
463
+	return
464
+}
465
+
466
+//ListAssets retrieves assets
467
+//lowerBoundSymbol: Lower bound of symbol names to retrieve
468
+//limit: Maximum number of assets to fetch, if the constant AssetsListAll is passed, all existing assets will be retrieved.
469
+func (p *bitsharesAPI) ListAssets(lowerBoundSymbol string, limit int) (types.Assets, error) {
470
+	if limit > AssetsMaxBatchSize || limit == AssetsListAll {
471
+		limit = AssetsMaxBatchSize
472
+	}
473
+
474
+	resp, err := p.wsClient.CallAPI(0, "list_assets", lowerBoundSymbol, limit)
475
+	if err != nil {
476
+		return nil, err
477
+	}
478
+
479
+	logging.DDumpJSON("list_assets <", resp)
480
+
481
+	ret := types.Assets{}
482
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
483
+		return nil, errors.Annotate(err, "unmarshal Assets")
484
+	}
485
+
486
+	return ret, nil
487
+}
488
+
489
+//GetRequiredFees calculates the required fee for each operation by the specified asset type.
490
+func (p *bitsharesAPI) GetRequiredFees(ops types.Operations, feeAsset types.GrapheneObject) (types.AssetAmounts, error) {
491
+	resp, err := p.wsClient.CallAPI(0, "get_required_fees", ops.Envelopes(), feeAsset.ID())
492
+	if err != nil {
493
+		return nil, err
494
+	}
495
+
496
+	logging.DDumpJSON("get_required_fees <", resp)
497
+
498
+	ret := types.AssetAmounts{}
499
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
500
+		return nil, errors.Annotate(err, "unmarshal AssetAmounts")
501
+	}
502
+
503
+	return ret, nil
504
+}
505
+
506
+//GetLimitOrders returns LimitOrders type.
507
+func (p *bitsharesAPI) GetLimitOrders(base, quote types.GrapheneObject, limit int) (types.LimitOrders, error) {
508
+	if limit > GetLimitOrdersLimit {
509
+		limit = GetLimitOrdersLimit
510
+	}
511
+
512
+	resp, err := p.wsClient.CallAPI(0, "get_limit_orders", base.ID(), quote.ID(), limit)
513
+	if err != nil {
514
+		return nil, err
515
+	}
516
+
517
+	logging.DDumpJSON("get_limit_orders <", resp)
518
+
519
+	ret := types.LimitOrders{}
520
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
521
+		return nil, errors.Annotate(err, "unmarshal LimitOrders")
522
+	}
523
+
524
+	return ret, nil
525
+}
526
+
527
+//GetOrderBook returns the OrderBook for the market base:quote.
528
+func (p *bitsharesAPI) GetOrderBook(base, quote types.GrapheneObject, depth int) (ret types.OrderBook, err error) {
529
+
530
+	resp, err := p.wsClient.CallAPI(0, "get_order_book", base.ID(), quote.ID(), depth)
531
+	if err != nil {
532
+		return
533
+	}
534
+
535
+	logging.DDumpJSON("get_order_book <", resp)
536
+
537
+	if err = ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
538
+		err = errors.Annotate(err, "unmarshal LimitOrders")
539
+		return
540
+	}
541
+
542
+	return
543
+}
544
+
545
+//GetSettleOrders returns SettleOrders type.
546
+func (p *bitsharesAPI) GetSettleOrders(assetID types.GrapheneObject, limit int) (types.SettleOrders, error) {
547
+	if limit > GetSettleOrdersLimit {
548
+		limit = GetSettleOrdersLimit
549
+	}
550
+
551
+	resp, err := p.wsClient.CallAPI(0, "get_settle_orders", assetID.ID(), limit)
552
+	if err != nil {
553
+		return nil, err
554
+	}
555
+
556
+	logging.DDumpJSON("get_settle_orders <", resp)
557
+
558
+	ret := types.SettleOrders{}
559
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
560
+		return nil, errors.Annotate(err, "unmarshal SettleOrders")
561
+	}
562
+
563
+	return ret, nil
564
+}
565
+
566
+//GetCallOrders returns CallOrders type.
567
+func (p *bitsharesAPI) GetCallOrders(assetID types.GrapheneObject, limit int) (types.CallOrders, error) {
568
+	if limit > GetCallOrdersLimit {
569
+		limit = GetCallOrdersLimit
570
+	}
571
+
572
+	resp, err := p.wsClient.CallAPI(0, "get_call_orders", assetID.ID(), limit)
573
+	if err != nil {
574
+		return nil, err
575
+	}
576
+
577
+	logging.DDumpJSON("get_call_orders <", resp)
578
+
579
+	ret := types.CallOrders{}
580
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
581
+		return nil, errors.Annotate(err, "unmarshal CallOrders")
582
+	}
583
+
584
+	return ret, nil
585
+}
586
+
587
+//GetMarginPositions returns CallOrders type.
588
+func (p *bitsharesAPI) GetMarginPositions(accountID types.GrapheneObject) (types.CallOrders, error) {
589
+	resp, err := p.wsClient.CallAPI(0, "get_margin_positions", accountID.ID())
590
+	if err != nil {
591
+		return nil, err
592
+	}
593
+
594
+	logging.DDumpJSON("get_margin_positions <", resp)
595
+
596
+	ret := types.CallOrders{}
597
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
598
+		return nil, errors.Annotate(err, "unmarshal CallOrders")
599
+	}
600
+
601
+	return ret, nil
602
+}
603
+
604
+//GetTradeHistory returns MarketTrades type.
605
+func (p *bitsharesAPI) GetTradeHistory(base, quote types.GrapheneObject, toTime, fromTime time.Time, limit int) (types.MarketTrades, error) {
606
+	if limit > GetTradeHistoryLimit {
607
+		limit = GetTradeHistoryLimit
608
+	}
609
+
610
+	resp, err := p.wsClient.CallAPI(0, "get_trade_history", base.ID(), quote.ID(), toTime, fromTime, limit)
611
+	if err != nil {
612
+		return nil, err
613
+	}
614
+
615
+	logging.DDumpJSON("get_trade_history <", resp)
616
+
617
+	ret := types.MarketTrades{}
618
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
619
+		return nil, errors.Annotate(err, "unmarshal MarketTrades")
620
+	}
621
+
622
+	return ret, nil
623
+}
624
+
625
+//GetChainID returns the ID of the chain we are connected to.
626
+func (p *bitsharesAPI) GetChainID() (string, error) {
627
+	resp, err := p.wsClient.CallAPI(p.databaseAPIID, "get_chain_id", types.EmptyParams)
628
+	if err != nil {
629
+		return "", err
630
+	}
631
+
632
+	logging.DDumpJSON("get_chain_id <", resp)
633
+
634
+	return resp.(string), nil
635
+}
636
+
637
+//GetObjects returns a list of Graphene Objects by ID.
638
+func (p *bitsharesAPI) GetObjects(ids ...types.GrapheneObject) ([]interface{}, error) {
639
+	params := types.GrapheneObjects(ids).ToStrings()
640
+	resp, err := p.wsClient.CallAPI(0, "get_objects", params)
641
+	if err != nil {
642
+		return nil, err
643
+	}
644
+
645
+	logging.DDumpJSON("get_objects <", resp)
646
+
647
+	data := resp.([]interface{})
648
+	ret := make([]interface{}, 0)
649
+	id := types.GrapheneID{}
650
+
651
+	for _, obj := range data {
652
+		if obj == nil {
653
+			continue
654
+		}
655
+
656
+		if err := id.FromRawData(obj); err != nil {
657
+			return nil, errors.Annotate(err, "from raw data")
658
+		}
659
+
660
+		b := util.ToBytes(obj)
661
+
662
+		//TODO: implement
663
+		// ObjectTypeBase
664
+		// ObjectTypeWitness
665
+		// ObjectTypeCustom
666
+		// ObjectTypeProposal
667
+		// ObjectTypeWithdrawPermission
668
+		// ObjectTypeVestingBalance
669
+		// ObjectTypeWorker
670
+		switch id.Space() {
671
+		case types.SpaceTypeProtocol:
672
+			switch id.Type() {
673
+			case types.ObjectTypeAccount:
674
+				acc := types.Account{}
675
+				if err := ffjson.Unmarshal(b, &acc); err != nil {
676
+					return nil, errors.Annotate(err, "unmarshal Account")
677
+				}
678
+				ret = append(ret, acc)
679
+			case types.ObjectTypeAsset:
680
+				ass := types.Asset{}
681
+				if err := ffjson.Unmarshal(b, &ass); err != nil {
682
+					return nil, errors.Annotate(err, "unmarshal Asset")
683
+				}
684
+				ret = append(ret, ass)
685
+			case types.ObjectTypeForceSettlement:
686
+				set := types.SettleOrder{}
687
+				if err := ffjson.Unmarshal(b, &set); err != nil {
688
+					return nil, errors.Annotate(err, "unmarshal SettleOrder")
689
+				}
690
+				ret = append(ret, set)
691
+			case types.ObjectTypeLimitOrder:
692
+				lim := types.LimitOrder{}
693
+				if err := ffjson.Unmarshal(b, &lim); err != nil {
694
+					return nil, errors.Annotate(err, "unmarshal LimitOrder")
695
+				}
696
+				ret = append(ret, lim)
697
+			case types.ObjectTypeCallOrder:
698
+				cal := types.CallOrder{}
699
+				if err := ffjson.Unmarshal(b, &cal); err != nil {
700
+					return nil, errors.Annotate(err, "unmarshal CallOrder")
701
+				}
702
+				ret = append(ret, cal)
703
+			case types.ObjectTypeCommiteeMember:
704
+				mem := types.CommiteeMember{}
705
+				if err := ffjson.Unmarshal(b, &mem); err != nil {
706
+					return nil, errors.Annotate(err, "unmarshal CommiteeMember")
707
+				}
708
+				ret = append(ret, mem)
709
+			case types.ObjectTypeOperationHistory:
710
+				hist := types.OperationHistory{}
711
+				if err := ffjson.Unmarshal(b, &hist); err != nil {
712
+					return nil, errors.Annotate(err, "unmarshal OperationHistory")
713
+				}
714
+				ret = append(ret, hist)
715
+			case types.ObjectTypeBalance:
716
+				bal := types.Balance{}
717
+				if err := ffjson.Unmarshal(b, &bal); err != nil {
718
+					return nil, errors.Annotate(err, "unmarshal Balance")
719
+				}
720
+				ret = append(ret, bal)
721
+
722
+			default:
723
+				logging.DDumpUnmarshaled(id.Type().String(), b)
724
+				return nil, errors.Errorf("unable to parse GrapheneObject with ID %s", id)
725
+			}
726
+
727
+		case types.SpaceTypeImplementation:
728
+			switch id.Type() {
729
+			case types.ObjectTypeAssetBitAssetData:
730
+				bit := types.BitAssetData{}
731
+				if err := ffjson.Unmarshal(b, &bit); err != nil {
732
+					return nil, errors.Annotate(err, "unmarshal BitAssetData")
733
+				}
734
+				ret = append(ret, bit)
735
+
736
+			default:
737
+				logging.DDumpUnmarshaled(id.Type().String(), b)
738
+				return nil, errors.Errorf("unable to parse GrapheneObject with ID %s", id)
739
+			}
740
+		}
741
+	}
742
+
743
+	return ret, nil
744
+}
745
+
746
+// CancelOrder cancels an order given by orderID
747
+func (p *bitsharesAPI) CancelOrder(orderID types.GrapheneObject, broadcast bool) (*types.SignedTransaction, error) {
748
+	resp, err := p.wsClient.CallAPI(0, "cancel_order", orderID.ID(), broadcast)
749
+	if err != nil {
750
+		return nil, err
751
+	}
752
+
753
+	logging.DDumpJSON("cancel_order <", resp)
754
+
755
+	ret := types.SignedTransaction{}
756
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
757
+		return nil, errors.Annotate(err, "unmarshal Transaction")
758
+	}
759
+
760
+	return &ret, nil
761
+}
762
+
763
+func (p *bitsharesAPI) DatabaseAPIID() int {
764
+	return p.databaseAPIID
765
+}
766
+
767
+func (p *bitsharesAPI) BroadcastAPIID() int {
768
+	return p.broadcastAPIID
769
+}
770
+
771
+func (p *bitsharesAPI) HistoryAPIID() int {
772
+	return p.historyAPIID
773
+}
774
+
775
+//CallWsAPI invokes a websocket API call
776
+func (p *bitsharesAPI) CallWsAPI(apiID int, method string, args ...interface{}) (interface{}, error) {
777
+	return p.wsClient.CallAPI(apiID, method, args...)
778
+}
779
+
780
+//OnError - hook your notify callback here
781
+func (p *bitsharesAPI) OnNotify(subscriberID int, notifyFn func(msg interface{}) error) error {
782
+	return p.wsClient.OnNotify(subscriberID, notifyFn)
783
+}
784
+
785
+//OnError - hook your error callback here
786
+func (p *bitsharesAPI) OnError(errorFn ErrorFunc) {
787
+	p.wsClient.OnError(errorFn)
788
+}
789
+
790
+//SetCredentials defines username and password for Websocket API login.
791
+func (p *bitsharesAPI) SetCredentials(username, password string) {
792
+	p.username = username
793
+	p.password = password
794
+}
795
+
796
+// Connect initializes the API and connects underlying resources
797
+func (p *bitsharesAPI) Connect() error {
798
+	if p.rpcClient != nil {
799
+		if err := p.rpcClient.Connect(); err != nil {
800
+			return errors.Annotate(err, "Connect [rpc]")
801
+		}
802
+	}
803
+
804
+	if p.wsClient != nil {
805
+		if err := p.wsClient.Connect(); err != nil {
806
+			return errors.Annotate(err, "Connect [ws]")
807
+		}
808
+	}
809
+
810
+	if ok, err := p.login(); err != nil || !ok {
811
+		if err != nil {
812
+			return errors.Annotate(err, "login")
813
+		}
814
+		return errors.New("login failed")
815
+	}
816
+
817
+	if err := p.getAPIIDs(); err != nil {
818
+		return errors.Annotate(err, "getApiIDs")
819
+	}
820
+
821
+	chainID, err := p.GetChainID()
822
+	if err != nil {
823
+		return errors.Annotate(err, "GetChainID")
824
+	}
825
+
826
+	if err := config.SetCurrentConfig(chainID); err != nil {
827
+		return errors.Annotate(err, "SetCurrentConfig")
828
+	}
829
+
830
+	return nil
831
+}
832
+
833
+func (p *bitsharesAPI) getAPIIDs() (err error) {
834
+	p.databaseAPIID, err = p.getAPIID("database")
835
+	if err != nil {
836
+		return errors.Annotate(err, "database")
837
+	}
838
+
839
+	p.historyAPIID, err = p.getAPIID("history")
840
+	if err != nil {
841
+		return errors.Annotate(err, "history")
842
+	}
843
+
844
+	p.broadcastAPIID, err = p.getAPIID("network_broadcast")
845
+	if err != nil {
846
+		return errors.Annotate(err, "network")
847
+	}
848
+
849
+	return nil
850
+}
851
+
852
+//Close shuts the API down and closes underlying resources.
853
+func (p *bitsharesAPI) Close() error {
854
+	if p.rpcClient != nil {
855
+		if err := p.rpcClient.Close(); err != nil {
856
+			return errors.Annotate(err, "Close [wallet rpc]")
857
+		}
858
+		p.rpcClient = nil
859
+	}
860
+
861
+	if p.wsClient != nil {
862
+		if err := p.wsClient.Close(); err != nil {
863
+			return errors.Annotate(err, "Close [ws]")
864
+		}
865
+		p.wsClient = nil
866
+	}
867
+
868
+	return nil
869
+}
870
+
871
+//New creates a new BitsharesAPI interface.
872
+//wsEndpointURL: Is a mandatory websocket node URL.
873
+//rpcEndpointURL: Is an optional RPC endpoint to your local `cli_wallet`.
874
+//The use of wallet functions without this argument will throw an error.
875
+//If you do not use wallet API, provide an empty string.
876
+func New(wsEndpointURL, rpcEndpointURL string) BitsharesAPI {
877
+	var rpcClient RPCClient
878
+
879
+	if rpcEndpointURL != "" {
880
+		rpcClient = NewRPCClient(rpcEndpointURL)
881
+	}
882
+
883
+	api := &bitsharesAPI{
884
+		rpcClient:      rpcClient,
885
+		databaseAPIID:  InvalidApiID,
886
+		historyAPIID:   InvalidApiID,
887
+		broadcastAPIID: InvalidApiID,
888
+	}
889
+
890
+	api.wsClient = NewSimpleClientProvider(wsEndpointURL, api)
891
+	return api
892
+}
893
+
894
+//NewWithAutoEndpoint creates a new BitsharesAPI interface with automatic node latency checking.
895
+//It's best to use this API instance type for a long API lifecycle because the latency tester takes time to unleash its magic.
896
+//startupEndpointURL: Iss a mandatory websocket node URL to startup the latency tester quickly.
897
+//rpcEndpointURL: Is an optional RPC endpoint to your local `cli_wallet`.
898
+//The use of wallet functions without this argument
899
+//will throw an error. If you do not use wallet API, provide an empty string.
900
+func NewWithAutoEndpoint(startupEndpointURL, rpcEndpointURL string) (BitsharesAPI, error) {
901
+	var rpcClient RPCClient
902
+
903
+	if rpcEndpointURL != "" {
904
+		rpcClient = NewRPCClient(rpcEndpointURL)
905
+	}
906
+
907
+	api := &bitsharesAPI{
908
+		rpcClient:      rpcClient,
909
+		databaseAPIID:  InvalidApiID,
910
+		historyAPIID:   InvalidApiID,
911
+		broadcastAPIID: InvalidApiID,
912
+	}
913
+
914
+	pr, err := NewBestNodeClientProvider(startupEndpointURL, api)
915
+	if err != nil {
916
+		return nil, errors.Annotate(err, "NewBestNodeClientProvider")
917
+	}
918
+
919
+	api.wsClient = pr
920
+	return api, nil
921
+}

+ 142
- 0
api/provider.go View File

@@ -0,0 +1,142 @@
1
+package api
2
+
3
+import (
4
+	"github.com/denkhaus/logging"
5
+	"github.com/juju/errors"
6
+	deadlock "github.com/sasha-s/go-deadlock"
7
+	"github.com/tevino/abool"
8
+)
9
+
10
+type ClientProvider interface {
11
+	OnError(fn ErrorFunc)
12
+	Connect() error
13
+	OnNotify(subscriberID int, fn NotifyFunc) error
14
+	CallAPI(apiID int, method string, args ...interface{}) (interface{}, error)
15
+	Close() error
16
+}
17
+
18
+type SimpleClientProvider struct {
19
+	WebsocketClient
20
+	api BitsharesAPI
21
+}
22
+
23
+func NewSimpleClientProvider(endpointURL string, api BitsharesAPI) ClientProvider {
24
+	wsc := NewWebsocketClient(endpointURL)
25
+	sim := SimpleClientProvider{
26
+		api:             api,
27
+		WebsocketClient: wsc,
28
+	}
29
+
30
+	return &sim
31
+}
32
+
33
+func (p *SimpleClientProvider) CallAPI(apiID int, method string, args ...interface{}) (interface{}, error) {
34
+	if !p.WebsocketClient.IsConnected() {
35
+		if err := p.api.Connect(); err != nil {
36
+			return nil, errors.Annotate(err, "Connect [api]")
37
+		}
38
+	}
39
+
40
+	return p.WebsocketClient.CallAPI(apiID, method, args...)
41
+}
42
+
43
+type BestNodeClientProvider struct {
44
+	WebsocketClient
45
+	mu          deadlock.RWMutex
46
+	nodeChanged *abool.AtomicBool
47
+	api         BitsharesAPI
48
+	tester      LatencyTester
49
+}
50
+
51
+func NewBestNodeClientProvider(endpointURL string, api BitsharesAPI) (ClientProvider, error) {
52
+	tester, err := NewLatencyTester(endpointURL)
53
+	if err != nil {
54
+		return nil, errors.Annotate(err, "NewLatencyTester")
55
+	}
56
+
57
+	pr := &BestNodeClientProvider{
58
+		api:             api,
59
+		tester:          tester,
60
+		nodeChanged:     abool.NewBool(false),
61
+		WebsocketClient: tester.TopNodeClient(),
62
+	}
63
+
64
+	tester.OnTopNodeChanged(pr.onTopNodeChanged)
65
+	tester.Start()
66
+
67
+	return pr, nil
68
+}
69
+
70
+func (p *BestNodeClientProvider) onTopNodeChanged(newEndpoint string) error {
71
+	logging.Infof("change top node client -> %s\n", newEndpoint)
72
+	p.nodeChanged.Set()
73
+	return nil
74
+}
75
+
76
+func (p *BestNodeClientProvider) renewClient() error {
77
+	p.mu.Lock()
78
+	defer p.mu.Unlock()
79
+
80
+	if p.WebsocketClient.IsConnected() {
81
+		logging.Debug("close [client]")
82
+		if err := p.WebsocketClient.Close(); err != nil {
83
+			return errors.Annotate(err, "Close [client]")
84
+		}
85
+	}
86
+
87
+	p.WebsocketClient = p.tester.TopNodeClient()
88
+	return nil
89
+}
90
+
91
+func (p *BestNodeClientProvider) handleReconnect() error {
92
+	if err := p.renewClient(); err != nil {
93
+		return errors.Annotate(err, "renewClient")
94
+	}
95
+
96
+	logging.Debug("reconnect api")
97
+	if err := p.api.Connect(); err != nil {
98
+		return errors.Annotate(err, "Connect [api]")
99
+	}
100
+
101
+	return nil
102
+}
103
+
104
+func (p *BestNodeClientProvider) needsReconnect() bool {
105
+	return p.nodeChanged.IsSet() ||
106
+		!p.WebsocketClient.IsConnected()
107
+}
108
+
109
+func (p *BestNodeClientProvider) CallAPI(apiID int, method string, args ...interface{}) (interface{}, error) {
110
+	//ensure reliable connection
111
+	if p.needsReconnect() {
112
+		// either way unsignal nodeChanged
113
+		p.nodeChanged.UnSet()
114
+		if err := p.handleReconnect(); err != nil {
115
+			return nil, errors.Annotate(err, "handleReconnect")
116
+		}
117
+	}
118
+
119
+	p.mu.RLock()
120
+	defer p.mu.RUnlock()
121
+	return p.WebsocketClient.CallAPI(apiID, method, args...)
122
+}
123
+
124
+func (p *BestNodeClientProvider) Close() error {
125
+	p.mu.Lock()
126
+	defer p.mu.Unlock()
127
+
128
+	logging.Debug("close provider")
129
+	if p.WebsocketClient.IsConnected() {
130
+		logging.Debug("close [client]")
131
+		if err := p.WebsocketClient.Close(); err != nil {
132
+			return errors.Annotate(err, "Close [client]")
133
+		}
134
+	}
135
+
136
+	logging.Debug("close [tester]")
137
+	if err := p.tester.Close(); err != nil {
138
+		return errors.Annotate(err, "Close [tester]")
139
+	}
140
+
141
+	return nil
142
+}

+ 94
- 0
api/rpcclient.go View File

@@ -0,0 +1,94 @@
1
+package api
2
+
3
+import (
4
+	"bytes"
5
+	"math/rand"
6
+	"net/http"
7
+	"time"
8
+
9
+	"github.com/denkhaus/logging"
10
+	"github.com/juju/errors"
11
+	"github.com/pquerna/ffjson/ffjson"
12
+)
13
+
14
+type RPCClient interface {
15
+	CallAPI(method string, args ...interface{}) (interface{}, error)
16
+	Close() error
17
+	Connect() error
18
+}
19
+
20
+type rpcClient struct {
21
+	*http.Client
22
+	*ffjson.Encoder
23
+	*ffjson.Decoder
24
+
25
+	decBuf      *bytes.Buffer
26
+	endpointURL string
27
+	req         rpcRequest
28
+	res         rpcResponseString
29
+	timeout     int
30
+}
31
+
32
+func (p *rpcClient) Connect() error {
33
+	p.Client = &http.Client{
34
+		Timeout: 10 * time.Second,
35
+	}
36
+
37
+	p.decBuf = new(bytes.Buffer)
38
+	p.Encoder = ffjson.NewEncoder(p.decBuf)
39
+	p.Decoder = ffjson.NewDecoder()
40
+
41
+	return nil
42
+}
43
+
44
+func (p *rpcClient) Close() error {
45
+	return nil
46
+}
47
+
48
+func (p *rpcClient) CallAPI(method string, args ...interface{}) (interface{}, error) {
49
+	p.req.Method = method
50
+	p.req.ID = uint64(rand.Int63())
51
+	p.req.Params = args
52
+
53
+	if err := p.Encode(&p.req); err != nil {
54
+		return nil, errors.Annotate(err, "Encode")
55
+	}
56
+
57
+	logging.DDumpJSON("rpc req >", p.req)
58
+
59
+	req, err := http.NewRequest("POST", p.endpointURL, p.decBuf)
60
+	if err != nil {
61
+		return nil, errors.Annotate(err, "NewRequest")
62
+	}
63
+
64
+	req.Header.Set("Content-Type", "application/json")
65
+	req.Header.Set("Accept", "application/json")
66
+
67
+	resp, err := p.Do(req)
68
+	if err != nil {
69
+		return nil, errors.Annotate(err, "do request")
70
+	}
71
+
72
+	defer resp.Body.Close()
73
+
74
+	if err := p.DecodeReader(resp.Body, &p.res); err != nil {
75
+		return nil, errors.Annotate(err, "Decode")
76
+	}
77
+
78
+	if p.res.HasError() {
79
+		return p.res.Result, p.res.Error
80
+	}
81
+
82
+	logging.DDumpJSON("rpc resp <", p.res.Result)
83
+
84
+	return p.res.Result, nil
85
+}
86
+
87
+//NewRPCClient creates a new RPC Client
88
+func NewRPCClient(rpcEndpointURL string) RPCClient {
89
+	cli := rpcClient{
90
+		endpointURL: rpcEndpointURL,
91
+	}
92
+
93
+	return &cli
94
+}

+ 349
- 0
api/tester.go View File

@@ -0,0 +1,349 @@
1
+package api
2
+
3
+import (
4
+	"context"
5
+	"fmt"
6
+	"math"
7
+	"strings"
8
+	"time"
9
+
10
+	"eprosume/bitshares/types"
11
+	"github.com/denkhaus/logging"
12
+	sort "github.com/emirpasic/gods/utils"
13
+	deadlock "github.com/sasha-s/go-deadlock"
14
+	"gopkg.in/tomb.v2"
15
+)
16
+
17
+var (
18
+	//LoopSeconds = time for one pass to calc dynamic delay
19
+	LoopSeconds = 60
20
+	//our known node endpoints
21
+	knownEndpoints = []string{
22
+		"wss://eu.openledger.info/ws",
23
+		"wss://bitshares.openledger.info/ws",
24
+		"wss://dex.rnglab.org",
25
+		"wss://api.bitshares.bhuz.info/ws",
26
+		"wss://bitshares.crypto.fans/ws",
27
+		"wss://node.market.rudex.org",
28
+		"wss://api.bts.blckchnd.com",
29
+		"wss://eu.nodes.bitshares.ws",
30
+		"wss://btsws.roelandp.nl/ws",
31
+		"wss://btsfullnode.bangzi.info/ws",
32
+		"wss://api-ru.bts.blckchnd.com",
33
+		"wss://kc-us-dex.xeldal.com/ws",
34
+		"wss://api.btsxchng.com",
35
+		"wss://api.bts.network",
36
+		"wss://dexnode.net/ws",
37
+		"wss://us.nodes.bitshares.ws",
38
+		"wss://api.bts.mobi/ws",
39
+		"wss://blockzms.xyz/ws",
40
+		"wss://bts-api.lafona.net/ws",
41
+		"wss://api.bts.ai/",
42
+		"wss://la.dexnode.net/ws",
43
+		"wss://openledger.hk/ws",
44
+		"wss://sg.nodes.bitshares.ws",
45
+		"wss://bts.open.icowallet.net/ws",
46
+		"wss://ws.gdex.io",
47
+		"wss://bitshares-api.wancloud.io/ws",
48
+		"wss://ws.hellobts.com/",
49
+		"wss://bitshares.dacplay.org/ws",
50
+		"wss://crazybit.online",
51
+		"wss://kimziv.com/ws",
52
+		"wss://wss.ioex.top",
53
+		"wss://node.btscharts.com/ws",
54
+		"wss://bts-seoul.clockwork.gr/",
55
+		"wss://bitshares.cyberit.io/",
56
+		"wss://api.btsgo.net/ws",
57
+		"wss://ws.winex.pro",
58
+		"wss://bts.to0l.cn:4443/ws",
59
+		"wss://bitshares.bts123.cc:15138/",
60
+		"wss://bit.btsabc.org/ws",
61
+		"wss://ws.gdex.top",
62
+	}
63
+)
64
+
65
+type LatencyTester interface {
66
+	Start()
67
+	Close() error
68
+	String() string
69
+	AddEndpoint(ep string)
70
+	OnTopNodeChanged(fn func(string) error)
71
+	TopNodeEndpoint() string
72
+	TopNodeClient() WebsocketClient
73
+	Done() <-chan struct{}
74
+}
75
+
76
+//NodeStats holds stat data for each endpoint
77
+type NodeStats struct {
78
+	cli        WebsocketClient
79
+	latency    time.Duration
80
+	requiredDB []string
81
+	attempts   int64
82
+	errors     int64
83
+	endpoint   string
84
+}
85
+
86
+func (p *NodeStats) onError(err error) {
87
+	p.errors++
88
+}
89
+
90
+//Latency returns the nodes latency
91
+func (p *NodeStats) Latency() time.Duration {
92
+	if p.attempts > 0 {
93
+		return time.Duration(int64(p.latency) / p.attempts)
94
+	}
95
+
96
+	return 0
97
+}
98
+
99
+//Score returns reliability score for each node. The less the better.
100
+func (p *NodeStats) Score() int64 {
101
+	lat := int64(p.Latency())
102
+
103
+	if lat == 0 {
104
+		return math.MaxInt64
105
+	}
106
+
107
+	if p.errors == 0 {
108
+		return lat
109
+	}
110
+
111
+	// add 50ms per error
112
+	return lat + p.errors*50000000
113
+}
114
+
115
+// String returns the stats string representation
116
+func (p *NodeStats) String() string {
117
+	return fmt.Sprintf("ep: %s | attempts: %d | errors: %d | latency: %s | score: %d",
118
+		p.endpoint, p.attempts, p.errors, p.Latency(), p.Score())
119
+}
120
+
121
+//NewNodeStats creates a new stat object
122
+func NewNodeStats(wsRPCEndpoint string) *NodeStats {
123
+	stats := &NodeStats{
124
+		endpoint: wsRPCEndpoint,
125
+		cli:      NewWebsocketClient(wsRPCEndpoint),
126
+		requiredDB: []string{
127
+			"database",
128
+			"history",
129
+			"network_broadcast",
130
+			"crypto",
131
+		},
132
+	}
133
+
134
+	stats.cli.OnError(stats.onError)
135
+	return stats
136
+}
137
+
138
+func (p *NodeStats) Equals(n *NodeStats) bool {
139
+	return p.endpoint == n.endpoint
140
+}
141
+
142
+func (p *NodeStats) checkNode() {
143
+	_, err := p.cli.CallAPI(1, "login", "", "")
144
+	if err != nil {
145
+		p.errors++
146
+	}
147
+
148
+	for _, name := range p.requiredDB {
149
+		_, err := p.cli.CallAPI(1, name, types.EmptyParams)
150
+		if err != nil {
151
+			p.errors++
152
+		}
153
+	}
154
+}
155
+
156
+func (p *NodeStats) check() {
157
+	p.attempts++
158
+	if err := p.cli.Connect(); err != nil {
159
+		p.errors++
160
+	}
161
+	defer p.cli.Close()
162
+
163
+	tm := time.Now()
164
+	p.checkNode()
165
+	p.latency += time.Since(tm)
166
+}
167
+
168
+type latencyTester struct {
169
+	mu               deadlock.RWMutex
170
+	tmb              *tomb.Tomb
171
+	toApply          []string
172
+	fallbackURL      string
173
+	onTopNodeChanged func(string) error
174
+	stats            []interface{}
175
+	pass             int
176
+}
177
+
178
+func NewLatencyTester(fallbackURL string) (LatencyTester, error) {
179
+	return NewLatencyTesterWithContext(context.Background(), fallbackURL)
180
+}
181
+
182
+func NewLatencyTesterWithContext(ctx context.Context, fallbackURL string) (LatencyTester, error) {
183
+	tmb, _ := tomb.WithContext(ctx)
184
+	lat := latencyTester{
185
+		fallbackURL: fallbackURL,
186
+		stats:       make([]interface{}, 0, len(knownEndpoints)),
187
+		tmb:         tmb,
188
+	}
189
+
190
+	lat.createStats(knownEndpoints)
191
+	return &lat, nil
192
+}
193
+
194
+func (p *latencyTester) String() string {
195
+	builder := strings.Builder{}
196
+
197
+	p.mu.Lock()
198
+	defer p.mu.Unlock()
199
+	for _, s := range p.stats {
200
+		stat := s.(*NodeStats)
201
+		builder.WriteString(stat.String())
202
+		builder.WriteString("\n")
203
+	}
204
+
205
+	return builder.String()
206
+}
207
+
208
+func (p *latencyTester) OnTopNodeChanged(fn func(string) error) {
209
+	p.onTopNodeChanged = fn
210
+}
211
+
212
+//AddEndpoint adds a new endpoint while the latencyTester is running
213
+func (p *latencyTester) AddEndpoint(ep string) {
214
+	p.toApply = append(p.toApply, ep)
215
+}
216
+
217
+func (p *latencyTester) sortResults(notify bool) error {
218
+
219
+	p.mu.Lock()
220
+	oldTop := p.stats[0].(*NodeStats)
221
+	sort.Sort(p.stats, func(a, b interface{}) int {
222
+		sa := a.(*NodeStats).Score()
223
+		sb := b.(*NodeStats).Score()
224
+		if sa > sb {
225
+			return 1
226
+		}
227
+
228
+		if sa < sb {
229
+			return -1
230
+		}
231
+
232
+		return 0
233
+	})
234
+
235
+	newTop := p.stats[0].(*NodeStats)
236
+	p.mu.Unlock()
237
+
238
+	if notify && !oldTop.Equals(newTop) {
239
+		if p.onTopNodeChanged != nil {
240
+			return p.onTopNodeChanged(newTop.endpoint)
241
+		}
242
+	}
243
+
244
+	return nil
245
+}
246
+
247
+func (p *latencyTester) createStats(eps []string) {
248
+	p.mu.Lock()
249
+	defer p.mu.Unlock()
250
+
251
+	for _, ep := range eps {
252
+		found := false
253
+		for _, s := range p.stats {
254
+			stat := s.(*NodeStats)
255
+			if stat.endpoint == ep {
256
+				found = true
257
+			}
258
+		}
259
+
260
+		if !found {
261
+			p.stats = append(
262
+				p.stats,
263
+				NewNodeStats(ep),
264
+			)
265
+		}
266
+	}
267
+}
268
+
269
+//TopNodeEndpoint returns the fastest endpoint URL. If the tester has no validated results
270
+//your given fallback endpoint is returned.
271
+func (p *latencyTester) TopNodeEndpoint() string {
272
+	if p.pass > 0 {
273
+		p.mu.RLock()
274
+		defer p.mu.RUnlock()
275
+		st := p.stats[0].(*NodeStats)
276
+		return st.endpoint
277
+	}
278
+
279
+	return p.fallbackURL
280
+}
281
+
282
+//TopNodeClient returns a new WebsocketClient to connect to the fastest node.
283
+//If the tester has no validated results, a client with your given
284
+//fallback endpoint is returned. You need to call Connect for yourself.
285
+func (p *latencyTester) TopNodeClient() WebsocketClient {
286
+	return NewWebsocketClient(
287
+		p.TopNodeEndpoint(),
288
+	)
289
+}
290
+
291
+// Done returns the channel that can be used to wait until
292
+// the tester has finished.
293
+func (p *latencyTester) Done() <-chan struct{} {
294
+	return p.tmb.Dead()
295
+}
296
+
297
+func (p *latencyTester) runPass() error {
298
+	// dynamic sleep time
299
+	slp := time.Duration(LoopSeconds/len(p.stats)) * time.Second
300
+	for i := 0; i < len(p.stats); i++ {
301
+		select {
302
+		case <-p.tmb.Dying():
303
+			return tomb.ErrDying
304
+		default:
305
+			time.Sleep(slp)
306
+			p.mu.RLock()
307
+			st := p.stats[i].(*NodeStats)
308
+			st.check()
309
+			p.mu.RUnlock()
310
+		}
311
+	}
312
+
313
+	return nil
314
+}
315
+
316
+//Start starts the testing process
317
+func (p *latencyTester) Start() {
318
+	logging.Debug("latencytester: start")
319
+
320
+	p.tmb.Go(func() error {
321
+		for {
322
+			select {
323
+			case <-p.tmb.Dying():
324
+				p.sortResults(false)
325
+				return tomb.ErrDying
326
+			default:
327
+				//apply later incoming endpoints
328
+				p.createStats(p.toApply)
329
+				if err := p.runPass(); err != nil {
330
+					//provide sorted results on return
331
+					p.sortResults(false)
332
+					return err
333
+				}
334
+
335
+				p.sortResults(true)
336
+				p.pass++
337
+			}
338
+		}
339
+	})
340
+}
341
+
342
+//Close stops the tester and waits until all goroutines have finished.
343
+func (p *latencyTester) Close() error {
344
+	logging.Debug("latencytester: kill [tomb]")
345
+	p.tmb.Kill(nil)
346
+
347
+	logging.Debug("latencytester: wait [tomb]")
348
+	return p.tmb.Wait()
349
+}

+ 139
- 0
api/types.go View File

@@ -0,0 +1,139 @@
1
+package api
2
+
3
+import "log"
4
+
5
+type NotifyFunc func(msg interface{}) error
6
+type ErrorFunc func(error)
7
+
8
+type WebsocketClient interface {
9
+	IsConnected() bool
10
+	OnError(fn ErrorFunc)
11
+	OnNotify(subscriberID int, fn NotifyFunc) error
12
+	Call(method string, args []interface{}) (*RPCCall, error)
13
+	CallAPI(apiID int, method string, args ...interface{}) (interface{}, error)
14
+	Close() error
15
+	Connect() error
16
+}
17
+
18
+type RPCCall struct {
19
+	Method  string
20
+	Request rpcRequest
21
+	Reply   interface{}
22
+	Error   error
23
+	Done    chan *RPCCall
24
+}
25
+
26
+func (call *RPCCall) done() {
27
+	select {
28
+	case call.Done <- call:
29
+		// ok
30
+	default:
31
+		log.Println("rpc: discarding Call reply due to insufficient Done chan capacity")
32
+	}
33
+}
34
+
35
+type rpcRequest struct {
36
+	Method string        `json:"method"`
37
+	Params []interface{} `json:"params"`
38
+	ID     uint64        `json:"id"`
39
+}
40
+
41
+func (p *rpcRequest) reset() {
42
+	p.ID = 0
43
+	p.Params = nil
44
+	p.Method = ""
45
+}
46
+
47
+type ResponseErrorContext struct {
48
+	Level      string `json:"level"`
49
+	File       string `json:"file"`
50
+	Line       int    `json:"line"`
51
+	Method     string `json:"method"`
52
+	Hostname   string `json:"hostname"`
53
+	ThreadName string `json:"thread_name"`
54
+	Timestamp  string `json:"timestamp"`
55
+}
56
+type ResponseErrorStack struct {
57
+	Context ResponseErrorContext `json:"context"`
58
+	Format  string               `json:"format"`
59
+	Data    interface{}          `json:"data"`
60
+}
61
+
62
+type ResponseErrorData struct {
63
+	Code    int                  `json:"code"`
64
+	Name    string               `json:"name"`
65
+	Message string               `json:"message"`
66
+	Stack   []ResponseErrorStack `json:"stack"`
67
+}
68
+
69
+type ResponseError struct {
70
+	Code    int               `json:"code"`
71
+	Message string            `json:"message"`
72
+	Data    ResponseErrorData `json:"data"`
73
+}
74
+
75
+func (p ResponseError) Error() string {
76
+	return p.Message
77
+}
78
+
79
+//wallet API uses string id ???
80
+type rpcResponseString struct {
81
+	ID     string        `json:"id"`
82
+	Result interface{}   `json:"result,omitempty"`
83
+	Error  ResponseError `json:"error"`
84
+}
85
+
86
+func (p rpcResponseString) HasError() bool {
87
+	return p.Error.Code != 0
88
+}
89
+
90
+type rpcResponse struct {
91
+	ID     uint64        `json:"id"`
92
+	Result interface{}   `json:"result"`
93
+	Error  ResponseError `json:"error"`
94
+}
95
+
96
+func (p rpcResponse) Is(in interface{}) bool {
97
+	if data, ok := in.(map[string]interface{}); ok {
98
+		if _, ok := data["id"]; ok {
99
+			if _, ok := data["result"]; ok {
100
+				return true
101
+			}
102
+			if _, ok := data["error"]; ok {
103
+				return true
104
+			}
105
+		}
106
+	}
107
+	return false
108
+}
109
+
110
+func (p rpcResponse) HasError() bool {
111
+	return p.Error.Code != 0
112
+}
113
+
114
+func (p *rpcResponse) reset() {
115
+	p.Error = ResponseError{}
116
+	p.Result = nil
117
+	p.ID = 0
118
+}
119
+
120
+type rpcNotify struct {
121
+	Method string      `json:"method"`
122
+	Params interface{} `json:"params"`
123
+}
124
+
125
+func (p rpcNotify) Is(in interface{}) bool {
126
+	if data, ok := in.(map[string]interface{}); ok {
127
+		if _, ok := data["method"]; ok {
128
+			if _, ok := data["params"]; ok {
129
+				return true
130
+			}
131
+		}
132
+	}
133
+	return false
134
+}
135
+
136
+func (p *rpcNotify) reset() {
137
+	p.Method = ""
138
+	p.Params = nil
139
+}

+ 268
- 0
api/wallet.go View File

@@ -0,0 +1,268 @@
1
+package api
2
+
3
+import (
4
+	"fmt"
5
+
6
+	"eprosume/bitshares/types"
7
+	"eprosume/bitshares/util"
8
+	"github.com/denkhaus/logging"
9
+	"github.com/juju/errors"
10
+	"github.com/pquerna/ffjson/ffjson"
11
+)
12
+
13
+// WalletLock locks the wallet
14
+func (p *bitsharesAPI) WalletLock() error {
15
+	if p.rpcClient == nil {
16
+		return types.ErrRPCClientNotInitialized
17
+	}
18
+
19
+	_, err := p.rpcClient.CallAPI("lock", types.EmptyParams)
20
+	return err
21
+}
22
+
23
+// WalletUnlock unlocks the wallet
24
+func (p *bitsharesAPI) WalletUnlock(password string) error {
25
+	if p.rpcClient == nil {
26
+		return types.ErrRPCClientNotInitialized
27
+	}
28
+
29
+	_, err := p.rpcClient.CallAPI("unlock", password)
30
+	return err
31
+}
32
+
33
+// WalletIsLocked checks if wallet is locked.
34
+func (p *bitsharesAPI) WalletIsLocked() (bool, error) {
35
+	if p.rpcClient == nil {
36
+		return false, types.ErrRPCClientNotInitialized
37
+	}
38
+
39
+	resp, err := p.rpcClient.CallAPI("is_locked", types.EmptyParams)
40
+
41
+	if err != nil {
42
+		return false, err
43
+	}
44
+
45
+	logging.DDumpJSON("is_locked <", resp)
46
+
47
+	return resp.(bool), err
48
+}
49
+
50
+// WalletBuy places a limit order attempting to buy one asset with another.
51
+// This API call abstracts away some of the details of the sell_asset call to be more
52
+// user friendly. All orders placed with buy never timeout and will not be killed if they
53
+// cannot be filled immediately. If you wish for one of these parameters to be different,
54
+// then sell_asset should be used instead.
55
+//
56
+// @param account The account buying the asset for another asset.
57
+// @param base The name or id of the asset to buy.
58
+// @param quote The name or id of the assest being offered as payment.
59
+// @param rate The rate in base:quote at which you want to buy.
60
+// @param amount the amount of base you want to buy.
61
+// @param broadcast true to broadcast the transaction on the network.
62
+// @returns The signed transaction selling the funds.
63
+// @returns The error of operation.
64
+func (p *bitsharesAPI) WalletBuy(account types.GrapheneObject, base, quote types.GrapheneObject, rate string, amount string, broadcast bool) (*types.SignedTransaction, error) {
65
+	if p.rpcClient == nil {
66
+		return nil, types.ErrRPCClientNotInitialized
67
+	}
68
+
69
+	resp, err := p.rpcClient.CallAPI(
70
+		"buy", account.ID(),
71
+		base.ID(), quote.ID(),
72
+		rate, amount, broadcast,
73
+	)
74
+
75
+	if err != nil {
76
+		return nil, err
77
+	}
78
+
79
+	logging.DDumpJSON("buy <", resp)
80
+
81
+	ret := types.SignedTransaction{}
82
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
83
+		return nil, errors.Annotate(err, "unmarshal Transaction")
84
+	}
85
+
86
+	return &ret, nil
87
+}
88
+
89
+// WalletSell places a limit order attempting to sell one asset for another.
90
+// This API call abstracts away some of the details of the sell_asset call to be more
91
+// user friendly. All orders placed with sell never timeout and will not be killed if they
92
+// cannot be filled immediately. If you wish for one of these parameters to be different,
93
+// then sell_asset should be used instead.
94
+//
95
+// @param account the account providing the asset being sold, and which will receive the processed of the sale.
96
+// @param base The name or id of the asset to sell.
97
+// @param quote The name or id of the asset to receive.
98
+// @param rate The rate in base:quote at which you want to sell.
99
+// @param amount The amount of base you want to sell.
100
+// @param broadcast true to broadcast the transaction on the network.
101
+// @returns The signed transaction selling the funds.
102
+// @returns The error of operation.
103
+func (p *bitsharesAPI) WalletSell(account types.GrapheneObject, base, quote types.GrapheneObject, rate string, amount string, broadcast bool) (*types.SignedTransaction, error) {
104
+	if p.rpcClient == nil {
105
+		return nil, types.ErrRPCClientNotInitialized
106
+	}
107
+
108
+	resp, err := p.rpcClient.CallAPI(
109
+		"sell", account.ID(),
110
+		base.ID(), quote.ID(),
111
+		rate, amount, broadcast,
112
+	)
113
+
114
+	if err != nil {
115
+		return nil, err
116
+	}
117
+
118
+	logging.DDumpJSON("sell <", resp)
119
+
120
+	ret := types.SignedTransaction{}
121
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
122
+		return nil, errors.Annotate(err, "unmarshal Transaction")
123
+	}
124
+
125
+	return &ret, nil
126
+}
127
+
128
+func (p *bitsharesAPI) WalletBuyEx(account types.GrapheneObject, base, quote types.GrapheneObject,
129
+	rate float64, amount float64, broadcast bool) (*types.SignedTransaction, error) {
130
+	//TODO: use proper precision, avoid rounding
131
+	minToReceive := fmt.Sprintf("%f", amount)
132
+	amountToSell := fmt.Sprintf("%f", rate*amount)
133
+
134
+	return p.WalletSellAsset(account, amountToSell, quote, minToReceive, base, 0, false, broadcast)
135
+}
136
+
137
+func (p *bitsharesAPI) WalletSellEx(account types.GrapheneObject, base, quote types.GrapheneObject,
138
+	rate float64, amount float64, broadcast bool) (*types.SignedTransaction, error) {
139
+
140
+	//TODO: use proper precision, avoid rounding
141
+	amountToSell := fmt.Sprintf("%f", amount)
142
+	minToReceive := fmt.Sprintf("%f", rate*amount)
143
+
144
+	return p.WalletSellAsset(account, amountToSell, base, minToReceive, quote, 0, false, broadcast)
145
+}
146
+
147
+// SellAsset
148
+func (p *bitsharesAPI) WalletSellAsset(account types.GrapheneObject, amountToSell string, symbolToSell types.GrapheneObject,
149
+	minToReceive string, symbolToReceive types.GrapheneObject, timeout uint32, fillOrKill bool, broadcast bool) (*types.SignedTransaction, error) {
150
+	if p.rpcClient == nil {
151
+		return nil, types.ErrRPCClientNotInitialized
152
+	}
153
+
154
+	resp, err := p.rpcClient.CallAPI(
155
+		"sell_asset", account.ID(),
156
+		amountToSell, symbolToSell.ID(),
157
+		minToReceive, symbolToReceive.ID(),
158
+		timeout, fillOrKill, broadcast,
159
+	)
160
+	if err != nil {
161
+		return nil, err
162
+	}
163
+
164
+	logging.DDumpJSON("sell_asset <", resp)
165
+
166
+	ret := types.SignedTransaction{}
167
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
168
+		return nil, errors.Annotate(err, "unmarshal Transaction")
169
+	}
170
+
171
+	return &ret, nil
172
+}
173
+
174
+// WalletBorrowAsset borrows an asset or update the debt/collateral ratio for the loan.
175
+// @param account: the id of the account associated with the transaction.
176
+// @param amountToBorrow: the amount of the asset being borrowed. Make this value negative to pay back debt.
177
+// @param symbolToBorrow: the symbol or id of the asset being borrowed.
178
+// @param amountOfCollateral: the amount of the backing asset to add to your collateral position. Make this negative to claim back some of your collateral. The backing asset is defined in the bitasset_options for the asset being borrowed.
179
+// @param broadcast: true to broadcast the transaction on the network
180
+func (p *bitsharesAPI) WalletBorrowAsset(account types.GrapheneObject, amountToBorrow string, symbolToBorrow types.GrapheneObject,
181
+	amountOfCollateral string, broadcast bool) (*types.SignedTransaction, error) {
182
+
183
+	if p.rpcClient == nil {
184
+		return nil, types.ErrRPCClientNotInitialized
185
+	}
186
+
187
+	resp, err := p.rpcClient.CallAPI(
188
+		"borrow_asset", account.ID(),
189
+		amountToBorrow, symbolToBorrow.ID(),
190
+		amountOfCollateral, broadcast,
191
+	)
192
+	if err != nil {
193
+		return nil, err
194
+	}
195
+
196
+	logging.DDumpJSON("borrow_asset <", resp)
197
+
198
+	ret := types.SignedTransaction{}
199
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
200
+		return nil, errors.Annotate(err, "unmarshal Transaction")
201
+	}
202
+
203
+	return &ret, nil
204
+}
205
+
206
+func (p *bitsharesAPI) WalletListAccountBalances(account types.GrapheneObject) (types.AssetAmounts, error) {
207
+
208
+	if p.rpcClient == nil {
209
+		return nil, types.ErrRPCClientNotInitialized
210
+	}
211
+
212
+	resp, err := p.rpcClient.CallAPI("list_account_balances", account.ID())
213
+	if err != nil {
214
+		return nil, err
215
+	}
216
+
217
+	logging.DDumpJSON("list_account_balances <", resp)
218
+
219
+	ret := types.AssetAmounts{}
220
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
221
+		return nil, errors.Annotate(err, "unmarshal AssetAmounts")
222
+	}
223
+
224
+	return ret, nil
225
+}
226
+
227
+// SerializeTransaction converts a signed_transaction in JSON form to its binary representation.
228
+// @param tx the transaction to serialize
229
+// Returns the binary form of the transaction. It will not be hex encoded, this returns a raw string that may have null characters embedded in it.
230
+func (p *bitsharesAPI) WalletSerializeTransaction(tx *types.SignedTransaction) (string, error) {
231
+	if p.rpcClient == nil {
232
+		return "", types.ErrRPCClientNotInitialized
233
+	}
234
+
235
+	resp, err := p.rpcClient.CallAPI("serialize_transaction", tx)
236
+	if err != nil {
237
+		return "", err
238
+	}
239
+
240
+	logging.DDumpJSON("serialize_transaction <", resp)
241
+
242
+	return resp.(string), nil
243
+}
244
+
245
+// SignTransaction signs a transaction
246
+// @param tx the transaction to sign
247
+// @param broadcast bool defines if the transaction should be broadcasted
248
+// Returns the signed transaction.
249
+func (p *bitsharesAPI) WalletSignTransaction(tx *types.SignedTransaction, broadcast bool) (*types.SignedTransaction, error) {
250
+	if p.rpcClient == nil {
251
+		return nil, types.ErrRPCClientNotInitialized
252
+	}
253
+
254
+	resp, err := p.rpcClient.CallAPI("sign_transaction", tx, broadcast)
255
+	if err != nil {
256
+		return nil, err
257
+	}
258
+
259
+	logging.DDumpJSON("wallet sign_transaction <", resp)
260
+
261
+	ret := types.SignedTransaction{}
262
+	if err := ffjson.Unmarshal(util.ToBytes(resp), &ret); err != nil {
263
+		return nil, errors.Annotate(err, "unmarshal Transaction")
264
+	}
265
+
266
+	return &ret, nil
267
+
268
+}

+ 324
- 0
api/wsclient.go View File

@@ -0,0 +1,324 @@
1
+package api
2
+
3
+import (
4
+	"io"
5
+	"net"
6
+	"os"
7
+	"syscall"
8
+
9
+	"sync"
10
+	"time"
11
+
12
+	"github.com/denkhaus/logging"
13
+	"github.com/juju/errors"
14
+	"github.com/mitchellh/mapstructure"
15
+	"github.com/pquerna/ffjson/ffjson"
16
+	"github.com/tevino/abool"
17
+	"golang.org/x/net/websocket"
18
+)
19
+
20
+var (
21
+	DialerTimeout    = time.Duration(5 * time.Second)
22
+	ReadWriteTimeout = time.Duration(10 * time.Second)
23
+	ErrShutdown      = errors.New("connection is shut down")
24
+)
25
+
26
+type wsClient struct {
27
+	*ffjson.Decoder
28
+	*ffjson.Encoder
29
+	conn        *websocket.Conn
30
+	url         string
31
+	resp        rpcResponse // unmarshal target
32
+	notify      rpcNotify   // unmarshal target
33
+	onError     ErrorFunc
34
+	errors      chan error
35
+	closing     *abool.AtomicBool
36
+	shutdown    *abool.AtomicBool
37
+	currentID   uint64
38
+	wg          sync.WaitGroup
39
+	mutex       sync.Mutex // protects the following
40
+	pending     map[uint64]*RPCCall
41
+	mutexNotify sync.Mutex // protects the following
42
+	notifyFns   map[int]NotifyFunc
43
+}
44
+
45
+func NewWebsocketClient(endpointURL string) WebsocketClient {
46
+	cli := wsClient{
47
+		closing:   abool.NewBool(false),
48
+		shutdown:  abool.NewBool(false),
49
+		pending:   make(map[uint64]*RPCCall),
50
+		notifyFns: make(map[int]NotifyFunc),
51
+		currentID: 1,
52
+		url:       endpointURL,
53
+	}
54
+
55
+	return &cli
56
+}
57
+
58
+func (p *wsClient) Connect() error {
59
+	config, err := websocket.NewConfig(p.url, "http://localhost/")
60
+	if err != nil {
61
+		return errors.Annotate(err, "NewConfig")
62
+	}
63
+
64
+	config.Dialer = &net.Dialer{
65
+		Timeout: DialerTimeout,
66
+	}
67
+
68
+	conn, err := websocket.DialConfig(config)
69
+	if err != nil {
70
+		return errors.Annotate(err, "Dial")
71
+	}
72
+
73
+	p.shutdown.UnSet()
74
+	p.closing.UnSet()
75
+
76
+	p.errors = make(chan error, 10)
77
+	p.Decoder = ffjson.NewDecoder()
78
+	p.Encoder = ffjson.NewEncoder(conn)
79
+	p.conn = conn
80
+
81
+	p.wg.Add(1)
82
+	go p.monitor()
83
+
84
+	p.wg.Add(1)
85
+	go p.receive()
86
+
87
+	return nil
88
+}
89
+
90
+func (p *wsClient) Close() error {
91
+	if p.conn != nil {
92
+		p.closing.Set()
93
+		if !p.shutdown.IsSet() {
94
+			if err := p.conn.SetDeadline(time.Now().Add(ReadWriteTimeout)); err != nil {
95
+				return errors.Annotate(err, "SetDeadline")
96
+			}
97
+			if err := p.conn.Close(); err != nil {
98
+				return errors.Annotate(err, "Close [conn]")
99
+			}
100
+		}
101
+
102
+		p.wg.Wait()
103
+		close(p.errors)
104
+		p.conn = nil
105
+	}
106
+
107
+	return nil
108
+}
109
+
110
+func (p *wsClient) IsConnected() bool {
111
+	if p.shutdown.IsSet() || p.closing.IsSet() {
112
+		return false
113
+	}
114
+
115
+	return p.conn != nil
116
+}
117
+
118
+func (p *wsClient) monitor() {
119
+	defer p.wg.Done()
120
+
121
+	for !p.shutdown.IsSet() {
122
+		select {
123
+		case err := <-p.errors:
124
+			if err != nil {
125
+				if p.onError != nil {
126
+					p.onError(err)
127
+				} else {
128
+					logging.Errorf("WebsocketClient error: %s", err)
129
+					logging.Warn("please set the API OnError hook to avoid this message")
130
+				}
131
+			}
132
+		default:
133
+			time.Sleep(time.Millisecond)
134
+		}
135
+	}
136
+}
137
+
138
+func (p *wsClient) handleCustomData(data map[string]interface{}) error {
139
+	logging.DDumpJSON("ws notify <", data)
140
+
141
+	switch {
142
+	case p.notify.Is(data):
143
+		p.notify.reset()
144
+		err := mapstructure.Decode(data, &p.notify)
145
+		if err != nil {
146
+			return errors.Annotate(err, "Decode [notify]")
147
+		}
148
+
149
+		params := p.notify.Params.([]interface{})
150
+		subscriberID := int(params[0].(float64))
151
+
152
+		var fn NotifyFunc
153
+		p.mutexNotify.Lock()
154
+		fn = p.notifyFns[subscriberID]
155
+		p.mutexNotify.Unlock()
156
+
157
+		if fn != nil {
158
+			if err := fn(params[1]); err != nil {
159
+				return errors.Annotate(err, "handle notify")
160
+			}
161
+		}
162
+	default:
163
+		return errors.Errorf("unhandled custom data: %v", data)
164
+	}
165
+
166
+	return nil
167
+}
168
+
169
+func (p *wsClient) mustEndReceive(err error) bool {
170
+	if e, ok := err.(*net.OpError); ok {
171
+		if e.Err.Error() == "use of closed network connection" {
172
+			return true
173
+		}
174
+
175
+		if syscallErr, ok := e.Err.(*os.SyscallError); ok {
176
+			if syscallErr.Err == syscall.ECONNRESET {
177
+				return true
178
+			}
179
+		}
180
+	}
181
+
182
+	if e, ok := err.(net.Error); ok {
183
+		if e.Timeout() {
184
+			return true
185
+		}
186
+	}
187
+
188
+	return false
189
+}
190
+
191
+func (p *wsClient) receive() {
192
+	defer p.wg.Done()
193
+
194
+	for !p.closing.IsSet() {
195
+		//TODO: is there a faster way to distinguish between RPCResponse and RPCNotify data
196
+		var data map[string]interface{}
197
+		if err := p.DecodeReader(p.conn, &data); err != nil {
198
+			if p.mustEndReceive(err) {
199
+				break
200
+			}
201
+
202
+			//report all errors but EOF
203
+			if err != io.EOF {
204
+				p.errors <- errors.Annotate(err, "DecodeReader")
205
+			}
206
+			
207
+			continue
208
+		}
209
+
210
+		if p.resp.Is(data) {
211
+			p.resp.reset()
212
+			if err := mapstructure.Decode(data, &p.resp); err != nil {
213
+				p.errors <- errors.Annotate(err, "Decode [resp]")
214
+				continue
215
+			}
216
+
217
+			logging.DDumpJSON("ws resp <", data)
218
+
219
+			p.mutex.Lock()
220
+			call, ok := p.pending[p.resp.ID]
221
+			p.mutex.Unlock()
222
+
223
+			if ok {
224
+				p.mutex.Lock()
225
+				delete(p.pending, p.resp.ID)
226
+				p.mutex.Unlock()
227
+
228
+				call.Reply = p.resp.Result
229
+				if p.resp.HasError() {
230
+					call.Error = p.resp.Error
231
+				}
232
+
233
+				call.done()
234
+			} else {
235
+				p.errors <- errors.Errorf("no corresponding call found for incoming rpc data %v", p.resp)
236
+				continue
237
+			}
238
+		} else if err := p.handleCustomData(data); err != nil {
239
+			p.errors <- errors.Annotate(err, "handleCustomData")
240
+			continue
241
+		}
242
+	}
243
+
244
+	// Terminate pending calls
245
+	p.mutex.Lock()
246
+	defer p.mutex.Unlock()
247
+
248
+	p.shutdown.Set()
249
+	for _, call := range p.pending {
250
+		call.Error = ErrShutdown
251
+		call.done()
252
+	}
253
+}
254
+
255
+func (p *wsClient) OnNotify(subscriberID int, fn NotifyFunc) error {
256
+	if _, ok := p.notifyFns[subscriberID]; ok {
257
+		return errors.Errorf("a notify hook for subscriberID %d is already defined", subscriberID)
258
+	}
259
+
260
+	p.mutexNotify.Lock()
261
+	p.notifyFns[subscriberID] = fn
262
+	p.mutexNotify.Unlock()
263
+
264
+	return nil
265
+}
266
+
267
+func (p *wsClient) OnError(fn ErrorFunc) {
268
+	p.onError = fn
269
+}
270
+
271
+func (p *wsClient) CallAPI(apiID int, method string, args ...interface{}) (interface{}, error) {
272
+	param := []interface{}{
273
+		apiID,
274
+		method,
275
+		args,
276
+	}
277
+
278
+	call, err := p.Call("call", param)
279
+	if err != nil {
280
+		return nil, errors.Annotate(err, "Call")
281
+	}
282
+
283
+	<-call.Done
284
+	return call.Reply, call.Error
285
+}
286
+
287
+func (p *wsClient) Call(method string, args []interface{}) (*RPCCall, error) {
288
+	if !p.IsConnected() {
289
+		return nil, ErrShutdown
290
+	}
291
+
292
+	call := &RPCCall{
293
+		Request: rpcRequest{
294
+			Method: method,
295
+			Params: args,
296
+			ID:     p.currentID,
297
+		},
298
+		Done: make(chan *RPCCall, 200),
299
+	}
300
+
301
+	p.mutex.Lock()
302
+	p.currentID++
303
+	p.pending[call.Request.ID] = call
304
+	p.mutex.Unlock()
305
+
306
+	logging.DDumpJSON("ws req >", call.Request)
307
+
308
+	if err := p.conn.SetDeadline(time.Now().Add(ReadWriteTimeout)); err != nil {
309
+		return nil, errors.Annotate(err, "SetDeadline")
310
+	}
311
+
312
+	if err := p.Encode(call.Request); err != nil {
313
+		p.mutex.Lock()
314
+		delete(p.pending, call.Request.ID)
315
+		p.mutex.Unlock()
316
+		if err == syscall.EPIPE {
317
+			p.closing.Set()
318
+		}
319
+
320
+		return nil, errors.Annotate(err, "Encode [req]")
321
+	}
322
+
323
+	return call, nil
324
+}

+ 102
- 0
config/chaincconfig.go View File

@@ -0,0 +1,102 @@
1
+package config
2
+
3
+import "github.com/juju/errors"
4
+
5
+type ChainConfig map[string]interface{}
6
+
7
+var currentConfig *ChainConfig
8
+
9
+var (
10
+	ChainIDUnknown = "n/a"
11
+	ChainIDBTS     = "4018d7844c78f6a6c41c6a552b898022310fc5dec06da467ee7905a8dad512c8"
12
+	ChainIDMuse    = "45ad2d3f9ef92a49b55c2227eb06123f613bb35dd08bd876f2aea21925a67a67"
13
+	ChainIDTest    = "39f5e2ede1f8bc1a3a54a7914414e3779e33193f1f5693510e73cb7a87617447"
14
+	ChainIDObelisk = "1cfde7c388b9e8ac06462d68aadbd966b58f88797637d9af805b4560b0e9661e"
15
+	ChainIDGPH     = "b8d1603965b3eb1acba27e62ff59f74efa3154d43a4188d381088ac7cdf35539"
16
+	ChainIDProsumeProd = "d63a0dfb3e880d7c9ecfcde0875b0a84bf086bf5c63fc9f1d03e5507532574e9"
17
+	ChainIDProsume = "87cb120b1efa93f9c5bc33fc58fb3aa37210742a91fe75ca1fdbbd97781ca155"
18
+)
19
+
20
+var (
21
+	knownNetworks = []ChainConfig{
22
+		ChainConfig{
23
+			"name":           "Unknown",
24
+			"core_asset":     "n/a",
25
+			"address_prefix": "n/a",
26
+			"chain_id":       ChainIDUnknown,
27
+		},
28
+		ChainConfig{
29
+			"name":           "Prosume Test",
30
+			"core_asset":     "BTS",
31
+			"address_prefix": "BTS",
32
+			"chain_id":       ChainIDProsume,
33
+		},
34
+		ChainConfig{
35
+			"name":           "Eprosume",
36
+			"core_asset":     "BTS",
37
+			"address_prefix": "BTS",
38
+			"chain_id":       ChainIDProsumeProd,
39
+		},
40
+		ChainConfig{
41
+			"name":           "Graphene",
42
+			"core_asset":     "CORE",
43
+			"address_prefix": "GPH",
44
+			"chain_id":       ChainIDGPH,
45
+		},
46
+		ChainConfig{
47
+			"name":           "BitShares",
48
+			"core_asset":     "BTS",
49
+			"address_prefix": "BTS",
50
+			"chain_id":       ChainIDBTS,
51
+		},
52
+		ChainConfig{
53
+			"name":           "Muse",
54
+			"core_asset":     "MUSE",
55
+			"address_prefix": "MUSE",
56
+			"chain_id":       ChainIDMuse,
57
+		},
58
+		ChainConfig{
59
+			"name":           "Test",
60
+			"core_asset":     "TEST",
61
+			"address_prefix": "TEST",
62
+			"chain_id":       ChainIDTest,
63
+		},
64
+		ChainConfig{
65
+			"name":           "Obelisk",
66
+			"core_asset":     "GOV",
67
+			"address_prefix": "FEW",
68
+			"chain_id":       ChainIDObelisk,
69
+		},
70
+	}
71
+)
72
+
73
+func (p ChainConfig) ID() string {
74
+	if id, ok := p["chain_id"]; ok {
75
+		return id.(string)
76
+	}
77
+
78
+	return "n/a"
79
+}
80
+
81
+func (p ChainConfig) Prefix() string {
82
+	if id, ok := p["address_prefix"]; ok {
83
+		return id.(string)
84
+	}
85
+
86
+	return "n/a"
87
+}
88
+
89
+func CurrentConfig() *ChainConfig {
90
+	return currentConfig
91
+}
92
+
93
+func SetCurrentConfig(chainID string) error {
94
+	for _, cnf := range knownNetworks {
95
+		if cnf["chain_id"] == chainID {
96
+			currentConfig = &cnf
97
+			return nil
98
+		}
99
+	}
100
+
101
+	return errors.Errorf("ChainConfig for chainID %q not found", chainID)
102
+}

+ 144
- 0
crypto/keybag.go View File

@@ -0,0 +1,144 @@
1
+package crypto
2
+
3
+import (
4
+	"bufio"
5
+	"os"
6
+	"strings"
7
+
8
+	"eprosume/bitshares/types"
9
+	"eprosume/bitshares/util"
10
+	"github.com/juju/errors"
11
+)
12
+
13
+// KeyBag is a PrivateKey collection for signing and verifying purposes.
14
+type KeyBag struct {
15
+	keys []*types.PrivateKey
16
+}
17
+
18
+func NewKeyBag() *KeyBag {
19
+	bag := KeyBag{
20
+		keys: make([]*types.PrivateKey, 0),
21
+	}
22
+
23
+	return &bag
24
+}
25
+
26
+func (p KeyBag) Marshal(enc *util.TypeEncoder) error {
27
+	if err := enc.EncodeUVarint(uint64(len(p.keys))); err != nil {
28
+		return errors.Annotate(err, "encode length")
29
+	}
30
+
31
+	for _, k := range p.keys {
32
+		if err := enc.Encode(k); err != nil {