dragonchain/dragonchain-sdk-go

View on GitHub
client.go

Summary

Maintainability
D
2 days
Test Coverage
// Copyright 2020 Dragonchain, Inc. or its affiliates. All Rights Reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//     http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package dragonchain is used to interact programmatically with dragonchains.
package dragonchain

import (
    "bytes"
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

// MaxBulkPutSize is the configurable limit of how many txn can be included in a bulk operation.
const MaxBulkPutSize = 250

// Response defines the standard response all chains will use.
type Response struct {
    OK       bool        `json:"ok"`
    Status   int         `json:"status"`
    Response interface{} `json:"response"`
}

// Client defines the structure of the DragonchainSDK client.
type Client struct {
    creds      Authenticator
    apiBaseURL string

    httpClient httpClient
    ctx        context.Context
}

type httpClient interface {
    Do(req *http.Request) (*http.Response, error)
    CloseIdleConnections()
    Get(url string) (resp *http.Response, err error)
    Head(url string) (resp *http.Response, err error)
    Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
    PostForm(url string, data url.Values) (resp *http.Response, err error)
}

// NewClient creates a new instance of client. By default, it does not generate usable credentials.
// Accepts Authenticator credentials created using dragonchain.NewCredentials.
// apiBaseUrl is optional and for use when interacting with chains outside of the managed service.
// httpClient is optional if you wish to designate custom headers for requests.
func NewClient(creds Authenticator, apiBaseURL string, httpClient httpClient) *Client {
    if apiBaseURL == "" {
        apiBaseURL = fmt.Sprintf("https://%s.api.dragonchain.com", creds.GetDragonchainID())
    }
    if httpClient == nil {
        httpClient = &http.Client{}
    }
    client := &Client{
        creds:      creds,
        apiBaseURL: apiBaseURL,
        httpClient: httpClient,
    }

    return client
}

// GetSmartContractSecret pulls a secret for the running smart contract from the chain.
func (client *Client) GetSmartContractSecret(secretName string) (_ string, err error) {
    scID := os.Getenv("SMART_CONTRACT_ID")
    var path string
    // Allow users to specify their own paths
    if strings.Contains(secretName, "/") {
        path = secretName
    } else {
        path = fmt.Sprintf("/var/openfaas/secrets/sc-%s-%s", scID, secretName)
    }

    file, err := os.Open(path)
    defer func() {
        _ = file.Close()
    }()
    if err == nil {
        return parseSecret(file)
    }
    return "", err
}

// parseSecret does the actual work of reading the secret. The functions are separated for testability.
func parseSecret(reader io.Reader) (string, error) {
    b, readErr := ioutil.ReadAll(reader)
    return string(b), readErr
}

// GetStatus returns the chain's status, such as Active or Updating.
func (client *Client) GetStatus() (*Response, error) {
    path := "/status"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)
    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }

    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    return resp, err
}

// QuerySmartContracts returns a list of matching contracts on the chain.
func (client *Client) QuerySmartContracts(query *Query) (*Response, error) {
    path := "/contract"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)
    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }

    buildQuery(req, query)
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    // Handle conversion of Response from an interface{} to []Contract for the user.
    var raw map[string]interface{}
    err = json.Unmarshal(resp.Response.([]byte), &raw)
    if err != nil {
        return nil, err
    }
    contractJSON, err := json.Marshal(raw["results"])
    if err != nil {
        return nil, err
    }
    var contract []Contract
    if err := json.Unmarshal(contractJSON, &contract); err != nil {
        return nil, err
    }

    resp.Response = make(map[string][]Contract)
    resp.Response.(map[string][]Contract)["results"] = contract
    return resp, err
}

// GetSmartContract returns details on a smart contract by ID or transactionType.
// If both contractID and transactionType are provided, contractID is used.
func (client *Client) GetSmartContract(smartContractID, transactionType string) (*Response, error) {
    var err error
    var uri string
    if smartContractID == "" && transactionType == "" {
        return nil, errors.New("invalid parameters: you must provide one of smartContractID or transactionType")
    } else if smartContractID != "" {
        path := "/contract"
        uri = fmt.Sprintf("%s%s/%s", client.apiBaseURL, path, smartContractID)
    } else if transactionType != "" {
        path := "/contract/txn_type"
        uri = fmt.Sprintf("%s%s/%s", client.apiBaseURL, path, transactionType)
    }
    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    var contract Contract
    if err := json.Unmarshal(resp.Response.([]byte), &contract); err != nil {
        return nil, err
    }
    resp.Response = contract
    return resp, err
}

