Skip to content

Commit

Permalink
Added new method: FastestQuote()
Browse files Browse the repository at this point in the history
  • Loading branch information
mrz1836 committed Oct 15, 2020
1 parent 5b0776a commit b4d0258
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ View documentation on hosting your own [mAPI server](https://github.com/bitcoin-
- Use your own HTTP client
- Current miner information located at `response.Miner.name` and [defaults](config.go)
- Automatic Signature Validation `response.Validated=true/false`
- `FastestQuote()` will get request quotes from all miners and return the quickest to respond
- `BestQuote()` will get quotes from all miners and return the best quote
- `CalculateFee()` will return the fee for a given transaction

Expand Down
27 changes: 27 additions & 0 deletions examples/fastest_quote/fastest_quote.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"log"

"github.com/tonicpow/go-minercraft"
)

func main() {

// Create a new client
client, err := minercraft.NewClient(nil, nil)
if err != nil {
log.Fatalf("error occurred: %s", err.Error())
}

log.Printf("querying %d miners for the fastest response...", len(client.Miners))

// Fetch fastest quote from all miners
var response *minercraft.FeeQuoteResponse
response, err = client.FastestQuote()
if err != nil {
log.Fatalf("error occurred: %s", err.Error())
}

log.Printf("found quote: %s", response.Miner.Name)
}
59 changes: 59 additions & 0 deletions fastest_quote.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package minercraft

import (
"context"
"sync"
)

// FastestQuote will check all known miners and return the fastest quote response
//
// Note: this might return different results each time if miners have the same rates as
// it's a race condition on which results come back first
func (c *Client) FastestQuote() (*FeeQuoteResponse, error) {

// Get the fastest quote
result := c.fetchFastestQuote()

// Check for error?
if result.Response.Error != nil {
return nil, result.Response.Error
}

// Parse the response
quote, err := result.parseQuote()
if err != nil {
return nil, err
}

// Return the quote
return &quote, nil
}

// fetchFastestQuote will return a quote that is the quickest to resolve
func (c *Client) fetchFastestQuote() *internalResult {

// The channel for the internal results
resultsChannel := make(chan *internalResult, len(c.Miners))

// Create a context (to cancel)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Loop each miner (break into a Go routine for each quote request)
var wg sync.WaitGroup
for _, miner := range c.Miners {
wg.Add(1)
go func(ctx context.Context, client *Client, miner *Miner) {
defer wg.Done()
resultsChannel <- getQuote(ctx, client, miner)
}(ctx, c, miner)
}

// Waiting for all requests to finish
go func() {
wg.Wait()
close(resultsChannel)
}()

return <-resultsChannel
}
152 changes: 152 additions & 0 deletions fastest_quote_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package minercraft

import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"testing"
)

// mockHTTPValidFastestQuote for mocking requests
type mockHTTPValidFastestQuote struct{}

// Do is a mock http request
func (m *mockHTTPValidFastestQuote) Do(req *http.Request) (*http.Response, error) {
resp := new(http.Response)
resp.StatusCode = http.StatusBadRequest

// No req found
if req == nil {
return resp, fmt.Errorf("missing request")
}

// Valid response
if req.URL.String() == defaultProtocol+"merchantapi.taal.com/mapi/feeQuote" {
resp.StatusCode = http.StatusOK
resp.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(`{
"payload": "{\"apiVersion\":\"` + testAPIVersion + `\",\"timestamp\":\"2020-10-09T21:26:17.410Z\",\"expiryTime\":\"2020-10-09T21:36:17.410Z\",\"minerId\":\"03e92d3e5c3f7bd945dfbf48e7a99393b1bfb3f11f380ae30d286e7ff2aec5a270\",\"currentHighestBlockHash\":\"0000000000000000035c5f8c0294802a01e500fa7b95337963bb3640da3bd565\",\"currentHighestBlockHeight\":656169,\"minerReputation\":null,\"fees\":[{\"id\":1,\"feeType\":\"standard\",\"miningFee\":{\"satoshis\":400,\"bytes\":1000},\"relayFee\":{\"satoshis\":250,\"bytes\":1000}},{\"id\":2,\"feeType\":\"data\",\"miningFee\":{\"satoshis\":500,\"bytes\":1000},\"relayFee\":{\"satoshis\":225,\"bytes\":1000}}]}",
"signature": "3045022100eed49f6bf75d8f975f581271e3df658fbe8ec67e6301ea8fc25a72d18c92e30e022056af253f0d24db6a8fde4e2c1ee95e7a5ecf2c7cdc93246f8328c9e0ca582fc4",
"publicKey": "03e92d3e5c3f7bd945dfbf48e7a99393b1bfb3f11f380ae30d286e7ff2aec5a270","encoding": "` + testEncoding + `","mimetype": "` + testMimeType + `"}`)))
}

