diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..ccb2431d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "printWidth": 100, + "arrowParens": "avoid", + "bracketSpacing": true, + "singleQuote": true, + "semi": true, + "trailingComma": "all", + "tabWidth": 4, + "useTabs": false, + "bracketSameLine": false +} diff --git a/api/types.go b/api/types.go index 23d4dd1f..a6b56035 100644 --- a/api/types.go +++ b/api/types.go @@ -158,7 +158,7 @@ type MultiTokenValue struct { // Token contains info about tokens held by an address type Token struct { - Type bchain.TokenTypeName `json:"type"` + Type bchain.TokenTypeName `json:"type" ts_type:"'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155'"` Name string `json:"name"` Path string `json:"path,omitempty"` Contract string `json:"contract,omitempty"` @@ -263,7 +263,7 @@ type Tx struct { FeesSat *Amount `json:"fees,omitempty"` Hex string `json:"hex,omitempty"` Rbf bool `json:"rbf,omitempty"` - CoinSpecificData json.RawMessage `json:"coinSpecificData,omitempty"` + CoinSpecificData json.RawMessage `json:"coinSpecificData,omitempty" ts_type:"any"` TokenTransfers []TokenTransfer `json:"tokenTransfers,omitempty"` EthereumSpecific *EthereumSpecific `json:"ethereumSpecific,omitempty"` AddressAliases AddressAliasesMap `json:"addressAliases,omitempty"` diff --git a/blockbook-api.d.ts b/blockbook-api.d.ts new file mode 100644 index 00000000..adb058e7 --- /dev/null +++ b/blockbook-api.d.ts @@ -0,0 +1,412 @@ +/* Do not change, this code is generated from Golang structs */ + +export interface APIError { + Text: string; + Public: boolean; +} +export interface AddressAlias { + Type: string; + Alias: string; +} +export interface EthereumInternalTransfer { + type: number; + from: string; + to: string; + value?: string; +} +export interface EthereumParsedInputParam { + type: string; + values?: string[]; +} +export interface EthereumParsedInputData { + methodId: string; + name: string; + function?: string; + params?: EthereumParsedInputParam[]; +} +export interface EthereumSpecific { + type?: number; + createdContract?: string; + status: number; + error?: string; + nonce: number; + gasLimit?: string; + gasUsed?: string; + gasPrice?: string; + data?: string; + parsedData?: EthereumParsedInputData; + internalTransfers?: EthereumInternalTransfer[]; +} +export interface MultiTokenValue { + id?: string; + value?: string; +} +export interface TokenTransfer { + type: string; + from: string; + to: string; + contract: string; + name: string; + symbol: string; + decimals: number; + value?: string; + multiTokenValues?: MultiTokenValue[]; +} +export interface Vout { + value?: string; + n: number; + spent?: boolean; + spentTxId?: string; + spentIndex?: number; + spentHeight?: number; + hex?: string; + asm?: string; + addresses: string[]; + isAddress: boolean; + isOwn?: boolean; + type?: string; +} +export interface Vin { + txid?: string; + vout?: number; + sequence?: number; + n: number; + addresses?: string[]; + isAddress: boolean; + isOwn?: boolean; + value?: string; + hex?: string; + asm?: string; + coinbase?: string; +} +export interface Tx { + txid: string; + version?: number; + lockTime?: number; + vin: Vin[]; + vout: Vout[]; + blockHash?: string; + blockHeight: number; + confirmations: number; + confirmationETABlocks?: number; + confirmationETASeconds?: number; + blockTime: number; + size?: number; + vsize?: number; + value?: string; + valueIn?: string; + fees?: string; + hex?: string; + rbf?: boolean; + coinSpecificData?: any; + tokenTransfers?: TokenTransfer[]; + ethereumSpecific?: EthereumSpecific; + addressAliases?: { [key: string]: AddressAlias }; +} +export interface FeeStats { + txCount: number; + totalFeesSat?: string; + averageFeePerKb: number; + decilesFeePerKb: number[]; +} +export interface ContractInfo { + type: string; + contract: string; + name: string; + symbol: string; + decimals: number; + createdInBlock?: number; + destructedInBlock?: number; +} +export interface Token { + type: 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155'; + name: string; + path?: string; + contract?: string; + transfers: number; + symbol?: string; + decimals?: number; + balance?: string; + baseValue?: number; + secondaryValue?: number; + ids?: string[]; + multiTokenValues?: MultiTokenValue[]; + totalReceived?: string; + totalSent?: string; +} +export interface Address { + page?: number; + totalPages?: number; + itemsOnPage?: number; + address: string; + balance?: string; + totalReceived?: string; + totalSent?: string; + unconfirmedBalance?: string; + unconfirmedTxs: number; + txs: number; + nonTokenTxs?: number; + internalTxs?: number; + transactions?: Tx[]; + txids?: string[]; + nonce?: string; + usedTokens?: number; + tokens?: Token[]; + secondaryValue?: number; + tokensBaseValue?: number; + tokensSecondaryValue?: number; + totalBaseValue?: number; + totalSecondaryValue?: number; + contractInfo?: ContractInfo; + erc20Contract?: ContractInfo; + addressAliases?: { [key: string]: AddressAlias }; +} +export interface Utxo { + txid: string; + vout: number; + value?: string; + height?: number; + confirmations: number; + address?: string; + path?: string; + lockTime?: number; + coinbase?: boolean; +} +export interface BalanceHistory { + time: number; + txs: number; + received?: string; + sent?: string; + sentToSelf?: string; + rates?: { [key: string]: number }; + txid?: string; +} +export interface BlockInfo { + Hash: string; + Time: number; + Txs: number; + Size: number; + Height: number; +} +export interface Blocks { + page?: number; + totalPages?: number; + itemsOnPage?: number; + blocks: BlockInfo[]; +} +export interface Block { + page?: number; + totalPages?: number; + itemsOnPage?: number; + hash: string; + previousBlockHash?: string; + nextBlockHash?: string; + height: number; + confirmations: number; + size: number; + time?: number; + version: string; + merkleRoot: string; + nonce: string; + bits: string; + difficulty: string; + tx?: string[]; + txCount: number; + txs?: Tx[]; + addressAliases?: { [key: string]: AddressAlias }; +} +export interface BlockRaw { + hex: string; +} +export interface BackendInfo { + error?: string; + chain?: string; + blocks?: number; + headers?: number; + bestBlockHash?: string; + difficulty?: string; + sizeOnDisk?: number; + version?: string; + subversion?: string; + protocolVersion?: string; + timeOffset?: number; + warnings?: string; + consensus_version?: string; + consensus?: any; +} +export interface InternalStateColumn { + name: string; + version: number; + rows: number; + keyBytes: number; + valueBytes: number; + updated: string; +} +export interface BlockbookInfo { + coin: string; + host: string; + version: string; + gitCommit: string; + buildTime: string; + syncMode: boolean; + initialSync: boolean; + inSync: boolean; + bestHeight: number; + lastBlockTime: string; + inSyncMempool: boolean; + lastMempoolTime: string; + mempoolSize: number; + decimals: number; + dbSize: number; + hasFiatRates?: boolean; + hasTokenFiatRates?: boolean; + currentFiatRatesTime?: string; + historicalFiatRatesTime?: string; + historicalTokenFiatRatesTime?: string; + dbSizeFromColumns?: number; + dbColumns?: InternalStateColumn[]; + about: string; +} +export interface SystemInfo { + blockbook?: BlockbookInfo; + backend?: BackendInfo; +} +export interface FiatTicker { + ts?: number; + rates: { [key: string]: number }; + error?: string; +} +export interface FiatTickers { + tickers: FiatTicker[]; +} +export interface AvailableVsCurrencies { + ts?: number; + available_currencies: string[]; + error?: string; +} +export interface WsReq { + id: string; + method: + | 'getAccountInfo' + | 'getInfo' + | 'getBlockHash' + | 'getAccountUtxo' + | 'getBalanceHistory' + | 'getTransaction' + | 'getTransactionSpecific' + | 'estimateFee' + | 'sendTransaction' + | 'subscribeNewBlock' + | 'unsubscribeNewBlock' + | 'subscribeNewTransaction' + | 'unsubscribeNewTransaction' + | 'subscribeAddresses' + | 'unsubscribeAddresses' + | 'subscribeFiatRates' + | 'unsubscribeFiatRates' + | 'ping' + | 'getCurrentFiatRates' + | 'getFiatRatesForTimestamps' + | 'getFiatRatesTickersList'; + params: any; +} +export interface WsRes { + id: string; + data: any; +} +export interface WsAccountInfoReq { + descriptor: string; + details?: 'basic' | 'tokens' | 'tokenBalances' | 'txids' | 'txslight' | 'txs'; + tokens?: 'derived' | 'used' | 'nonzero'; + pageSize?: number; + page?: number; + from?: number; + to?: number; + contractFilter?: string; + secondaryCurrency?: string; + gap?: number; +} +export interface WsBackendInfo { + version?: string; + subversion?: string; + consensus_version?: string; + consensus?: any; +} +export interface WsInfoRes { + name: string; + shortcut: string; + decimals: number; + version: string; + bestHeight: number; + bestHash: string; + block0Hash: string; + testnet: boolean; + backend: WsBackendInfo; +} +export interface WsBlockHashReq { + height: number; +} +export interface WsBlockHashRes { + hash: string; +} +export interface WsBlockReq { + id: string; + pageSize?: number; + page?: number; +} +export interface WsAccountUtxoReq { + descriptor: string; +} +export interface WsBalanceHistoryReq { + descriptor: string; + from?: number; + to?: number; + currencies?: string[]; + gap?: number; + groupBy?: number; +} +export interface WsTransactionReq { + txid: string; +} +export interface WsTransactionSpecificReq { + txid: string; +} +export interface WsEstimateFeeReq { + blocks?: number[]; + specific?: { + conservative?: boolean; + txsize?: number; + from?: string; + to?: string; + data?: string; + value?: string; + }; +} +export interface WsEstimateFeeRes { + feePerTx?: string; + feePerUnit?: string; + feeLimit?: string; +} +export interface WsSendTransactionReq { + hex: string; +} +export interface WsSubscribeAddressesReq { + addresses: string[]; +} +export interface WsSubscribeFiatRatesReq { + currency?: string; + tokens?: string[]; +} +export interface WsCurrentFiatRatesReq { + currencies?: string[]; + token?: string; +} +export interface WsFiatRatesForTimestampsReq { + timestamps: number[]; + currencies?: string[]; + token?: string; +} +export interface WsFiatRatesTickersListReq { + timestamp?: number; + token?: string; +} diff --git a/build/tools/typescriptify/typescriptify.go b/build/tools/typescriptify/typescriptify.go new file mode 100644 index 00000000..0de13003 --- /dev/null +++ b/build/tools/typescriptify/typescriptify.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "math/big" + "time" + + "github.com/tkrajina/typescriptify-golang-structs/typescriptify" + "github.com/trezor/blockbook/api" + "github.com/trezor/blockbook/server" +) + +func main() { + t := typescriptify.New() + t.CreateInterface = true + t.Indent = " " + t.BackupDir = "" + + t.ManageType(api.Amount{}, typescriptify.TypeOptions{TSType: "string"}) + t.ManageType([]api.Amount{}, typescriptify.TypeOptions{TSType: "string[]"}) + t.ManageType(big.Int{}, typescriptify.TypeOptions{TSType: "string"}) + t.ManageType(time.Time{}, typescriptify.TypeOptions{TSType: "string", TSDoc: "Time in ISO 8601 YYYY-MM-DDTHH:mm:ss.sssZd"}) + + // API - REST and Websocket + t.Add(api.APIError{}) + t.Add(api.Tx{}) + t.Add(api.FeeStats{}) + t.Add(api.Address{}) + t.Add(api.Utxo{}) + t.Add(api.BalanceHistory{}) + t.Add(api.Blocks{}) + t.Add(api.Block{}) + t.Add(api.BlockRaw{}) + t.Add(api.SystemInfo{}) + t.Add(api.FiatTicker{}) + t.Add(api.FiatTickers{}) + t.Add(api.AvailableVsCurrencies{}) + + // Websocket specific + t.Add(server.WsReq{}) + t.Add(server.WsRes{}) + t.Add(server.WsAccountInfoReq{}) + t.Add(server.WsInfoRes{}) + t.Add(server.WsBlockHashReq{}) + t.Add(server.WsBlockHashRes{}) + t.Add(server.WsBlockReq{}) + t.Add(server.WsAccountUtxoReq{}) + t.Add(server.WsBalanceHistoryReq{}) + t.Add(server.WsTransactionReq{}) + t.Add(server.WsTransactionSpecificReq{}) + t.Add(server.WsEstimateFeeReq{}) + t.Add(server.WsEstimateFeeRes{}) + t.Add(server.WsSendTransactionReq{}) + t.Add(server.WsSubscribeAddressesReq{}) + t.Add(server.WsSubscribeFiatRatesReq{}) + t.Add(server.WsCurrentFiatRatesReq{}) + t.Add(server.WsFiatRatesForTimestampsReq{}) + t.Add(server.WsFiatRatesTickersListReq{}) + + err := t.ConvertToFile("blockbook-api.d.ts") + if err != nil { + panic(err.Error()) + } + fmt.Println("OK") +} diff --git a/go.mod b/go.mod index a47d5548..9d0da168 100644 --- a/go.mod +++ b/go.mod @@ -75,6 +75,8 @@ require ( github.com/stretchr/testify v1.8.1 // indirect github.com/tklauser/go-sysconf v0.3.5 // indirect github.com/tklauser/numcpus v0.2.2 // indirect + github.com/tkrajina/go-reflector v0.5.5 // indirect + github.com/tkrajina/typescriptify-golang-structs v0.1.10 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5 // indirect golang.org/x/sys v0.5.0 // indirect diff --git a/go.sum b/go.sum index d7bff8ff..bf574ce6 100644 --- a/go.sum +++ b/go.sum @@ -367,6 +367,10 @@ github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= +github.com/tkrajina/go-reflector v0.5.5 h1:gwoQFNye30Kk7NrExj8zm3zFtrGPqOkzFMLuQZg1DtQ= +github.com/tkrajina/go-reflector v0.5.5/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= +github.com/tkrajina/typescriptify-golang-structs v0.1.10 h1:W/Ta9Kqo2lV+7bVXuQoUhZ0bDlnjwtPpKsy3A9M1nYg= +github.com/tkrajina/typescriptify-golang-structs v0.1.10/go.mod h1:sjU00nti/PMEOZb07KljFlR+lJ+RotsC0GBQMv9EKls= github.com/tyler-smith/go-bip39 v1.0.2 h1:+t3w+KwLXO6154GNJY+qUtIxLTmFjfUmpguQT1OlOT8= github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= diff --git a/server/public_test.go b/server/public_test.go index 65871377..eba62945 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -1262,7 +1262,7 @@ func websocketTestsBitcoinType(t *testing.T, ts *httptest.Server) { "timestamps": []string{"yesterday"}, }, }, - want: `{"id":"23","data":{"error":{"message":"json: cannot unmarshal string into Go struct field .timestamps of type int64"}}}`, + want: `{"id":"23","data":{"error":{"message":"json: cannot unmarshal string into Go struct field WsFiatRatesForTimestampsReq.timestamps of type int64"}}}`, }, { name: "websocket getFiatRatesForTimestamps empty currency", diff --git a/server/websocket.go b/server/websocket.go index 4ff5fd53..38f34297 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -34,21 +34,10 @@ var ( connectionCounter uint64 ) -type websocketReq struct { - ID string `json:"id"` - Method string `json:"method"` - Params json.RawMessage `json:"params"` -} - -type websocketRes struct { - ID string `json:"id"` - Data interface{} `json:"data"` -} - type websocketChannel struct { id uint64 conn *websocket.Conn - out chan *websocketRes + out chan *WsRes ip string requestHeader http.Header alive bool @@ -56,11 +45,6 @@ type websocketChannel struct { addrDescs []string // subscribed address descriptors as strings } -type fiatRatesSubscription struct { - Currency string `json:"currency"` - Tokens []string `json:"tokens"` -} - // WebsocketServer is a handle to websocket server type WebsocketServer struct { upgrader *websocket.Upgrader @@ -147,7 +131,7 @@ func (s *WebsocketServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { c := &websocketChannel{ id: atomic.AddUint64(&connectionCounter, 1), conn: conn, - out: make(chan *websocketRes, outChannelSize), + out: make(chan *WsRes, outChannelSize), ip: getIP(r), requestHeader: r.Header, alive: true, @@ -184,7 +168,7 @@ func (c *websocketChannel) CloseOut() bool { return false } -func (c *websocketChannel) DataOut(data *websocketRes) { +func (c *websocketChannel) DataOut(data *WsRes) { c.aliveLock.Lock() defer c.aliveLock.Unlock() if c.alive { @@ -215,7 +199,7 @@ func (s *WebsocketServer) inputLoop(c *websocketChannel) { } switch t { case websocket.TextMessage: - var req websocketReq + var req WsReq err := json.Unmarshal(d, &req) if err != nil { glog.Error("Error parsing message from ", c.id, ", ", string(d), ", ", err) @@ -229,7 +213,6 @@ func (s *WebsocketServer) inputLoop(c *websocketChannel) { return case websocket.PingMessage: c.conn.WriteControl(websocket.PongMessage, nil, time.Now().Add(defaultTimeout)) - break case websocket.CloseMessage: s.closeChannel(c) return @@ -270,36 +253,30 @@ func (s *WebsocketServer) onDisconnect(c *websocketChannel) { s.metrics.WebsocketClients.Dec() } -var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *websocketReq) (interface{}, error){ - "getAccountInfo": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { +var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *WsReq) (interface{}, error){ + "getAccountInfo": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { r, err := unmarshalGetAccountInfoRequest(req.Params) if err == nil { rv, err = s.getAccountInfo(r) } return }, - "getInfo": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "getInfo": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.getInfo() }, - "getBlockHash": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Height int `json:"height"` - }{} + "getBlockHash": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsBlockHashReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getBlockHash(r.Height) } return }, - "getBlock": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "getBlock": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { if !s.is.ExtendedIndex { return nil, errors.New("Not supported") } - r := struct { - Id string `json:"id"` - PageSize int `json:"pageSize"` - Page int `json:"page"` - }{} + r := WsBlockReq{} err = json.Unmarshal(req.Params, &r) if r.PageSize == 0 { r.PageSize = 1000000 @@ -309,25 +286,16 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs } return }, - "getAccountUtxo": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Descriptor string `json:"descriptor"` - }{} + "getAccountUtxo": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsAccountUtxoReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getAccountUtxo(r.Descriptor) } return }, - "getBalanceHistory": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Descriptor string `json:"descriptor"` - From int64 `json:"from"` - To int64 `json:"to"` - Currencies []string `json:"currencies"` - Gap int `json:"gap"` - GroupBy uint32 `json:"groupBy"` - }{} + "getBalanceHistory": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsBalanceHistoryReq{} err = json.Unmarshal(req.Params, &r) if err == nil { if r.From <= 0 { @@ -346,63 +314,57 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs } return }, - "getTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Txid string `json:"txid"` - }{} + "getTransaction": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsTransactionReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getTransaction(r.Txid) } return }, - "getTransactionSpecific": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Txid string `json:"txid"` - }{} + "getTransactionSpecific": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsTransactionSpecificReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getTransactionSpecific(r.Txid) } return }, - "estimateFee": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "estimateFee": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.estimateFee(c, req.Params) }, - "sendTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Hex string `json:"hex"` - }{} + "sendTransaction": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsSendTransactionReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.sendTransaction(r.Hex) } return }, - "subscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "subscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.subscribeNewBlock(c, req) }, - "unsubscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "unsubscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.unsubscribeNewBlock(c) }, - "subscribeNewTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "subscribeNewTransaction": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.subscribeNewTransaction(c, req) }, - "unsubscribeNewTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "unsubscribeNewTransaction": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.unsubscribeNewTransaction(c) }, - "subscribeAddresses": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "subscribeAddresses": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { ad, err := s.unmarshalAddresses(req.Params) if err == nil { rv, err = s.subscribeAddresses(c, ad, req) } return }, - "unsubscribeAddresses": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "unsubscribeAddresses": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.unsubscribeAddresses(c) }, - "subscribeFiatRates": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - var r fiatRatesSubscription + "subscribeFiatRates": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + var r WsSubscribeFiatRatesReq err = json.Unmarshal(req.Params, &r) if err != nil { return nil, err @@ -413,41 +375,31 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs } return s.subscribeFiatRates(c, &r, req) }, - "unsubscribeFiatRates": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "unsubscribeFiatRates": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.unsubscribeFiatRates(c) }, - "ping": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + "ping": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { r := struct{}{} return r, nil }, - "getCurrentFiatRates": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Currencies []string `json:"currencies"` - Token string `json:"token"` - }{} + "getCurrentFiatRates": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsCurrentFiatRatesReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getCurrentFiatRates(r.Currencies, r.Token) } return }, - "getFiatRatesForTimestamps": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Timestamps []int64 `json:"timestamps"` - Currencies []string `json:"currencies"` - Token string `json:"token"` - }{} + "getFiatRatesForTimestamps": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsFiatRatesForTimestampsReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getFiatRatesForTimestamps(r.Timestamps, r.Currencies, r.Token) } return }, - "getFiatRatesTickersList": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { - r := struct { - Timestamp int64 `json:"timestamp"` - Token string `json:"token"` - }{} + "getFiatRatesTickersList": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsFiatRatesTickersListReq{} err = json.Unmarshal(req.Params, &r) if err == nil { rv, err = s.getAvailableVsCurrencies(r.Timestamp, r.Token) @@ -456,7 +408,7 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs }, } -func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) { +func (s *WebsocketServer) onRequest(c *websocketChannel, req *WsReq) { var err error var data interface{} defer func() { @@ -469,7 +421,7 @@ func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) { } // nil data means no response if data != nil { - c.DataOut(&websocketRes{ + c.DataOut(&WsRes{ ID: req.ID, Data: data, }) @@ -499,21 +451,8 @@ func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) { } } -type accountInfoReq struct { - Descriptor string `json:"descriptor"` - Details string `json:"details"` - Tokens string `json:"tokens"` - PageSize int `json:"pageSize"` - Page int `json:"page"` - FromHeight int `json:"from"` - ToHeight int `json:"to"` - ContractFilter string `json:"contractFilter"` - SecondaryCurrency string `json:"secondaryCurrency"` - Gap int `json:"gap"` -} - -func unmarshalGetAccountInfoRequest(params []byte) (*accountInfoReq, error) { - var r accountInfoReq +func unmarshalGetAccountInfoRequest(params []byte) (*WsAccountInfoReq, error) { + var r WsAccountInfoReq err := json.Unmarshal(params, &r) if err != nil { return nil, err @@ -521,7 +460,7 @@ func unmarshalGetAccountInfoRequest(params []byte) (*accountInfoReq, error) { return &r, nil } -func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address, err error) { +func (s *WebsocketServer) getAccountInfo(req *WsAccountInfoReq) (res *api.Address, err error) { var opt api.AccountDetails switch req.Details { case "tokens": @@ -563,7 +502,7 @@ func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address, return a, nil } -func (s *WebsocketServer) getAccountUtxo(descriptor string) (interface{}, error) { +func (s *WebsocketServer) getAccountUtxo(descriptor string) (api.Utxos, error) { utxo, err := s.api.GetXpubUtxo(descriptor, false, 0) if err != nil { return s.api.GetAddressUtxo(descriptor, false) @@ -571,7 +510,7 @@ func (s *WebsocketServer) getAccountUtxo(descriptor string) (interface{}, error) return utxo, nil } -func (s *WebsocketServer) getTransaction(txid string) (interface{}, error) { +func (s *WebsocketServer) getTransaction(txid string) (*api.Tx, error) { return s.api.GetTransaction(txid, false, false) } @@ -579,31 +518,14 @@ func (s *WebsocketServer) getTransactionSpecific(txid string) (interface{}, erro return s.chain.GetTransactionSpecific(&bchain.Tx{Txid: txid}) } -func (s *WebsocketServer) getInfo() (interface{}, error) { +func (s *WebsocketServer) getInfo() (*WsInfoRes, error) { vi := common.GetVersionInfo() bi := s.is.GetBackendInfo() height, hash, err := s.db.GetBestBlock() if err != nil { return nil, err } - type backendInfo struct { - Version string `json:"version,omitempty"` - Subversion string `json:"subversion,omitempty"` - ConsensusVersion string `json:"consensus_version,omitempty"` - Consensus interface{} `json:"consensus,omitempty"` - } - type info struct { - Name string `json:"name"` - Shortcut string `json:"shortcut"` - Decimals int `json:"decimals"` - Version string `json:"version"` - BestHeight int `json:"bestHeight"` - BestHash string `json:"bestHash"` - Block0Hash string `json:"block0Hash"` - Testnet bool `json:"testnet"` - Backend backendInfo `json:"backend"` - } - return &info{ + return &WsInfoRes{ Name: s.is.Coin, Shortcut: s.is.CoinShortcut, Decimals: s.chainParser.AmountDecimals(), @@ -612,7 +534,7 @@ func (s *WebsocketServer) getInfo() (interface{}, error) { Version: vi.Version, Block0Hash: s.block0hash, Testnet: s.chain.IsTestnet(), - Backend: backendInfo{ + Backend: WsBackendInfo{ Version: bi.Version, Subversion: bi.Subversion, ConsensusVersion: bi.ConsensusVersion, @@ -621,15 +543,12 @@ func (s *WebsocketServer) getInfo() (interface{}, error) { }, nil } -func (s *WebsocketServer) getBlockHash(height int) (interface{}, error) { +func (s *WebsocketServer) getBlockHash(height int) (*WsBlockHashRes, error) { h, err := s.db.GetBlockHash(uint32(height)) if err != nil { return nil, err } - type hash struct { - Hash string `json:"hash"` - } - return &hash{ + return &WsBlockHashRes{ Hash: h, }, nil } @@ -643,21 +562,12 @@ func (s *WebsocketServer) getBlock(id string, page, pageSize int) (interface{}, } func (s *WebsocketServer) estimateFee(c *websocketChannel, params []byte) (interface{}, error) { - type estimateFeeReq struct { - Blocks []int `json:"blocks"` - Specific map[string]interface{} `json:"specific"` - } - type estimateFeeRes struct { - FeePerTx string `json:"feePerTx,omitempty"` - FeePerUnit string `json:"feePerUnit,omitempty"` - FeeLimit string `json:"feeLimit,omitempty"` - } - var r estimateFeeReq + var r WsEstimateFeeReq err := json.Unmarshal(params, &r) if err != nil { return nil, err } - res := make([]estimateFeeRes, len(r.Blocks)) + res := make([]WsEstimateFeeRes, len(r.Blocks)) if s.chainParser.GetChainType() == bchain.ChainEthereumType { gas, err := s.chain.EthereumTypeEstimateGas(r.Specific) if err != nil { @@ -729,7 +639,7 @@ type subscriptionResponseMessage struct { Message string `json:"message"` } -func (s *WebsocketServer) subscribeNewBlock(c *websocketChannel, req *websocketReq) (res interface{}, err error) { +func (s *WebsocketServer) subscribeNewBlock(c *websocketChannel, req *WsReq) (res interface{}, err error) { s.newBlockSubscriptionsLock.Lock() defer s.newBlockSubscriptionsLock.Unlock() s.newBlockSubscriptions[c] = req.ID @@ -745,7 +655,7 @@ func (s *WebsocketServer) unsubscribeNewBlock(c *websocketChannel) (res interfac return &subscriptionResponse{false}, nil } -func (s *WebsocketServer) subscribeNewTransaction(c *websocketChannel, req *websocketReq) (res interface{}, err error) { +func (s *WebsocketServer) subscribeNewTransaction(c *websocketChannel, req *WsReq) (res interface{}, err error) { s.newTransactionSubscriptionsLock.Lock() defer s.newTransactionSubscriptionsLock.Unlock() if !s.newTransactionEnabled { @@ -768,9 +678,7 @@ func (s *WebsocketServer) unsubscribeNewTransaction(c *websocketChannel) (res in } func (s *WebsocketServer) unmarshalAddresses(params []byte) ([]string, error) { - r := struct { - Addresses []string `json:"addresses"` - }{} + r := WsSubscribeAddressesReq{} err := json.Unmarshal(params, &r) if err != nil { return nil, err @@ -804,7 +712,7 @@ func (s *WebsocketServer) doUnsubscribeAddresses(c *websocketChannel) { c.addrDescs = nil } -func (s *WebsocketServer) subscribeAddresses(c *websocketChannel, addrDesc []string, req *websocketReq) (res interface{}, err error) { +func (s *WebsocketServer) subscribeAddresses(c *websocketChannel, addrDesc []string, req *WsReq) (res interface{}, err error) { s.addressSubscriptionsLock.Lock() defer s.addressSubscriptionsLock.Unlock() // unsubscribe all previous subscriptions @@ -847,7 +755,7 @@ func (s *WebsocketServer) doUnsubscribeFiatRates(c *websocketChannel) { } // subscribeFiatRates subscribes all FiatRates subscriptions by this channel -func (s *WebsocketServer) subscribeFiatRates(c *websocketChannel, d *fiatRatesSubscription, req *websocketReq) (res interface{}, err error) { +func (s *WebsocketServer) subscribeFiatRates(c *websocketChannel, d *WsSubscribeFiatRatesReq, req *WsReq) (res interface{}, err error) { s.fiatRatesSubscriptionsLock.Lock() defer s.fiatRatesSubscriptionsLock.Unlock() // unsubscribe all previous subscriptions @@ -891,7 +799,7 @@ func (s *WebsocketServer) onNewBlockAsync(hash string, height uint32) { Hash: hash, } for c, id := range s.newBlockSubscriptions { - c.DataOut(&websocketRes{ + c.DataOut(&WsRes{ ID: id, Data: &data, }) @@ -908,7 +816,7 @@ func (s *WebsocketServer) sendOnNewTx(tx *api.Tx) { s.newTransactionSubscriptionsLock.Lock() defer s.newTransactionSubscriptionsLock.Unlock() for c, id := range s.newTransactionSubscriptions { - c.DataOut(&websocketRes{ + c.DataOut(&WsRes{ ID: id, Data: &tx, }) @@ -936,7 +844,7 @@ func (s *WebsocketServer) sendOnNewTxAddr(stringAddressDescriptor string, tx *ap as, ok := s.addressSubscriptions[stringAddressDescriptor] if ok { for c, id := range as { - c.DataOut(&websocketRes{ + c.DataOut(&WsRes{ ID: id, Data: &data, }) @@ -1038,12 +946,12 @@ func (s *WebsocketServer) broadcastTicker(currency string, rates map[string]floa dataWithTokens.TokenRates[token] = rate } } - c.DataOut(&websocketRes{ + c.DataOut(&WsRes{ ID: id, Data: &dataWithTokens, }) } else { - c.DataOut(&websocketRes{ + c.DataOut(&WsRes{ ID: id, Data: &data, }) @@ -1063,17 +971,17 @@ func (s *WebsocketServer) OnNewFiatRatesTicker(ticker *common.CurrencyRatesTicke s.broadcastTicker(allFiatRates, ticker.Rates, nil) } -func (s *WebsocketServer) getCurrentFiatRates(currencies []string, token string) (interface{}, error) { +func (s *WebsocketServer) getCurrentFiatRates(currencies []string, token string) (*api.FiatTicker, error) { ret, err := s.api.GetCurrentFiatRates(currencies, strings.ToLower(token)) return ret, err } -func (s *WebsocketServer) getFiatRatesForTimestamps(timestamps []int64, currencies []string, token string) (interface{}, error) { +func (s *WebsocketServer) getFiatRatesForTimestamps(timestamps []int64, currencies []string, token string) (*api.FiatTickers, error) { ret, err := s.api.GetFiatRatesForTimestamps(timestamps, currencies, strings.ToLower(token)) return ret, err } -func (s *WebsocketServer) getAvailableVsCurrencies(timestamp int64, token string) (interface{}, error) { +func (s *WebsocketServer) getAvailableVsCurrencies(timestamp int64, token string) (*api.AvailableVsCurrencies, error) { ret, err := s.api.GetAvailableVsCurrencies(timestamp, strings.ToLower(token)) return ret, err } diff --git a/server/ws_types.go b/server/ws_types.go new file mode 100644 index 00000000..edcaabe8 --- /dev/null +++ b/server/ws_types.go @@ -0,0 +1,120 @@ +package server + +import "encoding/json" + +type WsReq struct { + ID string `json:"id"` + Method string `json:"method" ts_type:"'getAccountInfo' | 'getInfo' | 'getBlockHash' | 'getAccountUtxo' | 'getBalanceHistory' | 'getTransaction' | 'getTransactionSpecific' | 'estimateFee' | 'sendTransaction' | 'subscribeNewBlock' | 'unsubscribeNewBlock' | 'subscribeNewTransaction' | 'unsubscribeNewTransaction' | 'subscribeAddresses' | 'unsubscribeAddresses' | 'subscribeFiatRates' | 'unsubscribeFiatRates' | 'ping' | 'getCurrentFiatRates' | 'getFiatRatesForTimestamps' | 'getFiatRatesTickersList'"` + Params json.RawMessage `json:"params" ts_type:"any"` +} + +type WsRes struct { + ID string `json:"id"` + Data interface{} `json:"data"` +} + +type WsAccountInfoReq struct { + Descriptor string `json:"descriptor"` + Details string `json:"details,omitempty" ts_type:"'basic' | 'tokens' | 'tokenBalances' | 'txids' | 'txslight' | 'txs'"` + Tokens string `json:"tokens,omitempty" ts_type:"'derived' | 'used' | 'nonzero'"` + PageSize int `json:"pageSize,omitempty"` + Page int `json:"page,omitempty"` + FromHeight int `json:"from,omitempty"` + ToHeight int `json:"to,omitempty"` + ContractFilter string `json:"contractFilter,omitempty"` + SecondaryCurrency string `json:"secondaryCurrency,omitempty"` + Gap int `json:"gap,omitempty"` +} + +type WsBackendInfo struct { + Version string `json:"version,omitempty"` + Subversion string `json:"subversion,omitempty"` + ConsensusVersion string `json:"consensus_version,omitempty"` + Consensus interface{} `json:"consensus,omitempty"` +} + +type WsInfoRes struct { + Name string `json:"name"` + Shortcut string `json:"shortcut"` + Decimals int `json:"decimals"` + Version string `json:"version"` + BestHeight int `json:"bestHeight"` + BestHash string `json:"bestHash"` + Block0Hash string `json:"block0Hash"` + Testnet bool `json:"testnet"` + Backend WsBackendInfo `json:"backend"` +} + +type WsBlockHashReq struct { + Height int `json:"height"` +} + +type WsBlockHashRes struct { + Hash string `json:"hash"` +} + +type WsBlockReq struct { + Id string `json:"id"` + PageSize int `json:"pageSize,omitempty"` + Page int `json:"page,omitempty"` +} + +type WsAccountUtxoReq struct { + Descriptor string `json:"descriptor"` +} + +type WsBalanceHistoryReq struct { + Descriptor string `json:"descriptor"` + From int64 `json:"from,omitempty"` + To int64 `json:"to,omitempty"` + Currencies []string `json:"currencies,omitempty"` + Gap int `json:"gap,omitempty"` + GroupBy uint32 `json:"groupBy,omitempty"` +} + +type WsTransactionReq struct { + Txid string `json:"txid"` +} + +type WsTransactionSpecificReq struct { + Txid string `json:"txid"` +} + +type WsEstimateFeeReq struct { + Blocks []int `json:"blocks,omitempty"` + Specific map[string]interface{} `json:"specific,omitempty" ts_type:"{conservative?: boolean;txsize?: number;from?: string;to?: string;data?: string;value?: string;}"` +} + +type WsEstimateFeeRes struct { + FeePerTx string `json:"feePerTx,omitempty"` + FeePerUnit string `json:"feePerUnit,omitempty"` + FeeLimit string `json:"feeLimit,omitempty"` +} + +type WsSendTransactionReq struct { + Hex string `json:"hex"` +} + +type WsSubscribeAddressesReq struct { + Addresses []string `json:"addresses"` +} +type WsSubscribeFiatRatesReq struct { + Currency string `json:"currency,omitempty"` + Tokens []string `json:"tokens,omitempty"` +} + +type WsCurrentFiatRatesReq struct { + Currencies []string `json:"currencies,omitempty"` + Token string `json:"token,omitempty"` +} + +type WsFiatRatesForTimestampsReq struct { + Timestamps []int64 `json:"timestamps"` + Currencies []string `json:"currencies,omitempty"` + Token string `json:"token,omitempty"` +} + +type WsFiatRatesTickersListReq struct { + Timestamp int64 `json:"timestamp,omitempty"` + Token string `json:"token,omitempty"` +}