diff --git a/.github/CODE_STANDARDS.md b/.github/CODE_STANDARDS.md index 0d053619..1a5ac2d7 100644 --- a/.github/CODE_STANDARDS.md +++ b/.github/CODE_STANDARDS.md @@ -2,17 +2,29 @@ - [Code Standards \& Contributing Guidelines](#code-standards--contributing-guidelines) - [Most important rules - Quick Checklist](#most-important-rules---quick-checklist) - - [1. Code style and formatting - official guidelines](#1-code-style-and-formatting---official-guidelines) + - [1 Code style and formatting - official guidelines](#1-code-style-and-formatting---official-guidelines) - [1.1 In GO applications or libraries, we follow the official guidelines](#11-in-go-applications-or-libraries-we-follow-the-official-guidelines) - [Additional useful resources with GO recommendations, best practices and the common mistakes](#additional-useful-resources-with-go-recommendations-best-practices-and-the-common-mistakes) - - [2. Code Rules](#2-code-rules) - - [2.1. Self-documenting code](#21-self-documenting-code) + - [2 Code Rules](#2-code-rules) + - [2.1 Self-documenting code](#21-self-documenting-code) - [As a Developer](#as-a-developer) - [As a PR Reviewer](#as-a-pr-reviewer) - - [2.2. Tests](#22-tests) + - [2.2 Tests](#22-tests) - [Principle](#principle) - [Guidelines for Writing Tests](#guidelines-for-writing-tests) - - [Pull request title with a scope and task number](#pull-request-title-with-a-scope-and-task-number) + - [2.3 Code Review](#23-Code-Review) + - [Code Review Checklist](#code-review-checklist) + - [3 Contributing](#3-contributing) + - [3.1 Pull Requests && Issues](#31-pull-requests--issues) + - [3.2 Conventional Commits & Pull Requests Naming](#32-conventional-commits--pull-requests-naming) + - [Overview](#overview) + - [Structure](#structure) + - [Types](#types) + - [Conventional Commits - Automatic Versioning](#conventional-commits---automatic-versioning) + - [Scope](#scope) + - [Further Reading](#further-reading) + - [Examples](#examples) + - [Pull request title with a scope and task number](#pull-request-title-with-a-scope-and-task-number) - [3.3 Branching](#33-branching) - [Choosing branch names](#choosing-branch-names) - [Descriptiveness](#descriptiveness) @@ -20,15 +32,15 @@ - [Deleting Branches After Merging](#deleting-branches-after-merging) - [Remove Remote Branches](#remove-remote-branches) - [Recommendation: Clean Local Branches](#recommendation-clean-local-branches) - - [4. Documentation Code Standards](#4-documentation-code-standards) + - [4 Documentation Code Standards](#4-documentation-code-standards) - [4.1 Overview](#41-overview) - [4.2 Principles](#42-principles) - [4.3 Feature Documentation](#43-feature-documentation) - - [4.3.1 Necessity](#431-necessity) - - [4.3.2 Examples](#432-examples) + - [Necessity](#necessity) + - [Examples](#examples) - [4.4 External Features](#44-external-features) - [4.5 Markdown usage](#45-markdown-usage) - - [4.5 Conclusion](#45-conclusion) + - [4.6 Conclusion](#46-conclusion) ## Most important rules - Quick Checklist @@ -41,12 +53,12 @@ - [ ] Keep documentation clear, concise, up-to-date, and accessible. - [ ] Branching - choose consistent naming conventions, include issue number, delete branches after merging. -## 1. Code style and formatting - official guidelines +## 1 Code style and formatting - official guidelines ### 1.1 In GO applications or libraries, we follow the official guidelines - [Effective Go](https://go.dev/doc/effective_go) - official Go guidelines -- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) - official Go code review comments +- [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments) - official Go code review comments - [Go Examples](https://pkg.go.dev/testing#hdr-Examples) - official Go examples - used in libraries to explain how to use their exposed features - [Go Test](https://pkg.go.dev/testing) - official Go testing package & recommendations - [Go Linter](https://golangci-lint.run/) - golangci-lint - only codestyle checks @@ -57,9 +69,9 @@ - [Go Styles by Google](https://google.github.io/styleguide/go/) - Google's Go Style Guide - [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md) - Uber's Go Style Guide -- [Go Common Mistakes](https://github.com/golang/go/wiki/CommonMistakes) - Common Mistakes in Go +- [Go Common Mistakes](https://go.dev/wiki/CommonMistakes) - Common Mistakes in Go -## 2. Code Rules +## 2 Code Rules ### 2.1. Self-documenting code @@ -75,7 +87,7 @@ - Be vigilant of newly added comments during reviews. If a comment appears unnecessary, uninformative, or could be replaced with a function, do not hesitate to highlight this. - Assess the meaningfulness and clarity of function names, ensuring they contribute to self-documenting code. -### 2.2. Tests +### 2.2 Tests #### Principle @@ -140,7 +152,7 @@ Of course not only error paths should be covered - **we should highlight the hap 10. **Responsiveness**: Both authors and reviewers should be timely in their responses. Authors should address all review comments, and reviewers should re-review changes promptly. -#### 2.3.3 Code Review Checklist +#### Code Review Checklist - [ ] Does the code adhere to the project’s coding standards? - [ ] Are there sufficient tests, and do they cover a variety of cases? @@ -155,7 +167,7 @@ Of course not only error paths should be covered - **we should highlight the hap This checklist serves as a guide to both authors and reviewers to ensure a thorough and effective code review process. -## 3. Contributing +## 3 Contributing ### 3.1 Pull Requests && Issues @@ -163,11 +175,11 @@ We have separate templates for Pull Requests and Issues. Please use them when cr ### 3.2 Conventional Commits & Pull Requests Naming -#### 3.2.1 Overview +#### Overview In an effort to maintain clarity and coherence in our commit history, we are adopting the Conventional Commits style for all commit messages across our repositories. This uniform format not only enhances the readability of our commit history but also facilitates automated tools in generating changelogs and extracting valuable information effectively. -#### 3.2.2 Structure +#### Structure Conventional Commits follow a structured format: `type(scope): description`, where: @@ -175,9 +187,10 @@ Conventional Commits follow a structured format: `type(scope): description`, whe - `scope`: Denotes the relevant module or issue. - `description`: Provides a brief explanation of the change. -When introducing breaking changes, an `!` should be appended after the `type/scope`: `feat(#123)!: introduce a breaking change`. +When introducing breaking changes, an `!` should be appended after the `type/scope`:
+`feat(#123)!: introduce a breaking change`. -#### 3.2.3 Types +#### Types - `feat`: Utilized when introducing a new feature to the codebase. - `fix`: Employed when resolving a bug or issue in the code. @@ -191,20 +204,20 @@ When introducing breaking changes, an `!` should be appended after the `type/sco - `ci`: Applied to changes concerning the Continuous Integration (CI) configuration or scripts. - `deps`: Used when updating or modifying dependencies. -#### 3.2.4 Conventional Commits - Automatic Versioning +#### Conventional Commits - Automatic Versioning In our repositories, we use Conventional Commits to automatically generate the version number for our releases. It works like this: -`fix: which represents bug fixes, and correlates to a SemVer patch.` -`feat: which represents a new feature, and correlates to a SemVer minor.` +`fix: which represents bug fixes, and correlates to a SemVer patch.`
+`feat: which represents a new feature, and correlates to a SemVer minor.`
`feat!:, or fix!:, refactor!:, etc., which represent a breaking change (indicated by the !) and will result in a SemVer major.` Real life example: -`feat(#123)!: introduce breaking change - 1.0.0 -> 2.0.0` -`feat(#124): introduce new feature - 2.0.0 -> 2.1.0` +`feat(#123)!: introduce breaking change - 1.0.0 -> 2.0.0`
+`feat(#124): introduce new feature - 2.0.0 -> 2.1.0`
`fix(#125): fix a bug - 2.1.0 -> 2.1.1` Given a version number MAJOR.MINOR.PATCH, increment the: @@ -216,17 +229,17 @@ Additional labels for pre-release and build metadata are available as extensions More about Semantic Versioning can be found [here](https://semver.org/). -#### 3.2.5 Scope +#### Scope We have standardized the use of JIRA/GitHub issue numbers as the `scope` in commits within our team. This practice aids in easily tracing the origin of changes. In the absence of an existing issue for your changes, please create one in the client’s JIRA system. If the change is not client-related, establish a GitHub issue in the repository. -#### 3.2.6 Further Reading +#### Further Reading Additional information and guidelines on Conventional Commits can be found [here](https://www.conventionalcommits.org/en/v1.0.0/). -#### 3.2.7 Examples +#### Examples ##### Commit message with scope @@ -260,11 +273,13 @@ debugo feature - checkpoint full work #### Descriptiveness - Branch names should be descriptive and represent the task/feature at hand. -- Use hyphens to separate words for readability, e.g., `feature/add-login-button`. +- Use hyphens to separate words for readability, e.g.,
+ `feature/add-login-button`. #### Include Issue Number -- If applicable, include the issue number in the branch name for easy tracking, e.g., `feature/123-add-login-button`. +- If applicable, include the issue number in the branch name for easy tracking, e.g.,
+`feature/123-add-login-button`. #### Deleting Branches After Merging @@ -275,9 +290,10 @@ debugo feature - checkpoint full work #### Recommendation: Clean Local Branches -- Regularly prune local branches that have been deleted remotely with `git fetch -p && git branch -vv | grep 'origin/.*: gone]' | awk '{print $1}' | xargs git branch -d`. +- Regularly prune local branches that have been deleted remotely with
+`git fetch -p && git branch -vv | grep 'origin/.*: gone]' | awk '{print $1}' | xargs git branch -d`. -## 4. Documentation Code Standards +## 4 Documentation Code Standards ### 4.1 Overview @@ -291,11 +307,11 @@ A well-documented codebase is pivotal for both internal development and external ### 4.3 Feature Documentation -#### 4.3.1 Necessity +#### Necessity Every feature developed should be accompanied by adequate documentation. The necessity for documentation becomes even more pronounced for open-source projects, where clear instructions and examples facilitate easier adoption and contribution from the community. -#### 4.3.2 Examples +#### Examples - **Inclusion of Examples**: Where applicable, documentation should include practical examples demonstrating the feature’s usage and benefits. Examples act as a practical guide, aiding developers in understanding and implementing the feature correctly. - **Clarity of Examples**: Examples should be clear, concise, and relevant, illustrating the functionality of the feature effectively. @@ -311,8 +327,8 @@ For projects exposing external features: We should write documentation in Markdown format. It allows us to write documentation in a simple and readable way. It's also easy to convert Markdown to HTML or PDF or create a website from it. -[Markdown Guide](markdownguide.org) - Comprehensive guide to Markdown syntax. +[Markdown Guide](https://markdownguide.org) - Comprehensive guide to Markdown syntax. -### 4.5 Conclusion +### 4.6 Conclusion Adhering to documentation code standards is integral for maintaining a healthy, understandable, and contributable codebase. By ensuring every feature is well-documented, with the inclusion of clear examples where necessary, we foster a conducive environment for development and community engagement, particularly in open-source projects. diff --git a/README.md b/README.md index fad5db9a..6b0e85b5 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,31 @@ +
+ # BUX -> Bitcoin UTXO & xPub Management Engine [![Release](https://img.shields.io/github/release-pre/BuxOrg/bux.svg?logo=github&style=flat&v=2)](https://github.com/BuxOrg/bux/releases) [![Build Status](https://img.shields.io/github/actions/workflow/status/BuxOrg/bux/run-tests.yml?branch=master&v=2)](https://github.com/BuxOrg/bux/actions) [![Report](https://goreportcard.com/badge/github.com/BuxOrg/bux?style=flat&v=2)](https://goreportcard.com/report/github.com/BuxOrg/bux) [![codecov](https://codecov.io/gh/BuxOrg/bux/branch/master/graph/badge.svg?v=2)](https://codecov.io/gh/BuxOrg/bux) [![Mergify Status](https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/BuxOrg/bux&style=flat&v=2)](https://mergify.com) -[![Go](https://img.shields.io/github/go-mod/go-version/BuxOrg/bux?v=2)](https://golang.org/)
+ +[![Go](https://img.shields.io/github/go-mod/go-version/BuxOrg/bux?v=2)](https://golang.org/) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod&v=2)](https://gitpod.io/#https://github.com/BuxOrg/bux) [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat&v=2)](https://github.com/RichardLitt/standard-readme) [![Makefile Included](https://img.shields.io/badge/Makefile-Supported%20-brightgreen?=flat&logo=probot&v=2)](Makefile) -[![Sponsor](https://img.shields.io/badge/sponsor-mrz1836-181717.svg?logo=github&style=flat&v=2)](https://github.com/sponsors/mrz1836) -[![Donate](https://img.shields.io/badge/donate-bitcoin-ff9900.svg?logo=bitcoin&style=flat&v=2)](https://gobitcoinsv.com/#sponsor?utm_source=github&utm_medium=sponsor-link&utm_campaign=bux&utm_term=bux&utm_content=bux) -
+
+ +> Bitcoin UTXO & xPub Management Engine ## Table of Contents - [About](#about) - [Installation](#installation) - [Documentation](#documentation) -- [Examples & Tests](#examples--tests) -- [Benchmarks](#benchmarks) -- [Code Standards](#code-standards) - [Usage](#usage) + - [Examples & Tests](#examples--tests) + - [Benchmarks](#benchmarks) +- [Code Standards](#code-standards) - [Contributing](#contributing) - [License](#license) @@ -211,7 +213,12 @@ vet Run the Go vet application
-## Examples & Tests +## Usage + +### Examples & Tests + +Checkout all the [examples](examples)! + All unit tests and [examples](examples) run via [GitHub Actions](https://github.com/BuxOrg/bux/actions) and uses [Go version 1.19.x](https://golang.org/doc/go1.19). View the [configuration file](.github/workflows/run-tests.yml). @@ -238,7 +245,7 @@ make test-short
-## Benchmarks +### Benchmarks Run the Go benchmarks: ```shell script make bench @@ -275,41 +282,11 @@ Checkout all the [examples](examples)!
## Contributing -View the [contributing guidelines](.github/CONTRIBUTING.md) and follow the [code of conduct](.github/CODE_OF_CONDUCT.md). - +All kinds of contributions are welcome!
- -### How can I help? -All kinds of contributions are welcome :raised_hands:! -The most basic way to show your support is to star :star2: the project, or to raise issues :speech_balloon:. -You can also support this project by [becoming a sponsor on GitHub](https://github.com/sponsors/mrz1836) :clap: -or by making a [**bitcoin donation**](https://gobitcoinsv.com/#sponsor?utm_source=github&utm_medium=sponsor-link&utm_campaign=bux&utm_term=bux&utm_content=bux) to ensure this journey continues indefinitely! :rocket: - -[![Stars](https://img.shields.io/github/stars/BuxOrg/bux?label=Please%20like%20us&style=social&v=2)](https://github.com/BuxOrg/bux/stargazers) - +To get started, take a look at [code standards](.github/CODE_STANDARDS.md).
- -### Contributors ✨ -Thank you to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): - - - - - - - - - - - -

Mr. Z

🚇 💻 🚧 💼

Siggi

🚇 💻 🛡️

Dylan

🚇 💻

Satchmo

📝 🖋 🎨
- - - - - - -> This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. +View the [contributing guidelines](.github/CODE_STANDARDS.md#3-contributing) and follow the [code of conduct](.github/CODE_OF_CONDUCT.md).
diff --git a/action_access_key.go b/action_access_key.go index b610c968..030b77ca 100644 --- a/action_access_key.go +++ b/action_access_key.go @@ -61,7 +61,7 @@ func (c *Client) GetAccessKey(ctx context.Context, xPubID, id string) (*AccessKe if err != nil { return nil, err } else if accessKey == nil { - return nil, ErrMissingXpub + return nil, ErrAccessKeyNotFound } // make sure this is the correct accessKey diff --git a/action_transaction.go b/action_transaction.go index 103364eb..2186f54e 100644 --- a/action_transaction.go +++ b/action_transaction.go @@ -92,10 +92,7 @@ func (c *Client) NewTransaction(ctx context.Context, rawXpubKey string, config * return draftTransaction, nil } -// GetTransaction will get a transaction from the Datastore -// -// ctx is the context -// testTxID is the transaction ID +// GetTransaction will get a transaction by its ID from the Datastore func (c *Client) GetTransaction(ctx context.Context, xPubID, txID string) (*Transaction, error) { // Check for existing NewRelic transaction ctx = c.GetOrStartTxn(ctx, "get_transaction") @@ -114,18 +111,13 @@ func (c *Client) GetTransaction(ctx context.Context, xPubID, txID string) (*Tran return transaction, nil } -// GetTransactionByID will get a transaction from the Datastore by tx ID -// uses GetTransaction -func (c *Client) GetTransactionByID(ctx context.Context, txID string) (*Transaction, error) { - return c.GetTransaction(ctx, "", txID) -} - +// GetTransactionsByIDs returns array of transactions by their IDs from the Datastore func (c *Client) GetTransactionsByIDs(ctx context.Context, txIDs []string) ([]*Transaction, error) { // Check for existing NewRelic transaction ctx = c.GetOrStartTxn(ctx, "get_transactions_by_ids") // Create the conditions - conditions := generateTxIdFilterConditions(txIDs) + conditions := generateTxIDFilterConditions(txIDs) // Get the transactions by it's IDs transactions, err := getTransactions( @@ -169,25 +161,6 @@ func (c *Client) GetTransactions(ctx context.Context, metadataConditions *Metada return transactions, nil } -// GetTransactionsAggregate will get a count of all transactions per aggregate column from the Datastore -func (c *Client) GetTransactionsAggregate(ctx context.Context, metadataConditions *Metadata, - conditions *map[string]interface{}, aggregateColumn string, opts ...ModelOps, -) (map[string]interface{}, error) { - // Check for existing NewRelic transaction - ctx = c.GetOrStartTxn(ctx, "get_transactions") - - // Get the transactionAggregate - transactionAggregate, err := getTransactionsAggregate( - ctx, metadataConditions, conditions, aggregateColumn, - c.DefaultModelOptions(opts...)..., - ) - if err != nil { - return nil, err - } - - return transactionAggregate, nil -} - // GetTransactionsCount will get a count of all the transactions from the Datastore func (c *Client) GetTransactionsCount(ctx context.Context, metadataConditions *Metadata, conditions *map[string]interface{}, opts ...ModelOps, @@ -414,7 +387,7 @@ func (c *Client) RevertTransaction(ctx context.Context, id string) error { return err } -func generateTxIdFilterConditions(txIDs []string) *map[string]interface{} { +func generateTxIDFilterConditions(txIDs []string) *map[string]interface{} { orConditions := make([]map[string]interface{}, len(txIDs)) for i, txID := range txIDs { diff --git a/action_utxo.go b/action_utxo.go index fe6af27c..16e8d7aa 100644 --- a/action_utxo.go +++ b/action_utxo.go @@ -136,7 +136,7 @@ func (c *Client) UnReserveUtxos(ctx context.Context, xPubID, draftID string) err // Check for existing NewRelic transaction ctx = c.GetOrStartTxn(ctx, "unreserve_uxtos_by_draft_id") - return unReserveUtxos(ctx, xPubID, draftID) + return unReserveUtxos(ctx, xPubID, draftID, c.DefaultModelOptions()...) } // should this be optional in the results? diff --git a/authentication.go b/authentication.go index 9772dab4..e7705714 100644 --- a/authentication.go +++ b/authentication.go @@ -21,8 +21,8 @@ import ( // // Sets req.Context(xpub) and req.Context(xpub_hash) func (c *Client) AuthenticateRequest(ctx context.Context, req *http.Request, adminXPubs []string, - adminRequired, requireSigning, signingDisabled bool) (*http.Request, error) { - + adminRequired, requireSigning, signingDisabled bool, +) (*http.Request, error) { // Get the xPub/Access Key from the header xPub := strings.TrimSpace(req.Header.Get(AuthHeader)) authAccessKey := strings.TrimSpace(req.Header.Get(AuthAccessKey)) @@ -105,7 +105,6 @@ func (c *Client) AuthenticateRequest(ctx context.Context, req *http.Request, adm // checkSignature check the signature for the provided auth payload func (c *Client) checkSignature(ctx context.Context, xPubOrAccessKey string, auth *AuthPayload) error { - // Check that we have the basic signature components if err := checkSignatureRequirements(auth); err != nil { return err @@ -120,7 +119,6 @@ func (c *Client) checkSignature(ctx context.Context, xPubOrAccessKey string, aut // checkSignatureRequirements will check the payload for basic signature requirements func checkSignatureRequirements(auth *AuthPayload) error { - // Check that we have a signature if auth == nil || auth.Signature == "" { return ErrMissingSignature @@ -141,7 +139,6 @@ func checkSignatureRequirements(auth *AuthPayload) error { // verifyKeyXPub will verify the xPub key and the signature payload func verifyKeyXPub(xPub string, auth *AuthPayload) error { - // Validate that the xPub is an HD key (length, validation) if _, err := utils.ValidateXPub(xPub); err != nil { return err @@ -182,7 +179,6 @@ func verifyKeyXPub(xPub string, auth *AuthPayload) error { // verifyAccessKey will verify the access key and the signature payload func verifyAccessKey(ctx context.Context, key string, auth *AuthPayload, opts ...ModelOps) error { - // Get access key from DB // todo: add caching in the future, faster than DB accessKey, err := getAccessKey(ctx, utils.Hash(key), opts...) @@ -214,7 +210,6 @@ func verifyAccessKey(ctx context.Context, key string, auth *AuthPayload, opts .. // SetSignature will set the signature on the header for the request func SetSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString string) error { - // Create the signature authData, err := createSignature(xPriv, bodyString) if err != nil { @@ -229,7 +224,6 @@ func SetSignature(header *http.Header, xPriv *bip32.ExtendedKey, bodyString stri // SetSignatureFromAccessKey will set the signature on the header for the request from an access key func SetSignatureFromAccessKey(header *http.Header, privateKeyHex, bodyString string) error { - // Create the signature authData, err := createSignatureAccessKey(privateKeyHex, bodyString) if err != nil { @@ -243,7 +237,6 @@ func SetSignatureFromAccessKey(header *http.Header, privateKeyHex, bodyString st } func setSignatureHeaders(header *http.Header, authData *AuthPayload) error { - // Create the auth header hash header.Set(AuthHeaderHash, authData.AuthHash) @@ -287,8 +280,3 @@ func GetXpubIDFromRequest(req *http.Request) (string, bool) { func IsAdminRequest(req *http.Request) (bool, bool) { return getBoolFromRequest(req, ParamAdminRequest) } - -// GetXpubHashFromRequest gets the stored xPub hash from the request if found -func GetXpubHashFromRequest(req *http.Request) (string, bool) { - return getFromRequest(req, ParamXPubHashKey) -} diff --git a/authentication_test.go b/authentication_test.go index 22f1169b..76e2058a 100644 --- a/authentication_test.go +++ b/authentication_test.go @@ -57,7 +57,7 @@ func TestClient_AuthenticateRequest(t *testing.T) { assert.Equal(t, testXpubAuth, x) assert.Equal(t, true, ok) - x, ok = GetXpubHashFromRequest(req) + x, ok = GetXpubIDFromRequest(req) assert.Equal(t, testXpubAuthHash, x) assert.Equal(t, true, ok) }) @@ -233,7 +233,7 @@ func TestClient_AuthenticateRequest(t *testing.T) { assert.Equal(t, "", x) assert.Equal(t, false, ok) - x, ok = GetXpubHashFromRequest(req) + x, ok = GetXpubIDFromRequest(req) assert.Equal(t, "", x) assert.Equal(t, false, ok) }) @@ -259,7 +259,7 @@ func TestClient_AuthenticateRequest(t *testing.T) { assert.Equal(t, "", x) assert.Equal(t, false, ok) - x, ok = GetXpubHashFromRequest(req) + x, ok = GetXpubIDFromRequest(req) assert.Equal(t, "", x) assert.Equal(t, false, ok) }) @@ -367,7 +367,6 @@ func Test_verifyKeyXPub(t *testing.T) { t.Parallel() t.Run("error - missing auth data", func(t *testing.T) { - err := verifyKeyXPub(testXpubAuth, nil) require.Error(t, err) assert.ErrorIs(t, err, ErrMissingSignature) @@ -659,8 +658,8 @@ func TestIsAdminRequest(t *testing.T) { }) } -// TestGetXpubHashFromRequest will test the method GetXpubHashFromRequest() -func TestGetXpubHashFromRequest(t *testing.T) { +// TestGetXpubHashFromRequest will test the method GetXpubIDFromRequest() +func TestGetXpubIDFromRequest(t *testing.T) { t.Parallel() t.Run("valid value", func(t *testing.T) { @@ -670,7 +669,7 @@ func TestGetXpubHashFromRequest(t *testing.T) { req = setOnRequest(req, ParamXPubHashKey, testXpubAuthHash) - xPubHash, success := GetXpubHashFromRequest(req) + xPubHash, success := GetXpubIDFromRequest(req) assert.Equal(t, testXpubAuthHash, xPubHash) assert.Equal(t, true, success) }) @@ -680,7 +679,7 @@ func TestGetXpubHashFromRequest(t *testing.T) { require.NoError(t, err) require.NotNil(t, req) - xPubHash, success := GetXpubHashFromRequest(req) + xPubHash, success := GetXpubIDFromRequest(req) assert.Equal(t, "", xPubHash) assert.Equal(t, false, success) }) diff --git a/beef_bump.go b/beef_bump.go index 939d5b52..374ecb30 100644 --- a/beef_bump.go +++ b/beef_bump.go @@ -62,7 +62,7 @@ func prepareBEEFFactors(ctx context.Context, tx *Transaction, store TransactionG return nil, nil, err } - var txIDs []string + txIDs := make([]string, 0, len(tx.draftTransaction.Configuration.Inputs)) for _, input := range tx.draftTransaction.Configuration.Inputs { txIDs = append(txIDs, input.UtxoPointer.TransactionID) } @@ -96,7 +96,7 @@ func prepareBEEFFactors(ctx context.Context, tx *Transaction, store TransactionG } func checkParentTransactions(ctx context.Context, store TransactionGetter, btTx *bt.Tx) ([]*bt.Tx, []*Transaction, error) { - var parentTxIDs []string + parentTxIDs := make([]string, 0, len(btTx.Inputs)) for _, txIn := range btTx.Inputs { parentTxIDs = append(parentTxIDs, txIn.PreviousTxIDStr()) } @@ -106,8 +106,8 @@ func checkParentTransactions(ctx context.Context, store TransactionGetter, btTx return nil, nil, err } - var validTxs []*Transaction - var validBtTxs []*bt.Tx + validTxs := make([]*Transaction, 0, len(parentTxs)) + validBtTxs := make([]*bt.Tx, 0, len(parentTxs)) for _, parentTx := range parentTxs { parentBtTx, err := bt.NewTxFromString(parentTx.Hex) if err != nil { diff --git a/beef_tx.go b/beef_tx.go index b8b60e69..4e53b233 100644 --- a/beef_tx.go +++ b/beef_tx.go @@ -28,8 +28,11 @@ func ToBeef(ctx context.Context, tx *Transaction, store TransactionGetter) (stri } bumps, err := calculateMergedBUMP(bumpFactors) + if err != nil { + return "", err + } sortedTxs := kahnTopologicalSortTransactions(bumpBtFactors) - beefHex, err := toBeefHex(ctx, bumps, sortedTxs) + beefHex, err := toBeefHex(bumps, sortedTxs) if err != nil { return "", fmt.Errorf("ToBeef() error: %w", err) } @@ -37,8 +40,8 @@ func ToBeef(ctx context.Context, tx *Transaction, store TransactionGetter) (stri return beefHex, nil } -func toBeefHex(ctx context.Context, bumps BUMPs, parentTxs []*bt.Tx) (string, error) { - beef, err := newBeefTx(ctx, 1, bumps, parentTxs) +func toBeefHex(bumps BUMPs, parentTxs []*bt.Tx) (string, error) { + beef, err := newBeefTx(1, bumps, parentTxs) if err != nil { return "", fmt.Errorf("ToBeefHex() error: %w", err) } @@ -51,7 +54,7 @@ func toBeefHex(ctx context.Context, bumps BUMPs, parentTxs []*bt.Tx) (string, er return hex.EncodeToString(beefBytes), nil } -func newBeefTx(ctx context.Context, version uint32, bumps BUMPs, parentTxs []*bt.Tx) (*beefTx, error) { +func newBeefTx(version uint32, bumps BUMPs, parentTxs []*bt.Tx) (*beefTx, error) { if version > maxBeefVer { return nil, fmt.Errorf("version above 0x%X", maxBeefVer) } diff --git a/beef_tx_sorting.go b/beef_tx_sorting.go index 6f322bb7..7d0da13f 100644 --- a/beef_tx_sorting.go +++ b/beef_tx_sorting.go @@ -26,7 +26,7 @@ func prepareSortStructures(dag []*bt.Tx) (txByID map[string]*bt.Tx, incomingEdge incomingEdgesMap = make(map[string]int, dagLen) for _, tx := range dag { - txByID[tx.TxID()] = tx // TODO: perf -> In bt, the TxID is calculated every time we try to get it, which means we hash the tx bytes twice each time. It's expensive operation - try to avoid calulation each time + txByID[tx.TxID()] = tx // TODO: perf -> In bt, the TxID is calculated every time we try to get it, which means we hash the tx bytes twice each time. It's expensive operation - try to avoid calculation each time incomingEdgesMap[tx.TxID()] = 0 } @@ -39,7 +39,7 @@ func prepareSortStructures(dag []*bt.Tx) (txByID map[string]*bt.Tx, incomingEdge func calculateIncomingEdges(inDegree map[string]int, txByID map[string]*bt.Tx) { for _, tx := range txByID { for _, input := range tx.Inputs { - inputUtxoTxID := input.PreviousTxIDStr() // TODO: perf -> In bt, the TxID is calculated every time we try to get it, which means we hash the tx bytes twice each time. It's expensive operation - try to avoid calulation each time + inputUtxoTxID := input.PreviousTxIDStr() // TODO: perf -> In bt, the TxID is calculated every time we try to get it, which means we hash the tx bytes twice each time. It's expensive operation - try to avoid calculation each time if _, ok := txByID[inputUtxoTxID]; ok { // transaction can contains inputs we are not interested in inDegree[inputUtxoTxID]++ } @@ -61,7 +61,7 @@ func getTxWithZeroIncomingEdges(incomingEdgesMap map[string]int) []string { func removeTxFromIncomingEdges(tx *bt.Tx, incomingEdgesMap map[string]int, zeroIncomingEdgeQueue []string) []string { for _, input := range tx.Inputs { - neighborID := input.PreviousTxIDStr() // TODO: perf -> In bt, the TxID is calculated every time we try to get it, which means we hash the tx bytes twice each time. It's expensive operation - try to avoid calulation each time + neighborID := input.PreviousTxIDStr() // TODO: perf -> In bt, the TxID is calculated every time we try to get it, which means we hash the tx bytes twice each time. It's expensive operation - try to avoid calculation each time incomingEdgesMap[neighborID]-- if incomingEdgesMap[neighborID] == 0 { diff --git a/bux_suite_mocks_test.go b/bux_suite_mocks_test.go index 1dfada21..995db762 100644 --- a/bux_suite_mocks_test.go +++ b/bux_suite_mocks_test.go @@ -48,7 +48,7 @@ func (tm *taskManagerMockBase) IsNewRelicEnabled() bool { return false } -func (tm *taskManagerMockBase) CronJobsInit(cronJobsMap taskmanager.CronJobs) error { +func (tm *taskManagerMockBase) CronJobsInit(taskmanager.CronJobs) error { return nil } diff --git a/chainstate/broadcast.go b/chainstate/broadcast.go index a4e5e762..552c8f22 100644 --- a/chainstate/broadcast.go +++ b/chainstate/broadcast.go @@ -6,8 +6,6 @@ import ( "strings" "sync" "time" - - "github.com/BuxOrg/bux/utils" ) var ( @@ -90,7 +88,8 @@ func (c *Client) broadcast(ctx context.Context, id, hex string, timeout time.Dur func createActiveProviders(c *Client, txID, txHex string) []txBroadcastProvider { providers := make([]txBroadcastProvider, 0, 10) - if shouldBroadcastWithMAPI(c) { + switch c.ActiveProvider() { + case ProviderMinercraft: for _, miner := range c.options.config.minercraftConfig.broadcastMiners { if miner == nil { continue @@ -99,26 +98,16 @@ func createActiveProviders(c *Client, txID, txHex string) []txBroadcastProvider pvdr := mapiBroadcastProvider{miner: miner, txID: txID, txHex: txHex} providers = append(providers, &pvdr) } - } - - if shouldBroadcastWithBroadcastClient(c) { + case ProviderBroadcastClient: pvdr := broadcastClientProvider{txID: txID, txHex: txHex} providers = append(providers, &pvdr) + default: + c.options.logger.Warn().Msg("no active provider for broadcast") } return providers } -func shouldBroadcastWithMAPI(c *Client) bool { - return !utils.StringInSlice(ProviderMAPI, c.options.config.excludedProviders) && - (c.Network() == MainNet || c.Network() == TestNet) // Only supported on main and test right now -} - -func shouldBroadcastWithBroadcastClient(c *Client) bool { - return !utils.StringInSlice(ProviderBroadcastClient, c.options.config.excludedProviders) && - c.BroadcastClient() != nil -} - func broadcastToProvider(ctx, fallbackCtx context.Context, provider txBroadcastProvider, txID string, c *Client, fallbackTimeout time.Duration, resultsChannel chan broadcastResult, status *broadcastStatus, diff --git a/chainstate/broadcast_client_init.go b/chainstate/broadcast_client_init.go new file mode 100644 index 00000000..68152141 --- /dev/null +++ b/chainstate/broadcast_client_init.go @@ -0,0 +1,45 @@ +package chainstate + +import ( + "context" + "errors" + + "github.com/BuxOrg/bux/utils" + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + "github.com/newrelic/go-agent/v3/newrelic" +) + +func (c *Client) broadcastClientInit(ctx context.Context) error { + if txn := newrelic.FromContext(ctx); txn != nil { + defer txn.StartSegment("start_broadcast_client").End() + } + + bc := c.options.config.broadcastClient + if bc == nil { + err := errors.New("broadcast client is not configured") + return err + } + + if c.isFeeQuotesEnabled() { + // get the lowest fee + var feeQuotes []*broadcast.FeeQuote + feeQuotes, err := bc.GetFeeQuote(ctx) + if err != nil { + return err + } + if len(feeQuotes) == 0 { + return errors.New("no fee quotes returned from broadcast client") + } + c.options.logger.Info().Msgf("got %d fee quote(s) from broadcast client", len(feeQuotes)) + fees := make([]utils.FeeUnit, len(feeQuotes)) + for index, fee := range feeQuotes { + fees[index] = utils.FeeUnit{ + Satoshis: int(fee.MiningFee.Satoshis), + Bytes: int(fee.MiningFee.Bytes), + } + } + c.options.config.feeUnit = utils.LowestFee(fees, c.options.config.feeUnit) + } + + return nil +} diff --git a/chainstate/broadcast_providers.go b/chainstate/broadcast_providers.go index a52a49ac..6b3c064a 100644 --- a/chainstate/broadcast_providers.go +++ b/chainstate/broadcast_providers.go @@ -18,16 +18,16 @@ type txBroadcastProvider interface { // mAPI provider type mapiBroadcastProvider struct { - miner *Miner + miner *minercraft.Miner txID, txHex string } func (provider mapiBroadcastProvider) getName() string { - return provider.miner.Miner.Name + return provider.miner.Name } func (provider mapiBroadcastProvider) broadcast(ctx context.Context, c *Client) error { - return broadcastMAPI(ctx, c, provider.miner.Miner, provider.txID, provider.txHex) + return broadcastMAPI(ctx, c, provider.miner, provider.txID, provider.txHex) } // broadcastMAPI will broadcast a transaction to a miner using mAPI @@ -101,7 +101,7 @@ func broadcastWithBroadcastClient(ctx context.Context, client ClientInterface, t Hex: hex, } - result, err := client.BroadcastClient().SubmitTransaction(ctx, &tx) + result, err := client.BroadcastClient().SubmitTransaction(ctx, &tx, broadcast.WithRawFormat()) if err != nil { debugLog(client, txID, "error broadcast request for "+ProviderBroadcastClient+" failed: "+err.Error()) return err diff --git a/chainstate/broadcast_test.go b/chainstate/broadcast_test.go index 28c0a306..27942abc 100644 --- a/chainstate/broadcast_test.go +++ b/chainstate/broadcast_test.go @@ -4,7 +4,6 @@ import ( "context" "strings" "testing" - "time" broadcast_client_mock "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client-mock" "github.com/stretchr/testify/assert" @@ -127,120 +126,6 @@ func TestClient_Broadcast_BroadcastClient(t *testing.T) { }) } -// TestClient_Broadcast_MultipleClients will test the method Broadcast() with multiple clients -func TestClient_Broadcast_MultipleClients(t *testing.T) { - t.Parallel() - - t.Run("broadcast - success from multiple clients", func(t *testing.T) { - // given - bc := broadcast_client_mock.Builder(). - WithMockArc(broadcast_client_mock.MockSuccess). - Build() - c := NewTestClient( - context.Background(), t, - WithMinercraft(&minerCraftBroadcastSuccess{}), - WithBroadcastClient(bc), - ) - - // when - providers, err := c.Broadcast( - context.Background(), broadcastExample1TxID, broadcastExample1TxHex, defaultBroadcastTimeOut, - ) - - // then - require.NoError(t, err) - miners := strings.Split(providers, ",") - assert.GreaterOrEqual(t, len(miners), 1) - assert.True(t, containsAtLeastOneElement(miners, - ProviderBroadcastClient, - minercraft.MinerTaal, - minercraft.MinerMatterpool, - minercraft.MinerGorillaPool, - minercraft.MinerMempool, - )) - }) - - t.Run("broadcast - success from broadcastClient (mAPI timeouts)", func(t *testing.T) { - // given - bc := broadcast_client_mock.Builder(). - WithMockArc(broadcast_client_mock.MockSuccess). - Build() - c := NewTestClient( - context.Background(), t, - WithMinercraft(&minerCraftBroadcastTimeout{}), // Timeout - WithBroadcastClient(bc), // Success - ) - - // when - providers, err := c.Broadcast( - context.Background(), broadcastExample1TxID, broadcastExample1TxHex, defaultBroadcastTimeOut, - ) - - // then - require.NoError(t, err) - miners := strings.Split(providers, ",") - assert.GreaterOrEqual(t, len(miners), 1) - assert.True(t, containsAtLeastOneElement(miners, ProviderBroadcastClient)) - assert.NotContains(t, miners, minercraft.MinerTaal) - assert.NotContains(t, miners, minercraft.MinerMempool) - assert.NotContains(t, miners, minercraft.MinerGorillaPool) - assert.NotContains(t, miners, minercraft.MinerMatterpool) - }) - - t.Run("broadcast - success from mAPI (broadcastClient timeouts)", func(t *testing.T) { - // given - bc := broadcast_client_mock.Builder(). - WithMockArc(broadcast_client_mock.MockTimeout). - Build() - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - c := NewTestClient( - ctx, t, - WithBroadcastClient(bc), // Timeout - WithMinercraft(&minerCraftBroadcastSuccess{}), // Success - ) - - // when - providers, err := c.Broadcast( - context.Background(), broadcastExample1TxID, broadcastExample1TxHex, defaultBroadcastTimeOut, - ) - - // then - require.NoError(t, err) - miners := strings.Split(providers, ",") - assert.GreaterOrEqual(t, len(miners), 1) - assert.True(t, containsAtLeastOneElement( - miners, - minercraft.MinerTaal, - minercraft.MinerMempool, - minercraft.MinerGorillaPool, - minercraft.MinerMatterpool, - )) - assert.NotContains(t, miners, ProviderBroadcastClient) - }) - - t.Run("broadcast - all providers fail", func(t *testing.T) { - // given - bc := broadcast_client_mock.Builder(). - WithMockArc(broadcast_client_mock.MockFailure). - Build() - c := NewTestClient( - context.Background(), t, - WithMinercraft(&minerCraftTxNotFound{}), - WithBroadcastClient(bc), - ) - - // when - provider, err := c.Broadcast( - context.Background(), broadcastExample1TxID, broadcastExample1TxHex, defaultBroadcastTimeOut, - ) - - // then - require.Error(t, err) - assert.Equal(t, ProviderAll, provider) - }) -} - func containsAtLeastOneElement(coll1 []string, coll2 ...string) bool { m := make(map[string]bool) diff --git a/chainstate/client.go b/chainstate/client.go index 997a01a6..911d822e 100644 --- a/chainstate/client.go +++ b/chainstate/client.go @@ -2,17 +2,16 @@ package chainstate import ( "context" - "sync" + "errors" + "fmt" "time" "github.com/BuxOrg/bux/logging" "github.com/BuxOrg/bux/utils" "github.com/bitcoin-sv/go-broadcast-client/broadcast" - "github.com/libsv/go-bt/v2" "github.com/newrelic/go-agent/v3/newrelic" "github.com/rs/zerolog" "github.com/tonicpow/go-minercraft/v2" - "github.com/tonicpow/go-minercraft/v2/apis/mapi" ) type ( @@ -42,23 +41,17 @@ type ( queryTimeout time.Duration // Timeout for transaction query broadcastClient broadcast.Client // Broadcast client pulseClient *PulseClient // Pulse client + feeUnit *utils.FeeUnit // The lowest fees among all miners + feeQuotes bool // If set, feeUnit will be updated with fee quotes from miner's } // minercraftConfig is specific for minercraft configuration minercraftConfig struct { - broadcastMiners []*Miner // List of loaded miners for broadcasting - queryMiners []*Miner // List of loaded miners for querying transactions - feeUnit *utils.FeeUnit // The lowest fees among all miners - minercraftFeeQuotes bool // If set, feeUnit will be updated with fee quotes from miner's mAPI - apiType minercraft.APIType // MinerCraft APIType(ARC/mAPI) - minerAPIs []*minercraft.MinerAPIs // List of miners APIs - } + broadcastMiners []*minercraft.Miner // List of loaded miners for broadcasting + queryMiners []*minercraft.Miner // List of loaded miners for querying transactions - // Miner is the internal chainstate miner (wraps Minercraft miner with more information) - Miner struct { - FeeLastChecked time.Time `json:"fee_last_checked"` // Last time the fee was checked via mAPI - FeeUnit *utils.FeeUnit `json:"fee_unit"` // The fee unit returned from Policy request - Miner *minercraft.Miner `json:"miner"` // The minercraft miner + apiType minercraft.APIType // MinerCraft APIType(ARC/mAPI) + minerAPIs []*minercraft.MinerAPIs // List of miners APIs } // PulseClient is the internal chainstate pulse client @@ -89,8 +82,11 @@ func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error) client.options.logger = logging.GetDefaultLogger() } - // Start Minercraft - if err := client.startMinerCraft(ctx); err != nil { + if err := client.initActiveProvider(ctx); err != nil { + return nil, err + } + + if err := client.checkFeeUnit(); err != nil { return nil, err } @@ -168,110 +164,55 @@ func (c *Client) QueryTimeout() time.Duration { return c.options.config.queryTimeout } -// BroadcastMiners will return the broadcast miners -func (c *Client) BroadcastMiners() []*Miner { - return c.options.config.minercraftConfig.broadcastMiners -} - -// QueryMiners will return the query miners -func (c *Client) QueryMiners() []*Miner { - return c.options.config.minercraftConfig.queryMiners -} - // FeeUnit will return feeUnit func (c *Client) FeeUnit() *utils.FeeUnit { - return c.options.config.minercraftConfig.feeUnit + return c.options.config.feeUnit } -func (c *Client) isMinercraftFeeQuotesEnabled() bool { - return c.options.config.minercraftConfig.minercraftFeeQuotes -} - -// ValidateMiners will check if miner is reacheble by requesting its FeeQuote -// If there was on error on FeeQuote(), the miner will be deleted from miners list -// If usage of MapiFeeQuotes is enabled and miner is reacheble, miner's fee unit will be upadeted with MAPI fee quotes -// If FeeQuote returns some quote, but fee is not presented in it, it means that miner is valid but we can't use it's feequote -func (c *Client) ValidateMiners(ctx context.Context) { - ctxWithCancel, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - - var wg sync.WaitGroup - // Loop all broadcast miners - for index := range c.options.config.minercraftConfig.broadcastMiners { - wg.Add(1) - go func( - ctx context.Context, client *Client, - wg *sync.WaitGroup, miner *Miner, - ) { - defer wg.Done() - // Get the fee quote using the miner - // Switched from policyQuote to feeQuote as gorillapool doesn't have such endpoint - var fee *bt.Fee - if c.Minercraft().APIType() == minercraft.MAPI { - quote, err := c.Minercraft().FeeQuote(ctx, miner.Miner) - if err != nil { - client.options.logger.Error().Msgf("No FeeQuote response from miner %s. Reason: %s", miner.Miner.Name, err) - miner.FeeUnit = nil - return - } - - fee = quote.Quote.GetFee(mapi.FeeTypeData) - if fee == nil { - client.options.logger.Error().Msgf("Fee is missing in %s's FeeQuote response", miner.Miner.Name) - return - } - // Arc doesn't support FeeQuote right now(2023.07.21), that's why PolicyQuote is used - } else if c.Minercraft().APIType() == minercraft.Arc { - quote, err := c.Minercraft().PolicyQuote(ctx, miner.Miner) - if err != nil { - client.options.logger.Error().Msgf("No FeeQuote response from miner %s. Reason: %s", miner.Miner.Name, err) - miner.FeeUnit = nil - return - } - - fee = quote.Quote.Fees[0] - } - if c.isMinercraftFeeQuotesEnabled() { - miner.FeeUnit = &utils.FeeUnit{ - Satoshis: fee.MiningFee.Satoshis, - Bytes: fee.MiningFee.Bytes, - } - miner.FeeLastChecked = time.Now().UTC() - } - }(ctxWithCancel, c, &wg, c.options.config.minercraftConfig.broadcastMiners[index]) +// ActiveProvider returns a name of a provider based on config. +func (c *Client) ActiveProvider() string { + excluded := c.options.config.excludedProviders + if !utils.StringInSlice(ProviderBroadcastClient, excluded) && c.BroadcastClient() != nil { + return ProviderBroadcastClient } - wg.Wait() - - c.DeleteUnreacheableMiners() - - if c.isMinercraftFeeQuotesEnabled() { - c.SetLowestFees() + if !utils.StringInSlice(ProviderMinercraft, excluded) && (c.Network() == MainNet || c.Network() == TestNet) { + return ProviderMinercraft } + return ProviderNone } -// SetLowestFees takes the lowest fees among all miners and sets them as the feeUnit for future transactions -func (c *Client) SetLowestFees() { - minFees := DefaultFee - for _, m := range c.options.config.minercraftConfig.broadcastMiners { - if float64(minFees.Satoshis)/float64(minFees.Bytes) > float64(m.FeeUnit.Satoshis)/float64(m.FeeUnit.Bytes) { - minFees = m.FeeUnit - } +func (c *Client) isFeeQuotesEnabled() bool { + return c.options.config.feeQuotes +} + +func (c *Client) initActiveProvider(ctx context.Context) error { + switch c.ActiveProvider() { + case ProviderMinercraft: + return c.minercraftInit(ctx) + case ProviderBroadcastClient: + return c.broadcastClientInit(ctx) + default: + return errors.New("no active provider found") } - c.options.config.minercraftConfig.feeUnit = minFees } -// DeleteUnreacheableMiners deletes miners which can't be reacheable from config -func (c *Client) DeleteUnreacheableMiners() { - validMinerIndex := 0 - for _, miner := range c.options.config.minercraftConfig.broadcastMiners { - if miner.FeeUnit != nil { - c.options.config.minercraftConfig.broadcastMiners[validMinerIndex] = miner - validMinerIndex++ +func (c *Client) checkFeeUnit() error { + feeUnit := c.options.config.feeUnit + switch { + case feeUnit == nil: + return errors.New("no fee unit found") + case !feeUnit.IsValid(): + return fmt.Errorf("invalid fee unit found: %s", feeUnit) + case feeUnit.IsZero(): + c.options.logger.Warn().Msg("fee unit suggests no fees (free)") + default: + var feeUnitSource string + if c.isFeeQuotesEnabled() { + feeUnitSource = "fee quotes" + } else { + feeUnitSource = "configured fee_unit" } + c.options.logger.Info().Msgf("using fee unit: %s from %s", feeUnit, feeUnitSource) } - // Prevent memory leak by erasing truncated miners - for i := validMinerIndex; i < len(c.options.config.minercraftConfig.broadcastMiners); i++ { - c.options.config.minercraftConfig.broadcastMiners[i] = nil - } - c.options.config.minercraftConfig.broadcastMiners = c.options.config.minercraftConfig.broadcastMiners[:validMinerIndex] + return nil } diff --git a/chainstate/client_internal.go b/chainstate/client_internal.go deleted file mode 100644 index b6863794..00000000 --- a/chainstate/client_internal.go +++ /dev/null @@ -1,68 +0,0 @@ -package chainstate - -import ( - "context" - - "github.com/BuxOrg/bux/utils" - "github.com/newrelic/go-agent/v3/newrelic" - "github.com/tonicpow/go-minercraft/v2" -) - -// defaultMinercraftOptions will create the defaults -func (c *Client) defaultMinercraftOptions() (opts *minercraft.ClientOptions) { - opts = minercraft.DefaultClientOptions() - if len(c.options.userAgent) > 0 { - opts.UserAgent = c.options.userAgent - } - return -} - -// startMinerCraft will start Minercraft (if no custom client is found) -func (c *Client) startMinerCraft(ctx context.Context) (err error) { - if txn := newrelic.FromContext(ctx); txn != nil { - defer txn.StartSegment("start_minercraft").End() - } - - // No client? - if c.Minercraft() == nil { - var optionalMiners []*minercraft.Miner - var loadedMiners []string - - // Loop all broadcast miners and append to the list of miners - for i := range c.options.config.minercraftConfig.broadcastMiners { - if !utils.StringInSlice(c.options.config.minercraftConfig.broadcastMiners[i].Miner.MinerID, loadedMiners) { - optionalMiners = append(optionalMiners, c.options.config.minercraftConfig.broadcastMiners[i].Miner) - loadedMiners = append(loadedMiners, c.options.config.minercraftConfig.broadcastMiners[i].Miner.MinerID) - } - } - - // Loop all query miners and append to the list of miners - for i := range c.options.config.minercraftConfig.queryMiners { - if !utils.StringInSlice(c.options.config.minercraftConfig.queryMiners[i].Miner.MinerID, loadedMiners) { - optionalMiners = append(optionalMiners, c.options.config.minercraftConfig.queryMiners[i].Miner) - loadedMiners = append(loadedMiners, c.options.config.minercraftConfig.queryMiners[i].Miner.MinerID) - } - } - c.options.config.minercraft, err = minercraft.NewClient( - c.defaultMinercraftOptions(), - c.HTTPClient(), - c.options.config.minercraftConfig.apiType, - optionalMiners, - c.options.config.minercraftConfig.minerAPIs, - ) - } - - c.ValidateMiners(ctx) - - // Check for broadcast miners - if len(c.BroadcastMiners()) == 0 { - return ErrMissingBroadcastMiners - } - - // Check for query miners - if len(c.QueryMiners()) == 0 { - return ErrMissingQueryMiners - } - - return nil -} diff --git a/chainstate/client_options.go b/chainstate/client_options.go index 551cca2f..2497091d 100644 --- a/chainstate/client_options.go +++ b/chainstate/client_options.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/BuxOrg/bux/utils" "github.com/bitcoin-sv/go-broadcast-client/broadcast" "github.com/newrelic/go-agent/v3/newrelic" "github.com/rs/zerolog" @@ -18,58 +19,23 @@ type ClientOps func(c *clientOptions) // // Useful for starting with the default and then modifying as needed func defaultClientOptions() *clientOptions { - // Create the default miners - bm, qm := defaultMiners() - apis, _ := minercraft.DefaultMinersAPIs() - // Set the default options return &clientOptions{ config: &syncConfig{ - httpClient: nil, - minercraftConfig: &minercraftConfig{ - broadcastMiners: bm, - queryMiners: qm, - minerAPIs: apis, - minercraftFeeQuotes: true, - feeUnit: DefaultFee, - }, - minercraft: nil, - network: MainNet, - queryTimeout: defaultQueryTimeOut, - broadcastClient: nil, + httpClient: nil, + minercraftConfig: defaultMinecraftConfig(), + minercraft: nil, + network: MainNet, + queryTimeout: defaultQueryTimeOut, + broadcastClient: nil, + feeQuotes: true, + feeUnit: nil, // fee has to be set explicitly or via fee quotes }, debug: false, newRelicEnabled: false, } } -// defaultMiners will return the miners for default configuration -func defaultMiners() (broadcastMiners []*Miner, queryMiners []*Miner) { - // Set the broadcast miners - miners, _ := minercraft.DefaultMiners() - - // Loop and add (only miners that support ALL TX QUERY) - for index, miner := range miners { - broadcastMiners = append(broadcastMiners, &Miner{ - FeeLastChecked: time.Now().UTC(), - FeeUnit: DefaultFee, - Miner: miners[index], - }) - - // Only miners that support querying - if miner.Name == minercraft.MinerTaal || miner.Name == minercraft.MinerMempool { - // minercraft.MinerGorillaPool, (does not have -t index enabled - 4.25.22) - // minercraft.MinerMatterpool, (does not have -t index enabled - 4.25.22) - queryMiners = append(queryMiners, &Miner{ - // FeeLastChecked: time.Now().UTC(), - // FeeUnit: DefaultFee, - Miner: miners[index], - }) - } - } - return -} - // getTxnCtx will check for an existing transaction func (c *clientOptions) getTxnCtx(ctx context.Context) context.Context { if c.newRelicEnabled { @@ -120,31 +86,6 @@ func WithMAPI() ClientOps { } } -// WithArc will specify Arc as an API for minercraft client -func WithArc() ClientOps { - return func(c *clientOptions) { - c.config.minercraftConfig.apiType = minercraft.Arc - } -} - -// WithBroadcastMiners will set a list of miners for broadcasting -func WithBroadcastMiners(miners []*Miner) ClientOps { - return func(c *clientOptions) { - if len(miners) > 0 { - c.config.minercraftConfig.broadcastMiners = miners - } - } -} - -// WithQueryMiners will set a list of miners for querying transactions -func WithQueryMiners(miners []*Miner) ClientOps { - return func(c *clientOptions) { - if len(miners) > 0 { - c.config.minercraftConfig.queryMiners = miners - } - } -} - // WithQueryTimeout will set a different timeout for transaction querying func WithQueryTimeout(timeout time.Duration) ClientOps { return func(c *clientOptions) { @@ -209,10 +150,17 @@ func WithExcludedProviders(providers []string) ClientOps { } } -// WithMinercraftFeeQuotes will set minercraftFeeQuotes flag as true -func WithMinercraftFeeQuotes() ClientOps { +// WithFeeQuotes will set minercraftFeeQuotes flag as true +func WithFeeQuotes(enabled bool) ClientOps { + return func(c *clientOptions) { + c.config.feeQuotes = enabled + } +} + +// WithFeeUnit will set the fee unit +func WithFeeUnit(feeUnit *utils.FeeUnit) ClientOps { return func(c *clientOptions) { - c.config.minercraftConfig.minercraftFeeQuotes = true + c.config.feeUnit = feeUnit } } diff --git a/chainstate/client_options_test.go b/chainstate/client_options_test.go index 11136f18..65c9d113 100644 --- a/chainstate/client_options_test.go +++ b/chainstate/client_options_test.go @@ -14,7 +14,6 @@ import ( // TestWithNewRelic will test the method WithNewRelic() func TestWithNewRelic(t *testing.T) { - t.Run("get opts", func(t *testing.T) { opt := WithNewRelic() assert.IsType(t, *new(ClientOps), opt) @@ -35,7 +34,6 @@ func TestWithNewRelic(t *testing.T) { // TestWithDebugging will test the method WithDebugging() func TestWithDebugging(t *testing.T) { - t.Run("get opts", func(t *testing.T) { opt := WithDebugging() assert.IsType(t, *new(ClientOps), opt) @@ -141,64 +139,6 @@ func TestWithBroadcastClient(t *testing.T) { }) } -// TestWithBroadcastMiners will test the method WithBroadcastMiners() -func TestWithBroadcastMiners(t *testing.T) { - t.Parallel() - - t.Run("check type", func(t *testing.T) { - opt := WithBroadcastMiners(nil) - assert.IsType(t, *new(ClientOps), opt) - }) - - t.Run("test applying nil", func(t *testing.T) { - options := &clientOptions{ - config: &syncConfig{minercraftConfig: &minercraftConfig{}}, - } - opt := WithBroadcastMiners(nil) - opt(options) - assert.Nil(t, options.config.minercraftConfig.broadcastMiners) - }) - - t.Run("test applying option", func(t *testing.T) { - options := &clientOptions{ - config: &syncConfig{minercraftConfig: &minercraftConfig{}}, - } - miners := []*Miner{{Miner: minerTaal}} - opt := WithBroadcastMiners(miners) - opt(options) - assert.Equal(t, miners, options.config.minercraftConfig.broadcastMiners) - }) -} - -// TestWithQueryMiners will test the method WithQueryMiners() -func TestWithQueryMiners(t *testing.T) { - t.Parallel() - - t.Run("check type", func(t *testing.T) { - opt := WithQueryMiners(nil) - assert.IsType(t, *new(ClientOps), opt) - }) - - t.Run("test applying nil", func(t *testing.T) { - options := &clientOptions{ - config: &syncConfig{minercraftConfig: &minercraftConfig{}}, - } - opt := WithQueryMiners(nil) - opt(options) - assert.Nil(t, options.config.minercraftConfig.queryMiners) - }) - - t.Run("test applying option", func(t *testing.T) { - options := &clientOptions{ - config: &syncConfig{minercraftConfig: &minercraftConfig{}}, - } - miners := []*Miner{{Miner: minerTaal}} - opt := WithQueryMiners(miners) - opt(options) - assert.Equal(t, miners, options.config.minercraftConfig.queryMiners) - }) -} - // TestWithQueryTimeout will test the method WithQueryTimeout() func TestWithQueryTimeout(t *testing.T) { t.Parallel() @@ -334,9 +274,9 @@ func TestWithExcludedProviders(t *testing.T) { options := &clientOptions{ config: &syncConfig{}, } - opt := WithExcludedProviders([]string{ProviderWhatsOnChain}) + opt := WithExcludedProviders([]string{ProviderBroadcastClient}) opt(options) assert.Equal(t, 1, len(options.config.excludedProviders)) - assert.Equal(t, ProviderWhatsOnChain, options.config.excludedProviders[0]) + assert.Equal(t, ProviderBroadcastClient, options.config.excludedProviders[0]) }) } diff --git a/chainstate/client_test.go b/chainstate/client_test.go index fb65e16a..e152bc3a 100644 --- a/chainstate/client_test.go +++ b/chainstate/client_test.go @@ -7,6 +7,7 @@ import ( "time" broadcast_client "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tonicpow/go-minercraft/v2" @@ -45,9 +46,10 @@ func TestNewClient(t *testing.T) { t.Run("custom broadcast client", func(t *testing.T) { arcConfig := broadcast_client.ArcClientConfig{ Token: "", - APIUrl: "https://tapi.taal.com/arc", + APIUrl: "https://arc.gorillapool.io", } - customClient := broadcast_client.Builder().WithArc(arcConfig).Build() + logger := zerolog.Nop() + customClient := broadcast_client.Builder().WithArc(arcConfig, &logger).Build() require.NotNil(t, customClient) c, err := NewClient( context.Background(), @@ -62,7 +64,7 @@ func TestNewClient(t *testing.T) { t.Run("custom minercraft client", func(t *testing.T) { customClient, err := minercraft.NewClient( - minercraft.DefaultClientOptions(), &http.Client{}, minercraft.Arc, nil, nil, + minercraft.DefaultClientOptions(), &http.Client{}, minercraft.MAPI, nil, nil, ) require.NoError(t, err) require.NotNil(t, customClient) @@ -78,30 +80,6 @@ func TestNewClient(t *testing.T) { assert.Equal(t, customClient, c.Minercraft()) }) - t.Run("custom list of broadcast miners", func(t *testing.T) { - miners, _ := defaultMiners() - c, err := NewClient( - context.Background(), - WithBroadcastMiners(miners), - WithMinercraft(&MinerCraftBase{}), - ) - require.NoError(t, err) - require.NotNil(t, c) - assert.Equal(t, miners, c.BroadcastMiners()) - }) - - t.Run("custom list of query miners", func(t *testing.T) { - miners, _ := defaultMiners() - c, err := NewClient( - context.Background(), - WithQueryMiners(miners), - WithMinercraft(&MinerCraftBase{}), - ) - require.NoError(t, err) - require.NotNil(t, c) - assert.Equal(t, miners, c.QueryMiners()) - }) - t.Run("custom query timeout", func(t *testing.T) { timeout := 55 * time.Second c, err := NewClient( @@ -125,21 +103,20 @@ func TestNewClient(t *testing.T) { assert.Equal(t, TestNet, c.Network()) }) - t.Run("custom network - stn", func(t *testing.T) { - c, err := NewClient( + t.Run("no provider when using minercraft with customNet", func(t *testing.T) { + _, err := NewClient( context.Background(), WithNetwork(StressTestNet), WithMinercraft(&MinerCraftBase{}), + WithFeeUnit(MockDefaultFee), ) - require.NoError(t, err) - require.NotNil(t, c) - assert.Equal(t, StressTestNet, c.Network()) + require.Error(t, err) }) - t.Run("unreacheble miners", func(t *testing.T) { + t.Run("unreachable miners", func(t *testing.T) { _, err := NewClient( context.Background(), - WithMinercraft(&minerCraftUnreachble{}), + WithMinercraft(&minerCraftUnreachable{}), ) require.Error(t, err) assert.ErrorIs(t, err, ErrMissingBroadcastMiners) diff --git a/chainstate/definitions.go b/chainstate/definitions.go index 7ec42f9f..25d180eb 100644 --- a/chainstate/definitions.go +++ b/chainstate/definitions.go @@ -2,8 +2,6 @@ package chainstate import ( "time" - - "github.com/BuxOrg/bux/utils" ) // Chainstate configuration defaults @@ -45,20 +43,11 @@ const ( // List of providers const ( ProviderAll = "all" // All providers (used for errors etc) - ProviderMAPI = "mapi" // Query & broadcast provider for mAPI (using given miners) - ProviderWhatsOnChain = "whatsonchain" // Query & broadcast provider for WhatsOnChain + ProviderMinercraft = "minercraft" // Query & broadcast provider for mAPI (using given miners) ProviderBroadcastClient = "broadcastclient" // Query & broadcast provider for configured miners - ProviderPulse = "pulse" // MerkleProof provider + ProviderNone = "none" // No providers (used to indicate no providers) ) -// DefaultFee is used when a fee has not been set by the user -// This default is currently accepted by all BitcoinSV miners (50/1000) (7.27.23) -// Actual TAAL FeeUnit - 1/1000, GorillaPool - 50/1000 (7.27.23) -var DefaultFee = &utils.FeeUnit{ - Satoshis: 1, - Bytes: 20, -} - // BlockInfo is the response info about a returned block type BlockInfo struct { Bits string `json:"bits"` diff --git a/chainstate/interface.go b/chainstate/interface.go index 3828f2cd..555fec4a 100644 --- a/chainstate/interface.go +++ b/chainstate/interface.go @@ -35,14 +35,6 @@ type ProviderServices interface { BroadcastClient() broadcast.Client } -// MinercraftServices is the minercraft services interface -type MinercraftServices interface { - BroadcastMiners() []*Miner - QueryMiners() []*Miner - ValidateMiners(ctx context.Context) - FeeUnit() *utils.FeeUnit -} - // HeaderService is header services interface type HeaderService interface { VerifyMerkleRoots(ctx context.Context, merkleRoots []MerkleRootConfirmationRequestItem) error @@ -52,7 +44,6 @@ type HeaderService interface { type ClientInterface interface { ChainService ProviderServices - MinercraftServices HeaderService Close(ctx context.Context) Debug(on bool) @@ -63,6 +54,7 @@ type ClientInterface interface { Monitor() MonitorService Network() Network QueryTimeout() time.Duration + FeeUnit() *utils.FeeUnit } // MonitorClient interface diff --git a/chainstate/merkle_root.go b/chainstate/merkle_root.go index 246a6c17..462ad4a4 100644 --- a/chainstate/merkle_root.go +++ b/chainstate/merkle_root.go @@ -8,6 +8,10 @@ import ( // VerifyMerkleRoots will try to verify merkle roots with all available providers func (c *Client) VerifyMerkleRoots(ctx context.Context, merkleRoots []MerkleRootConfirmationRequestItem) error { + if c.options.config.pulseClient == nil { + c.options.logger.Warn().Msg("VerifyMerkleRoots is called even though no pulse client is configured; this likely indicates that the paymail capabilities have been cached.") + return errors.New("no pulse client found") + } pulseProvider := createPulseProvider(c) merkleRootsRes, err := pulseProvider.verifyMerkleRoots(ctx, c, merkleRoots) if err != nil { diff --git a/chainstate/merkle_root_provider.go b/chainstate/merkle_root_provider.go index 9fc1c991..19b024ee 100644 --- a/chainstate/merkle_root_provider.go +++ b/chainstate/merkle_root_provider.go @@ -85,7 +85,7 @@ func (p pulseClientProvider) verifyMerkleRoots( err = json.Unmarshal(bodyBytes, &merkleRootsRes) if err != nil { - return nil, fmt.Errorf("error during unmarshaling response body: %s", err.Error()) + return nil, fmt.Errorf("error during unmarshalling response body: %s", err.Error()) } return &merkleRootsRes, nil diff --git a/chainstate/minercraft_default.go b/chainstate/minercraft_default.go new file mode 100644 index 00000000..1e6e28a0 --- /dev/null +++ b/chainstate/minercraft_default.go @@ -0,0 +1,28 @@ +package chainstate + +import "github.com/tonicpow/go-minercraft/v2" + +func defaultMinecraftConfig() *minercraftConfig { + miners, _ := minercraft.DefaultMiners() + apis, _ := minercraft.DefaultMinersAPIs() + + broadcastMiners := []*minercraft.Miner{} + queryMiners := []*minercraft.Miner{} + for _, miner := range miners { + broadcastMiners = append(broadcastMiners, miner) + + if supportsQuerying(miner) { + queryMiners = append(queryMiners, miner) + } + } + + return &minercraftConfig{ + broadcastMiners: broadcastMiners, + queryMiners: queryMiners, + minerAPIs: apis, + } +} + +func supportsQuerying(mm *minercraft.Miner) bool { + return mm.Name == minercraft.MinerTaal || mm.Name == minercraft.MinerMempool +} diff --git a/chainstate/minercraft_init.go b/chainstate/minercraft_init.go new file mode 100644 index 00000000..58b785f3 --- /dev/null +++ b/chainstate/minercraft_init.go @@ -0,0 +1,181 @@ +package chainstate + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/BuxOrg/bux/utils" + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/tonicpow/go-minercraft/v2" + "github.com/tonicpow/go-minercraft/v2/apis/mapi" +) + +func (c *Client) minercraftInit(ctx context.Context) error { + if txn := newrelic.FromContext(ctx); txn != nil { + defer txn.StartSegment("start_minercraft").End() + } + mi := &minercraftInitializer{client: c, ctx: ctx, minersWithFee: make(minerToFeeMap)} + + if err := mi.newClient(); err != nil { + return err + } + + if err := mi.validateMiners(); err != nil { + return err + } + + if c.isFeeQuotesEnabled() { + c.options.config.feeUnit = mi.lowestFee() + } + + return nil +} + +type minercraftInitializer struct { + client *Client + ctx context.Context + minersWithFee minerToFeeMap + lock sync.Mutex +} + +type ( + minerID string + minerToFeeMap map[minerID]utils.FeeUnit +) + +func (i *minercraftInitializer) defaultMinercraftOptions() (opts *minercraft.ClientOptions) { + c := i.client + opts = minercraft.DefaultClientOptions() + if len(c.options.userAgent) > 0 { + opts.UserAgent = c.options.userAgent + } + return +} + +func (i *minercraftInitializer) newClient() (err error) { + c := i.client + + if c.Minercraft() == nil { + var optionalMiners []*minercraft.Miner + var loadedMiners []string + + // Loop all broadcast miners and append to the list of miners + for _, broadcastMiner := range c.options.config.minercraftConfig.broadcastMiners { + if !utils.StringInSlice(broadcastMiner.MinerID, loadedMiners) { + optionalMiners = append(optionalMiners, broadcastMiner) + loadedMiners = append(loadedMiners, broadcastMiner.MinerID) + } + } + + // Loop all query miners and append to the list of miners + for _, queryMiner := range c.options.config.minercraftConfig.queryMiners { + if !utils.StringInSlice(queryMiner.MinerID, loadedMiners) { + optionalMiners = append(optionalMiners, queryMiner) + loadedMiners = append(loadedMiners, queryMiner.MinerID) + } + } + c.options.config.minercraft, err = minercraft.NewClient( + i.defaultMinercraftOptions(), + c.HTTPClient(), + c.options.config.minercraftConfig.apiType, + optionalMiners, + c.options.config.minercraftConfig.minerAPIs, + ) + } + return +} + +// validateMiners will check if miner is reachable by requesting its FeeQuote +// If there was on error on FeeQuote(), the miner will be deleted from miners list +// If usage of MapiFeeQuotes is enabled and miner is reachable, miner's fee unit will be updated with MAPI fee quotes +// If FeeQuote returns some quote, but fee is not presented in it, it means that miner is valid but we can't use it's feequote +func (i *minercraftInitializer) validateMiners() error { + ctxWithCancel, cancel := context.WithTimeout(i.ctx, 5*time.Second) + defer cancel() + + c := i.client + var wg sync.WaitGroup + + for _, miner := range c.options.config.minercraftConfig.broadcastMiners { + wg.Add(1) + currentMiner := miner + go func() { + defer wg.Done() + feeUnit, err := i.getFeeQuote(ctxWithCancel, currentMiner) + if err != nil { + c.options.logger.Warn().Msgf("No FeeQuote response from miner %s. Reason: %s", currentMiner.Name, err) + return + } + i.addToMinersWithFee(currentMiner, feeUnit) + }() + } + wg.Wait() + + i.deleteUnreachableMiners() + + switch { + case len(c.options.config.minercraftConfig.broadcastMiners) == 0: + return ErrMissingBroadcastMiners + case len(c.options.config.minercraftConfig.queryMiners) == 0: + return ErrMissingQueryMiners + default: + return nil + } +} + +func (i *minercraftInitializer) getFeeQuote(ctx context.Context, miner *minercraft.Miner) (*utils.FeeUnit, error) { + c := i.client + + apiType := c.Minercraft().APIType() + + if apiType == minercraft.Arc { + return nil, fmt.Errorf("we no longer support ARC with Minercraft. (%s)", miner.Name) + } + + quote, err := c.Minercraft().FeeQuote(ctx, miner) + if err != nil { + return nil, fmt.Errorf("no FeeQuote response from miner %s. Reason: %s", miner.Name, err) + } + + btFee := quote.Quote.GetFee(mapi.FeeTypeData) + if btFee == nil { + return nil, fmt.Errorf("Fee is missing in %s's FeeQuote response", miner.Name) + } + + feeUnit := &utils.FeeUnit{ + Satoshis: btFee.MiningFee.Satoshis, + Bytes: btFee.MiningFee.Bytes, + } + return feeUnit, nil +} + +func (i *minercraftInitializer) addToMinersWithFee(miner *minercraft.Miner, feeUnit *utils.FeeUnit) { + i.lock.Lock() + defer i.lock.Unlock() + i.minersWithFee[minerID(miner.MinerID)] = *feeUnit +} + +// deleteUnreachableMiners deletes miners which can't be reachable from config +func (i *minercraftInitializer) deleteUnreachableMiners() { + c := i.client + validMiners := []*minercraft.Miner{} + for _, miner := range c.options.config.minercraftConfig.broadcastMiners { + _, ok := i.minersWithFee[minerID(miner.MinerID)] + if ok { + validMiners = append(validMiners, miner) + } + } + c.options.config.minercraftConfig.broadcastMiners = validMiners +} + +// lowestFees takes the lowest fees among all miners and sets them as the feeUnit for future transactions +func (i *minercraftInitializer) lowestFee() *utils.FeeUnit { + fees := make([]utils.FeeUnit, 0) + for _, fee := range i.minersWithFee { + fees = append(fees, fee) + } + lowest := utils.LowestFee(fees, i.client.options.config.feeUnit) + return lowest +} diff --git a/chainstate/mock_const.go b/chainstate/mock_const.go index 55bd5e02..c5b380d1 100644 --- a/chainstate/mock_const.go +++ b/chainstate/mock_const.go @@ -1,5 +1,7 @@ package chainstate +import "github.com/BuxOrg/bux/utils" + const ( // Dummy transaction data broadcastExample1TxID = "15d31d00ed7533a83d7ab206115d7642812ec04a2cbae4248365febb82576ff3" @@ -10,6 +12,7 @@ const ( onChainExample1Confirmations = int64(314) onChainExample1TxHex = "01000000025b7439a0c9effa3f19d0e441d2eea596e44a8c49240b6e389c29498285f92ad3010000006a4730440220482c1c896678d7307e1de35cef2aae4907f2684617a26d8abd24c444d527c80d02204c550f8f9d69b9cf65780e2e066041750261702639d02605a2eb694ade4ca1d64121029ce7958b2aa3c627334f50bb810c678e2b284db0ef6f7d067f7fccfa05d0f095ffffffff1998b0e4955e1d8ba976d943c43f32e143ba90e805f0e882d3b8edc0f7473b77020000006a47304402204beb486e5d99a15d4d2267e328abb5466a05fdc20d64903d0ace1c4fabb71a34022024803ae9e18b3c11683b2ff2b5fb4ca973a22fdd390f6ab1f99396604a3f06af4121038ea0f258fb838b5193e9739ddd808bb97aaab52a60ba8a83958b13109ab183ccffffffff030000000000000000fd8901006a0372756e0105004d7d017b22696e223a312c22726566223a5b22653864393134303764643461646164363366333739353032303861383532653562306334383037333563656235346133653334333539346163313839616331625f6f31222c22376135346462326162303030306161303035316134383230343162336135653761636239386333363135363863623334393063666564623066653161356438385f6f33225d2c226f7574223a5b2233356463303036313539393333623438353433343565663663633363366261663165666462353263343837313933386632366539313034343632313562343036225d2c2264656c223a5b5d2c22637265223a5b5d2c2265786563223a5b7b226f70223a2243414c4c222c2264617461223a5b7b22246a6967223a307d2c22757064617465222c5b7b22246a6967223a317d2c7b2267726164756174696f6e506f736974696f6e223a6e756c6c2c226c6576656c223a382c226e616d65223a22e38395e383abe38380222c227870223a373030307d5d5d7d5d7d11010000000000001976a914058cae340a2ef8fd2b43a074b75fb6b38cb2765788acd4020000000000001976a914160381a3811b474ff77f31f64f4e57a5bb5ebf1788ac00000000" onChainExample1TxID = "908c26f8227fa99f1b26f99a19648653a1382fb3b37b03870e9c138894d29b3b" + onChainExampleArcTxID = "a11b9e1ee08e264f9add02e4afa40dad3c00a23f250ac04449face095c68fab7" // API key // testDummyKey = "test-dummy-api-key-value" //nolint:gosec // this is a dummy key @@ -22,3 +25,9 @@ const ( utf8Type = "UTF-8" applicationJSONType = "application/json" ) + +// MockDefaultFee is a mock default fee used for assertions +var MockDefaultFee = &utils.FeeUnit{ + Satoshis: 1, + Bytes: 20, +} diff --git a/chainstate/mock_minercraft.go b/chainstate/mock_minercraft.go index c9f86684..6359cbac 100644 --- a/chainstate/mock_minercraft.go +++ b/chainstate/mock_minercraft.go @@ -128,7 +128,7 @@ func (m *MinerCraftBase) FeeQuote(context.Context, *minercraft.Miner) (*minercra Fees: []*bt.Fee{ { FeeType: bt.FeeTypeData, - MiningFee: bt.FeeUnit(*DefaultFee), + MiningFee: bt.FeeUnit(*MockDefaultFee), }, }, }, @@ -524,290 +524,11 @@ func (m *minerCraftBroadcastSuccess) SubmitTransaction(_ context.Context, miner return nil, errors.New("missing miner response") } -// //nolint: unused //TODO: remove this when the method is implemented -type minerCraftInMempool struct { - minerCraftTxOnChain -} - -// SubmitTransaction submits a transaction to the miner. -// //nolint: unused // TODO: remove this when the method is implemented -func (m *minerCraftInMempool) SubmitTransaction(_ context.Context, miner *minercraft.Miner, - _ *minercraft.Transaction, -) (*minercraft.SubmitTransactionResponse, error) { - if miner.Name == minercraft.MinerTaal { - sig := "30440220008615778c5b8610c29b12925c8eb479f692ad6de9e62b7e622a3951baf9fbd8022014aaa27698cd3aba4144bfd707f3323e12ac20101d6e44f22eb8ed0856ef341a" - pubKey := miner.MinerID - return &minercraft.SubmitTransactionResponse{ - JSONEnvelope: minercraft.JSONEnvelope{ - Miner: miner, - Validated: true, - JSONEnvelope: envelope.JSONEnvelope{ - Payload: "{\"apiVersion\":\"1.4.0\",\"timestamp\":\"2022-02-01T15:19:40.889523Z\",\"txid\":\"683e11d4db8a776e293dc3bfe446edf66cf3b145a6ec13e1f5f1af6bb5855364\",\"returnResult\":\"failure\",\"resultDescription\":\"Missing inputs\",\"minerId\":\"030d1fe5c1b560efe196ba40540ce9017c20daa9504c4c4cec6184fc702d9f274e\",\"currentHighestBlockHash\":\"00000000000000000652def5827ad3de6380376f8fc8d3e835503095a761e0d2\",\"currentHighestBlockHeight\":724807,\"txSecondMempoolExpiry\":0}", - Signature: &sig, - PublicKey: &pubKey, - Encoding: utf8Type, - MimeType: applicationJSONType, - }, - }, - Results: &minercraft.UnifiedSubmissionPayload{ - APIVersion: "1.4.0", - CurrentHighestBlockHash: "00000000000000000652def5827ad3de6380376f8fc8d3e835503095a761e0d2", - CurrentHighestBlockHeight: 724807, - MinerID: miner.MinerID, - ResultDescription: "Missing inputs", - ReturnResult: mAPIFailure, - Timestamp: "2022-02-01T15:19:40.889523Z", - TxID: onChainExample1TxID, - }, - }, nil - } else if miner.Name == minercraft.MinerMempool { - return &minercraft.SubmitTransactionResponse{ - JSONEnvelope: minercraft.JSONEnvelope{ - Miner: miner, - Validated: true, - JSONEnvelope: envelope.JSONEnvelope{ - Payload: "{\"apiVersion\":\"\",\"timestamp\":\"2022-02-01T17:47:52.518Z\",\"txid\":\"\",\"returnResult\":\"failure\",\"resultDescription\":\"ERROR: Missing inputs\",\"minerId\":null,\"currentHighestBlockHash\":\"0000000000000000064c900b1fceb316302426aedb2242852530b5e78144f2c1\",\"currentHighestBlockHeight\":724816,\"txSecondMempoolExpiry\":0}", - Encoding: utf8Type, - MimeType: applicationJSONType, - }, - }, - Results: &minercraft.UnifiedSubmissionPayload{ - APIVersion: "", - CurrentHighestBlockHash: "0000000000000000064c900b1fceb316302426aedb2242852530b5e78144f2c1", - CurrentHighestBlockHeight: 724816, - MinerID: miner.MinerID, - ResultDescription: "ERROR: Missing inputs", - ReturnResult: mAPIFailure, - Timestamp: "2022-02-01T17:47:52.518Z", - TxID: "", - }, - }, nil - } else if miner.Name == minercraft.MinerMatterpool { - sig := matterCloudSig1 - pubKey := miner.MinerID - return &minercraft.SubmitTransactionResponse{ - JSONEnvelope: minercraft.JSONEnvelope{ - Miner: miner, - Validated: true, - JSONEnvelope: envelope.JSONEnvelope{ - Payload: "{\"apiVersion\":\"1.1.0-1-g35ba2d3\",\"timestamp\":\"2022-02-01T17:50:15.130Z\",\"txid\":\"\",\"returnResult\":\"failure\",\"resultDescription\":\"ERROR: Missing inputs\",\"minerId\":\"0253a9b2d017254b91704ba52aad0df5ca32b4fb5cb6b267ada6aefa2bc5833a93\",\"currentHighestBlockHash\":\"0000000000000000064c900b1fceb316302426aedb2242852530b5e78144f2c1\",\"currentHighestBlockHeight\":724816,\"txSecondMempoolExpiry\":0}", - Signature: &sig, - PublicKey: &pubKey, - Encoding: utf8Type, - MimeType: applicationJSONType, - }, - }, - Results: &minercraft.UnifiedSubmissionPayload{ - APIVersion: "1.1.0-1-g35ba2d3", - CurrentHighestBlockHash: "0000000000000000064c900b1fceb316302426aedb2242852530b5e78144f2c1", - CurrentHighestBlockHeight: 724816, - MinerID: miner.MinerID, - ResultDescription: "ERROR: Missing inputs", - ReturnResult: mAPIFailure, - Timestamp: "2022-02-01T17:50:15.130Z", - TxID: "", - }, - }, nil - } else if miner.Name == minercraft.MinerGorillaPool { - sig := gorillaPoolSig1 - pubKey := miner.MinerID - return &minercraft.SubmitTransactionResponse{ - JSONEnvelope: minercraft.JSONEnvelope{ - Miner: miner, - Validated: true, - JSONEnvelope: envelope.JSONEnvelope{ - Payload: "{\"apiVersion\":\"\",\"timestamp\":\"2022-02-01T17:52:04.405Z\",\"txid\":\"\",\"returnResult\":\"failure\",\"resultDescription\":\"ERROR: Missing inputs\",\"minerId\":\"03ad780153c47df915b3d2e23af727c68facaca4facd5f155bf5018b979b9aeb83\",\"currentHighestBlockHash\":\"0000000000000000064c900b1fceb316302426aedb2242852530b5e78144f2c1\",\"currentHighestBlockHeight\":724816,\"txSecondMempoolExpiry\":0}", - Signature: &sig, - PublicKey: &pubKey, - Encoding: utf8Type, - MimeType: applicationJSONType, - }, - }, - Results: &minercraft.UnifiedSubmissionPayload{ - APIVersion: "", - CurrentHighestBlockHash: "0000000000000000064c900b1fceb316302426aedb2242852530b5e78144f2c1", - CurrentHighestBlockHeight: 724816, - MinerID: miner.MinerID, - ResultDescription: "ERROR: Missing inputs", - ReturnResult: mAPIFailure, - Timestamp: "2022-02-01T17:52:04.405Z", - TxID: "", - }, - }, nil - } - - return nil, errors.New("missing miner response") -} - -type minerCraftTxNotFound struct { - MinerCraftBase -} - -// SubmitTransaction submits a transaction to the miner. -func (m *minerCraftTxNotFound) SubmitTransaction(_ context.Context, miner *minercraft.Miner, - _ *minercraft.Transaction, -) (*minercraft.SubmitTransactionResponse, error) { - return &minercraft.SubmitTransactionResponse{ - JSONEnvelope: minercraft.JSONEnvelope{ - Miner: miner, - Validated: true, - JSONEnvelope: envelope.JSONEnvelope{ - Payload: "{\"apiVersion\":\"\",\"timestamp\":\"2022-02-01T17:47:52.518Z\",\"txid\":\"\",\"returnResult\":\"failure\",\"resultDescription\":\"ERROR: Mempool conflict\",\"minerId\":null,\"currentHighestBlockHash\":\"0000000000000000064c900b1fceb316302426aedb2242852530b5e78144f2c1\",\"currentHighestBlockHeight\":724816,\"txSecondMempoolExpiry\":0}", - Encoding: utf8Type, - MimeType: applicationJSONType, - }, - }, - Results: &minercraft.UnifiedSubmissionPayload{ - APIVersion: "", - CurrentHighestBlockHash: "0000000000000000064c900b1fceb316302426aedb2242852530b5e78144f2c1", - CurrentHighestBlockHeight: 724816, - MinerID: miner.MinerID, - ResultDescription: "ERROR: Mempool conflict", - ReturnResult: mAPIFailure, - Timestamp: "2022-02-01T17:47:52.518Z", - TxID: "", - }, - }, nil -} - -// QueryTransaction queries a transaction from the miner. -func (m *minerCraftTxNotFound) QueryTransaction(_ context.Context, miner *minercraft.Miner, - _ string, _ ...minercraft.QueryTransactionOptFunc, -) (*minercraft.QueryTransactionResponse, error) { - if miner.Name == minerTaal.Name { - sig := "304402201aae61ec65500cf38af48e552c0ea0c62c7937805a99ff6b2dc62bad1a23c183022027a0bb97890f92d41e7b333e8f3dec106aedcd16b782f2f8b46501e104104322" - pubKey := minerTaal.MinerID - return &minercraft.QueryTransactionResponse{ - JSONEnvelope: minercraft.JSONEnvelope{ - Miner: minerTaal, - Validated: true, - JSONEnvelope: envelope.JSONEnvelope{ - Payload: "{\"apiVersion\":\"1.4.0\",\"timestamp\":\"2022-01-24T01:36:23.0767761Z\",\"txid\":\"918c26f8227fa99f1b26f99a19648653a1382fb3b37b03870e9c138894d29b3b\",\"returnResult\":\"failure\",\"resultDescription\":\"No such mempool or blockchain transaction. Use gettransaction for wallet transactions.\",\"minerId\":\"030d1fe5c1b560efe196ba40540ce9017c20daa9504c4c4cec6184fc702d9f274e\",\"txSecondMempoolExpiry\":0}", - Signature: &sig, - PublicKey: &pubKey, - Encoding: utf8Type, - MimeType: applicationJSONType, - }, - }, - Query: &minercraft.QueryTxResponse{ - APIVersion: "1.4.0", - Timestamp: "2022-01-24T01:36:23.0767761Z", - TxID: notFoundExample1TxID, - ReturnResult: mAPIFailure, - ResultDescription: "No such mempool or blockchain transaction. Use gettransaction for wallet transactions.", - MinerID: minerTaal.MinerID, - }, - }, nil - } else if miner.Name == minerMempool.Name { - return &minercraft.QueryTransactionResponse{ - JSONEnvelope: minercraft.JSONEnvelope{ - Miner: minerMempool, - Validated: true, - JSONEnvelope: envelope.JSONEnvelope{ - Payload: "{\"apiVersion\":\"\",\"timestamp\":\"2022-01-24T01:39:58.066Z\",\"txid\":\"918c26f8227fa99f1b26f99a19648653a1382fb3b37b03870e9c138894d29b3b\",\"returnResult\":\"failure\",\"resultDescription\":\"ERROR: No such mempool or blockchain transaction. Use gettransaction for wallet transactions.\",\"blockHash\":null,\"blockHeight\":null,\"confirmations\":0,\"minerId\":null,\"txSecondMempoolExpiry\":0}", - Encoding: utf8Type, - MimeType: applicationJSONType, - }, - }, - Query: &minercraft.QueryTxResponse{ - APIVersion: "", // NOTE: missing from mempool response - Timestamp: "2022-01-24T01:39:58.066Z", - TxID: notFoundExample1TxID, - ReturnResult: mAPIFailure, - ResultDescription: "ERROR: No such mempool or blockchain transaction. Use gettransaction for wallet transactions.", - MinerID: "", // NOTE: missing from mempool response - }, - }, nil - } else if miner.Name == minerGorillaPool.Name { - sig := "3045022100eaf52c498ee79c7deb7f67ebcdb174b446e3f8e826ef6c9faa3e3365c14008a9022036b9a355574af9576e3f2c124855c81c56164df8713d0615bc0be09c50e103c8" - pubKey := minerGorillaPool.MinerID - return &minercraft.QueryTransactionResponse{ - JSONEnvelope: minercraft.JSONEnvelope{ - Miner: minerGorillaPool, - Validated: true, - JSONEnvelope: envelope.JSONEnvelope{ - Payload: "{\"apiVersion\":\"\",\"timestamp\":\"2022-01-24T01:40:41.136Z\",\"txid\":\"918c26f8227fa99f1b26f99a19648653a1382fb3b37b03870e9c138894d29b3b\",\"returnResult\":\"failure\",\"resultDescription\":\"Mixed results\",\"blockHash\":null,\"blockHeight\":null,\"confirmations\":0,\"minerId\":\"03ad780153c47df915b3d2e23af727c68facaca4facd5f155bf5018b979b9aeb83\",\"txSecondMempoolExpiry\":0}", - Encoding: utf8Type, - Signature: &sig, - PublicKey: &pubKey, - MimeType: applicationJSONType, - }, - }, - Query: &minercraft.QueryTxResponse{ - APIVersion: "", - Timestamp: "2022-01-24T01:40:41.136Z", - TxID: notFoundExample1TxID, - ReturnResult: mAPIFailure, - ResultDescription: "Mixed results", - MinerID: minerGorillaPool.MinerID, - }, - }, nil - } else if miner.Name == minerMatterPool.Name { - sig := "304402200abaf73f5b70f225f52dadc328fd5facf689d8e99ddd731b1c8a17522635c2aa022028c5d040402d8ddd7d64d94f2d7dbcd600ac50e1c503ae40fb314e0435c78b7f" - pubKey := minerMatterPool.MinerID - return &minercraft.QueryTransactionResponse{ - JSONEnvelope: minercraft.JSONEnvelope{ - Miner: minerMatterPool, - Validated: true, - JSONEnvelope: envelope.JSONEnvelope{ - Payload: "{\"apiVersion\":\"1.1.0-1-g35ba2d3\",\"timestamp\":\"2022-01-24T01:41:01.683Z\",\"txid\":\"918c26f8227fa99f1b26f99a19648653a1382fb3b37b03870e9c138894d29b3b\",\"returnResult\":\"failure\",\"resultDescription\":\"ERROR: No such mempool transaction. Use -txindex to enable blockchain transaction queries. Use gettransaction for wallet transactions.\",\"blockHash\":null,\"blockHeight\":null,\"confirmations\":0,\"minerId\":\"0253a9b2d017254b91704ba52aad0df5ca32b4fb5cb6b267ada6aefa2bc5833a93\",\"txSecondMempoolExpiry\":0}", - Encoding: utf8Type, - Signature: &sig, - PublicKey: &pubKey, - MimeType: applicationJSONType, - }, - }, - Query: &minercraft.QueryTxResponse{ - APIVersion: "1.1.0-1-g35ba2d3", - Timestamp: "2022-01-24T01:41:01.683Z", - TxID: notFoundExample1TxID, - ReturnResult: mAPIFailure, - ResultDescription: "ERROR: No such mempool transaction. Use -txindex to enable blockchain transaction queries. Use gettransaction for wallet transactions.", - MinerID: minerMatterPool.MinerID, - }, - }, nil - } - - return nil, nil -} - -type minerCraftUnreachble struct { +type minerCraftUnreachable struct { MinerCraftBase } // FeeQuote returns an error. -func (m *minerCraftUnreachble) FeeQuote(context.Context, *minercraft.Miner) (*minercraft.FeeQuoteResponse, error) { +func (m *minerCraftUnreachable) FeeQuote(context.Context, *minercraft.Miner) (*minercraft.FeeQuoteResponse, error) { return nil, errors.New("minercraft is unreachable") } - -type minerCraftBroadcastTimeout struct { - MinerCraftBase -} - -func (m *minerCraftBroadcastTimeout) SubmitTransaction(_ context.Context, miner *minercraft.Miner, - _ *minercraft.Transaction, -) (*minercraft.SubmitTransactionResponse, error) { - time.Sleep(defaultBroadcastTimeOut * 2) - - return &minercraft.SubmitTransactionResponse{ - JSONEnvelope: minercraft.JSONEnvelope{ - Miner: miner, - Validated: true, - JSONEnvelope: envelope.JSONEnvelope{ - Payload: "{\"apiVersion\":\"\",\"timestamp\":\"2022-02-01T17:47:52.518Z\",\"txid\":\"\",\"returnResult\":\"failure\",\"resultDescription\":\"ERROR: Mempool conflict\",\"minerId\":null,\"currentHighestBlockHash\":\"0000000000000000064c900b1fceb316302426aedb2242852530b5e78144f2c1\",\"currentHighestBlockHeight\":724816,\"txSecondMempoolExpiry\":0}", - Encoding: utf8Type, - MimeType: applicationJSONType, - }, - }, - Results: &minercraft.UnifiedSubmissionPayload{ - APIVersion: "", - CurrentHighestBlockHash: "0000000000000000064c900b1fceb316302426aedb2242852530b5e78144f2c1", - CurrentHighestBlockHeight: 724816, - MinerID: miner.MinerID, - ResultDescription: "ERROR: Mempool conflict", - ReturnResult: mAPIFailure, - Timestamp: "2022-02-01T17:47:52.518Z", - TxID: "", - }, - }, nil -} diff --git a/chainstate/requirements.go b/chainstate/requirements.go index bc22493a..e2f63d58 100644 --- a/chainstate/requirements.go +++ b/chainstate/requirements.go @@ -16,14 +16,23 @@ func (c *Client) validRequirement(requirement RequiredIn) bool { return requirement == RequiredOnChain || requirement == RequiredInMempool } -// checkRequirement will check to see if the requirement has been met -func checkRequirement(requirement RequiredIn, id string, txInfo *TransactionInfo) bool { - if requirement == RequiredInMempool { // Good response, and only has TX +func checkRequirement(requirement RequiredIn, id string, txInfo *TransactionInfo, onChainCondition bool) bool { + switch requirement { + case RequiredInMempool: return txInfo.ID == id - } else if requirement == RequiredOnChain { // Good response, found block hash - if len(txInfo.BlockHash) > 0 && txInfo.Confirmations > 0 { - return true - } + case RequiredOnChain: + return onChainCondition + default: + return false } - return false +} + +func checkRequirementArc(requirement RequiredIn, id string, txInfo *TransactionInfo) bool { + isConfirmedOnChain := len(txInfo.BlockHash) > 0 && txInfo.TxStatus != "" + return checkRequirement(requirement, id, txInfo, isConfirmedOnChain) +} + +func checkRequirementMapi(requirement RequiredIn, id string, txInfo *TransactionInfo) bool { + isConfirmedOnChain := len(txInfo.BlockHash) > 0 && txInfo.Confirmations > 0 + return checkRequirement(requirement, id, txInfo, isConfirmedOnChain) } diff --git a/chainstate/requirements_test.go b/chainstate/requirements_test.go index ae4ef99d..3ca33adf 100644 --- a/chainstate/requirements_test.go +++ b/chainstate/requirements_test.go @@ -10,7 +10,7 @@ func Test_checkRequirement(t *testing.T) { t.Parallel() t.Run("found in mempool - mAPI", func(t *testing.T) { - success := checkRequirement(requiredInMempool, onChainExample1TxID, &TransactionInfo{ + success := checkRequirementMapi(requiredInMempool, onChainExample1TxID, &TransactionInfo{ BlockHash: "", BlockHeight: 0, Confirmations: 0, @@ -22,7 +22,7 @@ func Test_checkRequirement(t *testing.T) { }) t.Run("found in mempool - on-chain - mAPI", func(t *testing.T) { - success := checkRequirement(requiredInMempool, onChainExample1TxID, &TransactionInfo{ + success := checkRequirementMapi(requiredInMempool, onChainExample1TxID, &TransactionInfo{ BlockHash: onChainExample1BlockHash, BlockHeight: onChainExample1BlockHeight, Confirmations: 1, @@ -34,7 +34,7 @@ func Test_checkRequirement(t *testing.T) { }) t.Run("found in mempool - whatsonchain", func(t *testing.T) { - success := checkRequirement(requiredInMempool, onChainExample1TxID, &TransactionInfo{ + success := checkRequirementMapi(requiredInMempool, onChainExample1TxID, &TransactionInfo{ BlockHash: "", BlockHeight: 0, Confirmations: 0, @@ -46,7 +46,7 @@ func Test_checkRequirement(t *testing.T) { }) t.Run("not in mempool - mAPI", func(t *testing.T) { - success := checkRequirement(requiredInMempool, onChainExample1TxID, &TransactionInfo{ + success := checkRequirementMapi(requiredInMempool, onChainExample1TxID, &TransactionInfo{ BlockHash: "", BlockHeight: 0, Confirmations: 0, @@ -58,7 +58,7 @@ func Test_checkRequirement(t *testing.T) { }) t.Run("found on chain - mAPI", func(t *testing.T) { - success := checkRequirement(requiredOnChain, onChainExample1TxID, &TransactionInfo{ + success := checkRequirementMapi(requiredOnChain, onChainExample1TxID, &TransactionInfo{ BlockHash: onChainExample1BlockHash, BlockHeight: onChainExample1BlockHeight, Confirmations: 1, @@ -70,7 +70,7 @@ func Test_checkRequirement(t *testing.T) { }) t.Run("not on chain - mAPI", func(t *testing.T) { - success := checkRequirement(requiredOnChain, onChainExample1TxID, &TransactionInfo{ + success := checkRequirementMapi(requiredOnChain, onChainExample1TxID, &TransactionInfo{ BlockHash: "", BlockHeight: 0, Confirmations: 0, diff --git a/chainstate/transaction.go b/chainstate/transaction.go index 63ddf680..4f018ed1 100644 --- a/chainstate/transaction.go +++ b/chainstate/transaction.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/BuxOrg/bux/utils" + "github.com/libsv/go-bc" "github.com/tonicpow/go-minercraft/v2" ) @@ -18,34 +18,28 @@ func (c *Client) query(ctx context.Context, id string, requiredIn RequiredIn, ctxWithCancel, cancel := context.WithTimeout(ctx, timeout) defer cancel() - // First: try all mAPI miners (Only supported on main and test right now) - if !utils.StringInSlice(ProviderMAPI, c.options.config.excludedProviders) { - if c.Network() == MainNet || c.Network() == TestNet { - for index := range c.options.config.minercraftConfig.queryMiners { - if c.options.config.minercraftConfig.queryMiners[index] != nil { - if res, err := queryMinercraft( - ctxWithCancel, c, c.options.config.minercraftConfig.queryMiners[index].Miner, id, - ); err == nil && checkRequirement(requiredIn, id, res) { - return res - } + switch c.ActiveProvider() { + case ProviderMinercraft: + for index := range c.options.config.minercraftConfig.queryMiners { + if c.options.config.minercraftConfig.queryMiners[index] != nil { + if res, err := queryMinercraft( + ctxWithCancel, c, c.options.config.minercraftConfig.queryMiners[index], id, + ); err == nil && checkRequirementMapi(requiredIn, id, res) { + return res } } } - } - - // Next: try with BroadcastClient (if loaded) - if !utils.StringInSlice(ProviderBroadcastClient, c.options.config.excludedProviders) { - if c.BroadcastClient() != nil { - if resp, err := queryBroadcastClient( - ctxWithCancel, c, id, - ); err == nil && checkRequirement(requiredIn, id, resp) { - return resp - } + case ProviderBroadcastClient: + resp, err := queryBroadcastClient( + ctxWithCancel, c, id, + ) + if err == nil && checkRequirementArc(requiredIn, id, resp) { + return resp } + default: + c.options.logger.Warn().Msg("no active provider for query") } - - // No transaction information found - return nil + return nil // No transaction information found } // fastestQuery will try ALL providers on once and return the fastest "valid" response based on requirements @@ -64,38 +58,36 @@ func (c *Client) fastestQuery(ctx context.Context, id string, requiredIn Require // Loop each miner (break into a Go routine for each query) var wg sync.WaitGroup - if !utils.StringInSlice(ProviderMAPI, c.options.config.excludedProviders) { - if c.Network() == MainNet || c.Network() == TestNet { - for index := range c.options.config.minercraftConfig.queryMiners { - wg.Add(1) - go func( - ctx context.Context, client *Client, - wg *sync.WaitGroup, miner *minercraft.Miner, - id string, requiredIn RequiredIn, - ) { - defer wg.Done() - if res, err := queryMinercraft( - ctx, client, miner, id, - ); err == nil && checkRequirement(requiredIn, id, res) { - resultsChannel <- res - } - }(ctxWithCancel, c, &wg, c.options.config.minercraftConfig.queryMiners[index].Miner, id, requiredIn) - } - } - } - if !utils.StringInSlice(ProviderBroadcastClient, c.options.config.excludedProviders) { - if c.BroadcastClient() != nil { + switch c.ActiveProvider() { + case ProviderMinercraft: + for index := range c.options.config.minercraftConfig.queryMiners { wg.Add(1) - go func(ctx context.Context, client *Client, id string, requiredIn RequiredIn) { + go func( + ctx context.Context, client *Client, + wg *sync.WaitGroup, miner *minercraft.Miner, + id string, requiredIn RequiredIn, + ) { defer wg.Done() - if resp, err := queryBroadcastClient( - ctx, client, id, - ); err == nil && checkRequirement(requiredIn, id, resp) { - resultsChannel <- resp + if res, err := queryMinercraft( + ctx, client, miner, id, + ); err == nil && checkRequirementMapi(requiredIn, id, res) { + resultsChannel <- res } - }(ctxWithCancel, c, id, requiredIn) + }(ctxWithCancel, c, &wg, c.options.config.minercraftConfig.queryMiners[index], id, requiredIn) } + case ProviderBroadcastClient: + wg.Add(1) + go func(ctx context.Context, client *Client, id string, requiredIn RequiredIn) { + defer wg.Done() + if resp, err := queryBroadcastClient( + ctx, client, id, + ); err == nil && checkRequirementArc(requiredIn, id, resp) { + resultsChannel <- resp + } + }(ctxWithCancel, c, id, requiredIn) + default: + c.options.logger.Warn().Msg("no active provider for fastestQuery") } // Waiting for all requests to finish @@ -134,11 +126,19 @@ func queryBroadcastClient(ctx context.Context, client ClientInterface, id string client.DebugLog("error executing request using " + ProviderBroadcastClient + " failed: " + err.Error()) return nil, err } else if resp != nil && strings.EqualFold(resp.TxID, id) { + bump, err := bc.NewBUMPFromStr(resp.BaseTxResponse.MerklePath) + if err != nil { + return nil, err + } return &TransactionInfo{ BlockHash: resp.BlockHash, BlockHeight: resp.BlockHeight, ID: resp.TxID, Provider: resp.Miner, + TxStatus: resp.TxStatus, + BUMP: bump, + // it's not possible to get confirmations from broadcast client; zero would be treated as "not confirmed" that's why -1 + Confirmations: -1, }, nil } return nil, ErrTransactionIDMismatch diff --git a/chainstate/transaction_info.go b/chainstate/transaction_info.go index 4ef37d81..751dd095 100644 --- a/chainstate/transaction_info.go +++ b/chainstate/transaction_info.go @@ -1,22 +1,28 @@ package chainstate import ( + "github.com/bitcoin-sv/go-broadcast-client/broadcast" "github.com/libsv/go-bc" ) // TransactionInfo is the universal information about the transaction found from a chain provider type TransactionInfo struct { - BlockHash string `json:"block_hash,omitempty"` // mAPI, WOC - BlockHeight int64 `json:"block_height"` // mAPI, WOC - Confirmations int64 `json:"confirmations,omitempty"` // mAPI, WOC - ID string `json:"id"` // Transaction ID (Hex) - MinerID string `json:"miner_id,omitempty"` // mAPI ONLY - miner_id found - Provider string `json:"provider,omitempty"` // Provider is our internal source - MerkleProof *bc.MerkleProof `json:"merkle_proof,omitempty"` // mAPI 1.5 ONLY. Should be also supported by Arc in future + BlockHash string `json:"block_hash,omitempty"` // mAPI + BlockHeight int64 `json:"block_height"` // mAPI + Confirmations int64 `json:"confirmations,omitempty"` // mAPI + ID string `json:"id"` // Transaction ID (Hex) + MinerID string `json:"miner_id,omitempty"` // mAPI ONLY - miner_id found + Provider string `json:"provider,omitempty"` // Provider is our internal source + MerkleProof *bc.MerkleProof `json:"merkle_proof,omitempty"` // mAPI 1.5 ONLY + BUMP *bc.BUMP `json:"bump,omitempty"` // Arc + TxStatus broadcast.TxStatus `json:"tx_status,omitempty"` // Arc ONLY } // Valid validates TransactionInfo by checking if it contains -// BlockHash and MerkleProof (from mAPI) or MerklePath (from Arc) +// BlockHash and MerkleProof (from mAPI) or BUMP (from Arc) func (t *TransactionInfo) Valid() bool { - return !(t.BlockHash == "" || t.MerkleProof == nil || t.MerkleProof.TxOrID == "" || len(t.MerkleProof.Nodes) == 0) + arcInvalid := t.BUMP == nil + mApiInvalid := t.MerkleProof == nil || t.MerkleProof.TxOrID == "" || len(t.MerkleProof.Nodes) == 0 + invalid := t.BlockHash == "" || (arcInvalid && mApiInvalid) + return !invalid } diff --git a/chainstate/transaction_test.go b/chainstate/transaction_test.go index 88713e0c..c356d93c 100644 --- a/chainstate/transaction_test.go +++ b/chainstate/transaction_test.go @@ -115,14 +115,14 @@ func TestClient_Transaction_BroadcastClient(t *testing.T) { // when info, err := c.QueryTransaction( - context.Background(), onChainExample1TxID, + context.Background(), onChainExampleArcTxID, RequiredInMempool, defaultQueryTimeOut, ) // then require.NoError(t, err) require.NotNil(t, info) - assert.Equal(t, onChainExample1TxID, info.ID) + assert.Equal(t, onChainExampleArcTxID, info.ID) assert.Equal(t, broadcast_fixtures.TxBlockHash, info.BlockHash) assert.Equal(t, broadcast_fixtures.TxBlockHeight, info.BlockHeight) assert.Equal(t, broadcast_fixtures.ProviderMain, info.Provider) @@ -142,14 +142,14 @@ func TestClient_Transaction_BroadcastClient(t *testing.T) { // when info, err := c.QueryTransaction( - context.Background(), onChainExample1TxID, + context.Background(), onChainExampleArcTxID, RequiredInMempool, defaultQueryTimeOut, ) // then require.NoError(t, err) require.NotNil(t, info) - assert.Equal(t, onChainExample1TxID, info.ID) + assert.Equal(t, onChainExampleArcTxID, info.ID) assert.Equal(t, broadcast_fixtures.TxBlockHash, info.BlockHash) assert.Equal(t, broadcast_fixtures.TxBlockHeight, info.BlockHeight) assert.Equal(t, broadcast_fixtures.ProviderMain, info.Provider) @@ -169,36 +169,32 @@ func TestClient_Transaction_BroadcastClient(t *testing.T) { // when info, err := c.QueryTransaction( - context.Background(), onChainExample1TxID, + context.Background(), onChainExampleArcTxID, RequiredInMempool, defaultQueryTimeOut, ) // then require.NoError(t, err) require.NotNil(t, info) - assert.Equal(t, onChainExample1TxID, info.ID) + assert.Equal(t, onChainExampleArcTxID, info.ID) assert.Equal(t, broadcast_fixtures.TxBlockHash, info.BlockHash) assert.Equal(t, broadcast_fixtures.TxBlockHeight, info.BlockHeight) assert.Equal(t, broadcast_fixtures.ProviderMain, info.Provider) }) } -func TestClient_Transaction_MultipleClients(t *testing.T) { +func TestClient_Transaction_MAPI_Fastest(t *testing.T) { t.Parallel() - t.Run("valid - all clients", func(t *testing.T) { + t.Run("query transaction success - mAPI", func(t *testing.T) { // given - bc := broadcast_client_mock.Builder(). - WithMockArc(broadcast_client_mock.MockSuccess). - Build() c := NewTestClient( context.Background(), t, WithMinercraft(&minerCraftTxOnChain{}), - WithBroadcastClient(bc), ) // when - info, err := c.QueryTransaction( + info, err := c.QueryTransactionFastest( context.Background(), onChainExample1TxID, RequiredOnChain, defaultQueryTimeOut, ) @@ -214,45 +210,16 @@ func TestClient_Transaction_MultipleClients(t *testing.T) { assert.Equal(t, minerTaal.MinerID, info.MinerID) }) - t.Run("mAPI not found - broadcastClient", func(t *testing.T) { + t.Run("valid - test network - mAPI", func(t *testing.T) { // given - bc := broadcast_client_mock.Builder(). - WithMockArc(broadcast_client_mock.MockSuccess). - Build() c := NewTestClient( context.Background(), t, - WithMinercraft(&minerCraftTxNotFound{}), // NOT going to find the TX - WithBroadcastClient(bc), - ) - - // when - info, err := c.QueryTransaction( - context.Background(), onChainExample1TxID, - RequiredInMempool, defaultQueryTimeOut, - ) - - // then - require.NoError(t, err) - require.NotNil(t, info) - assert.Equal(t, onChainExample1TxID, info.ID) - assert.Equal(t, broadcast_fixtures.TxBlockHash, info.BlockHash) - assert.Equal(t, broadcast_fixtures.TxBlockHeight, info.BlockHeight) - assert.Equal(t, broadcast_fixtures.ProviderMain, info.Provider) - }) - - t.Run("broadcastClient not found - mAPI", func(t *testing.T) { - // given - bc := broadcast_client_mock.Builder(). - WithMockArc(broadcast_client_mock.MockFailure). - Build() - c := NewTestClient( - context.Background(), t, - WithBroadcastClient(bc), // NOT going to find the TX WithMinercraft(&minerCraftTxOnChain{}), + WithNetwork(TestNet), ) // when - info, err := c.QueryTransaction( + info, err := c.QueryTransactionFastest( context.Background(), onChainExample1TxID, RequiredOnChain, defaultQueryTimeOut, ) @@ -267,76 +234,19 @@ func TestClient_Transaction_MultipleClients(t *testing.T) { assert.Equal(t, minerTaal.Name, info.Provider) assert.Equal(t, minerTaal.MinerID, info.MinerID) }) - - t.Run("error - all not found", func(t *testing.T) { - // given - bc := broadcast_client_mock.Builder(). - WithMockArc(broadcast_client_mock.MockFailure). - Build() - c := NewTestClient( - context.Background(), t, - WithMinercraft(&minerCraftTxNotFound{}), // NOT going to find the TX - WithBroadcastClient(bc), // NOT going to find the TX - ) - - // when - info, err := c.QueryTransaction( - context.Background(), onChainExample1TxID, - RequiredOnChain, defaultQueryTimeOut, - ) - - // then - require.Error(t, err) - require.Nil(t, info) - assert.ErrorIs(t, err, ErrTransactionNotFound) - }) } -// TestClient_Transaction_MultipleClients_Fastest will test the method QueryTransactionFastest() -func TestClient_Transaction_MultipleClients_Fastest(t *testing.T) { +func TestClient_Transaction_BroadcastClient_Fastest(t *testing.T) { t.Parallel() - t.Run("error - missing id", func(t *testing.T) { - // given - c := NewTestClient(context.Background(), t, - WithMinercraft(&MinerCraftBase{})) - - // when - info, err := c.QueryTransactionFastest( - context.Background(), "", RequiredOnChain, defaultQueryTimeOut, - ) - - // then - require.Error(t, err) - require.Nil(t, info) - assert.ErrorIs(t, err, ErrInvalidTransactionID) - }) - - t.Run("error - missing requirements", func(t *testing.T) { - // given - c := NewTestClient(context.Background(), t, - WithMinercraft(&MinerCraftBase{})) - - // when - info, err := c.QueryTransactionFastest( - context.Background(), onChainExample1TxID, - "", defaultQueryTimeOut, - ) - - // then - require.Error(t, err) - require.Nil(t, info) - assert.ErrorIs(t, err, ErrInvalidRequirements) - }) - - t.Run("valid - all clients", func(t *testing.T) { + t.Run("query transaction success - broadcastClient", func(t *testing.T) { // given bc := broadcast_client_mock.Builder(). WithMockArc(broadcast_client_mock.MockSuccess). Build() c := NewTestClient( context.Background(), t, - WithMinercraft(&minerCraftTxOnChain{}), + WithMinercraft(&MinerCraftBase{}), WithBroadcastClient(bc), ) @@ -350,38 +260,21 @@ func TestClient_Transaction_MultipleClients_Fastest(t *testing.T) { require.NoError(t, err) require.NotNil(t, info) assert.Equal(t, onChainExample1TxID, info.ID) - assert.True(t, isOneOf( - info.BlockHash, - onChainExample1BlockHash, - broadcast_fixtures.TxBlockHash, - )) - assert.True(t, isOneOf( - info.BlockHeight, - onChainExample1BlockHeight, - broadcast_fixtures.TxBlockHeight, - )) - // todo: test is failing and needs to be fixed (@mrz) - /*assert.True(t, isOneOf( - info.Confirmations, - onChainExample1Confirmations, - 0, - ))*/ - assert.True(t, isOneOf( - info.Provider, - minerTaal.Name, - broadcast_fixtures.ProviderMain, - )) + assert.Equal(t, broadcast_fixtures.TxBlockHash, info.BlockHash) + assert.Equal(t, broadcast_fixtures.TxBlockHeight, info.BlockHeight) + assert.Equal(t, broadcast_fixtures.ProviderMain, info.Provider) }) - t.Run("mAPI not found - broadcastClient", func(t *testing.T) { + t.Run("valid - stress test network - broadcastClient", func(t *testing.T) { // given bc := broadcast_client_mock.Builder(). WithMockArc(broadcast_client_mock.MockSuccess). Build() c := NewTestClient( context.Background(), t, - WithMinercraft(&minerCraftTxNotFound{}), // NOT going to find the TX + WithMinercraft(&MinerCraftBase{}), WithBroadcastClient(bc), + WithNetwork(StressTestNet), ) // when @@ -399,69 +292,20 @@ func TestClient_Transaction_MultipleClients_Fastest(t *testing.T) { assert.Equal(t, broadcast_fixtures.ProviderMain, info.Provider) }) - t.Run("broadcastClient not found - mAPI", func(t *testing.T) { - // given - bc := broadcast_client_mock.Builder(). - WithMockArc(broadcast_client_mock.MockFailure). - Build() - c := NewTestClient( - context.Background(), t, - WithBroadcastClient(bc), // NOT going to find the TX - WithMinercraft(&minerCraftTxOnChain{}), - ) - - // when - info, err := c.QueryTransactionFastest( - context.Background(), onChainExample1TxID, - RequiredOnChain, defaultQueryTimeOut, - ) - - // then - require.NoError(t, err) - require.NotNil(t, info) - assert.Equal(t, onChainExample1TxID, info.ID) - assert.Equal(t, onChainExample1BlockHash, info.BlockHash) - assert.Equal(t, onChainExample1BlockHeight, info.BlockHeight) - assert.Equal(t, onChainExample1Confirmations, info.Confirmations) - }) - - t.Run("error - all not found", func(t *testing.T) { - // given - bc := broadcast_client_mock.Builder(). - WithMockArc(broadcast_client_mock.MockFailure). - Build() - c := NewTestClient( - context.Background(), t, - WithMinercraft(&minerCraftTxNotFound{}), // NOT going to find the TX - WithBroadcastClient(bc), // NOT going to find the TX - ) - - // when - info, err := c.QueryTransactionFastest( - context.Background(), onChainExample1TxID, - RequiredOnChain, defaultQueryTimeOut, - ) - - // then - require.Error(t, err) - require.Nil(t, info) - assert.ErrorIs(t, err, ErrTransactionNotFound) - }) - - t.Run("valid - stn network", func(t *testing.T) { + t.Run("valid - test network - broadcast", func(t *testing.T) { // given bc := broadcast_client_mock.Builder(). WithMockArc(broadcast_client_mock.MockSuccess). Build() c := NewTestClient( context.Background(), t, - WithMinercraft(&minerCraftTxOnChain{}), + WithMinercraft(&MinerCraftBase{}), WithBroadcastClient(bc), - WithNetwork(StressTestNet), + WithNetwork(TestNet), ) // when - info, err := c.QueryTransactionFastest( + info, err := c.QueryTransaction( context.Background(), onChainExample1TxID, RequiredInMempool, defaultQueryTimeOut, ) @@ -475,13 +319,3 @@ func TestClient_Transaction_MultipleClients_Fastest(t *testing.T) { assert.Equal(t, broadcast_fixtures.ProviderMain, info.Provider) }) } - -func isOneOf(val1 interface{}, val2 ...interface{}) bool { - for _, element := range val2 { - if val1 == element { - return true - } - } - - return false -} diff --git a/client.go b/client.go index bf595545..5e821645 100644 --- a/client.go +++ b/client.go @@ -114,7 +114,6 @@ type ( // taskManagerOptions holds the configuration for taskmanager taskManagerOptions struct { taskmanager.TaskEngine // Client for TaskManager - cronJobs taskmanager.CronJobs // List of cron jobs options []taskmanager.TaskManagerOptions // List of options cronCustomPeriods map[string]time.Duration // will override the default period of cronJob } diff --git a/client_options.go b/client_options.go index c9c0f826..7a439a9f 100644 --- a/client_options.go +++ b/client_options.go @@ -12,6 +12,7 @@ import ( "github.com/BuxOrg/bux/logging" "github.com/BuxOrg/bux/notifications" "github.com/BuxOrg/bux/taskmanager" + "github.com/BuxOrg/bux/utils" "github.com/bitcoin-sv/go-broadcast-client/broadcast" "github.com/bitcoin-sv/go-paymail" "github.com/bitcoin-sv/go-paymail/server" @@ -551,8 +552,8 @@ func WithTaskqConfig(config *taskq.QueueOptions) ClientOps { } } -// WithCronCustmPeriod will set the custom cron jobs period which will override the default -func WithCronCustmPeriod(cronJobName string, period time.Duration) ClientOps { +// WithCronCustomPeriod will set the custom cron jobs period which will override the default +func WithCronCustomPeriod(cronJobName string, period time.Duration) ClientOps { return func(c *clientOptions) { if c.taskManager != nil { c.taskManager.cronCustomPeriods[cronJobName] = period @@ -614,24 +615,6 @@ func WithChainstateOptions(broadcasting, broadcastInstant, paymailP2P, syncOnCha } } -// WithBroadcastMiners will set a list of miners for broadcasting -func WithBroadcastMiners(miners []*chainstate.Miner) ClientOps { - return func(c *clientOptions) { - if len(miners) > 0 { - c.chainstate.options = append(c.chainstate.options, chainstate.WithBroadcastMiners(miners)) - } - } -} - -// WithQueryMiners will set a list of miners for querying transactions -func WithQueryMiners(miners []*chainstate.Miner) ClientOps { - return func(c *clientOptions) { - if len(miners) > 0 { - c.chainstate.options = append(c.chainstate.options, chainstate.WithQueryMiners(miners)) - } - } -} - // WithExcludedProviders will set a list of excluded providers func WithExcludedProviders(providers []string) ClientOps { return func(c *clientOptions) { @@ -663,17 +646,17 @@ func WithCustomNotifications(customNotifications notifications.ClientInterface) } } -// WithMinercraftFeeQuotes will set usage of minercraft's fee quotes instead of default fees -func WithMinercraftFeeQuotes() ClientOps { +// WithFeeQuotes will find the lowest fee instead of using the fee set by the WithFeeUnit function +func WithFeeQuotes(enabled bool) ClientOps { return func(c *clientOptions) { - c.chainstate.options = append(c.chainstate.options, chainstate.WithMinercraftFeeQuotes()) + c.chainstate.options = append(c.chainstate.options, chainstate.WithFeeQuotes(enabled)) } } -// WithArc will specify Arc as an API for minercraft client -func WithArc() ClientOps { +// WithFeeUnit will set the fee unit to use for broadcasting +func WithFeeUnit(feeUnit *utils.FeeUnit) ClientOps { return func(c *clientOptions) { - c.chainstate.options = append(c.chainstate.options, chainstate.WithArc()) + c.chainstate.options = append(c.chainstate.options, chainstate.WithFeeUnit(feeUnit)) } } diff --git a/cron_job_declarations.go b/cron_job_declarations.go index f9e77f09..0f1abe30 100644 --- a/cron_job_declarations.go +++ b/cron_job_declarations.go @@ -7,7 +7,7 @@ import ( "github.com/BuxOrg/bux/taskmanager" ) -// Cron job names; defined as public constants to be used in WithCronCustmPeriod +// Cron job names to be used in WithCronCustomPeriod const ( CronJobNameDraftTransactionCleanUp = "draft_transaction_clean_up" CronJobNameIncomingTransaction = "incoming_transaction_process" diff --git a/definitions.go b/definitions.go index 576387ef..bcc12d75 100644 --- a/definitions.go +++ b/definitions.go @@ -23,7 +23,7 @@ const ( dustLimit = uint64(1) // Dust limit mongoTestVersion = "6.0.4" // Mongo Testing Version sqliteTestVersion = "3.37.0" // SQLite Testing Version (dummy version for now) - version = "v0.10.2" // bux version + version = "v0.13.0" // bux version ) // All the base models diff --git a/errors.go b/errors.go index 74e2208b..17562034 100644 --- a/errors.go +++ b/errors.go @@ -43,6 +43,9 @@ var ErrXpubIDMisMatch = errors.New("xpub_id mismatch") // ErrMissingXpub is when the field is required but missing var ErrMissingXpub = errors.New("could not find xpub") +// ErrAccessKeyNotFound is when the access key not found +var ErrAccessKeyNotFound = errors.New("access key not found") + // ErrMissingLockingScript is when the field is required but missing var ErrMissingLockingScript = errors.New("could not find locking script") @@ -112,9 +115,6 @@ var ErrUtxoAlreadySpent = errors.New("utxo has already been spent") // ErrDraftNotFound is when the requested draft transaction was not found var ErrDraftNotFound = errors.New("corresponding draft transaction not found") -// ErrTaskManagerNotLoaded is when the taskmanager was not loaded -var ErrTaskManagerNotLoaded = errors.New("taskmanager must be loaded") - // ErrTransactionNotParsed is when the transaction is not parsed but was expected var ErrTransactionNotParsed = errors.New("transaction is not parsed") diff --git a/examples/client/broadcast_miners/broadcast_miners.go b/examples/client/broadcast_miners/broadcast_miners.go index bcb09187..ff6e2cde 100644 --- a/examples/client/broadcast_miners/broadcast_miners.go +++ b/examples/client/broadcast_miners/broadcast_miners.go @@ -6,42 +6,36 @@ import ( "os" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux/chainstate" - "github.com/tonicpow/go-minercraft/v2" + "github.com/BuxOrg/bux/logging" + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + broadcastclient "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client" ) -func main() { - // Create a custom miner (using your api key for custom rates) - miners, _ := minercraft.DefaultMiners() - minerTaal := minercraft.MinerByName(miners, minercraft.MinerTaal) - minerCraftApis := []*minercraft.MinerAPIs{ - { - MinerID: minerTaal.MinerID, - APIs: []minercraft.API{ - { - Token: os.Getenv("BUX_TAAL_API_KEY"), - URL: "https://tapi.taal.com/arc", - Type: minercraft.Arc, - }, - }, +func buildBroadcastClient() broadcast.Client { + logger := logging.GetDefaultLogger() + builder := broadcastclient.Builder().WithArc( + broadcastclient.ArcClientConfig{ + APIUrl: "https://tapi.taal.com/arc", + Token: os.Getenv("BUX_TAAL_API_KEY"), }, - } + logger, + ) + + return builder.Build() +} + +func main() { + ctx := context.Background() - // Create the client client, err := bux.NewClient( - context.Background(), // Set context - bux.WithBroadcastMiners([]*chainstate.Miner{{Miner: minerTaal}}), // This will auto-fetch a policy using the token (api key) - bux.WithQueryMiners([]*chainstate.Miner{{Miner: minerTaal}}), // This will only use this as a query provider - bux.WithMinercraftAPIs(minerCraftApis), - bux.WithArc(), + ctx, + bux.WithBroadcastClient(buildBroadcastClient()), ) if err != nil { log.Fatalln("error: " + err.Error()) } - defer func() { - _ = client.Close(context.Background()) - }() + defer client.Close(ctx) log.Println("client loaded!", client.UserAgent()) } diff --git a/examples/client/custom_cron/custom_cron.go b/examples/client/custom_cron/custom_cron.go index acf5baaa..535053bc 100644 --- a/examples/client/custom_cron/custom_cron.go +++ b/examples/client/custom_cron/custom_cron.go @@ -11,8 +11,8 @@ import ( func main() { client, err := bux.NewClient( context.Background(), // Set context - bux.WithCronCustmPeriod(bux.CronJobNameDraftTransactionCleanUp, 2*time.Second), - bux.WithCronCustmPeriod(bux.CronJobNameIncomingTransaction, 4*time.Second), + bux.WithCronCustomPeriod(bux.CronJobNameDraftTransactionCleanUp, 2*time.Second), + bux.WithCronCustomPeriod(bux.CronJobNameIncomingTransaction, 4*time.Second), ) if err != nil { log.Fatalln("error: " + err.Error()) diff --git a/examples/client/custom_rates/custom_rates.go b/examples/client/custom_rates/custom_rates.go index 9593fc3d..d1009f12 100644 --- a/examples/client/custom_rates/custom_rates.go +++ b/examples/client/custom_rates/custom_rates.go @@ -7,67 +7,47 @@ import ( "time" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux/chainstate" - "github.com/tonicpow/go-minercraft/v2" + "github.com/BuxOrg/bux/logging" + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + broadcastclient "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client" ) +func buildBroadcastClient() broadcast.Client { + logger := logging.GetDefaultLogger() + builder := broadcastclient.Builder().WithArc( + broadcastclient.ArcClientConfig{ + APIUrl: "https://tapi.taal.com/arc", + Token: os.Getenv("BUX_TAAL_API_KEY"), + }, + logger, + ) + + return builder.Build() +} + func main() { + ctx := context.Background() const testXPub = "xpub661MyMwAqRbcFrBJbKwBGCB7d3fr2SaAuXGM95BA62X41m6eW2ehRQGW4xLi9wkEXUGnQZYxVVj4PxXnyrLk7jdqvBAs1Qq9gf6ykMvjR7J" - // Create a custom miner (using your api key for custom rates) - miners, _ := minercraft.DefaultMiners() - minerTaal := minercraft.MinerByName(miners, minercraft.MinerTaal) - minerCraftApis := []*minercraft.MinerAPIs{ - { - MinerID: minerTaal.MinerID, - APIs: []minercraft.API{ - { - Token: os.Getenv("BUX_TAAL_API_KEY"), - URL: "https://tapi.taal.com/arc", - Type: minercraft.Arc, - }, - }, - }, - } - - // Create the client client, err := bux.NewClient( - context.Background(), // Set context - bux.WithAutoMigrate(bux.BaseModels...), // All models - bux.WithBroadcastMiners([]*chainstate.Miner{{Miner: minerTaal}}), // This will auto-fetch a policy using the token (api key) - bux.WithMinercraftAPIs(minerCraftApis), - bux.WithArc(), + ctx, + bux.WithAutoMigrate(bux.BaseModels...), + bux.WithBroadcastClient(buildBroadcastClient()), ) if err != nil { log.Fatalln("error: " + err.Error()) } - defer func() { - _ = client.Close(context.Background()) - }() + defer client.Close(ctx) - // Get the miners - broadcastMiners := client.Chainstate().BroadcastMiners() - for _, miner := range broadcastMiners { - log.Println("miner", miner.Miner) - log.Println("fee", miner.FeeUnit) - log.Println("last_checked", miner.FeeLastChecked.String()) - } - - // Create an xPub - var xpub *bux.Xpub - if xpub, err = client.NewXpub( - context.Background(), - testXPub, - ); err != nil { + xpub, err := client.NewXpub(ctx, testXPub) + if err != nil { log.Fatalln("error: " + err.Error()) } - // Create a draft transaction - var draft *bux.DraftTransaction - draft, err = client.NewTransaction(context.Background(), xpub.RawXpub(), &bux.TransactionConfig{ + draft, err := client.NewTransaction(ctx, xpub.RawXpub(), &bux.TransactionConfig{ ExpiresIn: 10 * time.Second, - SendAllTo: &bux.TransactionOutput{To: "mrz@moneybutton.com"}, + SendAllTo: &bux.TransactionOutput{To: os.Getenv("BUX_MY_PAYMAIL")}, }) if err != nil { log.Fatalln("error: " + err.Error()) diff --git a/go.mod b/go.mod index d9a9de59..9d0fd462 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.21.5 require ( github.com/99designs/gqlgen v0.17.42 - github.com/DATA-DOG/go-sqlmock v1.5.1 - github.com/bitcoin-sv/go-broadcast-client v0.10.0 - github.com/bitcoin-sv/go-paymail v0.11.0 + github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/bitcoin-sv/go-broadcast-client v0.16.0 + github.com/bitcoin-sv/go-paymail v0.12.0 github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 github.com/bitcoinschema/go-map v0.1.0 github.com/centrifugal/centrifuge-go v0.10.2 @@ -22,8 +22,8 @@ require ( github.com/libsv/go-bk v0.1.6 github.com/libsv/go-bt v1.0.8 github.com/libsv/go-bt/v2 v2.2.5 - github.com/mrz1836/go-cache v0.9.2 - github.com/mrz1836/go-cachestore v0.3.3 + github.com/mrz1836/go-cache v0.9.3 + github.com/mrz1836/go-cachestore v0.3.4 github.com/mrz1836/go-datastore v0.5.9 github.com/mrz1836/go-logger v0.3.2 github.com/newrelic/go-agent/v3 v3.29.0 @@ -33,7 +33,7 @@ require ( github.com/rs/zerolog v1.31.0 github.com/stretchr/testify v1.8.4 github.com/tonicpow/go-minercraft/v2 v2.0.8 - github.com/tryvium-travels/memongo v0.10.0 + github.com/tryvium-travels/memongo v0.11.0 github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43 github.com/vmihailenco/taskq/v3 v3.2.9 go.elastic.co/ecszerolog v0.2.0 @@ -60,7 +60,7 @@ require ( github.com/dolthub/jsonpath v0.0.2-0.20230525180605-8dc13778fd72 // indirect github.com/dolthub/vitess v0.0.0-20230823204737-4a21a94e90c3 // indirect github.com/go-kit/kit v0.13.0 // indirect - github.com/go-resty/resty/v2 v2.10.0 // indirect + github.com/go-resty/resty/v2 v2.11.0 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/gocraft/dbr/v2 v2.7.6 // indirect github.com/gojektech/heimdall/v6 v6.1.0 // indirect @@ -124,10 +124,10 @@ require ( golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.16.1 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect google.golang.org/grpc v1.60.1 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/src-d/go-errors.v1 v1.0.0 // indirect diff --git a/go.sum b/go.sum index a165f2bf..1fc3e207 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/99designs/gqlgen v0.17.42 h1:BVWDOb2VVHQC5k3m6oa0XhDnxltLLrU4so7x/u39Zu4= github.com/99designs/gqlgen v0.17.42/go.mod h1:GQ6SyMhwFbgHR0a8r2Wn8fYgEwPxxmndLFPhU63+cJE= -github.com/DATA-DOG/go-sqlmock v1.5.1 h1:FK6RCIUSfmbnI/imIICmboyQBkOckutaa6R5YYlLZyo= -github.com/DATA-DOG/go-sqlmock v1.5.1/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/datadog-go v3.7.1+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -12,16 +12,16 @@ github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzd github.com/afex/hystrix-go v0.0.0-20180209013831-27fae8d30f1a/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.31.0 h1:ObEFUNlJwoIiyjxdrYF0QIDE7qXcLc7D3WpSH4c22PU= -github.com/alicebob/miniredis/v2 v2.31.0/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg= +github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6TTnfX8Y= +github.com/alicebob/miniredis/v2 v2.31.1/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/aws/aws-sdk-go v1.43.45 h1:2708Bj4uV+ym62MOtBnErm/CDX61C4mFe9V2gXy1caE= github.com/aws/aws-sdk-go v1.43.45/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/bitcoin-sv/go-broadcast-client v0.10.0 h1:G15Di58APGbBdUu9Z/QiziKImW4sqMQDasLjgNAXGAQ= -github.com/bitcoin-sv/go-broadcast-client v0.10.0/go.mod h1:VePGovAMTQM6G4kQDyXZkcMKCUm2c5eIgfP+WIvqXg4= -github.com/bitcoin-sv/go-paymail v0.11.0 h1:jEfBLLaUUIxN3WnkU9d6Wj9IUen1Xx09dWUHlz3gTnQ= -github.com/bitcoin-sv/go-paymail v0.11.0/go.mod h1:wH4jOVM24y7+OS2rLeJpwzJ73abM2rq/aeKZom0lHSk= +github.com/bitcoin-sv/go-broadcast-client v0.16.0 h1:KadOLv+i9Y6xAOkHsSl2PIECQ59SpUyYurY6Ysvpz5A= +github.com/bitcoin-sv/go-broadcast-client v0.16.0/go.mod h1:GRAliwumNBjEbLRIEkXqIKJpsgmMfjvlIDqgyw/NoJE= +github.com/bitcoin-sv/go-paymail v0.12.0 h1:y9Kc89wovrObllz0vilG/7olbMrba97D0XJR9YDBw6s= +github.com/bitcoin-sv/go-paymail v0.12.0/go.mod h1:/BGu//F4Ji7jIzvkcHxlwBB9vU90yVRx/tovX91Tbw0= github.com/bitcoinschema/go-bitcoin/v2 v2.0.5 h1:Sgh5Eb746Zck/46rFDrZZEXZWyO53fMuWYhNoZa1tck= github.com/bitcoinschema/go-bitcoin/v2 v2.0.5/go.mod h1:JjO1ivfZv6vhK0uAXzyH08AAHlzNMAfnyK1Fiv9r4ZA= github.com/bitcoinschema/go-bob v0.4.0 h1:adsAEboLQCg0D6e9vwcJUJEJScszsouAYCYu35UAiGo= @@ -84,8 +84,8 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-redis/redis_rate/v9 v9.1.2 h1:H0l5VzoAtOE6ydd38j8MCq3ABlGLnvvbA1xDSVVCHgQ= github.com/go-redis/redis_rate/v9 v9.1.2/go.mod h1:oam2de2apSgRG8aJzwJddXbNu91Iyz1m8IKJE2vpvlQ= -github.com/go-resty/resty/v2 v2.10.0 h1:Qla4W/+TMmv0fOeeRqzEpXPLfTUnR5HZ1+lGs+CkiCo= -github.com/go-resty/resty/v2 v2.10.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= +github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= +github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= @@ -217,10 +217,10 @@ github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/ github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/mrz1836/go-cache v0.9.2 h1:pQK3SJg6kfbn43oKW68R3qCWRILKSvSHs3qvwn8OzHc= -github.com/mrz1836/go-cache v0.9.2/go.mod h1:IphLH8lmLdSI5N+8MswFEaqP1bJQ8800CxXre5nJD4M= -github.com/mrz1836/go-cachestore v0.3.3 h1:E9f1frV38iAfpJ7vYR8bJ/F3GEdmPUTTFwxDf8DZP/A= -github.com/mrz1836/go-cachestore v0.3.3/go.mod h1:u1RyfM5lOOEWmYNmjrP2mKJwRahuvhapAP3vSSrXTmc= +github.com/mrz1836/go-cache v0.9.3 h1:SxDowagONmPmtIt9HgCK+dAHYBTfoyGYj/rt/dti8io= +github.com/mrz1836/go-cache v0.9.3/go.mod h1:kmtrLVW/wt/JBr1rE783eo04O9b36Emmfj1E/YyqAfA= +github.com/mrz1836/go-cachestore v0.3.4 h1:L/bO4aT1l6lyElWqvyeBca/XRw6p+p2zOyJzI0kGB0Y= +github.com/mrz1836/go-cachestore v0.3.4/go.mod h1:vlKAtr0TzWjReCWFdgxMDrIkYU6wgV1SU7u6AXdPQnY= github.com/mrz1836/go-datastore v0.5.9 h1:wNNUNCBCSddOieE5aM06GI5dN8JElbIQrzqEAxnxrPE= github.com/mrz1836/go-datastore v0.5.9/go.mod h1:tkc466oJtAPNxENZpfjlcerTrCy7kyCzJiVVgbIIguE= github.com/mrz1836/go-logger v0.3.2 h1:bjd23NwVaLWncXgXAyxAwWLQ02of0Ci3iJIZZEakkFU= @@ -254,8 +254,8 @@ github.com/rafaeljusto/redigomock v2.4.0+incompatible/go.mod h1:JaY6n2sDr+z2WTsX github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -314,8 +314,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tonicpow/go-minercraft/v2 v2.0.8 h1:gDjHOpmD0P5qRLpgRLUHDcDR39DdT5c/XhmOKPDfNjY= github.com/tonicpow/go-minercraft/v2 v2.0.8/go.mod h1:mfr1fgOpnu2GkTmPDT4Sanoh4wOfV6kcwOrjVdo8vPk= -github.com/tryvium-travels/memongo v0.10.0 h1:qDhyts06xFtGJDCd9NznSDFMiKa9UrzZ8N2+2+lqb6w= -github.com/tryvium-travels/memongo v0.10.0/go.mod h1:riRUHKRQ5JbeX2ryzFfmr7P2EYXIkNwgloSQJPpBikA= +github.com/tryvium-travels/memongo v0.11.0 h1:VpFkeigK7bge9aXH+oVG+H3OI2ih12riTROk0CvERrk= +github.com/tryvium-travels/memongo v0.11.0/go.mod h1:riRUHKRQ5JbeX2ryzFfmr7P2EYXIkNwgloSQJPpBikA= github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43 h1:QEePdg0ty2r0t1+qwfZmQ4OOl/MB2UXIeJSpIZv56lg= github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -344,8 +344,8 @@ github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= -github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.elastic.co/ecszerolog v0.2.0 h1:nbX4dQ08jb3+vsvACfmzAqGDoBh8F2HQDUgpqwAVTg0= go.elastic.co/ecszerolog v0.2.0/go.mod h1:wR5Mv0BVQJ17LopUX5Fd0LLKCC9iF++58iKY+lL09lc= go.mongodb.org/mongo-driver v1.11.7 h1:LIwYxASDLGUg/8wOhgOOZhX8tQa/9tgZPgzZoVqJvcs= @@ -422,8 +422,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -457,8 +457,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/interface.go b/interface.go index 5e661208..cd9f6bfa 100644 --- a/interface.go +++ b/interface.go @@ -131,7 +131,6 @@ type PaymailService interface { // TransactionService is the transaction actions type TransactionService interface { GetTransaction(ctx context.Context, xPubID, txID string) (*Transaction, error) - GetTransactionByID(ctx context.Context, txID string) (*Transaction, error) GetTransactionsByIDs(ctx context.Context, txIDs []string) ([]*Transaction, error) GetTransactionByHex(ctx context.Context, hex string) (*Transaction, error) GetTransactions(ctx context.Context, metadata *Metadata, conditions *map[string]interface{}, diff --git a/mock_chainstate_test.go b/mock_chainstate_test.go index 603fe9af..38d4b16c 100644 --- a/mock_chainstate_test.go +++ b/mock_chainstate_test.go @@ -30,10 +30,6 @@ func (c *chainStateBase) QueryTransactionFastest(context.Context, string, chains return nil, nil } -func (c *chainStateBase) BroadcastMiners() []*chainstate.Miner { - return nil -} - func (c *chainStateBase) Close(context.Context) {} func (c *chainStateBase) Debug(bool) {} @@ -56,18 +52,10 @@ func (c *chainStateBase) Minercraft() minercraft.ClientInterface { return nil } -func (c *chainStateBase) Miners() []*chainstate.Miner { - return nil -} - func (c *chainStateBase) Network() chainstate.Network { return chainstate.MainNet } -func (c *chainStateBase) QueryMiners() []*chainstate.Miner { - return nil -} - func (c *chainStateBase) QueryTimeout() time.Duration { return 10 * time.Second } @@ -161,7 +149,7 @@ func (c *chainStateEverythingOnChain) QueryTransactionFastest(_ context.Context, } func (c *chainStateEverythingOnChain) FeeUnit() *utils.FeeUnit { - return chainstate.DefaultFee + return chainstate.MockDefaultFee } func (c *chainStateEverythingOnChain) VerifyMerkleRoots(_ context.Context, _ []chainstate.MerkleRootConfirmationRequestItem) error { diff --git a/model_bump.go b/model_bump.go index 5ec3be04..db871ac7 100644 --- a/model_bump.go +++ b/model_bump.go @@ -10,6 +10,7 @@ import ( "reflect" "sort" + "github.com/BuxOrg/bux/utils" "github.com/libsv/go-bc" "github.com/libsv/go-bt/v2" ) @@ -287,14 +288,8 @@ func (bump *BUMP) Scan(value interface{}) error { return nil } - xType := fmt.Sprintf("%T", value) - var byteValue []byte - if xType == ValueTypeString { - byteValue = []byte(value.(string)) - } else { - byteValue = value.([]byte) - } - if bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { + byteValue, err := utils.ToByteArray(value) + if err != nil || bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { return nil } @@ -320,14 +315,8 @@ func (bumps *BUMPs) Scan(value interface{}) error { return nil } - xType := fmt.Sprintf("%T", value) - var byteValue []byte - if xType == ValueTypeString { - byteValue = []byte(value.(string)) - } else { - byteValue = value.([]byte) - } - if bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { + byteValue, err := utils.ToByteArray(value) + if err != nil || bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { return nil } @@ -348,7 +337,7 @@ func (bumps BUMPs) Value() (driver.Value, error) { } // MerkleProofToBUMP transforms Merkle Proof to BUMP -func MerkleProofToBUMP(merkleProof *bc.MerkleProof, blockHeight uint64) BUMP { +func merkleProofToBUMP(merkleProof *bc.MerkleProof, blockHeight uint64) BUMP { bump := BUMP{BlockHeight: blockHeight} height := len(merkleProof.Nodes) @@ -381,6 +370,28 @@ func MerkleProofToBUMP(merkleProof *bc.MerkleProof, blockHeight uint64) BUMP { return bump } +func bcBumpToBUMP(bcBump *bc.BUMP) BUMP { + path := make([][]BUMPLeaf, len(bcBump.Path)) + for i := range bcBump.Path { + path[i] = make([]BUMPLeaf, len(bcBump.Path[i])) + for j, source := range bcBump.Path[i] { + leaf := BUMPLeaf{} + + // All fields in bc.leaf are pointers, so we need to use SafeAssign to avoid dereferencing nil pointers + utils.SafeAssign(&leaf.Offset, source.Offset) + utils.SafeAssign(&leaf.Hash, source.Hash) + utils.SafeAssign(&leaf.TxID, source.Txid) + utils.SafeAssign(&leaf.Duplicate, source.Duplicate) + + path[i][j] = leaf + } + } + return BUMP{ + BlockHeight: bcBump.BlockHeight, + Path: path, + } +} + func sortAndAddToPath(txIDPath1 BUMPLeaf, offset uint64, txIDPath2 BUMPLeaf, pairOffset uint64) [][]BUMPLeaf { path := make([][]BUMPLeaf, 0) txIDPath := make([]BUMPLeaf, 2) diff --git a/model_bump_test.go b/model_bump_test.go index 6d435bd0..ea15b66e 100644 --- a/model_bump_test.go +++ b/model_bump_test.go @@ -734,10 +734,10 @@ func TestBUMPModel_CalculateMergedBUMPAndHex(t *testing.T) { "0e" + // 13 - tree height "02" + // nLeafs at this level "fd8004" + // offset - 1152 - "00" + // flags - data follows, not a cilent txid + "00" + // flags - data follows, not a client txid "a35764daec4a1cdec33d1108619109b00b9e37c04e9492a9bb875cc31dde4b4d" + // hash "fd8104" + // offset - 1153 - "02" + // flags - data follows, cilent txid + "02" + // flags - data follows, client txid "da148e7fde1906808a92e8d542cfc6591f697895fe3701a35613fecb3db63021" + // hash // ---------------------- // implied end of leaves at this height @@ -798,7 +798,7 @@ func TestBUMPModel_CalculateMergedBUMPAndHex(t *testing.T) { // when bumps := make([]BUMP, 0) for _, mp := range merkleProof { - bumps = append(bumps, MerkleProofToBUMP(&mp, 0)) + bumps = append(bumps, merkleProofToBUMP(&mp, 0)) } bump, err := CalculateMergedBUMP(bumps) actualHex := bump.Hex() @@ -810,8 +810,8 @@ func TestBUMPModel_CalculateMergedBUMPAndHex(t *testing.T) { }) } -// TestBUMPModel_MerkleProofToBUMP will test the method MerkleProofToBUMP() -func TestBUMPModel_MerkleProofToBUMP(t *testing.T) { +// TestBUMPModel_merkleProofToBUMP will test the method merkleProofToBUMP() +func TestBUMPModel_merkleProofToBUMP(t *testing.T) { t.Parallel() t.Run("Valid Merkle Proof #1", func(t *testing.T) { @@ -842,7 +842,7 @@ func TestBUMPModel_MerkleProofToBUMP(t *testing.T) { } // when - actualBUMP := MerkleProofToBUMP(&mp, blockHeight) + actualBUMP := merkleProofToBUMP(&mp, blockHeight) // then assert.Equal(t, expectedBUMP, actualBUMP) @@ -879,7 +879,7 @@ func TestBUMPModel_MerkleProofToBUMP(t *testing.T) { } // when - actualBUMP := MerkleProofToBUMP(&mp, blockHeight) + actualBUMP := merkleProofToBUMP(&mp, blockHeight) // then assert.Equal(t, expectedBUMP, actualBUMP) @@ -916,7 +916,7 @@ func TestBUMPModel_MerkleProofToBUMP(t *testing.T) { } // when - actualBUMP := MerkleProofToBUMP(&mp, blockHeight) + actualBUMP := merkleProofToBUMP(&mp, blockHeight) // then assert.Equal(t, expectedBUMP, actualBUMP) @@ -925,7 +925,7 @@ func TestBUMPModel_MerkleProofToBUMP(t *testing.T) { t.Run("Empty Merkle Proof", func(t *testing.T) { blockHeight := uint64(0) mp := bc.MerkleProof{} - actualBUMP := MerkleProofToBUMP(&mp, blockHeight) + actualBUMP := merkleProofToBUMP(&mp, blockHeight) assert.Equal(t, BUMP{BlockHeight: blockHeight}, actualBUMP) }) } diff --git a/model_draft_transactions.go b/model_draft_transactions.go index 026fce6b..ada89589 100644 --- a/model_draft_transactions.go +++ b/model_draft_transactions.go @@ -9,7 +9,6 @@ import ( "math/big" "time" - "github.com/BuxOrg/bux/chainstate" "github.com/BuxOrg/bux/utils" "github.com/bitcoinschema/go-bitcoin/v2" "github.com/libsv/go-bk/bec" @@ -62,14 +61,8 @@ func newDraftTransaction(rawXpubKey string, config *TransactionConfig, opts ...M ), } - // Set the fee (if not found) (if chainstate is loaded, use the first miner) - // todo: make this more intelligent or allow the config to dictate the miner selection if config.FeeUnit == nil { - if c := draft.Client(); c != nil { - draft.Configuration.FeeUnit = c.Chainstate().FeeUnit() - } else { - draft.Configuration.FeeUnit = chainstate.DefaultFee - } + draft.Configuration.FeeUnit = draft.Client().Chainstate().FeeUnit() } return draft } diff --git a/model_draft_transactions_test.go b/model_draft_transactions_test.go index 2f64f914..e4e1e802 100644 --- a/model_draft_transactions_test.go +++ b/model_draft_transactions_test.go @@ -45,7 +45,9 @@ func TestDraftTransaction_newDraftTransaction(t *testing.T) { t.Run("valid config", func(t *testing.T) { expires := time.Now().UTC().Add(defaultDraftTxExpiresIn) draftTx := newDraftTransaction( - testXPub, &TransactionConfig{}, New(), + testXPub, &TransactionConfig{ + FeeUnit: chainstate.MockDefaultFee, + }, New(), ) require.NotNil(t, draftTx) assert.NotEqual(t, "", draftTx.ID) @@ -53,7 +55,7 @@ func TestDraftTransaction_newDraftTransaction(t *testing.T) { assert.WithinDurationf(t, expires, draftTx.ExpiresAt, 1*time.Second, "within 1 second") assert.Equal(t, DraftStatusDraft, draftTx.Status) assert.Equal(t, testXPubID, draftTx.XpubID) - assert.Equal(t, chainstate.DefaultFee, draftTx.Configuration.FeeUnit) + assert.Equal(t, *chainstate.MockDefaultFee, *draftTx.Configuration.FeeUnit) }) } @@ -62,7 +64,9 @@ func TestDraftTransaction_GetModelName(t *testing.T) { t.Parallel() t.Run("model name", func(t *testing.T) { - draftTx := newDraftTransaction(testXPub, &TransactionConfig{}, New()) + draftTx := newDraftTransaction(testXPub, &TransactionConfig{ + FeeUnit: chainstate.MockDefaultFee, + }, New()) require.NotNil(t, draftTx) assert.Equal(t, ModelDraftTransaction.String(), draftTx.GetModelName()) }) @@ -76,6 +80,7 @@ func TestDraftTransaction_getOutputSatoshis(t *testing.T) { ChangeDestinations: []*Destination{{ LockingScript: testLockingScript, }}, + FeeUnit: chainstate.MockDefaultFee, }, ) changSatoshis, err := draftTx.getChangeSatoshis(1000000) @@ -91,6 +96,7 @@ func TestDraftTransaction_getOutputSatoshis(t *testing.T) { ChangeDestinations: []*Destination{{ LockingScript: testLockingScript, }}, + FeeUnit: chainstate.MockDefaultFee, }, ) changSatoshis, err := draftTx.getChangeSatoshis(1000000) @@ -107,6 +113,7 @@ func TestDraftTransaction_getOutputSatoshis(t *testing.T) { }, { LockingScript: testTxInScriptPubKey, }}, + FeeUnit: chainstate.MockDefaultFee, }, ) changSatoshis, err := draftTx.getChangeSatoshis(1000001) @@ -127,6 +134,7 @@ func TestDraftTransaction_getOutputSatoshis(t *testing.T) { }, { LockingScript: testTxScriptPubKey1, }}, + FeeUnit: chainstate.MockDefaultFee, }, ) satoshis := uint64(1000001) @@ -272,7 +280,7 @@ func TestDraftTransaction_createTransaction(t *testing.T) { assert.Equal(t, uint64(98988), draftTransaction.Configuration.ChangeSatoshis) assert.Equal(t, uint64(12), draftTransaction.Configuration.Fee) - assert.Equal(t, chainstate.DefaultFee, draftTransaction.Configuration.FeeUnit) + assert.Equal(t, *chainstate.MockDefaultFee, *draftTransaction.Configuration.FeeUnit) assert.Equal(t, 1, len(draftTransaction.Configuration.Inputs)) assert.Equal(t, testLockingScript, draftTransaction.Configuration.Inputs[0].ScriptPubKey) @@ -1398,7 +1406,7 @@ func TestDraftTransaction_estimateFees(t *testing.T) { b, _ := json.Marshal(in["feeUnit"]) _ = json.Unmarshal(b, &feeUnit) } else { - feeUnit = chainstate.DefaultFee + feeUnit = chainstate.MockDefaultFee } draftTransaction, tx, err2 := createDraftTransactionFromHex(in["hex"].(string), in["inputs"].([]interface{}), feeUnit) require.NoError(t, err2) diff --git a/model_ids.go b/model_ids.go index 1816f6c4..db47f2b8 100644 --- a/model_ids.go +++ b/model_ids.go @@ -3,17 +3,14 @@ package bux import ( "database/sql/driver" "encoding/json" - "fmt" "github.com/99designs/gqlgen/graphql" + "github.com/BuxOrg/bux/utils" "github.com/mrz1836/go-datastore" "gorm.io/gorm" "gorm.io/gorm/schema" ) -// ValueTypeString is the value type "string" -const ValueTypeString = "string" - // IDs are string ids saved as an array type IDs []string @@ -28,12 +25,9 @@ func (i *IDs) Scan(value interface{}) error { return nil } - xType := fmt.Sprintf("%T", value) - var byteValue []byte - if xType == ValueTypeString { - byteValue = []byte(value.(string)) - } else { - byteValue = value.([]byte) + byteValue, err := utils.ToByteArray(value) + if err != nil { + return nil } return json.Unmarshal(byteValue, &i) @@ -52,14 +46,6 @@ func (i IDs) Value() (driver.Value, error) { return string(marshal), nil } -// MarshalIDs will unmarshal the custom type -func MarshalIDs(i IDs) graphql.Marshaler { - if i == nil { - return graphql.Null - } - return graphql.MarshalAny(i) -} - // GormDBDataType the gorm data type for metadata func (IDs) GormDBDataType(db *gorm.DB, _ *schema.Field) string { if db.Dialector.Name() == datastore.Postgres { diff --git a/model_ids_test.go b/model_ids_test.go index 452ae72a..13490cd8 100644 --- a/model_ids_test.go +++ b/model_ids_test.go @@ -1,10 +1,8 @@ package bux import ( - "bytes" "testing" - "github.com/99designs/gqlgen/graphql" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -78,33 +76,6 @@ func TestIDs_Value(t *testing.T) { }) } -// TestMarshalIDs will test the method MarshalIDs() -func TestMarshalIDs(t *testing.T) { - t.Parallel() - - t.Run("nil", func(t *testing.T) { - writer := MarshalIDs(nil) - require.NotNil(t, writer) - assert.IsType(t, graphql.Null, writer) - }) - - t.Run("empty object", func(t *testing.T) { - writer := MarshalIDs(IDs{}) - require.NotNil(t, writer) - b := bytes.NewBufferString("") - writer.MarshalGQL(b) - assert.Equal(t, "[]\n", b.String()) - }) - - t.Run("map present", func(t *testing.T) { - writer := MarshalIDs(IDs{"test1"}) - require.NotNil(t, writer) - b := bytes.NewBufferString("") - writer.MarshalGQL(b) - assert.Equal(t, "[\"test1\"]\n", b.String()) - }) -} - // TestUnmarshalIDs will test the method UnmarshalIDs() func TestUnmarshalIDs(t *testing.T) { diff --git a/model_incoming_transactions.go b/model_incoming_transactions.go index 56575420..ab81b763 100644 --- a/model_incoming_transactions.go +++ b/model_incoming_transactions.go @@ -53,23 +53,6 @@ func newIncomingTransaction(hex string, opts ...ModelOps) (*IncomingTransaction, return tx, nil } -// getIncomingTransactionByID will get the incoming transactions to process -func getIncomingTransactionByID(ctx context.Context, id string, opts ...ModelOps) (*IncomingTransaction, error) { - // Construct an empty tx - tx := emptyIncomingTx(opts...) - tx.ID = id - - // Get the record - if err := Get(ctx, tx, nil, false, defaultDatabaseReadTimeout, false); err != nil { - if errors.Is(err, datastore.ErrNoResults) { - return nil, nil - } - return nil, err - } - - return tx, nil -} - // getIncomingTransactionsToProcess will get the incoming transactions to process func getIncomingTransactionsToProcess(ctx context.Context, queryParams *datastore.QueryParams, opts ...ModelOps, @@ -210,7 +193,8 @@ func (m *IncomingTransaction) Migrate(client datastore.ClientInterface) error { // processIncomingTransactions will process incoming transaction records func processIncomingTransactions(ctx context.Context, logClient *zerolog.Logger, maxTransactions int, - opts ...ModelOps) error { + opts ...ModelOps, +) error { queryParams := &datastore.QueryParams{Page: 1, PageSize: maxTransactions} // Get x records: @@ -241,8 +225,8 @@ func processIncomingTransactions(ctx context.Context, logClient *zerolog.Logger, // processIncomingTransaction will process the incoming transaction record into a transaction, or save the failure func processIncomingTransaction(ctx context.Context, logClient *zerolog.Logger, - incomingTx *IncomingTransaction) error { - + incomingTx *IncomingTransaction, +) error { if logClient == nil { logClient = incomingTx.client.Logger() } diff --git a/model_metadata.go b/model_metadata.go index 11f6fb71..af2091ee 100644 --- a/model_metadata.go +++ b/model_metadata.go @@ -4,9 +4,9 @@ import ( "bytes" "database/sql/driver" "encoding/json" - "fmt" "github.com/99designs/gqlgen/graphql" + "github.com/BuxOrg/bux/utils" "github.com/mrz1836/go-datastore" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/bsontype" @@ -38,14 +38,8 @@ func (m *Metadata) Scan(value interface{}) error { return nil } - xType := fmt.Sprintf("%T", value) - var byteValue []byte - if xType == ValueTypeString { - byteValue = []byte(value.(string)) - } else { - byteValue = value.([]byte) - } - if bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { + byteValue, err := utils.ToByteArray(value) + if err != nil || bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { return nil } @@ -79,14 +73,8 @@ func (x *XpubMetadata) Scan(value interface{}) error { return nil } - xType := fmt.Sprintf("%T", value) - var byteValue []byte - if xType == ValueTypeString { - byteValue = []byte(value.(string)) - } else { - byteValue = value.([]byte) - } - if bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { + byteValue, err := utils.ToByteArray(value) + if err != nil || bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { return nil } diff --git a/model_save.go b/model_save.go index 2f6ad978..ea0ed368 100644 --- a/model_save.go +++ b/model_save.go @@ -10,7 +10,6 @@ import ( // Save will save the model(s) into the Datastore func Save(ctx context.Context, model ModelInterface) (err error) { - // Check for a client c := model.Client() if c == nil { @@ -26,16 +25,9 @@ func Save(ctx context.Context, model ModelInterface) (err error) { // @siggi: we need this to be in a callback context for Mongo // NOTE: a DB error is not being returned from here return ds.NewTx(ctx, func(tx *datastore.Transaction) (err error) { - - // Fire the before hooks (parent model) - if model.IsNew() { - if err = model.BeforeCreating(ctx); err != nil { - return - } - } else { - if err = model.BeforeUpdating(ctx); err != nil { - return - } + parentBeforeHook := _beforeHook(model) + if err = parentBeforeHook(ctx); err != nil { + return _closeTxWithError(tx, err) } // Set the record's timestamps @@ -47,16 +39,11 @@ func Save(ctx context.Context, model ModelInterface) (err error) { // Add any child models (fire before hooks) if children := model.ChildModels(); len(children) > 0 { for _, child := range children { - if child.IsNew() { - if err = child.BeforeCreating(ctx); err != nil { - return - } - } else { - if err = child.BeforeUpdating(ctx); err != nil { - return - } - } + childBeforeHook := _beforeHook(child) + if err = childBeforeHook(ctx); err != nil { + return _closeTxWithError(tx, err) + } // Set the record's timestamps child.SetRecordTime(child.IsNew()) } @@ -76,7 +63,7 @@ func Save(ctx context.Context, model ModelInterface) (err error) { if err = modelsToSave[index].Client().Datastore().SaveModel( ctx, modelsToSave[index], tx, modelsToSave[index].IsNew(), false, ); err != nil { - return + return _closeTxWithError(tx, err) } } @@ -104,7 +91,6 @@ func Save(ctx context.Context, model ModelInterface) (err error) { err = errors.Wrap(err, afterErr.Error()) } } - // modelToSave.NotNew() // NOTE: moved to above from here } return @@ -129,3 +115,31 @@ func saveToCache(ctx context.Context, keys []string, model ModelInterface, ttl t } return nil } + +// _closeTxWithError will close the transaction with the given error +// It's crucial to run this rollback to prevent hanging db connections. +func _closeTxWithError(tx *datastore.Transaction, baseError error) error { + if tx == nil { + if baseError != nil { + return baseError + } + return errors.New("transaction is nil during rollback") + } + if err := tx.Rollback(); err != nil { + if baseError != nil { + return errors.Wrap(baseError, err.Error()) + } + return err + } + if baseError != nil { + return baseError + } + return errors.New("closing transaction with error") +} + +func _beforeHook(model ModelInterface) func(context.Context) error { + if model.IsNew() { + return model.BeforeCreating + } + return model.BeforeUpdating +} diff --git a/model_sync_config.go b/model_sync_config.go index a3863ea5..2dad83c9 100644 --- a/model_sync_config.go +++ b/model_sync_config.go @@ -4,7 +4,8 @@ import ( "bytes" "database/sql/driver" "encoding/json" - "fmt" + + "github.com/BuxOrg/bux/utils" ) // SyncConfig is the configuration used for syncing a transaction (on-chain) @@ -28,14 +29,8 @@ func (t *SyncConfig) Scan(value interface{}) error { return nil } - xType := fmt.Sprintf("%T", value) - var byteValue []byte - if xType == ValueTypeString { - byteValue = []byte(value.(string)) - } else { - byteValue = value.([]byte) - } - if bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { + byteValue, err := utils.ToByteArray(value) + if err != nil || bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { return nil } diff --git a/model_sync_results.go b/model_sync_results.go index 62e3fa96..e0cf9973 100644 --- a/model_sync_results.go +++ b/model_sync_results.go @@ -4,8 +4,9 @@ import ( "bytes" "database/sql/driver" "encoding/json" - "fmt" "time" + + "github.com/BuxOrg/bux/utils" ) // SyncResults is the results from all sync attempts (broadcast or sync) @@ -35,14 +36,8 @@ func (t *SyncResults) Scan(value interface{}) error { return nil } - xType := fmt.Sprintf("%T", value) - var byteValue []byte - if xType == ValueTypeString { - byteValue = []byte(value.(string)) - } else { - byteValue = value.([]byte) - } - if bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { + byteValue, err := utils.ToByteArray(value) + if err != nil || bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { return nil } diff --git a/model_sync_status.go b/model_sync_status.go index 0fc7dc7b..523b6b84 100644 --- a/model_sync_status.go +++ b/model_sync_status.go @@ -2,7 +2,8 @@ package bux import ( "database/sql/driver" - "fmt" + + "github.com/BuxOrg/bux/utils" ) // SyncStatus sync status @@ -33,12 +34,9 @@ const ( // Scan will scan the value into Struct, implements sql.Scanner interface func (t *SyncStatus) Scan(value interface{}) error { - xType := fmt.Sprintf("%T", value) - var stringValue string - if xType == ValueTypeString { - stringValue = value.(string) - } else { - stringValue = string(value.([]byte)) + stringValue, err := utils.StrOrBytesToString(value) + if err != nil { + return nil } switch stringValue { diff --git a/model_sync_transactions.go b/model_sync_transactions.go index e6e900e1..df9e9823 100644 --- a/model_sync_transactions.go +++ b/model_sync_transactions.go @@ -67,13 +67,6 @@ func (m *SyncTransaction) GetID() string { return m.ID } -// isSkipped will return true if Broadcasting, P2P and SyncOnChain are all skipped -func (m *SyncTransaction) isSkipped() bool { - return m.BroadcastStatus == SyncStatusSkipped && - m.SyncStatus == SyncStatusSkipped && - m.P2PStatus == SyncStatusSkipped -} - // GetModelName will get the name of the current model func (m *SyncTransaction) GetModelName() string { return ModelSyncTransaction.String() diff --git a/model_transaction_config.go b/model_transaction_config.go index df0d0c95..5fe3e2b0 100644 --- a/model_transaction_config.go +++ b/model_transaction_config.go @@ -143,14 +143,8 @@ func (t *TransactionConfig) Scan(value interface{}) error { return nil } - xType := fmt.Sprintf("%T", value) - var byteValue []byte - if xType == ValueTypeString { - byteValue = []byte(value.(string)) - } else { - byteValue = value.([]byte) - } - if bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { + byteValue, err := utils.ToByteArray(value) + if err != nil || bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { return nil } @@ -169,8 +163,8 @@ func (t TransactionConfig) Value() (driver.Value, error) { // processOutput will inspect the output to determine how to process func (t *TransactionOutput) processOutput(ctx context.Context, cacheStore cachestore.ClientInterface, - paymailClient paymail.ClientInterface, defaultFromSender, defaultNote string, checkSatoshis bool) error { - + paymailClient paymail.ClientInterface, defaultFromSender, defaultNote string, checkSatoshis bool, +) error { // Convert known handle formats ($handcash or 1relayx) if strings.Contains(t.To, handleHandcashPrefix) || (len(t.To) < handleMaxLength && len(t.To) > 1 && t.To[:1] == handleRelayPrefix) { @@ -204,8 +198,8 @@ func (t *TransactionOutput) processOutput(ctx context.Context, cacheStore caches // processPaymailOutput will detect how to process the Paymail output given func (t *TransactionOutput) processPaymailOutput(ctx context.Context, cacheStore cachestore.ClientInterface, - paymailClient paymail.ClientInterface, fromPaymail, defaultNote string) error { - + paymailClient paymail.ClientInterface, fromPaymail, defaultNote string, +) error { // Standardize the paymail address (break into parts) alias, domain, paymailAddress := paymail.SanitizePaymail(t.To) if len(paymailAddress) == 0 { @@ -251,8 +245,8 @@ func (t *TransactionOutput) processPaymailOutput(ctx context.Context, cacheStore // processPaymailViaAddressResolution will use a deprecated way to resolve a Paymail address func (t *TransactionOutput) processPaymailViaAddressResolution(ctx context.Context, cacheStore cachestore.ClientInterface, - paymailClient paymail.ClientInterface, capabilities *paymail.CapabilitiesPayload, defaultFromSender, defaultNote string) error { - + paymailClient paymail.ClientInterface, capabilities *paymail.CapabilitiesPayload, defaultFromSender, defaultNote string, +) error { // Requires a note value if len(t.PaymailP4.Note) == 0 { t.PaymailP4.Note = defaultNote @@ -291,7 +285,6 @@ func (t *TransactionOutput) processPaymailViaAddressResolution(ctx context.Conte // processPaymailViaP2P will process the output for P2P Paymail resolution func (t *TransactionOutput) processPaymailViaP2P(client paymail.ClientInterface, p2pDestinationURL, p2pSubmitTxURL string, fromPaymail string, format PaymailPayloadFormat) error { - // todo: this is a hack since paymail providers will complain if satoshis are empty (SendToAll has 0 satoshi) satoshis := t.Satoshis if satoshis <= 0 { @@ -338,7 +331,6 @@ func (t *TransactionOutput) processPaymailViaP2P(client paymail.ClientInterface, // processAddressOutput will process an output for a standard Bitcoin Address Transaction func (t *TransactionOutput) processAddressOutput() (err error) { - // Create the script from the Bitcoin address var s *bscript.Script if s, err = bscript.NewP2PKHFromAddress(t.To); err != nil { @@ -384,7 +376,6 @@ func (t *TransactionOutput) processScriptOutput() (err error) { // processOpReturnOutput will process an op_return output func (t *TransactionOutput) processOpReturnOutput() (err error) { - // Create the script from the Bitcoin address var script string if len(t.OpReturn.Hex) > 0 { diff --git a/model_transaction_config_test.go b/model_transaction_config_test.go index 07b48d67..684ec62f 100644 --- a/model_transaction_config_test.go +++ b/model_transaction_config_test.go @@ -47,7 +47,7 @@ var ( ChangeSatoshis: 124, ExpiresIn: defaultDraftTxExpiresIn, Fee: 12, - FeeUnit: chainstate.DefaultFee, + FeeUnit: chainstate.MockDefaultFee, Inputs: nil, Outputs: nil, } diff --git a/model_transaction_status.go b/model_transaction_status.go index 2d49a426..e7abfa87 100644 --- a/model_transaction_status.go +++ b/model_transaction_status.go @@ -2,7 +2,8 @@ package bux import ( "database/sql/driver" - "fmt" + + "github.com/BuxOrg/bux/utils" ) // DraftStatus draft transaction status @@ -24,12 +25,9 @@ const ( // Scan will scan the value into Struct, implements sql.Scanner interface func (t *DraftStatus) Scan(value interface{}) error { - xType := fmt.Sprintf("%T", value) - var stringValue string - if xType == ValueTypeString { - stringValue = value.(string) - } else { - stringValue = string(value.([]byte)) + stringValue, err := utils.StrOrBytesToString(value) + if err != nil { + return nil } switch stringValue { diff --git a/model_transactions.go b/model_transactions.go index 7371b5c9..cb919cc1 100644 --- a/model_transactions.go +++ b/model_transactions.go @@ -54,6 +54,7 @@ type Transaction struct { XpubMetadata XpubMetadata `json:"-" toml:"xpub_metadata" gorm:"<-;type:json;xpub_id specific metadata" bson:"xpub_metadata,omitempty"` XpubOutputValue XpubOutputValue `json:"-" toml:"xpub_output_value" gorm:"<-;type:json;xpub_id specific value" bson:"xpub_output_value,omitempty"` BUMP BUMP `json:"bump" toml:"bump" yaml:"bump" gorm:"<-;type:text;comment:BSV Unified Merkle Path (BUMP) Format" bson:"bump,omitempty"` + TxStatus string `json:"txStatus" toml:"txStatus" yaml:"txStatus" gorm:"<-;type:varchar(64);comment:TxStatus retrieved from Arc API." bson:"txStatus,omitempty"` // Virtual Fields OutputValue int64 `json:"output_value" toml:"-" yaml:"-" gorm:"-" bson:"-,omitempty"` @@ -70,6 +71,7 @@ type Transaction struct { beforeCreateCalled bool `gorm:"-" bson:"-"` // Private information that the transaction lifecycle method BeforeCreate was already called } +// TransactionGetter interface for getting transactions by their IDs type TransactionGetter interface { GetTransactionsByIDs(ctx context.Context, txIDs []string) ([]*Transaction, error) } @@ -234,19 +236,23 @@ func (m *Transaction) getValues() (outputValue uint64, fee uint64) { return } -func (m *Transaction) isExternal() bool { - return m.draftTransaction == nil -} - func (m *Transaction) setChainInfo(txInfo *chainstate.TransactionInfo) { m.BlockHash = txInfo.BlockHash m.BlockHeight = uint64(txInfo.BlockHeight) + m.TxStatus = txInfo.TxStatus.String() m.setBUMP(txInfo) } +// Converts from bc.BUMP to our BUMP struct in Transaction model func (m *Transaction) setBUMP(txInfo *chainstate.TransactionInfo) { - bump := MerkleProofToBUMP(txInfo.MerkleProof, uint64(txInfo.BlockHeight)) - m.BUMP = bump + switch { + case txInfo.MerkleProof != nil: + m.BUMP = merkleProofToBUMP(txInfo.MerkleProof, uint64(txInfo.BlockHeight)) + case txInfo.BUMP != nil: + m.BUMP = bcBumpToBUMP(txInfo.BUMP) + default: + m.client.Logger().Error().Msg("No BUMP or MerkleProof found") + } } func (m *Transaction) isMined() bool { diff --git a/model_transactions_output.go b/model_transactions_output.go index f0d291b8..eb87faf7 100644 --- a/model_transactions_output.go +++ b/model_transactions_output.go @@ -4,8 +4,8 @@ import ( "bytes" "database/sql/driver" "encoding/json" - "fmt" + "github.com/BuxOrg/bux/utils" "github.com/mrz1836/go-datastore" "gorm.io/gorm" "gorm.io/gorm/schema" @@ -20,14 +20,8 @@ func (x *XpubOutputValue) Scan(value interface{}) error { return nil } - xType := fmt.Sprintf("%T", value) - var byteValue []byte - if xType == ValueTypeString { - byteValue = []byte(value.(string)) - } else { - byteValue = value.([]byte) - } - if bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { + byteValue, err := utils.ToByteArray(value) + if err != nil || bytes.Equal(byteValue, []byte("")) || bytes.Equal(byteValue, []byte("\"\"")) { return nil } diff --git a/model_transactions_test.go b/model_transactions_test.go index 09113837..d2bfcc94 100644 --- a/model_transactions_test.go +++ b/model_transactions_test.go @@ -220,10 +220,10 @@ func TestTransaction_BeforeCreating(t *testing.T) { transaction := emptyTx() opts := DefaultClientOpts(false, false) - client, err := NewClient(context.Background(), opts...) + client, _ := NewClient(context.Background(), opts...) transaction.client = client - err = transaction.BeforeCreating(context.Background()) + err := transaction.BeforeCreating(context.Background()) assert.Error(t, err) assert.ErrorIs(t, ErrMissingFieldHex, err) }) diff --git a/model_utxos.go b/model_utxos.go index 7c3af3a8..27f0c20f 100644 --- a/model_utxos.go +++ b/model_utxos.go @@ -58,8 +58,8 @@ func newUtxo(xPubID, txID, scriptPubKey string, index uint32, satoshis uint64, o // getSpendableUtxos get all spendable utxos by page / pageSize func getSpendableUtxos(ctx context.Context, xPubID, utxoType string, queryParams *datastore.QueryParams, //nolint:nolintlint,unparam // this param will be used - fromUtxos []*UtxoPointer, opts ...ModelOps) ([]*Utxo, error) { - + fromUtxos []*UtxoPointer, opts ...ModelOps, +) ([]*Utxo, error) { // Construct the conditions and results var models []Utxo conditions := map[string]interface{}{ @@ -145,8 +145,8 @@ func unReserveUtxos(ctx context.Context, xPubID, draftID string, opts ...ModelOp // reserveUtxos reserve utxos for the given draft ID and amount func reserveUtxos(ctx context.Context, xPubID, draftID string, - satoshis uint64, feePerByte float64, fromUtxos []*UtxoPointer, opts ...ModelOps) ([]*Utxo, error) { - + satoshis uint64, feePerByte float64, fromUtxos []*UtxoPointer, opts ...ModelOps, +) ([]*Utxo, error) { // Create base model m := NewBaseModel(ModelNameEmpty, opts...) @@ -166,7 +166,7 @@ func reserveUtxos(ctx context.Context, xPubID, draftID string, queryParams := &datastore.QueryParams{} if fromUtxos == nil { - // if we are not getting all utxos, paginate the retreival + // if we are not getting all utxos, paginate the retrieval queryParams.Page = 1 queryParams.PageSize = m.pageSize if queryParams.PageSize == 0 { @@ -258,8 +258,8 @@ func newUtxoFromTxID(txID string, index uint32, opts ...ModelOps) *Utxo { // getUtxos will get all the utxos with the given conditions func getUtxos(ctx context.Context, metadata *Metadata, conditions *map[string]interface{}, - queryParams *datastore.QueryParams, opts ...ModelOps) ([]*Utxo, error) { - + queryParams *datastore.QueryParams, opts ...ModelOps, +) ([]*Utxo, error) { modelItems := make([]*Utxo, 0) if err := getModelsByConditions(ctx, ModelUtxo, &modelItems, metadata, conditions, queryParams, opts...); err != nil { return nil, err @@ -270,14 +270,15 @@ func getUtxos(ctx context.Context, metadata *Metadata, conditions *map[string]in // getAccessKeysCount will get a count of all the utxos with the given conditions func getUtxosCount(ctx context.Context, metadata *Metadata, conditions *map[string]interface{}, - opts ...ModelOps) (int64, error) { + opts ...ModelOps, +) (int64, error) { return getModelCountByConditions(ctx, ModelUtxo, Utxo{}, metadata, conditions, opts...) } // getTransactionsAggregate will get a count of all transactions per aggregate column with the given conditions func getUtxosAggregate(ctx context.Context, metadata *Metadata, conditions *map[string]interface{}, - aggregateColumn string, opts ...ModelOps) (map[string]interface{}, error) { - + aggregateColumn string, opts ...ModelOps, +) (map[string]interface{}, error) { modelItems := make([]*Utxo, 0) results, err := getModelsAggregateByConditions( ctx, ModelUtxo, &modelItems, metadata, conditions, aggregateColumn, opts..., @@ -291,9 +292,9 @@ func getUtxosAggregate(ctx context.Context, metadata *Metadata, conditions *map[ // getUtxosByXpubID will return utxos by a given xPub ID func getUtxosByXpubID(ctx context.Context, xPubID string, metadata *Metadata, conditions *map[string]interface{}, - queryParams *datastore.QueryParams, opts ...ModelOps) ([]*Utxo, error) { - - var dbConditions = map[string]interface{}{} + queryParams *datastore.QueryParams, opts ...ModelOps, +) ([]*Utxo, error) { + dbConditions := map[string]interface{}{} if conditions != nil { dbConditions = *conditions } @@ -308,8 +309,8 @@ func getUtxosByXpubID(ctx context.Context, xPubID string, metadata *Metadata, co // getUtxosByDraftID will return the utxos by a given draft id func getUtxosByDraftID(ctx context.Context, draftID string, - queryParams *datastore.QueryParams, opts ...ModelOps) ([]*Utxo, error) { - + queryParams *datastore.QueryParams, opts ...ModelOps, +) ([]*Utxo, error) { conditions := map[string]interface{}{ draftIDField: draftID, } @@ -318,8 +319,8 @@ func getUtxosByDraftID(ctx context.Context, draftID string, // getUtxosByConditions will get utxos by given conditions func getUtxosByConditions(ctx context.Context, conditions map[string]interface{}, - queryParams *datastore.QueryParams, opts ...ModelOps) ([]*Utxo, error) { - + queryParams *datastore.QueryParams, opts ...ModelOps, +) ([]*Utxo, error) { var models []Utxo if err := getModels( ctx, NewBaseModel( @@ -343,7 +344,6 @@ func getUtxosByConditions(ctx context.Context, conditions map[string]interface{} // getUtxo will get the utxo with the given conditions func getUtxo(ctx context.Context, txID string, index uint32, opts ...ModelOps) (*Utxo, error) { - // Start the new model utxo := newUtxoFromTxID(txID, index, opts...) @@ -389,7 +389,6 @@ func (m *Utxo) GetID() string { // BeforeCreating will fire before the model is being inserted into the Datastore func (m *Utxo) BeforeCreating(_ context.Context) error { - m.Client().Logger().Debug(). Str("utxoID", m.ID). Msgf("starting: %s BeforeCreate hook...", m.Name()) @@ -447,7 +446,6 @@ func (m *Utxo) GenerateID() string { // Migrate model specific migration on startup func (m *Utxo) Migrate(client datastore.ClientInterface) error { - tableName := client.GetTableName(tableUTXOs) if client.Engine() == datastore.MySQL { if err := m.migrateMySQL(client, tableName); err != nil { diff --git a/model_xpubs_test.go b/model_xpubs_test.go index 5edfa869..e132d9ce 100644 --- a/model_xpubs_test.go +++ b/model_xpubs_test.go @@ -185,10 +185,10 @@ func TestXpub_BeforeCreating(t *testing.T) { require.NotNil(t, xPub) opts := DefaultClientOpts(false, false) - client, err := NewClient(context.Background(), opts...) + client, _ := NewClient(context.Background(), opts...) xPub.client = client - err = xPub.BeforeCreating(context.Background()) + err := xPub.BeforeCreating(context.Background()) require.NoError(t, err) require.NotNil(t, xPub) }) @@ -209,10 +209,10 @@ func TestXpub_BeforeCreating(t *testing.T) { require.NotNil(t, xPub) opts := DefaultClientOpts(false, false) - client, err := NewClient(context.Background(), opts...) + client, _ := NewClient(context.Background(), opts...) xPub.client = client - err = xPub.BeforeCreating(context.Background()) + err := xPub.BeforeCreating(context.Background()) assert.Error(t, err) assert.EqualError(t, err, "xpub is an invalid length") }) @@ -227,10 +227,10 @@ func TestXpub_AfterCreated(t *testing.T) { require.NotNil(t, xPub) opts := DefaultClientOpts(false, false) - client, err := NewClient(context.Background(), opts...) + client, _ := NewClient(context.Background(), opts...) xPub.client = client - err = xPub.BeforeCreating(context.Background()) + err := xPub.BeforeCreating(context.Background()) require.NoError(t, err) require.NotNil(t, xPub) diff --git a/paymail_service_provider.go b/paymail_service_provider.go index 56eca032..5c8bd1b7 100644 --- a/paymail_service_provider.go +++ b/paymail_service_provider.go @@ -150,8 +150,8 @@ func (p *PaymailDefaultServiceProvider) CreateP2PDestinationResponse( // RecordTransaction will record the transaction // TODO: rename to HandleReceivedP2pTransaction func (p *PaymailDefaultServiceProvider) RecordTransaction(ctx context.Context, - p2pTx *paymail.P2PTransaction, requestMetadata *server.RequestMetadata) (*paymail.P2PTransactionPayload, error) { - + p2pTx *paymail.P2PTransaction, requestMetadata *server.RequestMetadata, +) (*paymail.P2PTransactionPayload, error) { // Create the metadata metadata := p.createMetadata(requestMetadata, "HandleReceivedP2pTransaction") metadata[p2pMetadataField] = p2pTx.MetaData @@ -338,7 +338,7 @@ func saveBEEFTxInputs(ctx context.Context, c ClientInterface, dBeef *beef.Decode } func getInputsWhichAreNotInDb(c ClientInterface, dBeef *beef.DecodedBEEF) ([]*beef.TxData, error) { - var txIDs []string + txIDs := make([]string, 0, len(dBeef.Transactions)) for _, tx := range dBeef.Transactions { txIDs = append(txIDs, tx.GetTxID()) } @@ -369,12 +369,12 @@ func getInputsWhichAreNotInDb(c ClientInterface, dBeef *beef.DecodedBEEF) ([]*be return txs, nil } -func getBump(bumpIndx int, bumps beef.BUMPs) (*BUMP, error) { - if bumpIndx > len(bumps) { +func getBump(bumpIndex int, bumps beef.BUMPs) (*BUMP, error) { + if bumpIndex > len(bumps) { return nil, fmt.Errorf("error in getBump: bump index exceeds bumps length") } - bump := bumps[bumpIndx] + bump := bumps[bumpIndex] paths := make([][]BUMPLeaf, 0) for _, path := range bump.Path { diff --git a/record_tx_strategy_external_incoming_tx.go b/record_tx_strategy_external_incoming_tx.go index 57a77a8f..b4b68b65 100644 --- a/record_tx_strategy_external_incoming_tx.go +++ b/record_tx_strategy_external_incoming_tx.go @@ -92,7 +92,7 @@ func _addTxToCheck(ctx context.Context, tx *externalIncomingTx, c ClientInterfac Msg("start ITC") if err = incomingTx.Save(ctx); err != nil { - return nil, fmt.Errorf("addind new IncomingTx to check queue failed. Reason: %w", err) + return nil, fmt.Errorf("adding new IncomingTx to check queue failed. Reason: %w", err) } result := incomingTx.toTransactionDto() @@ -161,7 +161,7 @@ func _externalIncomingBroadcast(ctx context.Context, logger *zerolog.Logger, tx Str("txID", tx.ID). Msgf("broadcasting failed, next try will be handled by task manager. Reason: %s", err) - // ignore error, transaction will be broadcaset in a cron task + // ignore error, transaction will be broadcasted in a cron task return nil } diff --git a/record_tx_strategy_internal_incoming_tx.go b/record_tx_strategy_internal_incoming_tx.go index 99b212e5..20ecf12d 100644 --- a/record_tx_strategy_internal_incoming_tx.go +++ b/record_tx_strategy_internal_incoming_tx.go @@ -92,7 +92,7 @@ func _internalIncomingBroadcast(ctx context.Context, logger *zerolog.Logger, tra Str("txID", transaction.ID). Msgf("broadcasting failed, next try will be handled by task manager. Reason: %s", err) - // ignore broadcast error - will be repeted by task manager + // ignore broadcast error - will be repeated by task manager return nil } diff --git a/sync_tx_repository.go b/sync_tx_repository.go index a1eaa4d4..8532521e 100644 --- a/sync_tx_repository.go +++ b/sync_tx_repository.go @@ -100,24 +100,6 @@ func getTransactionsToSync(ctx context.Context, queryParams *datastore.QueryPara return txs, nil } -// getTransactionsToNotifyP2P will get the sync transactions to notify p2p paymail providers -func getTransactionsToNotifyP2P(ctx context.Context, queryParams *datastore.QueryParams, - opts ...ModelOps, -) ([]*SyncTransaction, error) { - // Get the records by status - txs, err := _getSyncTransactionsByConditions( - ctx, - map[string]interface{}{ - p2pStatusField: SyncStatusReady.String(), - }, - queryParams, opts..., - ) - if err != nil { - return nil, err - } - return txs, nil -} - /*** /public unexported funcs ***/ // getTransactionsToSync will get the sync transactions to sync diff --git a/tx_repository.go b/tx_repository.go index c49bd468..b0a89bac 100644 --- a/tx_repository.go +++ b/tx_repository.go @@ -199,63 +199,6 @@ func getTransactionsCountInternal(ctx context.Context, conditions map[string]int return count, nil } -// getTransactionsByConditions will get the sync transactions to migrate -func getTransactionsByConditions(ctx context.Context, conditions map[string]interface{}, - queryParams *datastore.QueryParams, opts ...ModelOps, -) ([]*Transaction, error) { - if queryParams == nil { - queryParams = &datastore.QueryParams{ - OrderByField: createdAtField, - SortDirection: datastore.SortAsc, - } - } else if queryParams.OrderByField == "" || queryParams.SortDirection == "" { - queryParams.OrderByField = createdAtField - queryParams.SortDirection = datastore.SortAsc - } - - // Get the records - var models []Transaction - if err := getModels( - ctx, NewBaseModel(ModelNameEmpty, opts...).Client().Datastore(), - &models, conditions, queryParams, defaultDatabaseReadTimeout, - ); err != nil { - if errors.Is(err, datastore.ErrNoResults) { - return nil, nil - } - return nil, err - } - - // Loop and enrich - txs := make([]*Transaction, 0) - for index := range models { - models[index].enrich(ModelTransaction, opts...) - txs = append(txs, &models[index]) - } - - return txs, nil -} - -// getTransactionsToMigrateMerklePath will get the transactions where bump should be calculated -func getTransactionsToCalculateBUMP(ctx context.Context, queryParams *datastore.QueryParams, - opts ...ModelOps, -) ([]*Transaction, error) { - // Get the records by status - txs, err := getTransactionsByConditions( - ctx, - map[string]interface{}{ - bumpField: nil, - merkleProofField: map[string]interface{}{ - "$exists": true, - }, - }, - queryParams, opts..., - ) - if err != nil { - return nil, err - } - return txs, nil -} - func getTransactionByHex(ctx context.Context, hex string, opts ...ModelOps) (*Transaction, error) { btTx, err := bt.NewTxFromString(hex) if err != nil { diff --git a/utils/byte_array.go b/utils/byte_array.go new file mode 100644 index 00000000..03eeaddd --- /dev/null +++ b/utils/byte_array.go @@ -0,0 +1,27 @@ +package utils + +import "fmt" + +// ToByteArray converts string or []byte to byte array or returns an error +func ToByteArray(value interface{}) ([]byte, error) { + switch typedValue := value.(type) { + case []byte: + return typedValue, nil + case string: + return []byte(typedValue), nil + default: + return nil, fmt.Errorf("unsupported type: %T", value) + } +} + +// StrOrBytesToString converts string or []byte to string or returns an error +func StrOrBytesToString(value interface{}) (string, error) { + switch typedValue := value.(type) { + case []byte: + return string(typedValue), nil + case string: + return typedValue, nil + default: + return "", fmt.Errorf("unsupported type: %T", value) + } +} diff --git a/utils/byte_array_test.go b/utils/byte_array_test.go new file mode 100644 index 00000000..dc25607e --- /dev/null +++ b/utils/byte_array_test.go @@ -0,0 +1,57 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToByteArray(t *testing.T) { + t.Run("should convert string to byte array", func(t *testing.T) { + expected := []byte("test") + actual, err := ToByteArray("test") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, expected, actual) + }) + + t.Run("should leave byte array", func(t *testing.T) { + expected := []byte("test") + actual, err := ToByteArray([]byte("test")) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, expected, actual) + }) + + t.Run("should return error for unsupported type", func(t *testing.T) { + _, err := ToByteArray(1) + assert.EqualError(t, err, "unsupported type: int") + }) +} + +func TestStrOrBytesToString(t *testing.T) { + t.Run("should convert byte array to string", func(t *testing.T) { + expected := "test" + actual, err := StrOrBytesToString([]byte("test")) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, expected, actual) + }) + + t.Run("should leave string", func(t *testing.T) { + expected := "test" + actual, err := StrOrBytesToString("test") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, expected, actual) + }) + + t.Run("should return error for unsupported type", func(t *testing.T) { + _, err := StrOrBytesToString(1) + assert.EqualError(t, err, "unsupported type: int") + }) +} diff --git a/utils/fees.go b/utils/fees.go index 0d941eb7..9ac75229 100644 --- a/utils/fees.go +++ b/utils/fees.go @@ -2,6 +2,7 @@ package utils import ( "encoding/hex" + "fmt" "github.com/libsv/go-bt/v2" ) @@ -9,6 +10,52 @@ import ( // FeeUnit fee unit imported from go-bt/v2 type FeeUnit bt.FeeUnit +// IsLowerThan compare two fee units +func (f *FeeUnit) IsLowerThan(other *FeeUnit) bool { + return float64(f.Satoshis)/float64(f.Bytes) < float64(other.Satoshis)/float64(other.Bytes) +} + +// String returns the fee unit as a string +func (f *FeeUnit) String() string { + return fmt.Sprintf("FeeUnit(%d satoshis / %d bytes)", f.Satoshis, f.Bytes) +} + +// IsZero returns true if the fee unit suggest no fees (free) +func (f *FeeUnit) IsZero() bool { + return f.Satoshis == 0 +} + +// IsValid returns true if the Bytes in fee are greater than 0 +func (f *FeeUnit) IsValid() bool { + return f.Bytes > 0 +} + +// ValidFees filters out invalid fees from a list of fee units +func ValidFees(feeUnits []FeeUnit) []FeeUnit { + validFees := []FeeUnit{} + for _, fee := range feeUnits { + if fee.IsValid() { + validFees = append(validFees, fee) + } + } + return validFees +} + +// LowestFee get the lowest fee from a list of fee units, if defaultValue exists and none is found, return defaultValue +func LowestFee(feeUnits []FeeUnit, defaultValue *FeeUnit) *FeeUnit { + validFees := ValidFees(feeUnits) + if len(validFees) == 0 { + return defaultValue + } + minFee := validFees[0] + for i := 1; i < len(validFees); i++ { + if validFees[i].IsLowerThan(&minFee) { + minFee = validFees[i] + } + } + return &minFee +} + // GetInputSizeForType get an estimated size for the input based on the type func GetInputSizeForType(inputType string) uint64 { switch inputType { diff --git a/utils/fees_test.go b/utils/fees_test.go index 78958c28..2f6bf8d7 100644 --- a/utils/fees_test.go +++ b/utils/fees_test.go @@ -31,3 +31,91 @@ func TestGetOutputSizeForType(t *testing.T) { assert.Equal(t, uint64(500), GetOutputSize("")) }) } + +func TestIsLowerThan(t *testing.T) { + t.Run("same satoshis, different bytes", func(t *testing.T) { + one := FeeUnit{ + Satoshis: 1, + Bytes: 1000, + } + two := FeeUnit{ + Satoshis: 1, + Bytes: 20, + } + assert.True(t, one.IsLowerThan(&two)) + assert.False(t, two.IsLowerThan(&one)) + }) + t.Run("same bytes, different satoshis", func(t *testing.T) { + one := FeeUnit{ + Satoshis: 1, + Bytes: 20, + } + two := FeeUnit{ + Satoshis: 2, + Bytes: 20, + } + assert.True(t, one.IsLowerThan(&two)) + assert.False(t, two.IsLowerThan(&one)) + }) + + t.Run("zero as bytes in denominator", func(t *testing.T) { + one := FeeUnit{ + Satoshis: 1, + Bytes: 0, + } + two := FeeUnit{ + Satoshis: 2, + Bytes: 0, + } + assert.False(t, one.IsLowerThan(&two)) + assert.False(t, two.IsLowerThan(&one)) + }) +} + +func TestLowestFee(t *testing.T) { + initTest := func() (feeList []FeeUnit, defaultFee FeeUnit) { + feeList = []FeeUnit{ + { + Satoshis: 1, + Bytes: 20, + }, + { + Satoshis: 2, + Bytes: 20, + }, + { + Satoshis: 3, + Bytes: 20, + }, + } + defaultFee = FeeUnit{ + Satoshis: 4, + Bytes: 20, + } + return + } + + t.Run("lowest fee among feeList elements, despite defaultValue", func(t *testing.T) { + feeList, defaultFee := initTest() + defaultFee.Satoshis = 1 + defaultFee.Bytes = 50 + assert.Equal(t, feeList[0], *LowestFee(feeList, &defaultFee)) + }) + + t.Run("lowest fee as first value", func(t *testing.T) { + feeList, defaultFee := initTest() + assert.Equal(t, feeList[0], *LowestFee(feeList, &defaultFee)) + }) + + t.Run("lowest fee as middle value", func(t *testing.T) { + feeList, defaultFee := initTest() + feeList[1].Bytes = 50 + assert.Equal(t, feeList[1], *LowestFee(feeList, &defaultFee)) + }) + + t.Run("lowest fee as defaultValue", func(t *testing.T) { + _, defaultFee := initTest() + feeList := []FeeUnit{} + assert.Equal(t, defaultFee, *LowestFee(feeList, &defaultFee)) + }) +} diff --git a/utils/utils.go b/utils/utils.go index 6be76576..fef71ea3 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -94,3 +94,10 @@ func LittleEndianBytes64(value uint64, resultLength uint32) []byte { return buf } + +// SafeAssign - Assigns value (not pointer) the src to dest if src is not nil +func SafeAssign[T any](dest *T, src *T) { + if src != nil { + *dest = *src + } +}