diff --git a/README.md b/README.md index 44fac06..a88c4f2 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ import ( - [Revert](#revert) - [Type](#type) - [Scope](#scope) -- [| **markdown** | Markdown files |](#-markdown--markdown-files-) - [Subject](#subject) - [Body](#body) - [Footer](#footer) @@ -429,6 +428,7 @@ Samples: ``` docs(markdown): update readme examples + ``` ``` @@ -482,6 +482,7 @@ The following is the list of supported scopes: | **endpoint** | Changes related to api endpoints | | **godoc** | Go documentation | | **markdown** | Markdown files | + --- #### Subject diff --git a/account.go b/account.go index 62f7008..495e171 100644 --- a/account.go +++ b/account.go @@ -117,7 +117,11 @@ type ( // GetAccountList returns a list of all accounts. func (c *Client) GetAccountList(ctx context.Context) (res *AccountListResponse, err error) { res = &AccountListResponse{} - rsp, _ := c.request(ctx, &res.Response, "GET", "/account_list", nil, nil, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/account_list", nil, nil, nil) + + if err != nil { + return + } accs := []struct { ID StakeAddress `json:"id"` @@ -145,7 +149,10 @@ func (c *Client) GetAccountInfo(ctx context.Context, addr Address) (res *Account params := url.Values{} params.Set("_address", string(addr)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/account_info", nil, params, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/account_info", nil, params, nil) + if err != nil { + return + } addrs := []AccountInfo{} err = readAndUnmarshalResponse(rsp, &res.Response, &addrs) @@ -153,7 +160,6 @@ func (c *Client) GetAccountInfo(ctx context.Context, addr Address) (res *Account if len(addrs) == 1 { res.Data = &addrs[0] } - res.ready() return } @@ -171,10 +177,11 @@ func (c *Client) GetAccountRewards( params.Set("_epoch_no", fmt.Sprint(*epoch)) } - rsp, _ := c.request(ctx, &res.Response, "GET", "/account_rewards", nil, params, nil) - + rsp, err := c.request(ctx, &res.Response, "GET", "/account_rewards", nil, params, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() return } @@ -188,10 +195,11 @@ func (c *Client) GetAccountUpdates( params := url.Values{} params.Set("_stake_address", string(addr)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/account_updates", nil, params, nil) - + rsp, err := c.request(ctx, &res.Response, "GET", "/account_updates", nil, params, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() return } @@ -204,8 +212,10 @@ func (c *Client) GetAccountAddresses( params := url.Values{} params.Set("_address", string(addr)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/account_addresses", nil, params, nil) - + rsp, err := c.request(ctx, &res.Response, "GET", "/account_addresses", nil, params, nil) + if err != nil { + return + } addrs := []struct { Addr Address `json:"address"` }{} @@ -217,7 +227,6 @@ func (c *Client) GetAccountAddresses( res.Data = append(res.Data, a.Addr) } } - res.ready() return } @@ -230,10 +239,11 @@ func (c *Client) GetAccountAssets( params := url.Values{} params.Set("_address", string(addr)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/account_assets", nil, params, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/account_assets", nil, params, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - - res.ready() return } @@ -246,9 +256,10 @@ func (c *Client) GetAccountHistory( params := url.Values{} params.Set("_address", string(addr)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/account_history", nil, params, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/account_history", nil, params, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - - res.ready() return } diff --git a/address.go b/address.go index 84e8ea8..271d3cb 100644 --- a/address.go +++ b/address.go @@ -100,26 +100,27 @@ func (c *Client) GetAddressInfo(ctx context.Context, addr Address) (res *Address params := url.Values{} params.Set("_address", string(addr)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/address_info", nil, params, nil) - + rsp, err := c.request(ctx, &res.Response, "GET", "/address_info", nil, params, nil) + if err != nil { + return + } addrs := []AddressInfo{} err = readAndUnmarshalResponse(rsp, &res.Response, &addrs) if len(addrs) == 1 { res.Data = &addrs[0] } - res.ready() return } // GetAddressTxs returns the transaction hash list of input address array, // optionally filtering after specified block height (inclusive). -//nolint: dupl -func (c *Client) GetAddressTxs(ctx context.Context, addrs []Address, h uint64) (res *AddressTxsResponse, err error) { - res = &AddressTxsResponse{} + +func (c *Client) GetAddressTxs(ctx context.Context, addrs []Address, h uint64) (*AddressTxsResponse, error) { + res := &AddressTxsResponse{} if len(addrs) == 0 { - err = ErrNoAddress + err := ErrNoAddress res.applyError(nil, err) - return + return res, err } var payload = struct { @@ -136,8 +137,10 @@ func (c *Client) GetAddressTxs(ctx context.Context, addrs []Address, h uint64) ( defer w.Close() }() - rsp, _ := c.request(ctx, &res.Response, "POST", "/address_txs", rpipe, nil, nil) - + rsp, err := c.request(ctx, &res.Response, "POST", "/address_txs", rpipe, nil, nil) + if err != nil { + return res, err + } atxs := []struct { Hash TxHash `json:"tx_hash"` }{} @@ -149,7 +152,6 @@ func (c *Client) GetAddressTxs(ctx context.Context, addrs []Address, h uint64) ( res.Data = append(res.Data, tx.Hash) } } - res.ready() return res, err } @@ -165,16 +167,17 @@ func (c *Client) GetAddressAssets(ctx context.Context, addr Address) (res *Addre params := url.Values{} params.Set("_address", string(addr)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/address_assets", nil, params, nil) - + rsp, err := c.request(ctx, &res.Response, "GET", "/address_assets", nil, params, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() return } // GetCredentialTxs returns the transaction hash list of input // payment credential array, optionally filtering after specified block height (inclusive). -//nolint: dupl + func (c *Client) GetCredentialTxs( ctx context.Context, creds []PaymentCredential, @@ -201,8 +204,10 @@ func (c *Client) GetCredentialTxs( defer w.Close() }() - rsp, _ := c.request(ctx, &res.Response, "POST", "/credential_txs", rpipe, nil, nil) - + rsp, err := c.request(ctx, &res.Response, "POST", "/credential_txs", rpipe, nil, nil) + if err != nil { + return + } atxs := []struct { Hash TxHash `json:"tx_hash"` }{} @@ -214,6 +219,5 @@ func (c *Client) GetCredentialTxs( res.Data = append(res.Data, tx.Hash) } } - res.ready() return res, err } diff --git a/asset.go b/asset.go index 2af316f..d926c6b 100644 --- a/asset.go +++ b/asset.go @@ -156,7 +156,10 @@ type ( // GetAssetList returns the list of all native assets (paginated). func (c *Client) GetAssetList(ctx context.Context) (res *AssetListResponse, err error) { res = &AssetListResponse{} - rsp, _ := c.request(ctx, &res.Response, "GET", "/asset_list", nil, nil, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/asset_list", nil, nil, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) return } @@ -173,8 +176,12 @@ func (c *Client) GetAssetAddressList( params.Set("_asset_policy", string(policy)) params.Set("_asset_name", string(name)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/asset_address_list", nil, params, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/asset_address_list", nil, params, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) + return } @@ -192,15 +199,16 @@ func (c *Client) GetAssetInfo( params.Set("_asset_policy", string(policy)) params.Set("_asset_name", string(name)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/asset_info", nil, params, nil) - + rsp, err := c.request(ctx, &res.Response, "GET", "/asset_info", nil, params, nil) + if err != nil { + return + } info := []AssetInfo{} err = readAndUnmarshalResponse(rsp, &res.Response, &info) if len(info) == 1 { res.Data = &info[0] } - res.ready() return } @@ -219,15 +227,16 @@ func (c *Client) GetAssetSummary( params.Set("_asset_policy", string(policy)) params.Set("_asset_name", string(name)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/asset_summary", nil, params, nil) - + rsp, err := c.request(ctx, &res.Response, "GET", "/asset_summary", nil, params, nil) + if err != nil { + return + } summary := []AssetSummary{} err = readAndUnmarshalResponse(rsp, &res.Response, &summary) if len(summary) == 1 { res.Data = &summary[0] } - res.ready() return } @@ -244,14 +253,15 @@ func (c *Client) GetAssetTxs( params.Set("_asset_policy", string(policy)) params.Set("_asset_name", string(name)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/asset_txs", nil, params, nil) - + rsp, err := c.request(ctx, &res.Response, "GET", "/asset_txs", nil, params, nil) + if err != nil { + return + } atxs := []AssetTxs{} err = readAndUnmarshalResponse(rsp, &res.Response, &atxs) if len(atxs) == 1 { res.Data = &atxs[0] } - res.ready() return } diff --git a/block.go b/block.go index cb3d775..2040c19 100644 --- a/block.go +++ b/block.go @@ -87,9 +87,11 @@ type ( // GetBlocks returns summarised details about all blocks (paginated - latest first). func (c *Client) GetBlocks(ctx context.Context) (res *BlocksResponse, err error) { res = &BlocksResponse{} - rsp, _ := c.request(ctx, &res.Response, "GET", "/blocks", nil, nil, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/blocks", nil, nil, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() return } @@ -99,15 +101,16 @@ func (c *Client) GetBlockInfo(ctx context.Context, hash BlockHash) (res *BlockIn params := url.Values{} params.Set("_block_hash", string(hash)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/block_info", nil, params, nil) - + rsp, err := c.request(ctx, &res.Response, "GET", "/block_info", nil, params, nil) + if err != nil { + return + } blockpl := []Block{} err = readAndUnmarshalResponse(rsp, &res.Response, &blockpl) if len(blockpl) == 1 { res.Data = &blockpl[0] } - res.ready() return } @@ -118,8 +121,10 @@ func (c *Client) GetBlockTxHashes(ctx context.Context, hash BlockHash) (res *Blo params := url.Values{} params.Set("_block_hash", string(hash)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/block_txs", nil, params, nil) - + rsp, err := c.request(ctx, &res.Response, "GET", "/block_txs", nil, params, nil) + if err != nil { + return + } blockTxs := []struct { Hash TxHash `json:"tx_hash"` }{} @@ -130,6 +135,5 @@ func (c *Client) GetBlockTxHashes(ctx context.Context, hash BlockHash) (res *Blo res.Data = append(res.Data, tx.Hash) } } - res.ready() return } diff --git a/client.go b/client.go index 0bc93f2..d34eb70 100644 --- a/client.go +++ b/client.go @@ -28,6 +28,28 @@ import ( "time" ) +// WithOptions returns new light clone of client with modified options applied. +func (c *Client) WithOptions(opts ...Option) (*Client, error) { + nc := &Client{ + r: c.r, + reqStatsEnabled: c.reqStatsEnabled, + url: c.url, + commonHeaders: c.commonHeaders.Clone(), + } + // Apply provided options + for _, opt := range opts { + if err := opt.apply(c); err != nil { + return nil, err + } + } + + if nc.client == nil { + nc.client = c.client + } + + return nc, nil +} + // HEAD sends api http HEAD request to provided relative path with query params // and returns an HTTP response. func (c *Client) HEAD( @@ -68,18 +90,9 @@ func (c *Client) GET( // BaseURL returns currently used base url e.g. https://api.koios.rest/api/v0 func (c *Client) BaseURL() string { - c.mux.RLock() - defer c.mux.RUnlock() return c.url.String() } -// TotalRequests retruns number of total requests made by API client. -func (c *Client) TotalRequests() uint64 { - c.mux.RLock() - defer c.mux.RUnlock() - return c.totalReq -} - func (c *Client) request( ctx context.Context, res *Response, @@ -93,33 +106,22 @@ func (c *Client) request( ) path = strings.TrimLeft(path, "/") - c.mux.RLock() + if query == nil { requrl = c.url.ResolveReference(&url.URL{Path: path}).String() } else { requrl = c.url.ResolveReference(&url.URL{Path: path, RawQuery: query.Encode()}).String() } + if res != nil { res.RequestURL = requrl } - c.mux.RUnlock() - - // optain lock to update last ts and total - // request count. Lock will block if another request is already queued. - // e.g. in other go routine. - c.mux.Lock() - // handle rate limit - for !c.lastRequest.IsZero() && time.Since(c.lastRequest) < c.reqInterval { + if err := c.r.Wait(ctx); err != nil { + return nil, err } - c.lastRequest = time.Now() - c.totalReq++ - - // Release client so that other requests can use it. - c.mux.Unlock() - req, err := http.NewRequestWithContext(ctx, strings.ToUpper(method), requrl, body) if err != nil { if res != nil { @@ -227,14 +229,12 @@ func (c *Client) requestWithStats(req *http.Request, res *Response) (*http.Respo return rsp, nil } -func (c *Client) updateBaseURL() error { - c.mux.Lock() - defer c.mux.Unlock() - raw := fmt.Sprintf("%s://%s", c.schema, c.host) - if c.port != 80 && c.port != 443 { - raw = fmt.Sprintf("%s:%d", raw, c.port) +func (c *Client) setBaseURL(schema, host, version string, port uint16) error { + raw := fmt.Sprintf("%s://%s", schema, host) + if port != 80 && port != 443 { + raw = fmt.Sprintf("%s:%d", raw, port) } - raw += "/api/" + c.version + "/" + raw += "/api/" + version + "/" u, err := url.ParseRequestURI(raw) if err != nil { return err diff --git a/enpoint_test.go b/enpoint_test.go index 3a45a93..7eb7f48 100644 --- a/enpoint_test.go +++ b/enpoint_test.go @@ -53,6 +53,11 @@ func TestNetworkTipEndpoint(t *testing.T) { assert.Len(t, expected, 1) assert.Equal(t, &expected[0], res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetTip(context.TODO()) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestNetworkGenesiEndpoint(t *testing.T) { @@ -71,6 +76,11 @@ func TestNetworkGenesiEndpoint(t *testing.T) { assert.Len(t, expected, 1) assert.Equal(t, &expected[0], res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetGenesis(context.TODO()) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func Test404s(t *testing.T) { @@ -86,7 +96,6 @@ func Test404s(t *testing.T) { assert.Equal(t, http.StatusNotFound, res.StatusCode) // errors with stats should be same - koios.CollectRequestsStats(true)(api) res2, err := api.GetGenesis(context.TODO()) assert.Error(t, err) assert.Nil(t, res2.Data) @@ -119,6 +128,11 @@ func TestNetworkTotalsEndpoint(t *testing.T) { testHeaders(t, spec, res2.Response) assert.Len(t, expected, 1) assert.Equal(t, expected[0], res2.Data[0]) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetTotals(context.TODO(), nil) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestEpochInfoEndpoint(t *testing.T) { @@ -141,6 +155,11 @@ func TestEpochInfoEndpoint(t *testing.T) { assert.Len(t, expected, 1) assert.Equal(t, expected[0], res.Data[0]) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetEpochInfo(context.TODO(), &epoch) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestEpochParamsEndpoint(t *testing.T) { @@ -163,6 +182,11 @@ func TestEpochParamsEndpoint(t *testing.T) { assert.Len(t, expected, 1) assert.Equal(t, expected[0], res.Data[0]) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetEpochParams(context.TODO(), &epoch) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestAccountListEndpoint(t *testing.T) { @@ -184,6 +208,11 @@ func TestAccountListEndpoint(t *testing.T) { for _, e := range expected { assert.Contains(t, res.Data, e.StakeAddress) } + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAccountList(context.TODO()) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestAccountInfoEndpoint(t *testing.T) { @@ -207,6 +236,11 @@ func TestAccountInfoEndpoint(t *testing.T) { assert.ErrorIs(t, err, koios.ErrNoAddress) assert.Nil(t, res2.Data, "response data should be nil if arg is invalid") assert.Equal(t, res2.Error.Message, "missing address") + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAccountInfo(context.TODO(), koios.Address(spec.Request.Query.Get("_address"))) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestAccountRewardsEndpoint(t *testing.T) { @@ -229,6 +263,11 @@ func TestAccountRewardsEndpoint(t *testing.T) { assert.Len(t, expected, 1) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAccountRewards(context.TODO(), koios.StakeAddress(spec.Request.Query.Get("_address")), &epoch) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestAccountUpdatesEndpoint(t *testing.T) { @@ -246,6 +285,11 @@ func TestAccountUpdatesEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAccountUpdates(context.TODO(), koios.StakeAddress(spec.Request.Query.Get("_address"))) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestAccountAddressesEndpoint(t *testing.T) { @@ -267,6 +311,11 @@ func TestAccountAddressesEndpoint(t *testing.T) { for _, e := range expected { assert.Contains(t, res.Data, e.Address) } + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAccountAddresses(context.TODO(), koios.StakeAddress(spec.Request.Query.Get("_address"))) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestAccountAssetsEndpoint(t *testing.T) { expected := []koios.AccountAsset{} @@ -283,6 +332,11 @@ func TestAccountAssetsEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAccountAssets(context.TODO(), koios.StakeAddress(spec.Request.Query.Get("_address"))) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestAccountHistoryEndpoint(t *testing.T) { @@ -300,6 +354,11 @@ func TestAccountHistoryEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAccountHistory(context.TODO(), koios.StakeAddress(spec.Request.Query.Get("_address"))) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetAddressInfoEndpoint(t *testing.T) { @@ -322,6 +381,11 @@ func TestGetAddressInfoEndpoint(t *testing.T) { assert.ErrorIs(t, err, koios.ErrNoAddress) assert.Nil(t, res2.Data, "response data should be nil if arg is invalid") assert.Equal(t, res2.Error.Message, "missing address") + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAddressInfo(context.TODO(), koios.Address(spec.Request.Query.Get("_address"))) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetAddressTxsEndpoint(t *testing.T) { @@ -355,6 +419,11 @@ func TestGetAddressTxsEndpoint(t *testing.T) { assert.ErrorIs(t, err, koios.ErrNoAddress) assert.Nil(t, res2.Data, "response data should be nil if arg is invalid") assert.Equal(t, res2.Error.Message, "missing address") + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAddressTxs(context.TODO(), []koios.Address{koios.Address(spec.Request.Query.Get("_address"))}, 0) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetAddressAssetsEndpoint(t *testing.T) { @@ -377,6 +446,11 @@ func TestGetAddressAssetsEndpoint(t *testing.T) { assert.ErrorIs(t, err, koios.ErrNoAddress) assert.Nil(t, res2.Data, "response data should be nil if arg is invalid") assert.Equal(t, res2.Error.Message, "missing address") + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAddressAssets(context.TODO(), koios.Address(spec.Request.Query.Get("_address"))) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetCredentialTxsEndpoint(t *testing.T) { @@ -410,6 +484,11 @@ func TestGetCredentialTxsEndpoint(t *testing.T) { assert.ErrorIs(t, err, koios.ErrNoAddress) assert.Nil(t, res2.Data, "response data should be nil if arg is invalid") assert.Equal(t, res2.Error.Message, "missing address") + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetCredentialTxs(context.TODO(), payload.Credentials, payload.AfterBlockHeight) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestAssetListEndpoint(t *testing.T) { @@ -427,6 +506,11 @@ func TestAssetListEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAssetList(context.TODO()) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetAssetAddressListEndpoint(t *testing.T) { @@ -448,6 +532,15 @@ func TestGetAssetAddressListEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAssetAddressList( + context.TODO(), + koios.PolicyID(spec.Request.Query.Get("_asset_policy")), + koios.AssetName(spec.Request.Query.Get("_asset_name")), + ) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetAssetInfoEndpoint(t *testing.T) { @@ -469,6 +562,15 @@ func TestGetAssetInfoEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, &expected[0], res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAssetInfo( + context.TODO(), + koios.PolicyID(spec.Request.Query.Get("_asset_policy")), + koios.AssetName(spec.Request.Query.Get("_asset_name")), + ) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetAssetSummaryEndpoint(t *testing.T) { @@ -490,6 +592,15 @@ func TestGetAssetSummaryEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, &expected[0], res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAssetSummary( + context.TODO(), + koios.PolicyID(spec.Request.Query.Get("_asset_policy")), + koios.AssetName(spec.Request.Query.Get("_asset_name")), + ) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetAssetTxsEndpoint(t *testing.T) { @@ -511,6 +622,15 @@ func TestGetAssetTxsEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, &expected[0], res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetAssetTxs( + context.TODO(), + koios.PolicyID(spec.Request.Query.Get("_asset_policy")), + koios.AssetName(spec.Request.Query.Get("_asset_name")), + ) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetBlockInfoEndpoint(t *testing.T) { @@ -531,6 +651,14 @@ func TestGetBlockInfoEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, &expected[0], res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetBlockInfo( + context.TODO(), + koios.BlockHash(spec.Request.Query.Get("_block_hash")), + ) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetBlockTxsEndpoint(t *testing.T) { @@ -555,6 +683,14 @@ func TestGetBlockTxsEndpoint(t *testing.T) { for _, e := range expected { assert.Contains(t, res.Data, e.TxHash) } + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetBlockTxHashes( + context.TODO(), + koios.BlockHash(spec.Request.Query.Get("_block_hash")), + ) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetBlocksEndpoint(t *testing.T) { @@ -572,6 +708,11 @@ func TestGetBlocksEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetBlocks(context.TODO()) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetPoolBlocksEndpoint(t *testing.T) { @@ -597,6 +738,15 @@ func TestGetPoolBlocksEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetPoolBlocks( + context.TODO(), + koios.PoolID(spec.Request.Query.Get("_pool_bech32")), + &epoch, + ) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetPoolDelegatorsEndpoint(t *testing.T) { @@ -622,6 +772,15 @@ func TestGetPoolDelegatorsEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetPoolDelegators( + context.TODO(), + koios.PoolID(spec.Request.Query.Get("_pool_bech32")), + &epoch, + ) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetPoolInfoEndpoint(t *testing.T) { @@ -645,6 +804,14 @@ func TestGetPoolInfoEndpoint(t *testing.T) { assert.ErrorIs(t, err, koios.ErrNoPoolID) assert.Nil(t, res2.Data, "response data should be nil if arg is invalid") assert.Equal(t, res2.Error.Message, "missing pool id") + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetPoolInfo( + context.TODO(), + koios.PoolID(spec.Request.Query.Get("_pool_bech32")), + ) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetPoolListEndpoint(t *testing.T) { @@ -662,6 +829,11 @@ func TestGetPoolListEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetPoolList(context.TODO()) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetPoolMetadataEndpoint(t *testing.T) { @@ -679,6 +851,11 @@ func TestGetPoolMetadataEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetPoolMetadata(context.TODO()) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetPoolRelaysEndpoint(t *testing.T) { @@ -696,6 +873,11 @@ func TestGetPoolRelaysEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetPoolRelays(context.TODO()) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetPoolUpdatesEndpoint(t *testing.T) { @@ -715,6 +897,14 @@ func TestGetPoolUpdatesEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetPoolUpdates( + context.TODO(), + &poolID, + ) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetScriptListEndpoint(t *testing.T) { @@ -732,6 +922,11 @@ func TestGetScriptListEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetScriptList(context.TODO()) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetScriptRedeemersEndpoint(t *testing.T) { @@ -750,6 +945,14 @@ func TestGetScriptRedeemersEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, &expected[0], res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetScriptRedeemers( + context.TODO(), + koios.ScriptHash(spec.Request.Query.Get("_script_hash")), + ) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetTxInfoEndpoint(t *testing.T) { @@ -780,6 +983,11 @@ func TestGetTxInfoEndpoint(t *testing.T) { if assert.NotNil(t, res2.Error) { assert.Equal(t, koios.ErrNoTxHash.Error(), res2.Error.Message) } + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetTxInfo(context.TODO(), payload.TxHashes[0]) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetTxMetadataEndpoint(t *testing.T) { @@ -808,6 +1016,11 @@ func TestGetTxMetadataEndpoint(t *testing.T) { assert.ErrorIs(t, err, koios.ErrNoTxHash) assert.Nil(t, res2.Data, "response data should be nil if arg is invalid") assert.Equal(t, res2.Error.Message, "missing transaxtion hash(es)") + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetTxMetadata(context.TODO(), payload.TxHashes[0]) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetTxMetaLabelsEndpoint(t *testing.T) { @@ -825,6 +1038,11 @@ func TestGetTxMetaLabelsEndpoint(t *testing.T) { testHeaders(t, spec, res.Response) assert.Equal(t, expected, res.Data) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetTxMetaLabels(context.TODO()) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetTxStatusEndpoint(t *testing.T) { @@ -853,6 +1071,11 @@ func TestGetTxStatusEndpoint(t *testing.T) { assert.ErrorIs(t, err, koios.ErrNoTxHash) assert.Nil(t, res2.Data, "response data should be nil if arg is invalid") assert.Equal(t, res2.Error.Message, "missing transaxtion hash(es)") + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetTxStatus(context.TODO(), payload.TxHashes[0]) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetTxsUTxOsEndpoint(t *testing.T) { @@ -881,6 +1104,11 @@ func TestGetTxsUTxOsEndpoint(t *testing.T) { assert.ErrorIs(t, err, koios.ErrNoTxHash) assert.Nil(t, res2.Data, "response data should be nil if arg is invalid") assert.Equal(t, res2.Error.Message, "missing transaxtion hash(es)") + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.GetTxsUTxOs(context.TODO(), payload.TxHashes) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } func TestGetTxSubmit(t *testing.T) { @@ -902,6 +1130,11 @@ func TestGetTxSubmit(t *testing.T) { assert.Error(t, err, "submited tx should return error") testHeaders(t, spec, res2.Response) + + c, err := api.WithOptions(koios.Host("127.0.0.2:80")) + assert.NoError(t, err) + _, err = c.SubmitSignedTx(context.TODO(), payload) + assert.EqualError(t, err, "dial tcp: lookup 127.0.0.2:80: no such host") } // loadEndpointTestSpec load specs for endpoint. diff --git a/epoch.go b/epoch.go index 69cadd5..d03ecfd 100644 --- a/epoch.go +++ b/epoch.go @@ -173,9 +173,11 @@ func (c *Client) GetEpochInfo(ctx context.Context, epoch *EpochNo) (res *EpochIn params.Set("_epoch_no", fmt.Sprint(*epoch)) } - rsp, _ := c.request(ctx, &res.Response, "GET", "/epoch_info", nil, params, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/epoch_info", nil, params, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() return } @@ -188,8 +190,10 @@ func (c *Client) GetEpochParams(ctx context.Context, epoch *EpochNo) (res *Epoch params.Set("_epoch_no", fmt.Sprint(*epoch)) } - rsp, _ := c.request(ctx, &res.Response, "GET", "/epoch_params", nil, params, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/epoch_params", nil, params, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() return } diff --git a/go.mod b/go.mod index a1a249f..4d83584 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.0 golang.org/x/text v0.3.7 + golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 ) diff --git a/go.sum b/go.sum index be53028..10b2d94 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs= +golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/koios.go b/koios.go index 0b7762e..6ae48b2 100644 --- a/koios.go +++ b/koios.go @@ -32,12 +32,12 @@ import ( "net/url" "runtime" "strings" - "sync" "time" "github.com/shopspring/decimal" "golang.org/x/text/cases" "golang.org/x/text/language" + "golang.org/x/time/rate" ) // MainnetHost : is primay and default api host. @@ -55,9 +55,9 @@ const ( TestnetHost = "testnet.koios.rest" DefaultAPIVersion = "v0" DefaultPort uint16 = 443 - DefaultSchema = "https" + DefaultScheme = "https" LibraryVersion = "v0" - DefaultRateLimit uint8 = 5 + DefaultRateLimit int = 5 DefaultOrigin = "https://github.com/cardano-community/koios-go-client" ) @@ -73,29 +73,24 @@ var ( ErrNoAddress = errors.New("missing address") ErrNoPoolID = errors.New("missing pool id") ErrResponse = errors.New("got unexpected response") + ErrScheme = errors.New("scheme must be http or https") ) type ( // Client is api client instance. Client struct { - mux sync.RWMutex - host string - version string - port uint16 - schema string - origin string + r *rate.Limiter + reqStatsEnabled bool url *url.URL client *http.Client commonHeaders http.Header - reqInterval time.Duration - lastRequest time.Time - totalReq uint64 - reqStatsEnabled bool } - // Option is callback function which can be implemented - // to change configurations options of API Client. - Option func(*Client) error + // Option is callback function to apply + // configurations options of API Client. + Option struct { + apply func(*Client) error + } // Address defines type for _address. Address string @@ -248,16 +243,13 @@ type ( // ). func New(opts ...Option) (*Client, error) { c := &Client{ - host: MainnetHost, - version: DefaultAPIVersion, - port: DefaultPort, - schema: DefaultSchema, commonHeaders: make(http.Header), } // set default base url - _ = c.updateBaseURL() + _ = c.setBaseURL(DefaultScheme, MainnetHost, DefaultAPIVersion, DefaultPort) + // set default rate limit for outgoing requests. - _ = RateLimit(DefaultRateLimit)(c) + _ = RateLimit(DefaultRateLimit).apply(c) // set default common headers c.commonHeaders.Set("Accept", "application/json") @@ -275,20 +267,26 @@ func New(opts ...Option) (*Client, error) { ) // Apply provided options - for _, setOpt := range opts { - if err := setOpt(c); err != nil { + for _, opt := range opts { + if err := opt.apply(c); err != nil { + return nil, err + } + } + + if c.r == nil { + if err := RateLimit(DefaultRateLimit).apply(c); err != nil { return nil, err } } // Sets default origin if option was not provided. - _ = Origin(DefaultOrigin)(c) + _ = Origin(DefaultOrigin).apply(c) // If HttpClient option was not provided // use default http.Client if c.client == nil { // there is really no point to check that error - _ = HTTPClient(nil)(c) + _ = HTTPClient(nil).apply(c) } return c, nil @@ -297,84 +295,94 @@ func New(opts ...Option) (*Client, error) { // Host returns option apply func which can be used to change the // baseurl hostname https:///api/v0/ func Host(host string) Option { - return func(c *Client) error { - c.mux.Lock() - c.host = host - c.mux.Unlock() - return c.updateBaseURL() + return Option{ + apply: func(c *Client) error { + if c.url.Port() == "" || c.url.Port() == "80" || c.url.Port() == "443" { + c.url.Host = host + } else { + c.url.Host = fmt.Sprint(host, ":", c.url.Port()) + } + return nil + }, } } -// APIVersion returns option apply func which can be used to change the +// APIVersion returns option to apply change of the // baseurl api version https://api.koios.rest/api// func APIVersion(version string) Option { - return func(c *Client) error { - c.mux.Lock() - c.version = version - c.mux.Unlock() - return c.updateBaseURL() + return Option{ + apply: func(c *Client) error { + url, err := c.url.Parse("/api/" + version + "/") + if err != nil { + return err + } + c.url = url + return nil + }, } } // Port returns option apply func which can be used to change the // baseurl port https://api.koios.rest:/api/v0/ func Port(port uint16) Option { - return func(c *Client) error { - c.mux.Lock() - c.port = port - c.mux.Unlock() - return c.updateBaseURL() + return Option{ + apply: func(c *Client) error { + c.url.Host = fmt.Sprint(c.url.Hostname(), ":", port) + return nil + }, } } -// Schema returns option apply func which can be used to change the -// baseurl schema ://api.koios.rest/api/v0/. -func Schema(schema string) Option { - return func(c *Client) error { - c.mux.Lock() - c.schema = schema - c.mux.Unlock() - return c.updateBaseURL() +// Scheme returns option apply func which can be used to change the +// baseurl scheme ://api.koios.rest/api/v0/. +func Scheme(scheme string) Option { + return Option{ + apply: func(c *Client) error { + c.url.Scheme = scheme + if scheme != "http" && scheme != "https" { + return ErrScheme + } + return nil + }, } } // HTTPClient enables to set htt.Client to be used for requests. -// http.Client can only be set once. func HTTPClient(client *http.Client) Option { - return func(c *Client) error { - if c.client != nil { - return ErrHTTPClientChange - } - c.mux.Lock() - defer c.mux.Unlock() - if client == nil { - client = &http.Client{ - Timeout: time.Second * 60, + return Option{ + apply: func(c *Client) error { + if c.client != nil { + return ErrHTTPClientChange } - } - if client.Timeout == 0 { - return ErrHTTPClientTimeoutSetting - } - c.client = client - if c.client.Transport == nil { - c.client.Transport = http.DefaultTransport - } - return nil + if client == nil { + client = &http.Client{ + Timeout: time.Second * 60, + } + } + if client.Timeout == 0 { + return ErrHTTPClientTimeoutSetting + } + c.client = client + if c.client.Transport == nil { + c.client.Transport = http.DefaultTransport + } + return nil + }, } } // RateLimit sets requests per second this client is allowed to create // and effectievely rate limits outgoing requests. // Let's respect usage of the community provided resources. -func RateLimit(reqps uint8) Option { - return func(c *Client) error { - if reqps == 0 { - return ErrRateLimitRange - } - c.mux.Lock() - defer c.mux.Unlock() - c.reqInterval = time.Second / time.Duration(reqps) - return nil +func RateLimit(reqps int) Option { + return Option{ + apply: func(c *Client) error { + if reqps == 0 { + return ErrRateLimitRange + } + c.r = rate.NewLimiter(rate.Every(time.Second), reqps) + return nil + }, } } @@ -388,27 +396,26 @@ func RateLimit(reqps uint8) Option { // It's not required, but considered as good practice so that Cardano Community // can provide HA services for Cardano ecosystem. func Origin(origin string) Option { - return func(c *Client) error { - u, err := url.ParseRequestURI(origin) - if err != nil { - return err - } - - c.mux.Lock() - defer c.mux.Unlock() - c.origin = u.String() - return nil + return Option{ + apply: func(c *Client) error { + o, err := url.ParseRequestURI(origin) + if err != nil { + return err + } + c.commonHeaders.Set("Origin", o.String()) + return nil + }, } } // CollectRequestsStats when enabled uses httptrace is used // to collect detailed timing information about the request. func CollectRequestsStats(enabled bool) Option { - return func(c *Client) error { - c.mux.Lock() - defer c.mux.Unlock() - c.reqStatsEnabled = enabled - return nil + return Option{ + apply: func(c *Client) error { + c.reqStatsEnabled = enabled + return nil + }, } } @@ -433,6 +440,11 @@ func readAndUnmarshalResponse(rsp *http.Response, res *Response, dest interface{ res.applyError(body, err) return err } + if len(body) == 0 { + return nil + } + + defer res.ready() if err = json.Unmarshal(body, dest); err != nil { res.applyError(body, err) return err @@ -442,11 +454,20 @@ func readAndUnmarshalResponse(rsp *http.Response, res *Response, dest interface{ func (r *Response) applyError(body []byte, err error) { r.Error = &ResponseError{} - _ = json.Unmarshal(body, r.Error) - if err != nil && len(r.Error.Message) == 0 { + if len(body) != 0 { + _ = json.Unmarshal(body, r.Error) + } + defer r.ready() + + if err == nil { + return + } + + if len(r.Error.Message) == 0 { r.Error.Message = err.Error() + } else { + r.Error.Message = fmt.Sprintf("%s: %s", err.Error(), r.Error.Message) } - r.ready() } func (r *Response) ready() { diff --git a/koios_test.go b/koios_test.go index afcd606..38b5a74 100644 --- a/koios_test.go +++ b/koios_test.go @@ -29,11 +29,9 @@ func TestNewDefaults(t *testing.T) { api, err := New() assert.NoError(t, err) if assert.NotNil(t, api) { - assert.Equal(t, uint64(0), api.TotalRequests(), "total requests should be 0 by default") - raw := fmt.Sprintf( "%s://%s/api/%s/", - DefaultSchema, + DefaultScheme, MainnetHost, DefaultAPIVersion, ) @@ -48,25 +46,24 @@ func TestOptions(t *testing.T) { Host("localhost"), APIVersion("v1"), Port(8080), - Schema("http"), + Scheme("http"), RateLimit(100), Origin("http://localhost.localdomain"), CollectRequestsStats(true), ) assert.NoError(t, err) if assert.NotNil(t, api) { - assert.Equal(t, uint64(0), api.TotalRequests(), "total requests should be 0 by default") assert.Equal(t, "http://localhost:8080/api/v1/", api.BaseURL(), "invalid default base url") } } func TestOptionErrs(t *testing.T) { client, _ := New() - assert.Error(t, HTTPClient(http.DefaultClient)(client), + assert.Error(t, HTTPClient(http.DefaultClient).apply(client), "should not allow changing http client.") - assert.Error(t, RateLimit(0)(client), + assert.Error(t, RateLimit(0).apply(client), "should not unlimited requests p/s") - assert.Error(t, Origin("localhost")(client), + assert.Error(t, Origin("localhost").apply(client), "origin should be valid http origin") _, err := New(Origin("localhost.localdomain")) assert.Error(t, err, "New should return err when option is invalid") diff --git a/network.go b/network.go index 147374d..55552bb 100644 --- a/network.go +++ b/network.go @@ -138,47 +138,44 @@ type ( // GetTip returns the tip info about the latest block seen by chain. func (c *Client) GetTip(ctx context.Context) (res *TipResponse, err error) { res = &TipResponse{} - rsp, _ := c.request(ctx, &res.Response, "GET", "/tip", nil, nil, nil) - + rsp, err := c.request(ctx, &res.Response, "GET", "/tip", nil, nil, nil) + if err != nil { + return res, err + } tips := []Tip{} err = readAndUnmarshalResponse(rsp, &res.Response, &tips) if len(tips) == 1 { res.Data = &tips[0] } - res.ready() - return + return res, err } // GetGenesis returns the Genesis parameters used to start specific era on chain. -func (c *Client) GetGenesis(ctx context.Context) (res *GenesisResponse, err error) { - res = &GenesisResponse{} - rsp, _ := c.request(ctx, &res.Response, "GET", "/genesis", nil, nil, nil) - +func (c *Client) GetGenesis(ctx context.Context) (*GenesisResponse, error) { + res := &GenesisResponse{} + rsp, err := c.request(ctx, &res.Response, "GET", "/genesis", nil, nil, nil) + if err != nil { + return res, err + } genesisres := []Genesis{} err = readAndUnmarshalResponse(rsp, &res.Response, &genesisres) - if len(genesisres) == 1 { res.Data = &genesisres[0] } - res.ready() - return + return res, err } // GetTotals returns the circulating utxo, treasury, rewards, supply and // reserves in lovelace for specified epoch, all epochs if empty. -func (c *Client) GetTotals(ctx context.Context, epoch *EpochNo) (res *TotalsResponse, err error) { +func (c *Client) GetTotals(ctx context.Context, epoch *EpochNo) (*TotalsResponse, error) { params := url.Values{} if epoch != nil { params.Set("_epoch_no", fmt.Sprint(*epoch)) } - res = &TotalsResponse{} - rsp, _ := c.request(ctx, &res.Response, "GET", "/totals", nil, params, nil) - - totals := []Totals{} - err = readAndUnmarshalResponse(rsp, &res.Response, &totals) - if len(totals) > 0 { - res.Data = totals + res := &TotalsResponse{} + rsp, err := c.request(ctx, &res.Response, "GET", "/totals", nil, params, nil) + if err != nil { + return res, err } - res.ready() - return + return res, readAndUnmarshalResponse(rsp, &res.Response, &res.Data) } diff --git a/pool.go b/pool.go index 91df2d5..2a90b17 100644 --- a/pool.go +++ b/pool.go @@ -314,7 +314,10 @@ type ( // GetPoolList returns the list of all currently registered/retiring (not retired) pools. func (c *Client) GetPoolList(ctx context.Context) (res *PoolListResponse, err error) { res = &PoolListResponse{} - rsp, _ := c.request(ctx, &res.Response, "GET", "/pool_list", nil, nil, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/pool_list", nil, nil, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) return } @@ -339,9 +342,11 @@ func (c *Client) GetPoolInfos(ctx context.Context, pids []PoolID) (res *PoolInfo return } - rsp, _ := c.request(ctx, &res.Response, "POST", "/pool_info", poolIdsPL(pids), nil, nil) + rsp, err := c.request(ctx, &res.Response, "POST", "/pool_info", poolIdsPL(pids), nil, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() return } @@ -359,9 +364,12 @@ func (c *Client) GetPoolDelegators( if epoch != nil { params.Set("_epoch_no", fmt.Sprint(*epoch)) } - rsp, _ := c.request(ctx, &res.Response, "GET", "/pool_delegators", nil, params, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/pool_delegators", nil, params, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() + return } @@ -379,9 +387,11 @@ func (c *Client) GetPoolBlocks( if epoch != nil { params.Set("_epoch_no", fmt.Sprint(*epoch)) } - rsp, _ := c.request(ctx, &res.Response, "GET", "/pool_blocks", nil, params, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/pool_blocks", nil, params, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() return } @@ -398,9 +408,11 @@ func (c *Client) GetPoolUpdates( params.Set("_pool_bech32", fmt.Sprint(*pid)) } - rsp, _ := c.request(ctx, &res.Response, "GET", "/pool_updates", nil, params, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/pool_updates", nil, params, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() return } @@ -409,9 +421,11 @@ func (c *Client) GetPoolUpdates( func (c *Client) GetPoolRelays(ctx context.Context) (res *PoolRelaysResponse, err error) { res = &PoolRelaysResponse{} - rsp, _ := c.request(ctx, &res.Response, "GET", "/pool_relays", nil, nil, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/pool_relays", nil, nil, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() return } @@ -420,9 +434,11 @@ func (c *Client) GetPoolRelays(ctx context.Context) (res *PoolRelaysResponse, er func (c *Client) GetPoolMetadata(ctx context.Context) (res *PoolMetadataResponse, err error) { res = &PoolMetadataResponse{} - rsp, _ := c.request(ctx, &res.Response, "GET", "/pool_metadata", nil, nil, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/pool_metadata", nil, nil, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() return } diff --git a/script.go b/script.go index 7a3f85d..e377c12 100644 --- a/script.go +++ b/script.go @@ -87,7 +87,10 @@ type ( // hashes along with their creation transaction hashes. func (c *Client) GetScriptList(ctx context.Context) (res *ScriptListResponse, err error) { res = &ScriptListResponse{} - rsp, _ := c.request(ctx, &res.Response, "GET", "/script_list", nil, nil, nil) + rsp, err := c.request(ctx, &res.Response, "GET", "/script_list", nil, nil, nil) + if err != nil { + return + } err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) return } @@ -102,14 +105,15 @@ func (c *Client) GetScriptRedeemers( params := url.Values{} params.Set("_script_hash", fmt.Sprint(sh)) - rsp, _ := c.request(ctx, &res.Response, "GET", "/script_redeemers", nil, params, nil) - + rsp, err := c.request(ctx, &res.Response, "GET", "/script_redeemers", nil, params, nil) + if err != nil { + return + } r := []ScriptRedeemers{} err = readAndUnmarshalResponse(rsp, &res.Response, &r) if len(r) == 1 { res.Data = &r[0] } - res.ready() return } diff --git a/transaction.go b/transaction.go index 309a036..c1e5b88 100644 --- a/transaction.go +++ b/transaction.go @@ -259,33 +259,36 @@ func (c *Client) GetTxInfo(ctx context.Context, tx TxHash) (res *TxInfoResponse, } // GetTxsInfos returns detailed information about transaction(s). -func (c *Client) GetTxsInfos(ctx context.Context, txs []TxHash) (res *TxsInfosResponse, err error) { - res = &TxsInfosResponse{} +func (c *Client) GetTxsInfos(ctx context.Context, txs []TxHash) (*TxsInfosResponse, error) { + res := &TxsInfosResponse{} if len(txs) == 0 || len(txs[0]) == 0 { - err = ErrNoTxHash + err := ErrNoTxHash res.applyError(nil, err) - return + return res, err } - rsp, _ := c.request(ctx, &res.Response, "POST", "/tx_info", txHashesPL(txs), nil, nil) - err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() - return + rsp, err := c.request(ctx, &res.Response, "POST", "/tx_info", txHashesPL(txs), nil, nil) + if err != nil { + return res, err + } + return res, readAndUnmarshalResponse(rsp, &res.Response, &res.Data) } // GetTxsUTxOs returns UTxO set (inputs/outputs) of transactions. -func (c *Client) GetTxsUTxOs(ctx context.Context, txs []TxHash) (res *TxUTxOsResponse, err error) { - res = &TxUTxOsResponse{} +func (c *Client) GetTxsUTxOs(ctx context.Context, txs []TxHash) (*TxUTxOsResponse, error) { + res := &TxUTxOsResponse{} if len(txs) == 0 || len(txs[0]) == 0 { - err = ErrNoTxHash + err := ErrNoTxHash res.applyError(nil, err) - return + return res, err } - rsp, _ := c.request(ctx, &res.Response, "POST", "/tx_utxos", txHashesPL(txs), nil, nil) - err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() - return + rsp, err := c.request(ctx, &res.Response, "POST", "/tx_utxos", txHashesPL(txs), nil, nil) + if err != nil { + return res, err + } + + return res, readAndUnmarshalResponse(rsp, &res.Response, &res.Data) } // GetTxMetadata returns metadata information (if any) for given transaction. @@ -300,49 +303,53 @@ func (c *Client) GetTxMetadata(ctx context.Context, tx TxHash) (res *TxMetadataR } // GetTxsMetadata returns metadata for requested transaction(s). -func (c *Client) GetTxsMetadata(ctx context.Context, txs []TxHash) (res *TxsMetadataResponse, err error) { - res = &TxsMetadataResponse{} +func (c *Client) GetTxsMetadata(ctx context.Context, txs []TxHash) (*TxsMetadataResponse, error) { + res := &TxsMetadataResponse{} if len(txs) == 0 { - err = ErrNoTxHash + err := ErrNoTxHash res.applyError(nil, err) - return + return res, err } - rsp, _ := c.request(ctx, &res.Response, "POST", "/tx_metadata", txHashesPL(txs), nil, nil) - err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() - return + rsp, err := c.request(ctx, &res.Response, "POST", "/tx_metadata", txHashesPL(txs), nil, nil) + if err != nil { + return res, err + } + + return res, readAndUnmarshalResponse(rsp, &res.Response, &res.Data) } // GetTxMetaLabels retruns a list of all transaction metalabels. -func (c *Client) GetTxMetaLabels(ctx context.Context) (res *TxMetaLabelsResponse, err error) { - res = &TxMetaLabelsResponse{} - rsp, _ := c.request(ctx, &res.Response, "GET", "/tx_metalabels", nil, nil, nil) - err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() - return +func (c *Client) GetTxMetaLabels(ctx context.Context) (*TxMetaLabelsResponse, error) { + res := &TxMetaLabelsResponse{} + rsp, err := c.request(ctx, &res.Response, "GET", "/tx_metalabels", nil, nil, nil) + if err != nil { + return res, err + } + return res, readAndUnmarshalResponse(rsp, &res.Response, &res.Data) } // SubmitSignedTx Submit an transaction to the network. -func (c *Client) SubmitSignedTx(ctx context.Context, stx TxBodyJSON) (res *SubmitSignedTxResponse, err error) { - var cborb []byte - res = &SubmitSignedTxResponse{} +func (c *Client) SubmitSignedTx(ctx context.Context, stx TxBodyJSON) (*SubmitSignedTxResponse, error) { + res := &SubmitSignedTxResponse{} var method = "POST" - cborb, err = hex.DecodeString(stx.CborHex) + cborb, err := hex.DecodeString(stx.CborHex) if err != nil { res.RequestMethod = method res.StatusCode = 400 - return + res.applyError(nil, err) + return res, err } h := http.Header{} h.Set("Content-Type", "application/cbor") h.Set("Content-Length", fmt.Sprint(len(cborb))) - rsp, _ := c.request(ctx, &res.Response, method, "/submittx", bytes.NewBuffer(cborb), nil, h) - err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() - return + rsp, err := c.request(ctx, &res.Response, method, "/submittx", bytes.NewBuffer(cborb), nil, h) + if err != nil { + return res, err + } + return res, readAndUnmarshalResponse(rsp, &res.Response, &res.Data) } // GetTxStatus returns status of transaction. @@ -357,18 +364,19 @@ func (c *Client) GetTxStatus(ctx context.Context, tx TxHash) (res *TxStatusRespo } // GetTxsStatuses returns status of transaction(s). -func (c *Client) GetTxsStatuses(ctx context.Context, txs []TxHash) (res *TxsStatusesResponse, err error) { - res = &TxsStatusesResponse{} +func (c *Client) GetTxsStatuses(ctx context.Context, txs []TxHash) (*TxsStatusesResponse, error) { + res := &TxsStatusesResponse{} if len(txs) == 0 { - err = ErrNoTxHash + err := ErrNoTxHash res.applyError(nil, err) - return + return res, err } - rsp, _ := c.request(ctx, &res.Response, "POST", "/tx_status", txHashesPL(txs), nil, nil) - err = readAndUnmarshalResponse(rsp, &res.Response, &res.Data) - res.ready() - return + rsp, err := c.request(ctx, &res.Response, "POST", "/tx_status", txHashesPL(txs), nil, nil) + if err != nil { + return res, err + } + return res, readAndUnmarshalResponse(rsp, &res.Response, &res.Data) } func txHashesPL(txs []TxHash) io.Reader {