// CreateSmartContract creates a new smart contract on the chain.
func (client *Client) CreateSmartContract(contract *ContractConfiguration) (_ *Response, err error) {
    path := "/contract"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)
    b, err := json.Marshal(contract)
    if err != nil {
        return nil, err
    }

    resp, err := client.httpClient.Post(uri, "content/json", bytes.NewBuffer(b))
    if err != nil {
        return nil, err
    }
    defer func() {
        _ = resp.Body.Close()
    }()
    var statusMessage []byte
    statusMessage, err = ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    response := Response{
        Response: statusMessage,
        Status:   resp.StatusCode,
        OK:       200 <= resp.StatusCode && 300 > resp.StatusCode,
    }
    return &response, err
}

// UpdateSmartContract updates an existing contract with a new configuration.
// Configuration details that aren't provided will not be changed.
func (client *Client) UpdateSmartContract(contract *ContractConfiguration) (*Response, error) {
    path := "/contract"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)
    b, err := json.Marshal(contract)
    if err != nil {
        return nil, err
    }

    req, err := http.NewRequest("PUT", uri, bytes.NewBuffer(b))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    return resp, err
}

// DeleteContract removes a contract from the chain.
func (client *Client) DeleteContract(smartContractID string) (*Response, error) {
    path := "/contract"
    uri := fmt.Sprintf("%s%s/%s", client.apiBaseURL, path, smartContractID)
    req, err := http.NewRequest("DELETE", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    return resp, err
}

// GetTransaction gets a transaction from the chain by id.
func (client *Client) GetTransaction(txnID string) (*Response, error) {
    path := "/transaction"
    uri := fmt.Sprintf("%s%s/%s", client.apiBaseURL, path, txnID)

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    // Handle conversion of Response from an interface{} to Transaction for the user.
    var txn Transaction
    if err := json.Unmarshal(resp.Response.([]byte), &txn); err != nil {
        return nil, err
    }
    resp.Response = txn
    return resp, err
}

// CreateTransaction creates a transaction on the chain.
func (client *Client) CreateTransaction(txn *CreateTransaction) (_ *Response, err error) {
    path := "/transaction"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)

    b, err := json.Marshal(txn)
    if err != nil {
        return nil, err
    }

    req, err := http.NewRequest(http.MethodPost, uri, bytes.NewBuffer(b))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, string(b))
    return resp, err
}

// CreateBulkTransaction sends many transactions to a chain in a single HTTP request.
func (client *Client) CreateBulkTransaction(txn []*CreateTransaction) (_ *Response, err error) {
    path := "/transaction_bulk"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)

    if len(txn) > MaxBulkPutSize {
        return nil, ErrMaxBulkSizeExceeded
    }

    txnBytes, err := json.Marshal(txn)
    if err != nil {
        return nil, err
    }

    req, err := http.NewRequest(http.MethodPost, uri, bytes.NewBuffer(txnBytes))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, string(txnBytes))
    return resp, err
}

// QueryBlocks gets all blocks matching the given query.
func (client *Client) QueryBlocks(query *Query) (*Response, error) {
    path := "/block"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    buildQuery(req, query)
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    var results map[string][]Block
    err = json.Unmarshal(resp.Response.([]byte), &results)
    if err != nil {
        return nil, err
    }
    resp.Response = results
    return resp, err
}

// GetBlock returns a block by ID.
func (client *Client) GetBlock(blockID string) (*Response, error) {
    path := "/block"
    uri := fmt.Sprintf("%s%s/%s", client.apiBaseURL, path, blockID)

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    var block Block
    if err := json.Unmarshal(resp.Response.([]byte), &block); err != nil {
        return nil, err
    }
    resp.Response = block
    return resp, err
}

// GetVerifications returns a block's verification at a specific level of DragonNet.
func (client *Client) GetVerifications(blockID string, level int) (*Response, error) {
    path := "/verifications"
    uri := fmt.Sprintf("%s%s/%s", client.apiBaseURL, path, blockID)

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    if level > 0 {
        q := req.URL.Query()
        q.Add("level", strconv.Itoa(level))
        req.URL.RawQuery = q.Encode()
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    // Handle conversion of Response from an interface{} to Verification for the user.
    if level > 0 {
        var verificationBlocks []Block
        if err := json.Unmarshal(resp.Response.([]byte), &verificationBlocks); err != nil {
            return nil, err
        }
        resp.Response = verificationBlocks
    } else {
        var verification Verification
        if err := json.Unmarshal(resp.Response.([]byte), &verification); err != nil {
            return nil, err
        }
        resp.Response = verification
    }
    return resp, err
}

// QueryTransactions gets all transactions matching the given query on the chain.
func (client *Client) QueryTransactions(query *Query) (*Response, error) {
    path := "/transaction"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }

    buildQuery(req, query)
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    return resp, err
}