if req.URL.String() == defaultProtocol+"merchantapi.matterpool.io/mapi/feeQuote" {
resp.StatusCode = http.StatusOK
resp.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(`{
"payload": "{\"apiVersion\":\"` + testAPIVersion + `\",\"timestamp\":\"2020-10-09T22:08:26.236Z\",\"expiryTime\":\"2020-10-09T22:18:26.236Z\",\"minerId\":\"0211ccfc29e3058b770f3cf3eb34b0b2fd2293057a994d4d275121be4151cdf087\",\"currentHighestBlockHash\":\"0000000000000000028285a9168c95457521a743765f499de389c094e883f42a\",\"currentHighestBlockHeight\":656171,\"minerReputation\":null,\"fees\":[{\"feeType\":\"standard\",\"miningFee\":{\"satoshis\":400,\"bytes\":1000},\"relayFee\":{\"satoshis\":100,\"bytes\":1000}},{\"feeType\":\"data\",\"miningFee\":{\"satoshis\":430,\"bytes\":1000},\"relayFee\":{\"satoshis\":110,\"bytes\":1000}}]}",
"signature": "3044022011f90db2661726eb2659c3447ccaa9fd3368194f87d5d86a23e673c45d5d714502200c51eb600e3370b49d759aa4d441000286937b0803037a1d6de4c5a5c559d74c",
"publicKey": "0211ccfc29e3058b770f3cf3eb34b0b2fd2293057a994d4d275121be4151cdf087","encoding": "` + testEncoding + `","mimetype": "` + testMimeType + `"}`)))
}

if req.URL.String() == defaultProtocol+"www.ddpurse.com/openapi/mapi/feeQuote" {
resp.StatusCode = http.StatusOK
resp.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(`{
"payload": "{\"apiVersion\":\"` + testAPIVersion + `\",\"timestamp\":\"2020-10-09T22:09:04.433Z\",\"expiryTime\":\"2020-10-09T22:19:04.433Z\",\"minerId\":null,\"currentHighestBlockHash\":\"0000000000000000028285a9168c95457521a743765f499de389c094e883f42a\",\"currentHighestBlockHeight\":656171,\"minerReputation\":null,\"fees\":[{\"feeType\":\"standard\",\"miningFee\":{\"satoshis\":500,\"bytes\":1000},\"relayFee\":{\"satoshis\":250,\"bytes\":1000}},{\"feeType\":\"data\",\"miningFee\":{\"satoshis\":420,\"bytes\":1000},\"relayFee\":{\"satoshis\":150,\"bytes\":1000}}]}",
"signature": null,"publicKey": null,"encoding": "` + testEncoding + `","mimetype": "` + testMimeType + `"}`)))
}

// Default is valid
return resp, nil
}

// TestClient_FastestQuote tests the method FastestQuote()
func TestClient_FastestQuote(t *testing.T) {
t.Parallel()

// Create a client
client := newTestClient(&mockHTTPValidFastestQuote{})

// Create a req
response, err := client.FastestQuote()
if err != nil {
t.Fatalf("error occurred: %s", err.Error())
} else if response == nil {
t.Fatalf("expected response to not be nil")
}

// Check returned values
if response.Encoding != testEncoding {
t.Fatalf("expected response.Encoding to be %s, got %s", testEncoding, response.Encoding)
}
if response.MimeType != testMimeType {
t.Fatalf("expected response.MimeType to be %s, got %s", testMimeType, response.MimeType)
}

// Check that we got fees
if len(response.Quote.Fees) != 2 {
t.Fatalf("expected response.Quote.Fees to be a length of %d, got %d", 2, len(response.Quote.Fees))
}
}

// TestClient_FastestQuoteHTTPError tests the method FastestQuote()
func TestClient_FastestQuoteHTTPError(t *testing.T) {
t.Parallel()

// Create a client
client := newTestClient(&mockHTTPError{})

// Create a req
response, err := client.FastestQuote()
if err == nil {
t.Fatalf("error should have occurred")
} else if response != nil {
t.Fatalf("expected response to be nil")
}
}

// TestClient_FastestQuoteBadRequest tests the method FastestQuote()
func TestClient_FastestQuoteBadRequest(t *testing.T) {
t.Parallel()

// Create a client
client := newTestClient(&mockHTTPBadRequest{})

// Create a req
response, err := client.FastestQuote()
if err == nil {
t.Fatalf("error should have occurred")
} else if response != nil {
t.Fatalf("expected response to be nil")
}
}

// TestClient_FastestQuoteInvalidJSON tests the method FastestQuote()
func TestClient_FastestQuoteInvalidJSON(t *testing.T) {
t.Parallel()

// Create a client
client := newTestClient(&mockHTTPInvalidJSON{})

// Create a req
response, err := client.FastestQuote()
if err == nil {
t.Fatalf("error should have occurred")
} else if response != nil {
t.Fatalf("expected response to be nil")
}
}

// ExampleClient_FastestQuote example using FastestQuote()
func ExampleClient_FastestQuote() {
// Create a client (using a test client vs NewClient())
client := newTestClient(&mockHTTPValidFastestQuote{})

// Create a req
_, err := client.FastestQuote()
if err != nil {
fmt.Printf("error occurred: %s", err.Error())
return
}

// Note: cannot show response since the miner might be different each time
fmt.Printf("got fastest quote!")
// Output:got fastest quote!
}

// BenchmarkClient_FastestQuote benchmarks the method FastestQuote()
func BenchmarkClient_FastestQuote(b *testing.B) {
client := newTestClient(&mockHTTPValidFastestQuote{})
for i := 0; i < b.N; i++ {
_, _ = client.FastestQuote()
}
}

0 comments on commit b4d0258

Please sign in to comment.