// GetSmartContractObject returns a specific key from a smart contract's heap.
// If SCName is not provided, the SDK will try to pull it from the environment.
func (client *Client) GetSmartContractObject(key, smartContractID string) (*Response, error) {
    if len(smartContractID) == 0 {
        smartContractID = os.Getenv("SMART_CONTRACT_ID")
    }

    if len(key) == 0 {
        return nil, errors.New("key can not be empty")
    }

    path := "/get"
    uri := fmt.Sprintf("%s%s/%s/%s", client.apiBaseURL, path, smartContractID, key)
    _, _ = fmt.Fprintf(os.Stderr, "URL: %s\n", uri)

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    return resp, err
}

// ListSmartContractObjects lists out all keys from a smart contract's heap.
// Optionally, folder can be provided to only list a subset of keys.
func (client *Client) ListSmartContractObjects(folder, smartContractID string) (*Response, error) {
    if len(smartContractID) == 0 {
        smartContractID = os.Getenv("SMART_CONTRACT_ID")
    }

    path := "/list"
    uri := fmt.Sprintf("%s%s/%s/", client.apiBaseURL, path, smartContractID)

    if len(folder) > 0 {
        if strings.HasSuffix(folder, "/") {
            return nil, errors.New("folder can not end with '/'")
        }
        uri = fmt.Sprintf("%s%s", uri, folder)
    }

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    return resp, err
}

// GetTransactionType returns a transaction type on chain by its name.
func (client *Client) GetTransactionType(transactionType string) (*Response, error) {
    path := "/transaction-type"
    uri := fmt.Sprintf("%s%s/%s", client.apiBaseURL, path, transactionType)

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    var txnType TransactionType
    if err := json.Unmarshal(resp.Response.([]byte), &txnType); err != nil {
        return nil, err
    }
    resp.Response = txnType
    return resp, err
}

// ListTransactionTypes lists out all registered transaction types for a chain.
func (client *Client) ListTransactionTypes() (*Response, error) {
    path := "/transaction-types"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    var txnTypes map[string][]TransactionType
    if err := json.Unmarshal(resp.Response.([]byte), &txnTypes); err != nil {
        return nil, err
    }
    resp.Response = txnTypes
    return resp, err
}

// UpdateTransactionType updates a given transaction type.
func (client *Client) UpdateTransactionType(transactionType string, customIndexes []CustomIndexStructure) (*Response, error) {
    path := "/transaction-type"
    uri := fmt.Sprintf("%s%s/%s", client.apiBaseURL, path, transactionType)
    var params TransactionType
    params.Version = "1"
    params.CustomIndexes = customIndexes

    b, err := json.Marshal(params)
    if err != nil {
        return nil, err
    }

    req, err := http.NewRequest("PUT", uri, bytes.NewBuffer(b))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    var success map[string]bool
    if err := json.Unmarshal(resp.Response.([]byte), &success); err != nil {
        return nil, err
    }
    resp.Response = success
    return resp, err
}

// CreateTransactionType creates a new transaction type.
func (client *Client) CreateTransactionType(transactionType string, customIndexes []CustomIndexStructure) (_ *Response, err error) {
    path := "/transaction-type"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)
    var params TransactionType
    params.Version = "1"
    params.Type = transactionType
    params.CustomIndexes = customIndexes

    b, err := json.Marshal(params)
    if err != nil {
        return nil, err
    }

    resp, err := client.httpClient.Post(uri, "content/json", bytes.NewBuffer(b))
    if err != nil {
        return nil, err
    }
    defer func() {
        _ = resp.Body.Close()
    }()
    var chainResp Response
    respBytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    if err := json.Unmarshal(respBytes, &chainResp.Response); err != nil {
        return nil, err
    }
    chainResp.Status = resp.StatusCode
    if 200 <= resp.StatusCode && resp.StatusCode < 300 {
        chainResp.OK = true
    }
    return &chainResp, err
}

// DeleteTransactionType removes the specified transaction type. It will not affect transactions that have already been processed.
func (client *Client) DeleteTransactionType(transactionType string) (*Response, error) {
    path := "/transaction-type"
    uri := fmt.Sprintf("%s%s/%s", client.apiBaseURL, path, transactionType)

    req, err := http.NewRequest("DELETE", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    var success map[string]bool
    if err := json.Unmarshal(resp.Response.([]byte), &success); err != nil {
        return nil, err
    }
    resp.Response = success
    return resp, err
}

// ListFactors lists out all registered factors for a chain.
func (client *Client) ListFactors() (*Response, error) {
    path := "/factor"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }

    var factors map[string][]*Factor
    if err := json.Unmarshal(resp.Response.([]byte), &factors); err != nil {
        return nil, err
    }
    resp.Response = factors
    return resp, err
}

// CreateFactor creates a new factor.
func (client *Client) CreateFactor(factor string) (_ *Response, err error) {
    path := "/factor"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)
    var params Factor
    params.Version = "1"
    params.Factor = factor

    b, err := json.Marshal(params)
    if err != nil {
        return nil, err
    }

    resp, err := client.httpClient.Post(uri, "content/json", bytes.NewBuffer(b))
    if err != nil {
        return nil, err
    }
    defer func() {
        _ = resp.Body.Close()
    }()
    var chainResp Response
    respBytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    if err := json.Unmarshal(respBytes, &chainResp.Response); err != nil {
        return nil, err
    }
    chainResp.Status = resp.StatusCode
    if 200 <= resp.StatusCode && resp.StatusCode < 300 {
        chainResp.OK = true
    }
    return &chainResp, err
}

// DeleteFactor removes the specified factor.
func (client *Client) DeleteFactor(id string) (*Response, error) {
    path := "/factor"
    uri := fmt.Sprintf("%s%s/%s", client.apiBaseURL, path, id)

    req, err := http.NewRequest("DELETE", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }

    fmt.Println(string(resp.Response.([]byte)))

    var success map[string]bool
    if err := json.Unmarshal(resp.Response.([]byte), &success); err != nil {
        return nil, err
    }
    resp.Response = success
    return resp, err
}

// GetPublicBlockchainAddress returns a dictionary of this chain's interchain addresses.
// This method is only supported on L1 and L5 chains.
func (client *Client) GetPublicBlockchainAddress() (*Response, error) {
    path := "/public-blockchain-address"
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, path)

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    var addresses map[string]string
    if err := json.Unmarshal(resp.Response.([]byte), &addresses); err != nil {
        return nil, err
    }
    resp.Response = addresses
    return resp, err
}

// CreateBitcoinTransaction creates an interchain btc transaction using this chain's interchain address.
func (client *Client) CreateBitcoinTransaction(btcTransaction *BitcoinTransaction) (*Response, error) {
    if !BitcoinNetworks[btcTransaction.Network] {
        return nil, fmt.Errorf("bitcoin transactions can only be created on supported networks: %+v", BitcoinNetworks)
    }
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, "/public-blockchain-transaction")
    btcTransactionRequest := bitcoinBackEndTransaction{
        Network: btcTransaction.Network,
        Transaction: bitcoinTransactionWithoutNetwork{
            SatoshisPerByte: btcTransaction.SatoshisPerByte,
            Data:            btcTransaction.Data,
            ChangeAddress:   btcTransaction.ChangeAddress,
            Outputs:         btcTransaction.Outputs,
        },
    }

    b, err := json.Marshal(btcTransactionRequest)
    if err != nil {
        return nil, err
    }

    resp, err := client.httpClient.Post(uri, "content/json", bytes.NewBuffer(b))
    if err != nil {
        return nil, err
    }
    defer func() {
        _ = resp.Body.Close()
    }()
    var chainResp Response
    bs, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    var signed map[string]string
    if err := json.Unmarshal(bs, &signed); err != nil {
        return nil, err
    }
    chainResp.Response = signed
    chainResp.Status = resp.StatusCode
    if 200 <= resp.StatusCode && resp.StatusCode < 300 {
        chainResp.OK = true
    }
    return &chainResp, err
}

// CreateEthereumTransaction creates an interchain eth transaction using this chain's interchain address.
func (client *Client) CreateEthereumTransaction(ethTransaction *EthereumTransaction) (*Response, error) {
    if !EthereumNetworks[ethTransaction.Network] {
        return nil, fmt.Errorf("ethereum transactions can only be created on supported networks: %+v", EthereumNetworks)
    }
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, "/public-blockchain-transaction")
    ethTransactionRequest := ethereumBackEndTransaction{
        Network: ethTransaction.Network,
        Transaction: ethereumTransactionWithoutNetwork{
            To:       ethTransaction.To,
            Value:    ethTransaction.Value,
            Data:     ethTransaction.Data,
            GasPrice: ethTransaction.GasPrice,
            Gas:      ethTransaction.Gas,
        },
    }

    b, err := json.Marshal(ethTransactionRequest)
    if err != nil {
        return nil, err
    }

    resp, err := client.httpClient.Post(uri, "content/json", bytes.NewBuffer(b))
    if err != nil {
        return nil, err
    }
    defer func() {
        _ = resp.Body.Close()
    }()
    var chainResp Response
    bs, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    var signed map[string]string
    if err := json.Unmarshal(bs, &signed); err != nil {
        return nil, err
    }
    chainResp.Response = signed
    chainResp.Status = resp.StatusCode
    if 200 <= resp.StatusCode && resp.StatusCode < 300 {
        chainResp.OK = true
    }
    return &chainResp, err
}

// GetAPIKey returns an HMAC API key.
func (client *Client) GetAPIKey(keyID string) (*Response, error) {
    uri := fmt.Sprintf("%s%s/%s", client.apiBaseURL, "/api-key", keyID)

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    var key map[string]interface{}
    if err := json.Unmarshal(resp.Response.([]byte), &key); err != nil {
        return nil, err
    }
    resp.Response = key
    return resp, err
}

// ListAPIKeys for a chain.
func (client *Client) ListAPIKeys() (*Response, error) {
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, "/api-key")

    req, err := http.NewRequest("GET", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    var keys []map[string]interface{}
    if err := json.Unmarshal(resp.Response.([]byte), &keys); err != nil {
        return nil, err
    }
    resp.Response = map[string]interface{}{
        "keys": keys,
    }
    return resp, err
}

// CreateAPIKey to access chain with.
func (client *Client) CreateAPIKey() (*Response, error) {
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, "/api-key")
    resp, err := client.httpClient.Post(uri, "content/json", bytes.NewBuffer(nil))
    if err != nil {
        return nil, err
    }
    defer func() {
        _ = resp.Body.Close()
    }()
    var chainResp Response
    bs, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    var key map[string]interface{}
    if err := json.Unmarshal(bs, &key); err != nil {
        return nil, err
    }
    chainResp.Response = key
    chainResp.Status = resp.StatusCode
    if 200 <= resp.StatusCode && resp.StatusCode < 300 {
        chainResp.OK = true
    }
    return &chainResp, err
}

// DeleteAPIKey from chain.
func (client *Client) DeleteAPIKey(apiKey string) (*Response, error) {
    uri := fmt.Sprintf("%s%s", client.apiBaseURL, "/api-key/"+apiKey)
    req, err := http.NewRequest("DELETE", uri, bytes.NewBuffer([]byte("")))
    if err != nil {
        return nil, err
    }
    resp, err := client.performRequest(req, "")
    if err != nil {
        return nil, err
    }
    var success map[string]bool
    if err := json.Unmarshal(resp.Response.([]byte), &success); err != nil {
        return nil, err
    }
    resp.Response = success
    return resp, err
}

// setHeaders sets the http headers of a request to the chain with proper authorization.
func (client *Client) setHeaders(req *http.Request, httpVerb, path, contentType, content string) error {
    if client.creds == nil {
        return ErrNoCredentials
    }
    now := time.Now().UTC().Format("2006-01-02T15:04:05.000000Z07:00")

    if len(contentType) > 0 {
        req.Header.Set("Content-Type", contentType)
    }

    req.Header.Set("Accept", "application/json")
    req.Header.Set("Dragonchain", client.creds.GetDragonchainID())
    req.Header.Set("Timestamp", fmt.Sprintf("%s", now))
    req.Header.Set("Authorization", client.creds.GetAuthorization(httpVerb, path, now, contentType, content))
    return nil
}

func (client *Client) performRequest(req *http.Request, body string) (*Response, error) {
    err := client.setHeaders(req, req.Method, req.URL.RequestURI(), "application/json", body)
    if err != nil {
        return nil, err
    }
    resp, err := client.httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    var chainResp Response
    chainResp.Response, err = ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    chainResp.Status = resp.StatusCode
    if 200 <= resp.StatusCode && resp.StatusCode < 300 {
        chainResp.OK = true
    }
    return &chainResp, err
}

func buildQuery(req *http.Request, query *Query) {
    if query == nil {
        return
    }
    q := req.URL.Query()
    q.Add("q", query.Query)
    if query.Sort != "" {
        q.Add("sort", query.Sort)
    }
    if query.Limit != 0 {
        q.Add("limit", strconv.Itoa(query.Limit))
    }
    if query.Offset != 0 {
        q.Add("offset", strconv.Itoa(query.Offset))
    }
    req.URL.RawQuery = q.Encode()
}