From 9791c9c3d27efb50970c7c7f856739cbb037d490 Mon Sep 17 00:00:00 2001 From: jakubmkowalski Date: Tue, 13 Feb 2024 15:34:54 +0100 Subject: [PATCH 01/50] chore(BUX-586): renames bux-server to spv-wallet --- .github/CODE_STANDARDS.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 4 +- Makefile | 2 +- actions/access_keys/access_keys_test.go | 2 +- actions/access_keys/count.go | 10 +- actions/access_keys/create.go | 6 +- actions/access_keys/get.go | 6 +- actions/access_keys/revoke.go | 6 +- actions/access_keys/routes.go | 4 +- actions/access_keys/routes_test.go | 2 +- actions/access_keys/search.go | 14 +- actions/actions.go | 2 +- actions/admin/access_keys.go | 20 +-- actions/admin/destinations.go | 16 +-- actions/admin/paymail_addresses.go | 34 ++--- actions/admin/record.go | 8 +- actions/admin/routes.go | 4 +- actions/admin/stats.go | 6 +- actions/admin/status.go | 2 +- actions/admin/transactions.go | 20 +-- actions/admin/utxos.go | 16 +-- actions/admin/xpubs.go | 16 +-- actions/base/base_test.go | 2 +- actions/base/index.go | 2 +- actions/base/routes.go | 4 +- actions/base/routes_test.go | 2 +- actions/destinations/count.go | 10 +- actions/destinations/create.go | 12 +- actions/destinations/destination_test.go | 2 +- actions/destinations/get.go | 10 +- actions/destinations/routes.go | 4 +- actions/destinations/routes_test.go | 2 +- actions/destinations/search.go | 14 +- actions/destinations/update.go | 12 +- actions/methods.go | 8 +- actions/middleware.go | 14 +- actions/paymail/create.go | 8 +- actions/paymail/delete.go | 6 +- actions/paymail/routes.go | 6 +- actions/transactions/broadcast_callback.go | 2 +- actions/transactions/count.go | 10 +- actions/transactions/get.go | 6 +- actions/transactions/new.go | 21 ++- actions/transactions/record.go | 10 +- actions/transactions/routes.go | 4 +- actions/transactions/routes_test.go | 2 +- actions/transactions/search.go | 14 +- actions/transactions/transaction_test.go | 2 +- actions/transactions/update.go | 8 +- actions/utxos/count.go | 10 +- actions/utxos/get.go | 6 +- actions/utxos/routes.go | 4 +- actions/utxos/routes_test.go | 2 +- actions/utxos/search.go | 14 +- actions/utxos/unreserve.go | 4 +- actions/utxos/utxo_test.go | 2 +- actions/xpubs/create.go | 8 +- actions/xpubs/get.go | 8 +- actions/xpubs/routes.go | 4 +- actions/xpubs/routes_test.go | 2 +- actions/xpubs/update.go | 8 +- actions/xpubs/xpubs_test.go | 2 +- cmd/server/main.go | 22 +-- config.example.yaml | 14 +- config/config.go | 24 ++-- config/config_test.go | 2 +- config/defaults.go | 8 +- config/flags.go | 12 +- config/load.go | 4 +- config/load_test.go | 8 +- config/services.go | 32 ++--- config/services_test.go | 4 +- config/task_manager.go | 2 +- go.mod | 2 +- logging/logging.go | 2 +- mappings/access_keys.go | 10 +- mappings/admin.go | 8 +- mappings/common/common.go | 4 +- mappings/destination.go | 14 +- mappings/fee_unit.go | 12 +- mappings/metadata.go | 6 +- mappings/paymail_address.go | 20 +-- mappings/script_output.go | 12 +- mappings/sync_config.go | 12 +- mappings/transaction.go | 152 ++++++++++----------- mappings/utxo.go | 28 ++-- mappings/xpub.go | 10 +- metrics/collector.go | 6 +- metrics/global.go | 4 +- metrics/naming.go | 2 +- server/server.go | 22 +-- server/server_test.go | 2 +- bux_server.go => spv_wallet.go | 4 +- tests/tests.go | 4 +- 94 files changed, 470 insertions(+), 471 deletions(-) rename bux_server.go => spv_wallet.go (65%) diff --git a/.github/CODE_STANDARDS.md b/.github/CODE_STANDARDS.md index 1a5ac2d78..94bd5bb09 100644 --- a/.github/CODE_STANDARDS.md +++ b/.github/CODE_STANDARDS.md @@ -246,7 +246,7 @@ Additional information and guidelines on Conventional Commits can be found [here Good example: ```bash -feat: add possibility to create a new user by admin (#BUX-123) +feat(#123): add possibility to create a new user by admin ``` Bad example: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d9e297ba5..29b954e55 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,8 +5,8 @@ # Pull Request Checklist -- [ ] 📖 I created my PR using provided : [CODE_STANDARDS](https://github.com/BuxOrg/bux-server/blob/main/.github/CODE_STANDARDS.md) -- [ ] 📖 I have read the short Code of Conduct: [CODE_OF_CONDUCT](https://github.com/BuxOrg/bux-server/blob/main/.github/CODE_OF_CONDUCT.md) +- [ ] 📖 I created my PR using provided : [CODE_STANDARDS](https://github.com/BuxOrg/spv-wallet/blob/main/.github/CODE_STANDARDS.md) +- [ ] 📖 I have read the short Code of Conduct: [CODE_OF_CONDUCT](https://github.com/BuxOrg/spv-wallet/blob/main/.github/CODE_OF_CONDUCT.md) - [ ] 🏠 I tested my changes locally. - [ ] ✅ I have provided tests for my changes. - [ ] 📝 I have used conventional commits. diff --git a/Makefile b/Makefile index 800836f2d..f2d70f19e 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ include .make/go.mk ## Not defined? Use default repo name which is the application ifeq ($(REPO_NAME),) - REPO_NAME="bux-server" + REPO_NAME="spv-wallet" endif ## Not defined? Use default repo owner diff --git a/actions/access_keys/access_keys_test.go b/actions/access_keys/access_keys_test.go index 19f42e1b1..444c8a77d 100644 --- a/actions/access_keys/access_keys_test.go +++ b/actions/access_keys/access_keys_test.go @@ -3,7 +3,7 @@ package accesskeys import ( "testing" - "github.com/BuxOrg/bux-server/tests" + "github.com/BuxOrg/spv-wallet/tests" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" diff --git a/actions/access_keys/count.go b/actions/access_keys/count.go index 30123b46a..ade244853 100644 --- a/actions/access_keys/count.go +++ b/actions/access_keys/count.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,14 +20,14 @@ import ( // @Param conditions query string false "conditions" // @Success 200 // @Router /v1/access-key/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -35,7 +35,7 @@ func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Pa // Record a new transaction (get the hex from parameters)a var count int64 - if count, err = a.Services.Bux.GetAccessKeysByXPubIDCount( + if count, err = a.Services.SPV.GetAccessKeysByXPubIDCount( req.Context(), reqXPubID, metadata, diff --git a/actions/access_keys/create.go b/actions/access_keys/create.go index eacc320a5..fd572c241 100644 --- a/actions/access_keys/create.go +++ b/actions/access_keys/create.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -18,7 +18,7 @@ import ( // @Param metadata query string false "metadata" // @Success 201 // @Router /v1/access-key [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPub, _ := bux.GetXpubFromRequest(req) @@ -29,7 +29,7 @@ func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.P metadata := params.GetJSON("metadata") // Create a new accessKey - accessKey, err := a.Services.Bux.NewAccessKey( + accessKey, err := a.Services.SPV.NewAccessKey( req.Context(), reqXPub, bux.WithMetadatas(metadata), diff --git a/actions/access_keys/get.go b/actions/access_keys/get.go index d56cf0943..7fece99f9 100644 --- a/actions/access_keys/get.go +++ b/actions/access_keys/get.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -18,7 +18,7 @@ import ( // @Param id query string true "id" // @Success 200 // @Router /v1/access-key [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -32,7 +32,7 @@ func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Para } // Get access key - accessKey, err := a.Services.Bux.GetAccessKey( + accessKey, err := a.Services.SPV.GetAccessKey( req.Context(), reqXPubID, id, ) if err != nil { diff --git a/actions/access_keys/revoke.go b/actions/access_keys/revoke.go index f021f5d53..3341fdb44 100644 --- a/actions/access_keys/revoke.go +++ b/actions/access_keys/revoke.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -18,7 +18,7 @@ import ( // @Param id query string true "id" // @Success 201 // @Router /v1/access-key [delete] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) revoke(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPub, _ := bux.GetXpubFromRequest(req) @@ -32,7 +32,7 @@ func (a *Action) revoke(w http.ResponseWriter, req *http.Request, _ httprouter.P } // Create a new accessKey - accessKey, err := a.Services.Bux.RevokeAccessKey( + accessKey, err := a.Services.SPV.RevokeAccessKey( req.Context(), reqXPub, id, diff --git a/actions/access_keys/routes.go b/actions/access_keys/routes.go index 3aac282f6..650c638c2 100644 --- a/actions/access_keys/routes.go +++ b/actions/access_keys/routes.go @@ -1,8 +1,8 @@ package accesskeys import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/access_keys/routes_test.go b/actions/access_keys/routes_test.go index b2d62523b..6eb703586 100644 --- a/actions/access_keys/routes_test.go +++ b/actions/access_keys/routes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" ) diff --git a/actions/access_keys/search.go b/actions/access_keys/search.go index aae74e94d..2f7518a9f 100644 --- a/actions/access_keys/search.go +++ b/actions/access_keys/search.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -25,14 +25,14 @@ import ( // @Param conditions query string false "conditions" // @Success 200 // @Router /v1/access-key/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -40,7 +40,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P // Record a new transaction (get the hex from parameters)a var accessKeys []*bux.AccessKey - if accessKeys, err = a.Services.Bux.GetAccessKeysByXPubID( + if accessKeys, err = a.Services.SPV.GetAccessKeysByXPubID( req.Context(), reqXPubID, metadata, @@ -51,7 +51,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P return } - accessKeyContracts := make([]*buxmodels.AccessKey, 0) + accessKeyContracts := make([]*spvwalletmodels.AccessKey, 0) for _, accessKey := range accessKeys { accessKeyContracts = append(accessKeyContracts, mappings.MapToAccessKeyContract(accessKey)) } diff --git a/actions/actions.go b/actions/actions.go index f90968997..16ed0fc34 100644 --- a/actions/actions.go +++ b/actions/actions.go @@ -4,7 +4,7 @@ package actions import ( "net/http" - "github.com/BuxOrg/bux-server/dictionary" + "github.com/BuxOrg/spv-wallet/dictionary" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/admin/access_keys.go b/actions/admin/access_keys.go index 18a575c9c..d9f42e239 100644 --- a/actions/admin/access_keys.go +++ b/actions/admin/access_keys.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -25,19 +25,19 @@ import ( // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/access-keys/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) accessKeysSearch(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var accessKeys []*bux.AccessKey - if accessKeys, err = a.Services.Bux.GetAccessKeys( + if accessKeys, err = a.Services.SPV.GetAccessKeys( req.Context(), metadata, conditions, @@ -47,7 +47,7 @@ func (a *Action) accessKeysSearch(w http.ResponseWriter, req *http.Request, _ ht return } - accessKeyContracts := make([]*buxmodels.AccessKey, 0) + accessKeyContracts := make([]*spvwalletmodels.AccessKey, 0) for _, accessKey := range accessKeys { accessKeyContracts = append(accessKeyContracts, mappings.MapToAccessKeyContract(accessKey)) } @@ -66,19 +66,19 @@ func (a *Action) accessKeysSearch(w http.ResponseWriter, req *http.Request, _ ht // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/access-keys/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) accessKeysCount(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var count int64 - if count, err = a.Services.Bux.GetAccessKeysCount( + if count, err = a.Services.SPV.GetAccessKeysCount( req.Context(), metadata, conditions, diff --git a/actions/admin/destinations.go b/actions/admin/destinations.go index 67f4ecb5c..2763520aa 100644 --- a/actions/admin/destinations.go +++ b/actions/admin/destinations.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -24,19 +24,19 @@ import ( // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/destinations/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) destinationsSearch(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var destinations []*bux.Destination - if destinations, err = a.Services.Bux.GetDestinations( + if destinations, err = a.Services.SPV.GetDestinations( req.Context(), metadata, conditions, @@ -60,19 +60,19 @@ func (a *Action) destinationsSearch(w http.ResponseWriter, req *http.Request, _ // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/destinations/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) destinationsCount(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var count int64 - if count, err = a.Services.Bux.GetDestinationsCount( + if count, err = a.Services.SPV.GetDestinationsCount( req.Context(), metadata, conditions, diff --git a/actions/admin/paymail_addresses.go b/actions/admin/paymail_addresses.go index 88047d22d..2ace7715d 100644 --- a/actions/admin/paymail_addresses.go +++ b/actions/admin/paymail_addresses.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -19,7 +19,7 @@ import ( // @Produce json // @Success 200 // @Router /v1/admin/paymail/get [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) paymailGetAddress(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { params := apirouter.GetParams(req) address := params.GetString("address") @@ -29,9 +29,9 @@ func (a *Action) paymailGetAddress(w http.ResponseWriter, req *http.Request, _ h return } - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() - paymailAddress, err := a.Services.Bux.GetPaymailAddress(req.Context(), address, opts...) + paymailAddress, err := a.Services.SPV.GetPaymailAddress(req.Context(), address, opts...) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -54,19 +54,19 @@ func (a *Action) paymailGetAddress(w http.ResponseWriter, req *http.Request, _ h // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/paymails/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) paymailAddressesSearch(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var paymailAddresses []*bux.PaymailAddress - if paymailAddresses, err = a.Services.Bux.GetPaymailAddresses( + if paymailAddresses, err = a.Services.SPV.GetPaymailAddresses( req.Context(), metadata, conditions, @@ -90,19 +90,19 @@ func (a *Action) paymailAddressesSearch(w http.ResponseWriter, req *http.Request // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/paymails/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) paymailAddressesCount(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var count int64 - if count, err = a.Services.Bux.GetPaymailAddressesCount( + if count, err = a.Services.SPV.GetPaymailAddressesCount( req.Context(), metadata, conditions, @@ -128,7 +128,7 @@ func (a *Action) paymailAddressesCount(w http.ResponseWriter, req *http.Request, // @Produce json // @Success 201 // @Router /v1/admin/paymail/create [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) paymailCreateAddress(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -153,14 +153,14 @@ func (a *Action) paymailCreateAddress(w http.ResponseWriter, req *http.Request, return } - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() if metadata != nil { opts = append(opts, bux.WithMetadatas(*metadata)) } var paymailAddress *bux.PaymailAddress - paymailAddress, err = a.Services.Bux.NewPaymailAddress(req.Context(), xpub, address, publicName, avatar, opts...) + paymailAddress, err = a.Services.SPV.NewPaymailAddress(req.Context(), xpub, address, publicName, avatar, opts...) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -179,7 +179,7 @@ func (a *Action) paymailCreateAddress(w http.ResponseWriter, req *http.Request, // @Produce json // @Success 200 // @Router /v1/admin/paymail/delete [delete] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) paymailDeleteAddress(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -190,10 +190,10 @@ func (a *Action) paymailDeleteAddress(w http.ResponseWriter, req *http.Request, return } - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() // Delete a new paymail address - err := a.Services.Bux.DeletePaymailAddress(req.Context(), address, opts...) + err := a.Services.SPV.DeletePaymailAddress(req.Context(), address, opts...) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return diff --git a/actions/admin/record.go b/actions/admin/record.go index 9d8439753..e04ba74fd 100644 --- a/actions/admin/record.go +++ b/actions/admin/record.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" "github.com/mrz1836/go-datastore" @@ -20,7 +20,7 @@ import ( // @Param hex query string true "Transaction hex" // @Success 201 // @Router /v1/admin/transactions/record [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) transactionRecord(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -31,7 +31,7 @@ func (a *Action) transactionRecord(w http.ResponseWriter, req *http.Request, _ h opts := make([]bux.ModelOps, 0) // Record a new transaction (get the hex from parameters) - transaction, err := a.Services.Bux.RecordRawTransaction( + transaction, err := a.Services.SPV.RecordRawTransaction( req.Context(), hex, opts..., @@ -39,7 +39,7 @@ func (a *Action) transactionRecord(w http.ResponseWriter, req *http.Request, _ h if err != nil { if errors.Is(err, datastore.ErrDuplicateKey) { // already registered, just return the registered transaction - if transaction, err = a.Services.Bux.GetTransactionByHex(req.Context(), hex); err != nil { + if transaction, err = a.Services.SPV.GetTransactionByHex(req.Context(), hex); err != nil { apirouter.ReturnResponse(w, req, http.StatusUnprocessableEntity, err.Error()) return } diff --git a/actions/admin/routes.go b/actions/admin/routes.go index 53cf8d75f..2a499a039 100644 --- a/actions/admin/routes.go +++ b/actions/admin/routes.go @@ -1,8 +1,8 @@ package admin import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/admin/stats.go b/actions/admin/stats.go index a7f0c8a5e..48c258f82 100644 --- a/actions/admin/stats.go +++ b/actions/admin/stats.go @@ -3,7 +3,7 @@ package admin import ( "net/http" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -16,9 +16,9 @@ import ( // @Produce json // @Success 200 // @Router /v1/admin/stats [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) stats(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { - stats, err := a.Services.Bux.GetStats(req.Context()) + stats, err := a.Services.SPV.GetStats(req.Context()) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return diff --git a/actions/admin/status.go b/actions/admin/status.go index 85e0b781c..64e552b44 100644 --- a/actions/admin/status.go +++ b/actions/admin/status.go @@ -15,7 +15,7 @@ import ( // @Produce json // @Success 200 // @Router /v1/admin/status [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) status(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Return response apirouter.ReturnResponse(w, req, http.StatusOK, true) diff --git a/actions/admin/transactions.go b/actions/admin/transactions.go index 084c6cab5..580b41e0d 100644 --- a/actions/admin/transactions.go +++ b/actions/admin/transactions.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -25,19 +25,19 @@ import ( // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/transactions/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) transactionsSearch(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var transactions []*bux.Transaction - if transactions, err = a.Services.Bux.GetTransactions( + if transactions, err = a.Services.SPV.GetTransactions( req.Context(), metadata, conditions, @@ -47,7 +47,7 @@ func (a *Action) transactionsSearch(w http.ResponseWriter, req *http.Request, _ return } - contracts := make([]*buxmodels.Transaction, 0) + contracts := make([]*spvwalletmodels.Transaction, 0) for _, transaction := range transactions { contracts = append(contracts, mappings.MapToTransactionContractForAdmin(transaction)) } @@ -66,19 +66,19 @@ func (a *Action) transactionsSearch(w http.ResponseWriter, req *http.Request, _ // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/transactions/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) transactionsCount(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var count int64 - if count, err = a.Services.Bux.GetTransactionsCount( + if count, err = a.Services.SPV.GetTransactionsCount( req.Context(), metadata, conditions, diff --git a/actions/admin/utxos.go b/actions/admin/utxos.go index 60bf24bee..8b3ee5dac 100644 --- a/actions/admin/utxos.go +++ b/actions/admin/utxos.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -24,19 +24,19 @@ import ( // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/utxos/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) utxosSearch(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var utxos []*bux.Utxo - if utxos, err = a.Services.Bux.GetUtxos( + if utxos, err = a.Services.SPV.GetUtxos( req.Context(), metadata, conditions, @@ -60,19 +60,19 @@ func (a *Action) utxosSearch(w http.ResponseWriter, req *http.Request, _ httprou // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/utxos/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) utxosCount(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var count int64 - if count, err = a.Services.Bux.GetUtxosCount( + if count, err = a.Services.SPV.GetUtxosCount( req.Context(), metadata, conditions, diff --git a/actions/admin/xpubs.go b/actions/admin/xpubs.go index 917ea94bb..c63e92a5a 100644 --- a/actions/admin/xpubs.go +++ b/actions/admin/xpubs.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -24,12 +24,12 @@ import ( // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/xpubs/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) xpubsSearch(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) @@ -37,7 +37,7 @@ func (a *Action) xpubsSearch(w http.ResponseWriter, req *http.Request, _ httprou } var xpubs []*bux.Xpub - if xpubs, err = a.Services.Bux.GetXPubs( + if xpubs, err = a.Services.SPV.GetXPubs( req.Context(), metadata, conditions, @@ -61,19 +61,19 @@ func (a *Action) xpubsSearch(w http.ResponseWriter, req *http.Request, _ httprou // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/xpubs/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) xpubsCount(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var count int64 - if count, err = a.Services.Bux.GetXPubsCount( + if count, err = a.Services.SPV.GetXPubsCount( req.Context(), metadata, conditions, diff --git a/actions/base/base_test.go b/actions/base/base_test.go index 6a9ec2e54..a3f397d18 100644 --- a/actions/base/base_test.go +++ b/actions/base/base_test.go @@ -3,7 +3,7 @@ package base import ( "testing" - "github.com/BuxOrg/bux-server/tests" + "github.com/BuxOrg/spv-wallet/tests" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" diff --git a/actions/base/index.go b/actions/base/index.go index 41e58189c..ee757cb36 100644 --- a/actions/base/index.go +++ b/actions/base/index.go @@ -12,6 +12,6 @@ func index(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { apirouter.ReturnResponse( w, req, http.StatusOK, - map[string]interface{}{"message": "Welcome to the Bux Server ✌(◕‿-)✌"}, + map[string]interface{}{"message": "Welcome to the SPV Wallet ✌(◕‿-)✌"}, ) } diff --git a/actions/base/routes.go b/actions/base/routes.go index f0d57558c..1fe439654 100644 --- a/actions/base/routes.go +++ b/actions/base/routes.go @@ -4,8 +4,8 @@ import ( "net/http" "net/http/pprof" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/base/routes_test.go b/actions/base/routes_test.go index 0a2b7470f..438db6f67 100644 --- a/actions/base/routes_test.go +++ b/actions/base/routes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" ) diff --git a/actions/destinations/count.go b/actions/destinations/count.go index 693924171..fb29d8de6 100644 --- a/actions/destinations/count.go +++ b/actions/destinations/count.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,14 +20,14 @@ import ( // @Produce json // @Success 200 // @Router /v1/destination/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) _, metadataModels, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModels) + metadata := mappings.MapToSPVMetadata(metadataModels) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -35,7 +35,7 @@ func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Pa // Record a new transaction (get the hex from parameters) var count int64 - if count, err = a.Services.Bux.GetDestinationsByXpubIDCount( + if count, err = a.Services.SPV.GetDestinationsByXpubIDCount( req.Context(), reqXPubID, metadata, diff --git a/actions/destinations/create.go b/actions/destinations/create.go index 89d01ac24..39e10ed31 100644 --- a/actions/destinations/create.go +++ b/actions/destinations/create.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" "github.com/BuxOrg/bux/utils" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -22,14 +22,14 @@ import ( // @Param metadata query string false "metadata" // @Success 201 // @Router /v1/destination [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) // Get the xPub from the request (via authentication) reqXPub, _ := bux.GetXpubFromRequest(req) - xPub, err := a.Services.Bux.GetXpub(req.Context(), reqXPub) + xPub, err := a.Services.SPV.GetXpub(req.Context(), reqXPub) if err != nil { apirouter.ReturnResponse(w, req, http.StatusUnprocessableEntity, err.Error()) return @@ -53,7 +53,7 @@ func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.P metadata[bux.ReferenceIDField] = referenceID } - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() if metadata != nil { opts = append(opts, bux.WithMetadatas(metadata)) @@ -61,7 +61,7 @@ func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.P // Get a new destination var destination *bux.Destination - if destination, err = a.Services.Bux.NewDestination( + if destination, err = a.Services.SPV.NewDestination( req.Context(), xPub.RawXpub(), uint32(0), // todo: use a constant? protect this? diff --git a/actions/destinations/destination_test.go b/actions/destinations/destination_test.go index 3ae2cba19..ae7c07a86 100644 --- a/actions/destinations/destination_test.go +++ b/actions/destinations/destination_test.go @@ -3,7 +3,7 @@ package destinations import ( "testing" - "github.com/BuxOrg/bux-server/tests" + "github.com/BuxOrg/spv-wallet/tests" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" diff --git a/actions/destinations/get.go b/actions/destinations/get.go index a23087679..e1786efaa 100644 --- a/actions/destinations/get.go +++ b/actions/destinations/get.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,7 +20,7 @@ import ( // @Param locking_script query string false "Destination locking script" // @Success 200 // @Router /v1/destination [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -38,15 +38,15 @@ func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Para var destination *bux.Destination var err error if id != "" { - destination, err = a.Services.Bux.GetDestinationByID( + destination, err = a.Services.SPV.GetDestinationByID( req.Context(), reqXPubID, id, ) } else if address != "" { - destination, err = a.Services.Bux.GetDestinationByAddress( + destination, err = a.Services.SPV.GetDestinationByAddress( req.Context(), reqXPubID, address, ) } else { - destination, err = a.Services.Bux.GetDestinationByLockingScript( + destination, err = a.Services.SPV.GetDestinationByLockingScript( req.Context(), reqXPubID, lockingScript, ) } diff --git a/actions/destinations/routes.go b/actions/destinations/routes.go index 0dab46444..d39651ea2 100644 --- a/actions/destinations/routes.go +++ b/actions/destinations/routes.go @@ -1,8 +1,8 @@ package destinations import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/destinations/routes_test.go b/actions/destinations/routes_test.go index f16108cd6..7f6b68bd1 100644 --- a/actions/destinations/routes_test.go +++ b/actions/destinations/routes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" ) diff --git a/actions/destinations/search.go b/actions/destinations/search.go index ac7e1b776..b0f7b483f 100644 --- a/actions/destinations/search.go +++ b/actions/destinations/search.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -25,14 +25,14 @@ import ( // @Param condition query string false "condition" // @Success 200 // @Router /v1/destination/search [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -40,7 +40,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P // Record a new transaction (get the hex from parameters)a var destinations []*bux.Destination - if destinations, err = a.Services.Bux.GetDestinationsByXpubID( + if destinations, err = a.Services.SPV.GetDestinationsByXpubID( req.Context(), reqXPubID, metadata, @@ -51,7 +51,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P return } - contracts := make([]*buxmodels.Destination, 0) + contracts := make([]*spvwalletmodels.Destination, 0) for _, destination := range destinations { contracts = append(contracts, mappings.MapToDestinationContract(destination)) } diff --git a/actions/destinations/update.go b/actions/destinations/update.go index fe1eb8eec..5a7877940 100644 --- a/actions/destinations/update.go +++ b/actions/destinations/update.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -22,7 +22,7 @@ import ( // @Param metadata body string true "Destination Metadata" // @Success 200 // @Router /v1/destination [patch] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) update(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -41,15 +41,15 @@ func (a *Action) update(w http.ResponseWriter, req *http.Request, _ httprouter.P var destination *bux.Destination var err error if id != "" { - destination, err = a.Services.Bux.UpdateDestinationMetadataByID( + destination, err = a.Services.SPV.UpdateDestinationMetadataByID( req.Context(), reqXPubID, id, metadata, ) } else if address != "" { - destination, err = a.Services.Bux.UpdateDestinationMetadataByAddress( + destination, err = a.Services.SPV.UpdateDestinationMetadataByAddress( req.Context(), reqXPubID, address, metadata, ) } else { - destination, err = a.Services.Bux.UpdateDestinationMetadataByLockingScript( + destination, err = a.Services.SPV.UpdateDestinationMetadataByLockingScript( req.Context(), reqXPubID, lockingScript, metadata, ) } diff --git a/actions/methods.go b/actions/methods.go index 1a73ce1a5..96281fb41 100644 --- a/actions/methods.go +++ b/actions/methods.go @@ -4,8 +4,8 @@ import ( "encoding/json" "net/http" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/dictionary" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/dictionary" "github.com/julienschmidt/httprouter" "github.com/mrz1836/go-datastore" "github.com/mrz1836/go-parameters" @@ -51,7 +51,7 @@ func MethodNotAllowed(w http.ResponseWriter, req *http.Request) { } // GetQueryParameters get all filtering parameters related to the db query -func GetQueryParameters(params *parameters.Params) (*datastore.QueryParams, *buxmodels.Metadata, *map[string]interface{}, error) { +func GetQueryParameters(params *parameters.Params) (*datastore.QueryParams, *spvwalletmodels.Metadata, *map[string]interface{}, error) { var queryParams *datastore.QueryParams jsonQueryParams, ok := params.GetJSONOk("params") if ok { @@ -73,7 +73,7 @@ func GetQueryParameters(params *parameters.Params) (*datastore.QueryParams, *bux } metadataReq := params.GetJSON(MetadataField) - var metadata *buxmodels.Metadata + var metadata *spvwalletmodels.Metadata if len(metadataReq) > 0 { // marshal the metadata into the Metadata model metaJSON, _ := json.Marshal(metadataReq) //nolint:errchkjson // ignore for now diff --git a/actions/middleware.go b/actions/middleware.go index 3cccd92cd..727d5e20c 100644 --- a/actions/middleware.go +++ b/actions/middleware.go @@ -6,8 +6,8 @@ import ( "time" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/config" - "github.com/BuxOrg/bux-server/dictionary" + "github.com/BuxOrg/spv-wallet/config" + "github.com/BuxOrg/spv-wallet/dictionary" "github.com/gofrs/uuid" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" @@ -32,7 +32,7 @@ func (a *Action) RequireAuthentication(fn httprouter.Handle) httprouter.Handle { return func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { // Check the authentication var knownErr dictionary.ErrorMessage - if req, knownErr = CheckAuthentication(a.AppConfig, a.Services.Bux, req, false, true); knownErr.Code > 0 { + if req, knownErr = CheckAuthentication(a.AppConfig, a.Services.SPV, req, false, true); knownErr.Code > 0 { ReturnErrorResponse(w, req, knownErr, "") return } @@ -47,7 +47,7 @@ func (a *Action) RequireBasicAuthentication(fn httprouter.Handle) httprouter.Han return func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { // Check the authentication var knownErr dictionary.ErrorMessage - if req, knownErr = CheckAuthentication(a.AppConfig, a.Services.Bux, req, false, false); knownErr.Code > 0 { + if req, knownErr = CheckAuthentication(a.AppConfig, a.Services.SPV, req, false, false); knownErr.Code > 0 { ReturnErrorResponse(w, req, knownErr, "") return } @@ -62,7 +62,7 @@ func (a *Action) RequireAdminAuthentication(fn httprouter.Handle) httprouter.Han return func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { // Check the authentication var knownErr dictionary.ErrorMessage - if req, knownErr = CheckAuthentication(a.AppConfig, a.Services.Bux, req, true, true); knownErr.Code > 0 { + if req, knownErr = CheckAuthentication(a.AppConfig, a.Services.SPV, req, true, true); knownErr.Code > 0 { ReturnErrorResponse(w, req, knownErr, "") return } @@ -92,7 +92,7 @@ func (a *Action) Request(_ *apirouter.Router, h httprouter.Handle) httprouter.Ha } // CheckAuthentication will check the authentication -func CheckAuthentication(appConfig *config.AppConfig, bux bux.ClientInterface, req *http.Request, +func CheckAuthentication(appConfig *config.AppConfig, spv bux.ClientInterface, req *http.Request, adminRequired bool, requireSigning bool, ) (*http.Request, dictionary.ErrorMessage) { // Bad/Unknown scheme @@ -102,7 +102,7 @@ func CheckAuthentication(appConfig *config.AppConfig, bux bux.ClientInterface, r // AuthenticateFromRequest using the xPub scheme var err error - if req, err = bux.AuthenticateRequest( + if req, err = spv.AuthenticateRequest( req.Context(), req, []string{appConfig.Authentication.AdminKey}, adminRequired, requireSigning && appConfig.Authentication.RequireSigning, diff --git a/actions/paymail/create.go b/actions/paymail/create.go index ef9a6c191..5c37c142d 100644 --- a/actions/paymail/create.go +++ b/actions/paymail/create.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -22,7 +22,7 @@ import ( // @Produce json // @Success 201 // @Router /v1/paymail [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -34,13 +34,13 @@ func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.P avatar := params.GetString("avatar") // the avatar metadata := params.GetJSON("metadata") // optional metadata - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() if metadata != nil { opts = append(opts, bux.WithMetadatas(metadata)) } - paymailAddress, err := a.Services.Bux.NewPaymailAddress(req.Context(), key, address, publicName, avatar, opts...) + paymailAddress, err := a.Services.SPV.NewPaymailAddress(req.Context(), key, address, publicName, avatar, opts...) if err != nil { apirouter.ReturnResponse(w, req, http.StatusUnprocessableEntity, err.Error()) return diff --git a/actions/paymail/delete.go b/actions/paymail/delete.go index e140667bc..192658241 100644 --- a/actions/paymail/delete.go +++ b/actions/paymail/delete.go @@ -16,7 +16,7 @@ import ( // @Produce json // @Success 200 // @Router /v1/paymail [delete] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) delete(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -24,10 +24,10 @@ func (a *Action) delete(w http.ResponseWriter, req *http.Request, _ httprouter.P // params address := params.GetString("address") // the full paymail address - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() // Delete a new paymail address - err := a.Services.Bux.DeletePaymailAddress( + err := a.Services.SPV.DeletePaymailAddress( req.Context(), address, opts..., ) if err != nil { diff --git a/actions/paymail/routes.go b/actions/paymail/routes.go index 8393d6d73..16c10698f 100644 --- a/actions/paymail/routes.go +++ b/actions/paymail/routes.go @@ -1,8 +1,8 @@ package pmail import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) @@ -18,7 +18,7 @@ func RegisterRoutes(router *apirouter.Router, appConfig *config.AppConfig, servi requireAdmin.Use(a.RequireAdminAuthentication) // Register the custom Paymail routes - services.Bux.GetPaymailConfig().RegisterRoutes(router.HTTPRouter) + services.SPV.GetPaymailConfig().RegisterRoutes(router.HTTPRouter) // Create the action action := &Action{actions.Action{AppConfig: a.AppConfig, Services: a.Services}} diff --git a/actions/transactions/broadcast_callback.go b/actions/transactions/broadcast_callback.go index 34042b48c..18f4f002b 100644 --- a/actions/transactions/broadcast_callback.go +++ b/actions/transactions/broadcast_callback.go @@ -32,7 +32,7 @@ func (a *Action) broadcastCallback(w http.ResponseWriter, req *http.Request, _ h } }() - err = a.Services.Bux.UpdateTransaction(req.Context(), resp) + err = a.Services.SPV.UpdateTransaction(req.Context(), resp) if err != nil { a.Services.Logger.Err(err).Msgf("failed to update transaction - tx: %v", resp) apirouter.ReturnResponse(w, req, http.StatusInternalServerError, "") diff --git a/actions/transactions/count.go b/actions/transactions/count.go index c2a730570..9a44205b9 100644 --- a/actions/transactions/count.go +++ b/actions/transactions/count.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,14 +20,14 @@ import ( // @Param conditions query string false "conditions" // @Success 200 // @Router /v1/transaction/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -35,7 +35,7 @@ func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Pa // Record a new transaction (get the hex from parameters)a var count int64 - if count, err = a.Services.Bux.GetTransactionsByXpubIDCount( + if count, err = a.Services.SPV.GetTransactionsByXpubIDCount( req.Context(), reqXPubID, metadata, diff --git a/actions/transactions/get.go b/actions/transactions/get.go index f5b8f39c6..d7fe95f8c 100644 --- a/actions/transactions/get.go +++ b/actions/transactions/get.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -18,7 +18,7 @@ import ( // @Param id query string true "id" // @Success 200 // @Router /v1/transaction [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -27,7 +27,7 @@ func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Para reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Get a transaction by ID - transaction, err := a.Services.Bux.GetTransaction( + transaction, err := a.Services.SPV.GetTransaction( req.Context(), reqXPubID, params.GetString("id"), diff --git a/actions/transactions/new.go b/actions/transactions/new.go index 968bb1854..0de2b0600 100644 --- a/actions/transactions/new.go +++ b/actions/transactions/new.go @@ -5,9 +5,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -22,14 +22,14 @@ import ( // @Param metadata query string false "metadata" // @Success 201 // @Router /v1/transaction [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) newTransaction(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) // Get the xPub from the request (via authentication) reqXPub, _ := bux.GetXpubFromRequest(req) - xPub, err := a.Services.Bux.GetXpub(req.Context(), reqXPub) + xPub, err := a.Services.SPV.GetXpub(req.Context(), reqXPub) if err != nil { apirouter.ReturnResponse(w, req, http.StatusUnprocessableEntity, err.Error()) return @@ -39,8 +39,7 @@ func (a *Action) newTransaction(w http.ResponseWriter, req *http.Request, _ http } // Read transaction config from request body - // TODO: Austin's params package probably has a better way to do this than - // marshal/unmarshal... couldn't figure it out + // TODO: consider using go params package functions instead of marshal/unmarshal configMap, ok := params.GetJSONOk("config") if !ok { apirouter.ReturnResponse(w, req, http.StatusBadRequest, actions.ErrTxConfigNotFound.Error()) @@ -53,23 +52,23 @@ func (a *Action) newTransaction(w http.ResponseWriter, req *http.Request, _ http return } - txContract := buxmodels.TransactionConfig{} + txContract := spvwalletmodels.TransactionConfig{} if err = json.Unmarshal(configBytes, &txContract); err != nil { apirouter.ReturnResponse(w, req, http.StatusBadRequest, actions.ErrBadTxConfig.Error()) return } metadata := params.GetJSON(bux.ModelMetadata.String()) - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() if metadata != nil { opts = append(opts, bux.WithMetadatas(metadata)) } - txConfig := mappings.MapToTransactionConfigBux(&txContract) + txConfig := mappings.MapToTransactionConfigSPV(&txContract) // Record a new transaction (get the hex from parameters) var transaction *bux.DraftTransaction - if transaction, err = a.Services.Bux.NewTransaction( + if transaction, err = a.Services.SPV.NewTransaction( req.Context(), xPub.RawXpub(), txConfig, diff --git a/actions/transactions/record.go b/actions/transactions/record.go index 7bc5f3c29..337e4dcb9 100644 --- a/actions/transactions/record.go +++ b/actions/transactions/record.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -21,14 +21,14 @@ import ( // @Param metadata query string false "metadata" // @Success 200 // @Router /v1/transaction/record [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) record(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) // Get the xPub from the request (via authentication) reqXPub, _ := bux.GetXpubFromRequest(req) - xPub, err := a.Services.Bux.GetXpub(req.Context(), reqXPub) + xPub, err := a.Services.SPV.GetXpub(req.Context(), reqXPub) if err != nil { apirouter.ReturnResponse(w, req, http.StatusUnprocessableEntity, err.Error()) return @@ -46,7 +46,7 @@ func (a *Action) record(w http.ResponseWriter, req *http.Request, _ httprouter.P // Record a new transaction (get the hex from parameters) var transaction *bux.Transaction - if transaction, err = a.Services.Bux.RecordTransaction( + if transaction, err = a.Services.SPV.RecordTransaction( req.Context(), reqXPub, params.GetString("hex"), diff --git a/actions/transactions/routes.go b/actions/transactions/routes.go index d8a9123e8..0b4b1a6d5 100644 --- a/actions/transactions/routes.go +++ b/actions/transactions/routes.go @@ -1,8 +1,8 @@ package transactions import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/transactions/routes_test.go b/actions/transactions/routes_test.go index 7575d12f6..8e7bde728 100644 --- a/actions/transactions/routes_test.go +++ b/actions/transactions/routes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" ) diff --git a/actions/transactions/search.go b/actions/transactions/search.go index 1f01d7ba0..cbb4132d5 100644 --- a/actions/transactions/search.go +++ b/actions/transactions/search.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -25,14 +25,14 @@ import ( // @Param conditions query string false "conditions" // @Success 200 // @Router /v1/transaction/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -40,7 +40,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P // Record a new transaction (get the hex from parameters)a var transactions []*bux.Transaction - if transactions, err = a.Services.Bux.GetTransactionsByXpubID( + if transactions, err = a.Services.SPV.GetTransactionsByXpubID( req.Context(), reqXPubID, metadata, @@ -51,7 +51,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P return } - contracts := make([]*buxmodels.Transaction, 0) + contracts := make([]*spvwalletmodels.Transaction, 0) for _, transaction := range transactions { contracts = append(contracts, mappings.MapToTransactionContract(transaction)) } diff --git a/actions/transactions/transaction_test.go b/actions/transactions/transaction_test.go index 33ad55faa..15ee1a404 100644 --- a/actions/transactions/transaction_test.go +++ b/actions/transactions/transaction_test.go @@ -3,7 +3,7 @@ package transactions import ( "testing" - "github.com/BuxOrg/bux-server/tests" + "github.com/BuxOrg/spv-wallet/tests" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" diff --git a/actions/transactions/update.go b/actions/transactions/update.go index 334eae6db..d9adc20d4 100644 --- a/actions/transactions/update.go +++ b/actions/transactions/update.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,7 +20,7 @@ import ( // @Param metadata query string true "metadata" // @Success 200 // @Router /v1/transaction [patch] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) update(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Get the xPub from the request (via authentication) reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -30,7 +30,7 @@ func (a *Action) update(w http.ResponseWriter, req *http.Request, _ httprouter.P metadata := params.GetJSON(actions.MetadataField) // Get a transaction by ID - transaction, err := a.Services.Bux.UpdateTransactionMetadata( + transaction, err := a.Services.SPV.UpdateTransactionMetadata( req.Context(), reqXPubID, params.GetString("id"), diff --git a/actions/utxos/count.go b/actions/utxos/count.go index ea6df106b..ed0f54ecb 100644 --- a/actions/utxos/count.go +++ b/actions/utxos/count.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,14 +20,14 @@ import ( // @Param conditions query string false "conditions" // @Success 200 // @Router /v1/utxo/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -42,7 +42,7 @@ func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Pa // Get a utxo using a xPub var count int64 - if count, err = a.Services.Bux.GetUtxosCount( + if count, err = a.Services.SPV.GetUtxosCount( req.Context(), metadata, &dbConditions, diff --git a/actions/utxos/get.go b/actions/utxos/get.go index 1c0ad93de..cd9fe8b18 100644 --- a/actions/utxos/get.go +++ b/actions/utxos/get.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -19,7 +19,7 @@ import ( // @Param output_index query int true "output_index" // @Success 200 // @Router /v1/utxo [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -29,7 +29,7 @@ func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Para outputIndex := uint32(params.GetUint64("output_index")) // Get a utxo using a xPub - utxo, err := a.Services.Bux.GetUtxo( + utxo, err := a.Services.SPV.GetUtxo( req.Context(), reqXPubID, txID, diff --git a/actions/utxos/routes.go b/actions/utxos/routes.go index 5b092db1f..c358486fc 100644 --- a/actions/utxos/routes.go +++ b/actions/utxos/routes.go @@ -1,8 +1,8 @@ package utxos import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/utxos/routes_test.go b/actions/utxos/routes_test.go index 0c5d464fc..c8b662931 100644 --- a/actions/utxos/routes_test.go +++ b/actions/utxos/routes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" ) diff --git a/actions/utxos/search.go b/actions/utxos/search.go index 0f64038e1..76e2d7a70 100644 --- a/actions/utxos/search.go +++ b/actions/utxos/search.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -25,14 +25,14 @@ import ( // @Param conditions query string false "conditions" // @Success 200 // @Router /v1/utxo/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) queryParams, modelMetadata, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(modelMetadata) + metadata := mappings.MapToSPVMetadata(modelMetadata) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -40,7 +40,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P // Record a new transaction (get the hex from parameters)a var utxos []*bux.Utxo - if utxos, err = a.Services.Bux.GetUtxosByXpubID( + if utxos, err = a.Services.SPV.GetUtxosByXpubID( req.Context(), reqXPubID, metadata, @@ -51,7 +51,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P return } - contracts := make([]*buxmodels.Utxo, 0) + contracts := make([]*spvwalletmodels.Utxo, 0) for _, utxo := range utxos { contracts = append(contracts, mappings.MapToUtxoContract(utxo)) } diff --git a/actions/utxos/unreserve.go b/actions/utxos/unreserve.go index 068aad9f8..96de04f64 100644 --- a/actions/utxos/unreserve.go +++ b/actions/utxos/unreserve.go @@ -16,12 +16,12 @@ import ( // @Param reference_id query string false "draft tx id" // @Success 201 // @Router /v1/utxo/unreserve [patch] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) unreserve(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) params := apirouter.GetParams(req) - err := a.Services.Bux.UnReserveUtxos( + err := a.Services.SPV.UnReserveUtxos( req.Context(), reqXPubID, params.GetString(bux.ReferenceIDField), diff --git a/actions/utxos/utxo_test.go b/actions/utxos/utxo_test.go index fa36a6eab..a916145e1 100644 --- a/actions/utxos/utxo_test.go +++ b/actions/utxos/utxo_test.go @@ -3,7 +3,7 @@ package utxos import ( "testing" - "github.com/BuxOrg/bux-server/tests" + "github.com/BuxOrg/spv-wallet/tests" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" diff --git a/actions/xpubs/create.go b/actions/xpubs/create.go index bcf1218c2..4dba8a9de 100644 --- a/actions/xpubs/create.go +++ b/actions/xpubs/create.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,7 +20,7 @@ import ( // @Param metadata query string false "metadata" // @Success 201 // @Router /v1/xpub [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -30,7 +30,7 @@ func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.P metadata := params.GetJSON(actions.MetadataField) // Create a new xPub - xPub, err := a.Services.Bux.NewXpub( + xPub, err := a.Services.SPV.NewXpub( req.Context(), key, bux.WithMetadatas(metadata), ) diff --git a/actions/xpubs/get.go b/actions/xpubs/get.go index 9582ddf55..f67cade49 100644 --- a/actions/xpubs/get.go +++ b/actions/xpubs/get.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -18,7 +18,7 @@ import ( // @Param key query string false "key" // @Success 200 // @Router /v1/xpub [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPub, _ := bux.GetXpubFromRequest(req) reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -39,11 +39,11 @@ func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Para var xPub *bux.Xpub var err error if key != "" { - xPub, err = a.Services.Bux.GetXpub( + xPub, err = a.Services.SPV.GetXpub( req.Context(), key, ) } else { - xPub, err = a.Services.Bux.GetXpubByID( + xPub, err = a.Services.SPV.GetXpubByID( req.Context(), reqXPubID, ) } diff --git a/actions/xpubs/routes.go b/actions/xpubs/routes.go index 28bf38a8b..899eadb0e 100644 --- a/actions/xpubs/routes.go +++ b/actions/xpubs/routes.go @@ -1,8 +1,8 @@ package xpubs import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/xpubs/routes_test.go b/actions/xpubs/routes_test.go index ea3dddc26..3f542aa91 100644 --- a/actions/xpubs/routes_test.go +++ b/actions/xpubs/routes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" ) diff --git a/actions/xpubs/update.go b/actions/xpubs/update.go index 7ae938011..94b6045d3 100644 --- a/actions/xpubs/update.go +++ b/actions/xpubs/update.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -19,7 +19,7 @@ import ( // @Param metadata query string false "metadata" // @Success 200 // @Router /v1/xpub [patch] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) update(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPub, _ := bux.GetXpubFromRequest(req) reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -31,7 +31,7 @@ func (a *Action) update(w http.ResponseWriter, req *http.Request, _ httprouter.P // Get an xPub var xPub *bux.Xpub var err error - xPub, err = a.Services.Bux.UpdateXpubMetadata( + xPub, err = a.Services.SPV.UpdateXpubMetadata( req.Context(), reqXPubID, metadata, ) if err != nil { diff --git a/actions/xpubs/xpubs_test.go b/actions/xpubs/xpubs_test.go index e967b0bb8..74b006987 100644 --- a/actions/xpubs/xpubs_test.go +++ b/actions/xpubs/xpubs_test.go @@ -3,7 +3,7 @@ package xpubs import ( "testing" - "github.com/BuxOrg/bux-server/tests" + "github.com/BuxOrg/spv-wallet/tests" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" diff --git a/cmd/server/main.go b/cmd/server/main.go index 5d2924299..6a6127e25 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,5 +1,5 @@ /* -Package main is the core service layer for the BUX Server +Package main is the core service layer for the SPV Wallet */ package main @@ -9,19 +9,19 @@ import ( "os/signal" "time" - "github.com/BuxOrg/bux-server/config" - "github.com/BuxOrg/bux-server/dictionary" - _ "github.com/BuxOrg/bux-server/docs" - "github.com/BuxOrg/bux-server/logging" - "github.com/BuxOrg/bux-server/server" + "github.com/BuxOrg/spv-wallet/config" + "github.com/BuxOrg/spv-wallet/dictionary" + _ "github.com/BuxOrg/spv-wallet/docs" + "github.com/BuxOrg/spv-wallet/logging" + "github.com/BuxOrg/spv-wallet/server" ) -// main method starts everything for the BUX Server -// @title BUX: Server +// main method starts everything for the SPV Wallet +// @title SPV: Wallet // @version v0.12.0 -// @securityDefinitions.apikey bux-auth-xpub +// @securityDefinitions.apikey spv-wallet-auth-xpub // @in header -// @name bux-auth-xpub +// @name spv-wallet-auth-xpub // @securityDefinitions.apikey callback-auth // @in header @@ -49,7 +49,7 @@ func main() { return } - // Try to ping the pulse service if enabled + // Try to ping the block header service if enabled appConfig.CheckPulse(context.Background(), services.Logger) // @mrz New Relic is ready at this point diff --git a/config.example.yaml b/config.example.yaml index b67369a5e..b65773a0b 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -12,7 +12,7 @@ cache: # cluster coordinator - redis/memory coordinator: memory # prefix for channel names - prefix: bux_cluster_ + prefix: spv_cluster_ redis: null # cache engine - freecache/redis engine: freecache @@ -60,7 +60,7 @@ db: tx_timeout: 10s user: postgres sqlite: - database_path: ./bux.db + database_path: ./spv.db debug: false max_connection_idle_time: 0s max_connection_time: 0s @@ -83,7 +83,7 @@ new_relic: license_key: BOGUS-LICENSE-KEY-1234567890987654321234 nodes: # deployment id used annotating api calls in XDeployment-ID header - this value will be randomly generated if not set - _deployment_id: bux-deployment-id + _deployment_id: spv-deployment-id callback: callback_host: https://xyz.com # token to authenticate callback calls - default callback token will be generated from the Admin Key @@ -109,14 +109,14 @@ notifications: webhook_endpoint: "" paymail: beef: - pulse_auth_token: mQZQ6WmxURxWz5ch - # url to pulse, used for merkle root verification - pulse_url: http://localhost:8080/api/v1/chain/merkleroot/verify + bhs_auth_token: mQZQ6WmxURxWz5ch + # url to block header service, used for merkle root verification + bhs_url: http://localhost:8080/api/v1/chain/merkleroot/verify use_beef: false # set is as a default sender paymail if account does not have one default_from_paymail: from@domain.com # default note added into transactions - Deprecated - default_note: bux Address Resolution + default_note: spv Address Resolution # enable paymail domain validation, paymail domain must be in domains list to be valid and that the transaction can be processed domain_validation_enabled: false # list of domains used for paymail domain validation diff --git a/config/config.go b/config/config.go index 4408bea05..72b9f07ae 100644 --- a/config/config.go +++ b/config/config.go @@ -10,28 +10,28 @@ import ( "github.com/mrz1836/go-datastore" ) -// Config constants used for bux-server +// Config constants used for spv-wallet const ( - ApplicationName = "BuxServer" + ApplicationName = "SPVWallet" APIVersion = "v1" DefaultNewRelicShutdown = 10 * time.Second HealthRequestPath = "health" Version = "v0.12.0" ConfigFilePathKey = "config_file" DefaultConfigFilePath = "config.yaml" - ConfigEnvPrefix = "BUX_" + ConfigEnvPrefix = "SPV_" BroadcastCallbackRoute = "/transaction/broadcast/callback" ) // AppConfig is the configuration values and associated env vars type AppConfig struct { - // Authentication is the configuration for keys authentication in bux. + // Authentication is the configuration for keys authentication in spv. Authentication *AuthenticationConfig `json:"auth" mapstructure:"auth"` // Cache is the configuration for cache, memory or redis, and cluster cache settings. Cache *CacheConfig `json:"cache" mapstructure:"cache"` // Db is the configuration for database related settings. Db *DbConfig `json:"db" mapstructure:"db"` - // Debug is a flag for enabling additional information from bux. + // Debug is a flag for enabling additional information from spv. Debug bool `json:"debug" mapstructure:"debug"` // DebugProfiling is a flag for enabling additinal debug profiling. DebugProfiling bool `json:"debug_profiling" mapstructure:"debug_profiling"` @@ -39,7 +39,7 @@ type AppConfig struct { DisableITC bool `json:"disable_itc" mapstructure:"disable_itc"` // ImportBlockHeaders is a URL from where the headers can be downloaded. ImportBlockHeaders string `json:"import_block_headers" mapstructure:"import_block_headers"` - // Logging is the configuration for zerolog used in bux. + // Logging is the configuration for zerolog used in spv. Logging *LoggingConfig `json:"logging" mapstructure:"logging"` // NewRelic is New Relic related settings. NewRelic *NewRelicConfig `json:"new_relic" mapstructure:"new_relic"` @@ -51,11 +51,11 @@ type AppConfig struct { Paymail *PaymailConfig `json:"paymail" mapstructure:"paymail"` // RequestLogging is flag for enabling logging in go-api-router. RequestLogging bool `json:"request_logging" mapstructure:"request_logging"` - // Server is a general configuration for bux-server. + // Server is a general configuration for spv-wallet. Server *ServerConfig `json:"server_config" mapstructure:"server_config"` - // TaskManager is a configuration for Task Manager in bux. + // TaskManager is a configuration for Task Manager in spv. TaskManager *TaskManagerConfig `json:"task_manager" mapstructure:"task_manager"` - // Metrics is a configuration for metrics in bux. + // Metrics is a configuration for metrics in spv. Metrics *MetricsConfig `json:"metrics" mapstructure:"metrics"` } @@ -75,7 +75,7 @@ type AuthenticationConfig struct { type CacheConfig struct { // Engine is the cache engine to use (redis, freecache). Engine cachestore.Engine `json:"engine" mapstructure:"engine"` - // Cluster is the cluster-specific configuration for bux. + // Cluster is the cluster-specific configuration for spv. Cluster *ClusterConfig `json:"cluster" mapstructure:"cluster"` // Redis is a general config for redis if the engine is set to it. Redis *RedisConfig `json:"redis" mapstructure:"redis"` @@ -89,7 +89,7 @@ type CallbackConfig struct { CallbackToken string `json:"callback_token" mapstructure:"callback_token"` } -// ClusterConfig is a configuration for the Bux cluster +// ClusterConfig is a configuration for the SPV cluster type ClusterConfig struct { // Coordinator is a cluster coordinator (redis or memory). Coordinator cluster.Coordinator `json:"coordinator" mapstructure:"coordinator"` @@ -249,5 +249,5 @@ type MetricsConfig struct { // GetUserAgent will return the outgoing user agent func (a *AppConfig) GetUserAgent() string { - return "BUX-Server " + Version + return "SPV-Wallet " + Version } diff --git a/config/config_test.go b/config/config_test.go index 95f81ba2f..0c29b5a87 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/BuxOrg/bux-server/logging" + "github.com/BuxOrg/spv-wallet/logging" "github.com/mrz1836/go-cachestore" "github.com/mrz1836/go-datastore" "github.com/stretchr/testify/assert" diff --git a/config/defaults.go b/config/defaults.go index 61747783e..01a398edb 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -45,7 +45,7 @@ func getCacheDefaults() *CacheConfig { Engine: "freecache", Cluster: &ClusterConfig{ Coordinator: "memory", - Prefix: "bux_cluster_", + Prefix: "spv_cluster_", Redis: nil, }, Redis: &RedisConfig{ @@ -95,7 +95,7 @@ func getDbDefaults() *DbConfig { SslMode: "disable", }, SQLite: &datastore.SQLiteConfig{ - DatabasePath: "./bux.db", + DatabasePath: "./spv.db", ExistingConnection: nil, Shared: true, }, @@ -106,7 +106,7 @@ func getLoggingDefaults() *LoggingConfig { return &LoggingConfig{ Level: "info", Format: "console", - InstanceName: "bux-server", + InstanceName: "spv-wallet", LogOrigin: false, } } @@ -122,7 +122,7 @@ func getNewRelicDefaults() *NewRelicConfig { func getNodesDefaults() *NodesConfig { depIDSufix, _ := uuid.NewUUID() return &NodesConfig{ - DeploymentID: "bux-" + depIDSufix.String(), + DeploymentID: "spv-" + depIDSufix.String(), Protocol: NodesProtocolArc, Callback: getCallbackDefaults(), Apis: []*MinerAPI{ diff --git a/config/flags.go b/config/flags.go index 96fd6c999..8a41fc88f 100644 --- a/config/flags.go +++ b/config/flags.go @@ -20,22 +20,22 @@ func loadFlags() error { } cli := &cliFlags{} - buxFlags := pflag.NewFlagSet("buxFlags", pflag.ContinueOnError) + appFlags := pflag.NewFlagSet("appFlags", pflag.ContinueOnError) - initFlags(buxFlags, cli) + initFlags(appFlags, cli) - err := buxFlags.Parse(os.Args[1:]) + err := appFlags.Parse(os.Args[1:]) if err != nil { fmt.Printf("Flags can't be parsed: %v\n", err) os.Exit(1) } - err = viper.BindPFlag(ConfigFilePathKey, buxFlags.Lookup(ConfigFilePathKey)) + err = viper.BindPFlag(ConfigFilePathKey, appFlags.Lookup(ConfigFilePathKey)) if err != nil { return err } - parseCliFlags(buxFlags, cli) + parseCliFlags(appFlags, cli) return nil } @@ -59,7 +59,7 @@ func parseCliFlags(fs *pflag.FlagSet, cli *cliFlags) { } if cli.showVersion { - fmt.Println("bux-sever", "version", Version) + fmt.Println("spv-wallet", "version", Version) os.Exit(0) } diff --git a/config/load.go b/config/load.go index 3b519ee8a..c89681e25 100644 --- a/config/load.go +++ b/config/load.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - "github.com/BuxOrg/bux-server/dictionary" + "github.com/BuxOrg/spv-wallet/dictionary" "github.com/mitchellh/mapstructure" "github.com/rs/zerolog" "github.com/spf13/viper" @@ -68,7 +68,7 @@ func setDefaults() error { } func envConfig() { - viper.SetEnvPrefix("BUX") + viper.SetEnvPrefix("SPV") viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() } diff --git a/config/load_test.go b/config/load_test.go index e09fba777..0b3b189d9 100644 --- a/config/load_test.go +++ b/config/load_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "github.com/BuxOrg/bux-server/logging" + "github.com/BuxOrg/spv-wallet/logging" "github.com/spf13/viper" "github.com/stretchr/testify/assert" ) @@ -30,9 +30,9 @@ func TestLoadConfig(t *testing.T) { // when // IMPORTANT! If you need to change the name of this variable, it means you're - // making backwards incompatible changes. Please inform all Bux adoptors and + // making backwards incompatible changes. Please inform all SPV adoptors and // update your configs on all servers and scripts. - os.Setenv("BUX_CONFIG_FILE", anotherPath) + os.Setenv("SPV_CONFIG_FILE", anotherPath) _, err := Load(defaultLogger) // then @@ -40,6 +40,6 @@ func TestLoadConfig(t *testing.T) { assert.Error(t, err) // cleanup - os.Unsetenv("BUX_CONFIG_FILE") + os.Unsetenv("SPV_CONFIG_FILE") }) } diff --git a/config/services.go b/config/services.go index 6ea8dc643..bbab03b12 100644 --- a/config/services.go +++ b/config/services.go @@ -9,11 +9,11 @@ import ( "time" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/logging" - "github.com/BuxOrg/bux-server/metrics" "github.com/BuxOrg/bux/cluster" "github.com/BuxOrg/bux/taskmanager" "github.com/BuxOrg/bux/utils" + "github.com/BuxOrg/spv-wallet/logging" + "github.com/BuxOrg/spv-wallet/metrics" broadcastclient "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client" "github.com/go-redis/redis/v8" "github.com/mrz1836/go-cachestore" @@ -25,7 +25,7 @@ import ( // AppServices is the loaded services via config type ( AppServices struct { - Bux bux.ClientInterface + SPV bux.ClientInterface NewRelic *newrelic.Application Logger *zerolog.Logger } @@ -54,8 +54,8 @@ func (a *AppConfig) LoadServices(ctx context.Context) (*AppServices, error) { _services.Logger = logger - // Load BUX - if err = _services.loadBux(ctx, a, false, logger); err != nil { + // Load SPV Wallet + if err = _services.loadSPV(ctx, a, false, logger); err != nil { return nil, err } @@ -78,8 +78,8 @@ func (a *AppConfig) LoadTestServices(ctx context.Context) (*AppServices, error) txn := _services.NewRelic.StartTransaction("services_load_test") defer txn.End() - // Load bux for testing - if err = _services.loadBux(ctx, a, true, _services.Logger); err != nil { + // Load spv for testing + if err = _services.loadSPV(ctx, a, true, _services.Logger); err != nil { return nil, err } @@ -114,10 +114,10 @@ func (a *AppConfig) loadNewRelic(services *AppServices) (err error) { // CloseAll will close all connections to all services func (s *AppServices) CloseAll(ctx context.Context) { - // Close Bux - if s.Bux != nil { - _ = s.Bux.Close(ctx) - s.Bux = nil + // Close SPV + if s.SPV != nil { + _ = s.SPV.Close(ctx) + s.SPV = nil } // Close new relic @@ -132,8 +132,8 @@ func (s *AppServices) CloseAll(ctx context.Context) { } } -// loadBux will load the bux client (including CacheStore and DataStore) -func (s *AppServices) loadBux(ctx context.Context, appConfig *AppConfig, testMode bool, logger *zerolog.Logger) (err error) { +// loadSPV will load the spv client (including CacheStore and DataStore) +func (s *AppServices) loadSPV(ctx context.Context, appConfig *AppConfig, testMode bool, logger *zerolog.Logger) (err error) { var options []bux.ClientOps if appConfig.NewRelic.Enabled { @@ -148,8 +148,8 @@ func (s *AppServices) loadBux(ctx context.Context, appConfig *AppConfig, testMod options = append(options, bux.WithUserAgent(appConfig.GetUserAgent())) if logger != nil { - buxLogger := logger.With().Str("service", "bux").Logger() - options = append(options, bux.WithLogger(&buxLogger)) + spvLogger := logger.With().Str("service", "spv").Logger() + options = append(options, bux.WithLogger(&spvLogger)) } if appConfig.Debug { @@ -202,7 +202,7 @@ func (s *AppServices) loadBux(ctx context.Context, appConfig *AppConfig, testMod } // Create the new client - s.Bux, err = bux.NewClient(ctx, options...) + s.SPV, err = bux.NewClient(ctx, options...) return } diff --git a/config/services_test.go b/config/services_test.go index 3ea4cb32c..6ed7afc51 100644 --- a/config/services_test.go +++ b/config/services_test.go @@ -34,7 +34,7 @@ func TestAppServices_CloseAll(t *testing.T) { require.NotNil(t, s) s.CloseAll(context.Background()) - assert.Nil(t, s.Bux) + assert.Nil(t, s.SPV) assert.Nil(t, s.NewRelic) }) } @@ -47,6 +47,6 @@ func TestAppConfig_GetUserAgent(t *testing.T) { ac := newTestConfig(t) require.NotNil(t, ac) agent := ac.GetUserAgent() - assert.Equal(t, "BUX-Server "+Version, agent) + assert.Equal(t, "SPV-Wallet "+Version, agent) }) } diff --git a/config/task_manager.go b/config/task_manager.go index 506367381..68ca84431 100644 --- a/config/task_manager.go +++ b/config/task_manager.go @@ -2,5 +2,5 @@ package config // TaskManager defaults const ( - TaskManagerQueueName = "bux_queue" + TaskManagerQueueName = "spv_queue" ) diff --git a/go.mod b/go.mod index 6a248c2b6..74abdcdf9 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/BuxOrg/bux-server +module github.com/BuxOrg/spv-wallet go 1.21.5 diff --git a/logging/logging.go b/logging/logging.go index 3ff5cbade..bae08dc8b 100644 --- a/logging/logging.go +++ b/logging/logging.go @@ -59,7 +59,7 @@ func GetDefaultLogger() *zerolog.Logger { logger := ecszerolog.New(os.Stdout, ecszerolog.Level(zerolog.DebugLevel)). With(). Caller(). - Str("application", "bux-default"). + Str("application", "spv-default"). Logger() return &logger diff --git a/mappings/access_keys.go b/mappings/access_keys.go index a6ce12708..4d139c4b2 100644 --- a/mappings/access_keys.go +++ b/mappings/access_keys.go @@ -5,12 +5,12 @@ import ( "time" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/mappings/common" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/mappings/common" ) -// MapToAccessKeyContract will map the access key to the bux-models contract -func MapToAccessKeyContract(ac *bux.AccessKey) *buxmodels.AccessKey { +// MapToAccessKeyContract will map the access key to the spv-wallet-models contract +func MapToAccessKeyContract(ac *bux.AccessKey) *spvwalletmodels.AccessKey { if ac == nil { return nil } @@ -20,7 +20,7 @@ func MapToAccessKeyContract(ac *bux.AccessKey) *buxmodels.AccessKey { revokedAt = &ac.RevokedAt.Time } - return &buxmodels.AccessKey{ + return &spvwalletmodels.AccessKey{ Model: *common.MapToContract(&ac.Model), ID: ac.ID, XpubID: ac.XpubID, diff --git a/mappings/admin.go b/mappings/admin.go index e86e9029c..04a28d7a2 100644 --- a/mappings/admin.go +++ b/mappings/admin.go @@ -2,16 +2,16 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" + spvwalletmodels "github.com/BuxOrg/bux-models" ) -// MapToAdminStatsContract will map the model from bux to the bux-models contract -func MapToAdminStatsContract(s *bux.AdminStats) *buxmodels.AdminStats { +// MapToAdminStatsContract will map the model from spv-wallet to the spv-wallet-models contract +func MapToAdminStatsContract(s *bux.AdminStats) *spvwalletmodels.AdminStats { if s == nil { return nil } - return &buxmodels.AdminStats{ + return &spvwalletmodels.AdminStats{ Balance: s.Balance, Destinations: s.Destinations, PaymailAddresses: s.PaymailAddresses, diff --git a/mappings/common/common.go b/mappings/common/common.go index fff1f9a5e..5017b79fa 100644 --- a/mappings/common/common.go +++ b/mappings/common/common.go @@ -6,7 +6,7 @@ import ( "github.com/BuxOrg/bux-models/common" ) -// MapToContract will map the common model to the bux-models contract +// MapToContract will map the common model to the spv-wallet-models contract func MapToContract(m *bux.Model) *common.Model { if m == nil { return nil @@ -20,7 +20,7 @@ func MapToContract(m *bux.Model) *common.Model { } } -// MapToModel will map the bux-models contract to the common bux model +// MapToModel will map the spv-wallet-models contract to the common spv model func MapToModel(m *common.Model) *bux.Model { if m == nil { return nil diff --git a/mappings/destination.go b/mappings/destination.go index 98832aff1..04e6b491f 100644 --- a/mappings/destination.go +++ b/mappings/destination.go @@ -2,17 +2,17 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/mappings/common" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/mappings/common" ) -// MapToDestinationContract will map the bux destination model to the bux-models contract -func MapToDestinationContract(d *bux.Destination) *buxmodels.Destination { +// MapToDestinationContract will map the spv-wallet destination model to the spv-wallet-models contract +func MapToDestinationContract(d *bux.Destination) *spvwalletmodels.Destination { if d == nil { return nil } - return &buxmodels.Destination{ + return &spvwalletmodels.Destination{ Model: *common.MapToContract(&d.Model), ID: d.ID, XpubID: d.XpubID, @@ -25,8 +25,8 @@ func MapToDestinationContract(d *bux.Destination) *buxmodels.Destination { } } -// MapToDestinationBux will map the bux-models destination contract to the bux destination model -func MapToDestinationBux(d *buxmodels.Destination) *bux.Destination { +// MapToDestinationSPV will map the spv-wallet-models destination contract to the spv-wallet destination model +func MapToDestinationSPV(d *spvwalletmodels.Destination) *bux.Destination { if d == nil { return nil } diff --git a/mappings/fee_unit.go b/mappings/fee_unit.go index 516807051..8078b62c9 100644 --- a/mappings/fee_unit.go +++ b/mappings/fee_unit.go @@ -1,24 +1,24 @@ package mappings import ( - buxmodels "github.com/BuxOrg/bux-models" + spvwalletmodels "github.com/BuxOrg/bux-models" "github.com/BuxOrg/bux/utils" ) -// MapToFeeUnitContract will map the fee-unit model from bux to the bux-models contract -func MapToFeeUnitContract(fu *utils.FeeUnit) (fc *buxmodels.FeeUnit) { +// MapToFeeUnitContract will map the fee-unit model from spv-wallet to the spv-wallet-models contract +func MapToFeeUnitContract(fu *utils.FeeUnit) (fc *spvwalletmodels.FeeUnit) { if fu == nil { return nil } - return &buxmodels.FeeUnit{ + return &spvwalletmodels.FeeUnit{ Satoshis: fu.Satoshis, Bytes: fu.Bytes, } } -// MapToFeeUnitBux will map the fee-unit model from bux-models to the bux contract -func MapToFeeUnitBux(fu *buxmodels.FeeUnit) (fc *utils.FeeUnit) { +// MapToFeeUnitSPV will map the fee-unit model from spv-wallet-models to the spv-wallet contract +func MapToFeeUnitSPV(fu *spvwalletmodels.FeeUnit) (fc *utils.FeeUnit) { if fu == nil { return nil } diff --git a/mappings/metadata.go b/mappings/metadata.go index 08c777470..fcc1fca7f 100644 --- a/mappings/metadata.go +++ b/mappings/metadata.go @@ -2,11 +2,11 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" + spvwalletmodels "github.com/BuxOrg/bux-models" ) -// MapToBuxMetadata will map the *buxmodels.Metadata to *bux.Metadata -func MapToBuxMetadata(metadata *buxmodels.Metadata) *bux.Metadata { +// MapToSPVMetadata will map the *spvwalletmodels.Metadata to *spv.Metadata +func MapToSPVMetadata(metadata *spvwalletmodels.Metadata) *bux.Metadata { if metadata == nil { return nil } diff --git a/mappings/paymail_address.go b/mappings/paymail_address.go index 81b06a24f..a52461015 100644 --- a/mappings/paymail_address.go +++ b/mappings/paymail_address.go @@ -2,17 +2,17 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/mappings/common" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/mappings/common" ) -// MapToPaymailContract will map the bux paymail-address model to the bux-models contract -func MapToPaymailContract(pa *bux.PaymailAddress) *buxmodels.PaymailAddress { +// MapToPaymailContract will map the spv-wallet paymail-address model to the spv-wallet-models contract +func MapToPaymailContract(pa *bux.PaymailAddress) *spvwalletmodels.PaymailAddress { if pa == nil { return nil } - return &buxmodels.PaymailAddress{ + return &spvwalletmodels.PaymailAddress{ Model: *common.MapToContract(&pa.Model), ID: pa.ID, XpubID: pa.XpubID, @@ -24,13 +24,13 @@ func MapToPaymailContract(pa *bux.PaymailAddress) *buxmodels.PaymailAddress { } } -// MapToPaymailP4Contract will map the bux-models paymail-address contract to the bux paymail-address model -func MapToPaymailP4Contract(p *bux.PaymailP4) *buxmodels.PaymailP4 { +// MapToPaymailP4Contract will map the spv-wallet-models paymail-address contract to the spv-wallet paymail-address model +func MapToPaymailP4Contract(p *bux.PaymailP4) *spvwalletmodels.PaymailP4 { if p == nil { return nil } - return &buxmodels.PaymailP4{ + return &spvwalletmodels.PaymailP4{ Alias: p.Alias, Domain: p.Domain, FromPaymail: p.FromPaymail, @@ -42,8 +42,8 @@ func MapToPaymailP4Contract(p *bux.PaymailP4) *buxmodels.PaymailP4 { } } -// MapToPaymailP4Bux will map the bux-models paymail-address contract to the bux paymail-address model -func MapToPaymailP4Bux(p *buxmodels.PaymailP4) *bux.PaymailP4 { +// MapToPaymailP4SPV will map the spv-wallet-models paymail-address contract to the spv-wallet paymail-address model +func MapToPaymailP4SPV(p *spvwalletmodels.PaymailP4) *bux.PaymailP4 { if p == nil { return nil } diff --git a/mappings/script_output.go b/mappings/script_output.go index cbe02816c..8daa96dda 100644 --- a/mappings/script_output.go +++ b/mappings/script_output.go @@ -2,16 +2,16 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" + spvwalletmodels "github.com/BuxOrg/bux-models" ) -// MapToScriptOutputContract will map the script-output model from bux to the bux-models contract -func MapToScriptOutputContract(so *bux.ScriptOutput) (sc *buxmodels.ScriptOutput) { +// MapToScriptOutputContract will map the script-output model from spv-wallet to the spv-wallet-models contract +func MapToScriptOutputContract(so *bux.ScriptOutput) (sc *spvwalletmodels.ScriptOutput) { if so == nil { return nil } - return &buxmodels.ScriptOutput{ + return &spvwalletmodels.ScriptOutput{ Address: so.Address, Satoshis: so.Satoshis, Script: so.Script, @@ -19,8 +19,8 @@ func MapToScriptOutputContract(so *bux.ScriptOutput) (sc *buxmodels.ScriptOutput } } -// MapToScriptOutputBux will map the script-output model from bux-models to the bux contract -func MapToScriptOutputBux(so *buxmodels.ScriptOutput) (sc *bux.ScriptOutput) { +// MapToScriptOutputSPV will map the script-output model from spv-wallet-models to the spv-wallet contract +func MapToScriptOutputSPV(so *spvwalletmodels.ScriptOutput) (sc *bux.ScriptOutput) { if so == nil { return nil } diff --git a/mappings/sync_config.go b/mappings/sync_config.go index 322fe3e71..46dbf0a61 100644 --- a/mappings/sync_config.go +++ b/mappings/sync_config.go @@ -2,16 +2,16 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" + spvwalletmodels "github.com/BuxOrg/bux-models" ) -// MapToSyncConfigContract will map the sync-config model from bux to the bux-models contract -func MapToSyncConfigContract(sc *bux.SyncConfig) *buxmodels.SyncConfig { +// MapToSyncConfigContract will map the sync-config model from spv-wallet to the spv-wallet-models contract +func MapToSyncConfigContract(sc *bux.SyncConfig) *spvwalletmodels.SyncConfig { if sc == nil { return nil } - return &buxmodels.SyncConfig{ + return &spvwalletmodels.SyncConfig{ Broadcast: sc.Broadcast, BroadcastInstant: sc.BroadcastInstant, PaymailP2P: sc.PaymailP2P, @@ -19,8 +19,8 @@ func MapToSyncConfigContract(sc *bux.SyncConfig) *buxmodels.SyncConfig { } } -// MapToSyncConfigBux will map the sync-config model from bux-models to the bux contract -func MapToSyncConfigBux(sc *buxmodels.SyncConfig) *bux.SyncConfig { +// MapToSyncConfigSPV will map the sync-config model from spv-wallet-models to the spv-wallet contract +func MapToSyncConfigSPV(sc *spvwalletmodels.SyncConfig) *bux.SyncConfig { if sc == nil { return nil } diff --git a/mappings/transaction.go b/mappings/transaction.go index 14777ccc5..0a447a7de 100644 --- a/mappings/transaction.go +++ b/mappings/transaction.go @@ -2,17 +2,17 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/mappings/common" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/mappings/common" ) -// MapToTransactionContract will map the model from bux to the bux-models contract -func MapToTransactionContract(t *bux.Transaction) *buxmodels.Transaction { +// MapToTransactionContract will map the model from spv-wallet to the spv-wallet-models contract +func MapToTransactionContract(t *bux.Transaction) *spvwalletmodels.Transaction { if t == nil { return nil } - model := buxmodels.Transaction{ + model := spvwalletmodels.Transaction{ Model: *common.MapToContract(&t.Model), ID: t.ID, Hex: t.Hex, @@ -35,13 +35,13 @@ func MapToTransactionContract(t *bux.Transaction) *buxmodels.Transaction { return &model } -// MapToTransactionContractForAdmin will map the model from bux to the bux-models contract for admin -func MapToTransactionContractForAdmin(t *bux.Transaction) *buxmodels.Transaction { +// MapToTransactionContractForAdmin will map the model from spv-wallet to the spv-wallet-models contract for admin +func MapToTransactionContractForAdmin(t *bux.Transaction) *spvwalletmodels.Transaction { if t == nil { return nil } - model := buxmodels.Transaction{ + model := spvwalletmodels.Transaction{ Model: *common.MapToContract(&t.Model), ID: t.ID, Hex: t.Hex, @@ -63,10 +63,10 @@ func MapToTransactionContractForAdmin(t *bux.Transaction) *buxmodels.Transaction return &model } -func processMetadata(t *bux.Transaction, xpubID string, model *buxmodels.Transaction) { +func processMetadata(t *bux.Transaction, xpubID string, model *spvwalletmodels.Transaction) { if len(t.XpubMetadata) > 0 && len(t.XpubMetadata[xpubID]) > 0 { if t.Model.Metadata == nil { - model.Model.Metadata = make(buxmodels.Metadata) + model.Model.Metadata = make(spvwalletmodels.Metadata) } for key, value := range t.XpubMetadata[xpubID] { model.Model.Metadata[key] = value @@ -74,7 +74,7 @@ func processMetadata(t *bux.Transaction, xpubID string, model *buxmodels.Transac } } -func processOutputValue(t *bux.Transaction, xpubID string, model *buxmodels.Transaction) { +func processOutputValue(t *bux.Transaction, xpubID string, model *spvwalletmodels.Transaction) { model.OutputValue = int64(0) if len(t.XpubOutputValue) > 0 && t.XpubOutputValue[xpubID] != 0 { model.OutputValue = t.XpubOutputValue[xpubID] @@ -87,8 +87,8 @@ func processOutputValue(t *bux.Transaction, xpubID string, model *buxmodels.Tran } } -// MapToTransactionBux will map the model from bux-models to the bux contract -func MapToTransactionBux(t *buxmodels.Transaction) *bux.Transaction { +// MapToTransactionSPV will map the model from spv-wallet-models to the spv-wallet contract +func MapToTransactionSPV(t *spvwalletmodels.Transaction) *bux.Transaction { if t == nil { return nil } @@ -111,97 +111,97 @@ func MapToTransactionBux(t *buxmodels.Transaction) *bux.Transaction { } } -// MapToTransactionConfigBux will map the transaction-config model from bux to the bux-models contract -func MapToTransactionConfigBux(tx *buxmodels.TransactionConfig) *bux.TransactionConfig { +// MapToTransactionConfigSPV will map the transaction-config model from spv-wallet to the spv-wallet-models contract +func MapToTransactionConfigSPV(tx *spvwalletmodels.TransactionConfig) *bux.TransactionConfig { if tx == nil { return nil } return &bux.TransactionConfig{ - ChangeDestinations: mapToBuxDestinations(tx), + ChangeDestinations: mapToSPVDestinations(tx), ChangeDestinationsStrategy: bux.ChangeStrategy(tx.ChangeStrategy), ChangeMinimumSatoshis: tx.ChangeMinimumSatoshis, ChangeNumberOfDestinations: tx.ChangeNumberOfDestinations, ChangeSatoshis: tx.ChangeSatoshis, ExpiresIn: tx.ExpiresIn, Fee: tx.Fee, - FeeUnit: MapToFeeUnitBux(tx.FeeUnit), - FromUtxos: mapToBuxFromUtxos(tx), - IncludeUtxos: mapToBuxIncludeUtxos(tx), - Inputs: mapToBuxInputs(tx), - Outputs: mapToBuxOutputs(tx), - SendAllTo: MapToTransactionOutputBux(tx.SendAllTo), - Sync: MapToSyncConfigBux(tx.Sync), + FeeUnit: MapToFeeUnitSPV(tx.FeeUnit), + FromUtxos: mapToSPVFromUtxos(tx), + IncludeUtxos: mapToSPVIncludeUtxos(tx), + Inputs: mapToSPVInputs(tx), + Outputs: mapToSPVOutputs(tx), + SendAllTo: MapToTransactionOutputSPV(tx.SendAllTo), + Sync: MapToSyncConfigSPV(tx.Sync), } } -func mapToBuxOutputs(tx *buxmodels.TransactionConfig) []*bux.TransactionOutput { +func mapToSPVOutputs(tx *spvwalletmodels.TransactionConfig) []*bux.TransactionOutput { if tx.Outputs == nil { return nil } outputs := make([]*bux.TransactionOutput, 0) for _, output := range tx.Outputs { - outputs = append(outputs, MapToTransactionOutputBux(output)) + outputs = append(outputs, MapToTransactionOutputSPV(output)) } return outputs } -func mapToBuxInputs(tx *buxmodels.TransactionConfig) []*bux.TransactionInput { +func mapToSPVInputs(tx *spvwalletmodels.TransactionConfig) []*bux.TransactionInput { if tx.Inputs == nil { return nil } inputs := make([]*bux.TransactionInput, 0) for _, input := range tx.Inputs { - inputs = append(inputs, MapToTransactionInputBux(input)) + inputs = append(inputs, MapToTransactionInputSPV(input)) } return inputs } -func mapToBuxIncludeUtxos(tx *buxmodels.TransactionConfig) []*bux.UtxoPointer { +func mapToSPVIncludeUtxos(tx *spvwalletmodels.TransactionConfig) []*bux.UtxoPointer { if tx.IncludeUtxos == nil { return nil } includeUtxos := make([]*bux.UtxoPointer, 0) for _, utxo := range tx.IncludeUtxos { - includeUtxos = append(includeUtxos, MapToUtxoPointerBux(utxo)) + includeUtxos = append(includeUtxos, MapToUtxoPointerSPV(utxo)) } return includeUtxos } -func mapToBuxFromUtxos(tx *buxmodels.TransactionConfig) []*bux.UtxoPointer { +func mapToSPVFromUtxos(tx *spvwalletmodels.TransactionConfig) []*bux.UtxoPointer { if tx.FromUtxos == nil { return nil } fromUtxos := make([]*bux.UtxoPointer, 0) for _, utxo := range tx.FromUtxos { - fromUtxos = append(fromUtxos, MapToUtxoPointerBux(utxo)) + fromUtxos = append(fromUtxos, MapToUtxoPointerSPV(utxo)) } return fromUtxos } -func mapToBuxDestinations(tx *buxmodels.TransactionConfig) []*bux.Destination { +func mapToSPVDestinations(tx *spvwalletmodels.TransactionConfig) []*bux.Destination { if tx.ChangeDestinations == nil { return nil } destinations := make([]*bux.Destination, 0) for _, destination := range tx.ChangeDestinations { - destinations = append(destinations, MapToDestinationBux(destination)) + destinations = append(destinations, MapToDestinationSPV(destination)) } return destinations } -// MapToTransactionConfigContract will map the transaction-config model from bux-models to the bux contract -func MapToTransactionConfigContract(tx *bux.TransactionConfig) *buxmodels.TransactionConfig { +// MapToTransactionConfigContract will map the transaction-config model from spv-wallet-models to the spv-wallet contract +func MapToTransactionConfigContract(tx *bux.TransactionConfig) *spvwalletmodels.TransactionConfig { if tx == nil { return nil } - return &buxmodels.TransactionConfig{ + return &spvwalletmodels.TransactionConfig{ ChangeDestinations: mapToContractDestinations(tx), ChangeStrategy: string(tx.ChangeDestinationsStrategy), ChangeMinimumSatoshis: tx.ChangeMinimumSatoshis, @@ -218,73 +218,73 @@ func MapToTransactionConfigContract(tx *bux.TransactionConfig) *buxmodels.Transa } } -func mapToContractOutputs(tx *bux.TransactionConfig) []*buxmodels.TransactionOutput { +func mapToContractOutputs(tx *bux.TransactionConfig) []*spvwalletmodels.TransactionOutput { if tx.Outputs == nil { return nil } - outputs := make([]*buxmodels.TransactionOutput, 0) + outputs := make([]*spvwalletmodels.TransactionOutput, 0) for _, output := range tx.Outputs { outputs = append(outputs, MapToTransactionOutputContract(output)) } return outputs } -func mapToContractInputs(tx *bux.TransactionConfig) []*buxmodels.TransactionInput { +func mapToContractInputs(tx *bux.TransactionConfig) []*spvwalletmodels.TransactionInput { if tx.Inputs == nil { return nil } - inputs := make([]*buxmodels.TransactionInput, 0) + inputs := make([]*spvwalletmodels.TransactionInput, 0) for _, input := range tx.Inputs { inputs = append(inputs, MapToTransactionInputContract(input)) } return inputs } -func mapToContractIncludeUtxos(tx *bux.TransactionConfig) []*buxmodels.UtxoPointer { +func mapToContractIncludeUtxos(tx *bux.TransactionConfig) []*spvwalletmodels.UtxoPointer { if tx.IncludeUtxos == nil { return nil } - includeUtxos := make([]*buxmodels.UtxoPointer, 0) + includeUtxos := make([]*spvwalletmodels.UtxoPointer, 0) for _, utxo := range tx.IncludeUtxos { includeUtxos = append(includeUtxos, MapToUtxoPointer(utxo)) } return includeUtxos } -func mapToContractFromUtxos(tx *bux.TransactionConfig) []*buxmodels.UtxoPointer { +func mapToContractFromUtxos(tx *bux.TransactionConfig) []*spvwalletmodels.UtxoPointer { if tx.FromUtxos == nil { return nil } - fromUtxos := make([]*buxmodels.UtxoPointer, 0) + fromUtxos := make([]*spvwalletmodels.UtxoPointer, 0) for _, utxo := range tx.FromUtxos { fromUtxos = append(fromUtxos, MapToUtxoPointer(utxo)) } return fromUtxos } -func mapToContractDestinations(tx *bux.TransactionConfig) []*buxmodels.Destination { +func mapToContractDestinations(tx *bux.TransactionConfig) []*spvwalletmodels.Destination { if tx.ChangeDestinations == nil { return nil } - destinations := make([]*buxmodels.Destination, 0) + destinations := make([]*spvwalletmodels.Destination, 0) for _, destination := range tx.ChangeDestinations { destinations = append(destinations, MapToDestinationContract(destination)) } return destinations } -// MapToDraftTransactionContract will map the transaction-output model from bux to the bux-models contract -func MapToDraftTransactionContract(tx *bux.DraftTransaction) *buxmodels.DraftTransaction { +// MapToDraftTransactionContract will map the transaction-output model from spv-wallet to the spv-wallet-models contract +func MapToDraftTransactionContract(tx *bux.DraftTransaction) *spvwalletmodels.DraftTransaction { if tx == nil { return nil } - return &buxmodels.DraftTransaction{ + return &spvwalletmodels.DraftTransaction{ Model: *common.MapToContract(&tx.Model), ID: tx.ID, Hex: tx.Hex, @@ -294,42 +294,42 @@ func MapToDraftTransactionContract(tx *bux.DraftTransaction) *buxmodels.DraftTra } } -// MapToTransactionInputContract will map the transaction-output model from bux-models to the bux contract -func MapToTransactionInputContract(inp *bux.TransactionInput) *buxmodels.TransactionInput { +// MapToTransactionInputContract will map the transaction-output model from spv-wallet-models to the spv-wallet contract +func MapToTransactionInputContract(inp *bux.TransactionInput) *spvwalletmodels.TransactionInput { if inp == nil { return nil } - return &buxmodels.TransactionInput{ + return &spvwalletmodels.TransactionInput{ Utxo: *MapToUtxoContract(&inp.Utxo), Destination: *MapToDestinationContract(&inp.Destination), } } -// MapToTransactionInputBux will map the transaction-output model from bux to the bux-models contract -func MapToTransactionInputBux(inp *buxmodels.TransactionInput) *bux.TransactionInput { +// MapToTransactionInputSPV will map the transaction-output model from spv-wallet to the spv-wallet-models contract +func MapToTransactionInputSPV(inp *spvwalletmodels.TransactionInput) *bux.TransactionInput { if inp == nil { return nil } return &bux.TransactionInput{ - Utxo: *MapToUtxoBux(&inp.Utxo), - Destination: *MapToDestinationBux(&inp.Destination), + Utxo: *MapToUtxoSPV(&inp.Utxo), + Destination: *MapToDestinationSPV(&inp.Destination), } } -// MapToTransactionOutputContract will map the transaction-output model from bux to the bux-models contract -func MapToTransactionOutputContract(out *bux.TransactionOutput) *buxmodels.TransactionOutput { +// MapToTransactionOutputContract will map the transaction-output model from spv-wallet to the spv-wallet-models contract +func MapToTransactionOutputContract(out *bux.TransactionOutput) *spvwalletmodels.TransactionOutput { if out == nil { return nil } - scriptOutputs := make([]*buxmodels.ScriptOutput, 0) + scriptOutputs := make([]*spvwalletmodels.ScriptOutput, 0) for _, scriptOutput := range out.Scripts { scriptOutputs = append(scriptOutputs, MapToScriptOutputContract(scriptOutput)) } - return &buxmodels.TransactionOutput{ + return &spvwalletmodels.TransactionOutput{ OpReturn: MapToOpReturnContract(out.OpReturn), PaymailP4: MapToPaymailP4Contract(out.PaymailP4), Satoshis: out.Satoshis, @@ -340,20 +340,20 @@ func MapToTransactionOutputContract(out *bux.TransactionOutput) *buxmodels.Trans } } -// MapToTransactionOutputBux will map the transaction-output model from bux-models to the bux contract -func MapToTransactionOutputBux(out *buxmodels.TransactionOutput) *bux.TransactionOutput { +// MapToTransactionOutputSPV will map the transaction-output model from spv-wallet-models to the spv-wallet contract +func MapToTransactionOutputSPV(out *spvwalletmodels.TransactionOutput) *bux.TransactionOutput { if out == nil { return nil } scriptOutputs := make([]*bux.ScriptOutput, 0) for _, scriptOutput := range out.Scripts { - scriptOutputs = append(scriptOutputs, MapToScriptOutputBux(scriptOutput)) + scriptOutputs = append(scriptOutputs, MapToScriptOutputSPV(scriptOutput)) } return &bux.TransactionOutput{ - OpReturn: MapToOpReturnBux(out.OpReturn), - PaymailP4: MapToPaymailP4Bux(out.PaymailP4), + OpReturn: MapToOpReturnSPV(out.OpReturn), + PaymailP4: MapToPaymailP4SPV(out.PaymailP4), Satoshis: out.Satoshis, Script: out.Script, Scripts: scriptOutputs, @@ -362,21 +362,21 @@ func MapToTransactionOutputBux(out *buxmodels.TransactionOutput) *bux.Transactio } } -// MapToMapProtocolContract will map the transaction-output model from bux to the bux-models contract -func MapToMapProtocolContract(mp *bux.MapProtocol) *buxmodels.MapProtocol { +// MapToMapProtocolContract will map the transaction-output model from spv-wallet to the spv-wallet-models contract +func MapToMapProtocolContract(mp *bux.MapProtocol) *spvwalletmodels.MapProtocol { if mp == nil { return nil } - return &buxmodels.MapProtocol{ + return &spvwalletmodels.MapProtocol{ App: mp.App, Keys: mp.Keys, Type: mp.Type, } } -// MapToMapProtocolBux will map the transaction-output model from bux-models to the bux contract -func MapToMapProtocolBux(mp *buxmodels.MapProtocol) *bux.MapProtocol { +// MapToMapProtocolSPV will map the transaction-output model from spv-wallet-models to the spv-wallet contract +func MapToMapProtocolSPV(mp *spvwalletmodels.MapProtocol) *bux.MapProtocol { if mp == nil { return nil } @@ -388,13 +388,13 @@ func MapToMapProtocolBux(mp *buxmodels.MapProtocol) *bux.MapProtocol { } } -// MapToOpReturnContract will map the transaction-output model from bux to the bux-models contract -func MapToOpReturnContract(op *bux.OpReturn) *buxmodels.OpReturn { +// MapToOpReturnContract will map the transaction-output model from spv-wallet to the spv-wallet-models contract +func MapToOpReturnContract(op *bux.OpReturn) *spvwalletmodels.OpReturn { if op == nil { return nil } - return &buxmodels.OpReturn{ + return &spvwalletmodels.OpReturn{ Hex: op.Hex, HexParts: op.HexParts, Map: MapToMapProtocolContract(op.Map), @@ -402,8 +402,8 @@ func MapToOpReturnContract(op *bux.OpReturn) *buxmodels.OpReturn { } } -// MapToOpReturnBux will map the op-return model from bux-models to the bux contract -func MapToOpReturnBux(op *buxmodels.OpReturn) *bux.OpReturn { +// MapToOpReturnSPV will map the op-return model from spv-wallet-models to the spv-wallet contract +func MapToOpReturnSPV(op *spvwalletmodels.OpReturn) *bux.OpReturn { if op == nil { return nil } @@ -411,7 +411,7 @@ func MapToOpReturnBux(op *buxmodels.OpReturn) *bux.OpReturn { return &bux.OpReturn{ Hex: op.Hex, HexParts: op.HexParts, - Map: MapToMapProtocolBux(op.Map), + Map: MapToMapProtocolSPV(op.Map), StringParts: op.StringParts, } } diff --git a/mappings/utxo.go b/mappings/utxo.go index 060a6f12e..e00bf6feb 100644 --- a/mappings/utxo.go +++ b/mappings/utxo.go @@ -2,25 +2,25 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/mappings/common" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/mappings/common" customtypes "github.com/mrz1836/go-datastore/custom_types" ) -// MapToUtxoPointer will map the utxo-pointer model from bux to the bux-models contract -func MapToUtxoPointer(u *bux.UtxoPointer) *buxmodels.UtxoPointer { +// MapToUtxoPointer will map the utxo-pointer model from spv-wallet to the spv-wallet-models contract +func MapToUtxoPointer(u *bux.UtxoPointer) *spvwalletmodels.UtxoPointer { if u == nil { return nil } - return &buxmodels.UtxoPointer{ + return &spvwalletmodels.UtxoPointer{ TransactionID: u.TransactionID, OutputIndex: u.OutputIndex, } } -// MapToUtxoPointerBux will map the utxo-pointer model from bux-models to the bux contract -func MapToUtxoPointerBux(u *buxmodels.UtxoPointer) *bux.UtxoPointer { +// MapToUtxoPointerSPV will map the utxo-pointer model from spv-wallet-models to the spv-wallet contract +func MapToUtxoPointerSPV(u *spvwalletmodels.UtxoPointer) *bux.UtxoPointer { if u == nil { return nil } @@ -31,13 +31,13 @@ func MapToUtxoPointerBux(u *buxmodels.UtxoPointer) *bux.UtxoPointer { } } -// MapToUtxoContract will map the utxo model from bux to the bux-models contract -func MapToUtxoContract(u *bux.Utxo) *buxmodels.Utxo { +// MapToUtxoContract will map the utxo model from spv-wallet to the spv-wallet-models contract +func MapToUtxoContract(u *bux.Utxo) *spvwalletmodels.Utxo { if u == nil { return nil } - return &buxmodels.Utxo{ + return &spvwalletmodels.Utxo{ Model: *common.MapToContract(&u.Model), UtxoPointer: *MapToUtxoPointer(&u.UtxoPointer), ID: u.ID, @@ -51,8 +51,8 @@ func MapToUtxoContract(u *bux.Utxo) *buxmodels.Utxo { } } -// MapToUtxoBux will map the utxo model from bux-models to the bux contract -func MapToUtxoBux(u *buxmodels.Utxo) *bux.Utxo { +// MapToUtxoSPV will map the utxo model from spv-wallet-models to the spv-wallet contract +func MapToUtxoSPV(u *spvwalletmodels.Utxo) *bux.Utxo { if u == nil { return nil } @@ -65,7 +65,7 @@ func MapToUtxoBux(u *buxmodels.Utxo) *bux.Utxo { return &bux.Utxo{ Model: *common.MapToModel(&u.Model), - UtxoPointer: *MapToUtxoPointerBux(&u.UtxoPointer), + UtxoPointer: *MapToUtxoPointerSPV(&u.UtxoPointer), ID: u.ID, XpubID: u.XpubID, Satoshis: u.Satoshis, @@ -73,6 +73,6 @@ func MapToUtxoBux(u *buxmodels.Utxo) *bux.Utxo { Type: u.Type, DraftID: draftID, SpendingTxID: spendingTxID, - Transaction: MapToTransactionBux(u.Transaction), + Transaction: MapToTransactionSPV(u.Transaction), } } diff --git a/mappings/xpub.go b/mappings/xpub.go index 8af5b1ce1..4d985b133 100644 --- a/mappings/xpub.go +++ b/mappings/xpub.go @@ -2,17 +2,17 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/mappings/common" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/mappings/common" ) -// MapToXpubContract will map the xpub model from bux to the bux-models contract -func MapToXpubContract(xpub *bux.Xpub) *buxmodels.Xpub { +// MapToXpubContract will map the xpub model from spv-wallet to the spv-wallet-models contract +func MapToXpubContract(xpub *bux.Xpub) *spvwalletmodels.Xpub { if xpub == nil { return nil } - return &buxmodels.Xpub{ + return &spvwalletmodels.Xpub{ Model: *common.MapToContract(&xpub.Model), ID: xpub.ID, CurrentBalance: xpub.CurrentBalance, diff --git a/metrics/collector.go b/metrics/collector.go index f6bff1128..c21b20ec5 100644 --- a/metrics/collector.go +++ b/metrics/collector.go @@ -1,17 +1,17 @@ package metrics import ( - buxmetrics "github.com/BuxOrg/bux/metrics" + spvwalletmodels "github.com/BuxOrg/bux/metrics" "github.com/prometheus/client_golang/prometheus" ) -// PrometheusCollector is a collector for Prometheus metrics. It should implement buxmetrics.Collector. +// PrometheusCollector is a collector for Prometheus metrics. It should implement spvwalletmodels.Collector. type PrometheusCollector struct { reg prometheus.Registerer } // NewPrometheusCollector creates a new PrometheusCollector. -func NewPrometheusCollector(reg prometheus.Registerer) buxmetrics.Collector { +func NewPrometheusCollector(reg prometheus.Registerer) spvwalletmodels.Collector { return &PrometheusCollector{reg: reg} } diff --git a/metrics/global.go b/metrics/global.go index a37372d44..2eafc1dd6 100644 --- a/metrics/global.go +++ b/metrics/global.go @@ -1,13 +1,13 @@ package metrics import ( - buxmetrics "github.com/BuxOrg/bux/metrics" + spvmetrics "github.com/BuxOrg/bux/metrics" ) var metrics *Metrics // EnableMetrics will enable the metrics for the application -func EnableMetrics() buxmetrics.Collector { +func EnableMetrics() spvmetrics.Collector { metrics = newMetrics() return NewPrometheusCollector(metrics.registerer) } diff --git a/metrics/naming.go b/metrics/naming.go index 7f33b99af..e0cb053d8 100644 --- a/metrics/naming.go +++ b/metrics/naming.go @@ -1,3 +1,3 @@ package metrics -const appName = "bux-server" +const appName = "spv-wallet" diff --git a/server/server.go b/server/server.go index 6524b284b..5ab9daf16 100644 --- a/server/server.go +++ b/server/server.go @@ -1,4 +1,4 @@ -// Package server is for all the BUX server settings and HTTP server +// Package server is for all the SPV wallet settings and HTTP server package server import ( @@ -7,16 +7,16 @@ import ( "net/http" "strconv" - accessKeys "github.com/BuxOrg/bux-server/actions/access_keys" - "github.com/BuxOrg/bux-server/actions/admin" - "github.com/BuxOrg/bux-server/actions/base" - "github.com/BuxOrg/bux-server/actions/destinations" - pmail "github.com/BuxOrg/bux-server/actions/paymail" - "github.com/BuxOrg/bux-server/actions/transactions" - "github.com/BuxOrg/bux-server/actions/utxos" - "github.com/BuxOrg/bux-server/actions/xpubs" - "github.com/BuxOrg/bux-server/config" - "github.com/BuxOrg/bux-server/metrics" + accessKeys "github.com/BuxOrg/spv-wallet/actions/access_keys" + "github.com/BuxOrg/spv-wallet/actions/admin" + "github.com/BuxOrg/spv-wallet/actions/base" + "github.com/BuxOrg/spv-wallet/actions/destinations" + pmail "github.com/BuxOrg/spv-wallet/actions/paymail" + "github.com/BuxOrg/spv-wallet/actions/transactions" + "github.com/BuxOrg/spv-wallet/actions/utxos" + "github.com/BuxOrg/spv-wallet/actions/xpubs" + "github.com/BuxOrg/spv-wallet/config" + "github.com/BuxOrg/spv-wallet/metrics" apirouter "github.com/mrz1836/go-api-router" "github.com/newrelic/go-agent/v3/integrations/nrhttprouter" httpSwagger "github.com/swaggo/http-swagger" diff --git a/server/server_test.go b/server/server_test.go index 6c497d282..3dae11bf2 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -3,7 +3,7 @@ package server import ( "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/bux_server.go b/spv_wallet.go similarity index 65% rename from bux_server.go rename to spv_wallet.go index 76ca3544c..b3c97d043 100644 --- a/bux_server.go +++ b/spv_wallet.go @@ -1,4 +1,4 @@ -// Package buxserver is a complete stand-alone server using the BUX Engine +// Package spv-wallet is a complete stand-alone server using the SPV Wallet Engine // // Run: go run cmd/server/main.go // @@ -6,4 +6,4 @@ // this GitHub repository! // // By BuxOrg (https://github.com/BuxOrg) -package buxserver +package spvwallet diff --git a/tests/tests.go b/tests/tests.go index 7cc75d1ef..8c864ab47 100644 --- a/tests/tests.go +++ b/tests/tests.go @@ -5,8 +5,8 @@ import ( "context" "os" - "github.com/BuxOrg/bux-server/config" - "github.com/BuxOrg/bux-server/logging" + "github.com/BuxOrg/spv-wallet/config" + "github.com/BuxOrg/spv-wallet/logging" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" From 3a027d34df6da9de912deb1897e03c279721c968 Mon Sep 17 00:00:00 2001 From: jakubmkowalski Date: Tue, 13 Feb 2024 15:42:15 +0100 Subject: [PATCH 02/50] chore(BUX-586): removes unnecessary files --- .all-contributorsrc | 49 ------------------------------ .github/FUNDING.yml | 4 --- .github/IMAGES/go-share-image.png | Bin 30104 -> 0 bytes 3 files changed, 53 deletions(-) delete mode 100644 .all-contributorsrc delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/IMAGES/go-share-image.png diff --git a/.all-contributorsrc b/.all-contributorsrc deleted file mode 100644 index 374d6df58..000000000 --- a/.all-contributorsrc +++ /dev/null @@ -1,49 +0,0 @@ -{ - "projectName": "bux-server", - "projectOwner": "BuxOrg", - "repoType": "github", - "repoHost": "https://github.com", - "files": [ - "README.md" - ], - "imageSize": 100, - "commit": false, - "commitConvention": "none", - "contributorsPerLine": 7, - "contributorsSortAlphabetically": false, - "contributors": [ - { - "login": "mrz1836", - "name": "Mr. Z", - "avatar_url": "https://avatars.githubusercontent.com/u/3743002?v=4", - "profile": "https://mrz1818.com", - "contributions": [ - "infra", - "code", - "maintenance", - "security" - ] - }, - { - "login": "icellan", - "name": "Siggi", - "avatar_url": "https://avatars.githubusercontent.com/u/4411176?v=4", - "profile": "https://github.com/icellan", - "contributions": [ - "infra", - "code", - "security" - ] - }, - { - "login": "galt-tr", - "name": "Dylan", - "avatar_url": "https://avatars.githubusercontent.com/u/64976002?v=4", - "profile": "https://github.com/galt-tr", - "contributions": [ - "infra", - "code" - ] - } - ] -} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index da6889a3c..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,4 +0,0 @@ -# These are supported funding model platforms - -github: BuxOrg -custom: https://gobitcoinsv.com/?utm_source=github&utm_medium=sponsor-link&utm_campaign=bux-server&utm_term=bux-server&utm_content=bux-server diff --git a/.github/IMAGES/go-share-image.png b/.github/IMAGES/go-share-image.png deleted file mode 100644 index 00a14d3ebc1c4761cb3c1228259565e7f8511f64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30104 zcmeEt^;cWp6K#UKyF)3Z#i3Y>(-vBwP}~Xbn&Oh+#icD?w8fp`?i4NVkl^kRJTLvc z_b0p`ziW}TZo&!NJ7;Fko;`CS)IKT@;?dy&006@Gif_~b05m870F=SOLVdzLGV<>i zxGsu%U;qH0{ND=*NXwuB0Ng*keuI*srB-^P{86)#6q)=tK(Lh?Tla& zIULrI&jhAS+7$J&ojMbOZ&W=B8)>nq?QRLqRMba1aEY>-h4C zl*Kszz^ep`#xhTmYC%x*bsMNj3?bS+qj-bsH0mxwKd=2F<=~hy6ANN zrOv&PuA8Cn0>H@rIMh8Zt-yQ^ME?C<$A9aOZZ~HWG=wODssk-VV?D<}#h~V+H@x?} zG&$G+j;T@{H>b`+!n_h^QHx~iAQ>TgQq+CN{QKsMR*LR}jYY~QzTz|zlSsah4Wz+^ z23BSFUwLAg<6Hg)HA{V((S!=zuFWe}n%!X0zbdlvC@e1uMq|+v%>Wy=-*bHCF(TU# z1IRK5d)|w7&KrJzh6i=QzBus(`FM1mtr*v`;f!{Qhi|w&S&&BSJw!K0yCnZ--@$)3 zz>)qQjE<(c=L1H*hTnKz%EXu2lg;a-P+kkDCM*oxRW|HUwDCZw8 zX~4ftOwR@=&Wv32=oRkvM($;$_%mikAaO?f4CWRb`GXWpG2WSa+i7B$hRbq%+1Xpf zH_48A5%ez)**qnw{^+KUHNSy|QPtwi6#VVgtl2d>be5H;mB`x_kbi8v)A@w|0Rgb| zN79(Xk|J*Qx2rgt;g_DQbG7KzLp8bU|I%2UU@;)kUBLb3buLG89 zOafD`4~ko(KxNI$*oM<^xoJ`V~vZ$wL3{HdoNmF{d z-Px!`TG?~3AD`3~HGPlO6)KLiCz@l?Mm#uO-6N)AF!=)8U-4t9-XF( zA!vy1O^B#ra{%61hi}Ky>kfcov}~2~#uV?*q^+F%Ll&IMM3w;>bnN)|ye#RF^4}=l z+=at)EVobu!9kt^@F(q$t)9i?Y2-;i@XG>0!{s_LwO{L*|8BoZRdPD zMCsEv*lB1^!^7XHmx4u@g}WF8^62$R>G@IVg@i`E7vDwUS79*X%Z~ly+{S;Q=0N`v zC!{pBcaEKEyIlL?VI6i7_wcIW(r$5)Y8-tG<}5dlbmbq5mBWm4HFn3ST)r5>gUo;;W=O~AXyWV}GKb;nhUnNJ zSE8ZJw-_@oGF}A=;M~X-5fJbuzq4TIwt z%kwY{zJS|3jV^i8SqvY&aVNzfeRN@#8wY-2^N(Xg*{)U%Fs!t#+*6IUHTkNK8r+cf zH-1o$I$-x39gdgwEW=wn**MU&%e=w>vlpMpne{E{D?JFWwi7BwV}gAwIO zwDIMdgV-8(kKOm_4p7LnvIk z<+--0IQUwLa zu{qS*I}iLzzB$;;7~9iPK^b6|QBoWPD@oNXJypnAZQQ>s6*DUUj0eZg=1*LIlDgQv z`LOy9JO4Xpz`jH^l&K=b^Oz764q4Z((bdn8b=X5~{=J6I97}Hm5VwG#2u{p z{G24{3Fv037nt%#G$#hhXHoHpD<)V1Arb!4hHd(JnRvukT3MrF5ruwe?DH{-VMxU>})HtN@a3}7OQjqZ7PX2oS^X^F3k_1u09>xe^zjO?Uw+K-7e>bkLXdp9|`FI z=q;^?8V3qAJPkX2OGqBU;NwRYONd!nG1SUik!6@sgaaXZdi5i}n|Yo0n#dn$*5P!8 zDbIReWQ`=B>a8d{%M?}BF@e5pgLaIx;t((>_}DVdPq8V>m!v5DeXIcpe}$&KmvQq( z1&aA*WI(A!ceZu-?$zI2QEyH|o z&~{@=v)3MHR|lSYPYvUqpQ>wFMp9%pBZr-G$B^9ANYdXmS>a75t2&V@cp@=1R=V!G11o@HY)@S}0Z}nt#j+q+#S#dN;QP5uvtkVKSTYmOKOpulHE@(ji z#!Oqe61}D1XK;o4KQZN;*b~JsNoCJ3Z%H)~&TBbd&5{`q(W(9V%ag?g>B+*Gu$gb- z#CpEIDrdE)s`EPz4NdJffJyt7?e~-h*8&w_V9@Af`LRDfSK;smyOvw<4^4F{#%CT4 zcX!f`)SqecZjEr_OBRxO;HRaub4e5>n@_9jC3!o!=H>q?26;QkUCwJq$@+5=H(It{ zY;?Hfx+~7gRWjA!`}@+U-h4hSLP(GgmDw%!$kFqq?sj1Qn=|r<`Naudf6~;Vv2riw zw+K!@1u0_7V?N4&tuuyT>uqOQiZK0^%<|UgrC&%~@U99@5Kiy9!cR}pT>m=~C{SDy zTQ_Vu+9*T-ivx#0hJkAP8NVxBi`{J2G!3h693g;8z3FpF$LJ050w)M+YJsY6!JKL* zB?=&dG+X#k#hCf;*uzfMb5B9a3N?CUxc{Oq=h4HLhv=rBnMYdo3daTXFf-zMo2mB(8_V`-nt z>L>og9O(Y|pyovqwwkG=M!cls8cRdoA+F$eC784}o8uLP<9-;jc3QEc`_IvR>Kp(Y z8RoBkt#di|fpq88&#xWXpFuM+N7e?-h@?FYC}e*<3_-!MQxk^MFdnOqvcrDNEfMu$ zu9$;?CS2U7eeulGew!uy!$UlWI=fl{$h zE+MFNeiDB7ce4D*i4U?3qP<=0C#ML zP9SUe^B>Ux(3%nw4G5;KE%tEF5dT9x3v~db_qp}Wt`mw<{7l}0Uxkw3AW)^nr29t{ zHJf&51PqAxd-C$GT_`6xec|e%kssZ-zKg>B3FbIrBKU3iK4#3g{MB4wQ2{H@51Zm! zY`__;AHV?>ui0~rUFs1X|2SE}f|cD5!O@wOgt7z?N)dGENvCv!&z-w#uSptMWRJW> zxp@ft`~a%I@AZis_aJgBm%xGXyeoBghWM~Y*11b*F1?!&{7I52XPM!Qkd#;GlAkfG zI*9T)%W<~ME7KyMr0YZ12=kgDG>IFz`tWNrtZC`|6i!)e=ptJq1}jMgXaHVKK_ zC}GA5AK!xm05pd2lAc}Q6HgWdBMPjfWtH?mn@PBi_Z8WySu#$izLj2Jts%ES`ZfUK z5n|>n)NbXv*L)n7FKOT_UB=R|@3E8Pn;kBVF&Z)m?HBiwYJ2AsMiNQ%yB30CMPJO7 z+<9;NQZWrxY~r8am%Zqtc@qdf^rkd{*aO3+uU`YV8I+=gwC)NbQfFb2{=ar-lO^kA z2@d&irM8B_j^WY(*Z^y?KU3e*5Fn{L>wDU}qqFtD))0cBfj}@m2n_`+7|8Jo7MG1r z4SDTzGxl5gncT|2ovD+JAIQqb06Zn=sei+k6#m%sYG{TS(52);Hac)K7~VB(`7^Pq z?kWydxVV=3l2@9#{(M=(uT`#OcJjw;d7Il7tL3p-E-D^-m6+p;%OxW=Sb;Tzm<7cbpTnY*Ult#ib67EXC9 zmB_?On5`P)u_m*E1nBmc>qM^ww1TSve>5OzeShAN79G zQEpg*K@)tKc7#UWa9V(AmcK4LZ!E2cDH-?Mv>E0l_eSsWlsNl~Ha7ca0i zo|)HF@6T$Nh+zz%O*3W9(No-1_H8vC?~+_r;{T-g@KwkhCboF7|D}tiXD1=iQFoUM zHc)y^^JWHne6ps+Q6sp#P486ftBU+>Oqq8PAU^3 z3ylzbg9u2m0|Q-Xv4_9LQ-75E0HI0ES;V(a2ghb4l{ZfOC&vz?VRwk;Pk3#)k!9pr zVa5qyB>>qDXkWVJho(-sNps>s_Ejlp2%^>()XPtV-jab_VU-Y44jM7*r;{oC4`hZ_ z6{1ED=z#HBd`<(b5$?1?@241+G-el^P`}xhVKQzeEy!41dVG76Z{9u{2;z07`gOgc@UtM}1 zORuG?1&UjVnL2x*TpFa0VgPY(EQT_bLT)d_JUsu)_tvHfBW!?TVDM9FF;UIz4-%G3 zn6u?sSVQ5`L>(#OKq1e@&Dq}<&wX!1eTS})5{IJGf?Pp{h+s-|c^fwy)NXQBm0=a>*L~-juTeHo&ePZD)*UCeS79X5@F{bkC zUIEnzRy4o(5&-atJ0R=SrcYNw8b1?3akprL?6-sCu~wa>*!*5955xC2Ywkfsb~eh z8c7vkpqmd&2;9*>B+~ zofz9C%9E70Gem3V9v@JCEd(gFQ*|7;K?49Pxm~!L6OAwdUrMoqUPs>NGQg66m$gW2 zc76pH zLXp|I`p#-?wf1I^K>1>>#FHNj=yB~po1A^=|FZqh=3W-d;Fk-6x<==JnZAiySnDlR|{4mOx$U)|HhVpCN-v$rCwyKCVhh$u`N<5(2 z3E^U}PCWBemCGOzWOT?3&+gW_>Y`7T?K}^H<&+AFmL+-GaOH=w5{PUro3)GwHoit~ z`(1uAaqxb<@kM$Gy$|D7@?ZJLW={oZmYCK_fH5@Kdh!`2>NZ5A=7$FR z-s#ivD9;dY%P3B8S*gMSJyTI2pM8HMLsFaOnI{Jh$;NktGda3w(r~&pZ)W+e*eHR; z_9K0C>NcJ~{ja~K1Fx9@^82!yAW|_d1eLM;4`;_GhES$gcvd{b_ke?h_D40o5u^f& z)b!5$#7z+h3(UqxGyjf`btYP2UVqT{XHqAJp2LrcT22Zx%(m<%CI-yHoxYP;CUb_>M)PtFCyA4ZYiZy3u(1>*J%ygU=ICgE+Dk=Bdl4K9{-4=n&)_~E_-Jtt zFj~r?*vF0j%*K)sM%FC)89FUtF>H&#C!BgTXEbyB0x+ru$+emtSGj;n8SZCmkuOX? zQ0C|ph?lr^bo;;=T%s6^$GblE_COb9;v72BK$M3ql3&mi(+SLJX?3c}1UZ8BEGyr= z#^@8M&?yRYCDNb`$v6a3>*IeQ(rC3)P45bkgGdVay(U5GD=jAcq=&7XbMXu5uwXhv ztIy>_4_)QMEr+##0O$Z1Ora#d6%GXfRMvmD+lfafd6?sNn0f5RtYAr)T)ss&gf75$ zCo2=on|w1aV@FAd@P9I|RSa6T|NcW!@TYsk>{exA8YO&mje+THUu*qA*q<9Z!bjNL zwy;Y_!4noYoMET5$&!#)lV~*SI71w4)e#2w1hijG)xWxkJeA+xPev2RZb=L^czD%h z#L<$gRTyh4jP|@9@D|oEpVb{B#po*EVep>X$_>qqBP(CP#xTS#LM>$T1Is$q@3yRP zICE%9BZ1DnbH{vdQk?^xIcz~9rJ%ktS$#t!yEix07a<*V*9WoFAxUUQ`;qW8mq2Gs z!IWD9f3gKD>M2kcWLO=%K2X4dSV0#XLkIE1)YdZmSOS-RHkNnlR5K8JHX-@fM=IB6y+dQm* z5!EC+_o?PQNj~c?_za$*!Uvkeaxie3XGv?uL(Q5$njc$#OdG?ymRH95n*3!ECE8jg zO|uN~7_p{Vv5UdtDGacs%wOgFIj%9Y9#^Zl{GaA~9>%#>J~p3dtG|nVEwonq%<@CZ z^fHa+1SiJeJq!MjCpJnR=kE$15aWd=^yP;*%r$$_a*dIdko8ZH?Yuztk6G8QY*W`I*odi&z)4<65Uq*>(hzWM zDi{g1Vd1vrRDDSRNJezUU$NG_Mtd%aCH}|`YQ@srm)3ogbxp6>yrNU+HkczH-+df} zs)JRIO}5V3v3SwZqU7Rl;d{O~nFbJQ<@gR2-xw2)NRl_qFeV=Fat)r1=uYKU%=HD& z>|dF7C__{uc@(zXtb9$ZQwUDmlQle%*Oa}lIY*%~r zLy4EA^q+qrk<%V(;^r*=LU7DTv@U-qA0Jv<3Yt^NKE|<)*S!N4r;z6>kN#a3JasX_ zlKLZ8D{Q&gEB^1jOmbwlP3KxEzrg7S{ytD(Czrk#OIj~)+_;CvYjY@W*fGx)s}tvq z)R#^AAszki^f?L;v|V%)DANNq=)SLMab|rih+e$8>-%c)5;CyvYI040=GM0GE0S{f zC8!4ya>3OeITh6oa9m4=p$apGBwJ`YR)ggN(iQ*qKxn||UT|U^nOioSbjLh+EX`Ny zWHv0hdU1YP9uD$xR&c#uUy^I^y&JnLNWE-lAI{gYcBerRraIv=z$;Y4Bx60_nD5G? z@pSO=rE!ZL-}5?s6@3nDC)^#H%S}+JtWcR`;nzTcSi~Ex`-c{6@fwe>4mI3FZ;UvO z_SHL6jBfNUv0O(%F0r0rS{Y;%8LC4zeBnYg8StJpqKm;7PAA-^;apLXS1Q&c`jaxe zx8(e2qH-UMpx?ZJ$`C5hFH!LFD}3`?y#|lg`bzc;8rsm(7WAg8yt-JuZ@0idm|Wtq z2TKr4fQvb9xe>8^`xQ;KHH3O;5Q+enl+AO?0w4gE2wxU=X5Is7Y*3G?tdhiv z<~iT`6MnS3UnKzmR5uFMZm9D=uh1?)Lk%-nT$U+elIRloL=Lz(a6*v}S4`L6HyZ&u zL10+6bhyF2IEb=1pjK>JrXB?MALmvEy$#k8!_!uJ(@z^MSFy2yW^J1 zRgB@rue>dBy|PBsQhXGobUOdUct~_lx#xf`km&{>=nElEU54N)dDHyrA2d~#`gDQD z;C9Xbty5uP@jbu{Mj}QI;x^*ES61_3;m7hn^4@8jHKn1>-VPuzx+5i6H(Z1h0}LOK z^gnmT6Q3?dm#7BSY}KVk1z;rYhgEwcX}ZRZ7Nzb{Z9+ZBJ{5rRqcH#4_tU6)_q^F> z-TJbTYYaP|l%sDig&DIy-KG&pjdwA28n)TZwk@KG<3v{s1qfNiu?46mM^-wNyVx`2 zE%W+a&lv5jg7I&$oKY!N1KBH%{q13Z=t1Q7qco=H14arZ2EYbvf~}+BF=_i~q4}8F=afsgHMm>Z9iDR*SNh~b~zKjYUF;843lAz5ehc&@D18IV(x@J zK5@BYb9u*3t%Lak@dJoz^Rd<3nXn37^us3ZKze#9RS{k0hn+0paHhx0x+^~42e9t# z(d+oAhnVv{&reC zt2X-)N6s1}%O_X{Ril-*x$mOu;%_rVvdffKn<>^D;cc>PD(shg=JY&uS%QthYoB74 zE#-^X%Xd|M!E60JG}{Er$T6wfR`7exQ~&#zC=kdS_+7I=Y?}lF*T2~bnxCIwoc2#> z&QB;QH50(<6Xzb2wYXZwv)cPRiR(q355s@Q8hQ0awdhX7%k%z~;3Q;aK|n6FyjA_| z>qR<}CP)IO+pra|v-`;YbH#H3@`pL^dmEnny=t0|6e?VT>b35dHm{zL92wOA-?i*vCqOr&$(3s zWTuY5BJd;fvr1^G+WZ4Zsn>sTZAHJKIKbtiIlPofpo37W+Htgbyb61S+IJW9juYTp zTh*u$;*Y8ReDNr`N9EeE8r~USayY(dZ6)-VAoeTybBhu~W`)~UoaSHoTM?yKnT)%L zwInI$-p3!Pf40=3e-^;FO3&Rg>ZjkYqspgwFxkmO>~X!BzcNN$!NE0}u0ZDU`EOi8 zA$=g#iJKN(DiZd4Z7+N)zts$uV`bRE-t8$YG&w}0rh+z-6Mm>^yzDe@)v zKNyyK%L4Y>=u=!K(L3BSI&OSF(nD>WTGl)kV@wWU@;7fI@j>m42s_4Tf&0CUV9X?V zq+)t*uT2|&JI2ojf|J);oBZByOC7C3S?f6NE<%2J=jxB59S4&%b;Y$A{7chYzXK2j zwYgpQgZ*#mEMM<@J6-Tgu%yG)`P7FmPwD0FFAAg|c4L+T@z3=PqwRTAu^c;@uE$GK zY%EVOP~5!#bT1APJ-g~fp3EEbq`(#jBRKxo3|_8>27kODCVTYOEZj2MyuQDKD0y1@ zlJD!tTJ5Y!f4XyK-+cWT+mDak`{$iyQx%5aj$fl^gF(ErxA#znjzC^fw#>clXi8yg zp9j)wIZ2YY)XEy+wmVw#Eaq37_CwXEcF+Jjbq5c5wKc%ZhGy%4*i^4-#^ZC$m#&Yf zb`DW_rf1-0fj>N278S?4RjX*a4^obRj#9>fx6D|E=ucOf*!5(8T60QM{=GUzVgsH% zQ|B*TSba;3xI5VtHPaoOuAo4Z5wwPv&2u~L6&Xc%pW2bl(~XR>na`PQZ}|%3ulW?; zj;M-?JMEo*yH`%Y*dQp`0s~+NEs^xOPF_aS&S?eZ1*f;eZaZlsG$$X>ZteYm2KeRz zlqTY>(0S!``{p@^zK%q@8OZ`@E8Q^COET;n-)`v6!&J7!S863w!~V$ggHRRMPS@m{k z-1dj!3R9-tZ0&=#-L6=ZMG60*QjLXPUvm2y<^68f!?O*y#>9v1jCCEzrq0Q!$vx&5 zGw^B)%{q-6y;!>41BaO_(TIcJ*$@A(mYAL80U^1s6AUxnqIwx(6&{)AU|csM$>g_w zM=z4rCUM>o%QfzLcCOy4q|96BMBjU8{nQ%stBM-mvA!yx8&h6znSDX74!-8JpS#i+wMZc14)| zz_TbT9(Wy&=4Jn}%$H!*=XI>=%BT5*tf}GR@0nl8i}KUsbnLkE*V3K)7Xu!58y%Ln zF6~s}%3cYyg&1dPUJCEI3pCKoLaW|%4_2Sg`FdjWlf>J%&4%{(O%;#i)VR0dh%E`P zOqPAlM77CPR@(kJs;%z|*2GA;cwDN6|B$AWP6fv@x1QmxTpnSc%$fvHgqhBfqZv?z zg@>P_CVSwSzDJ$NQ~UNX)rWIPtcBqGMVpJ0>Tm8kF*-4JVTh1FKV#^i4O>;{GF7AOOB2ss`fz^d|}MRgDexb^y?lh`ml9 z!JIO&DY2E5q5vtn*45f3bWNdz0aND&S9_(hgu@YKyS8o<+Kjq2Z?^Qsuw+`R$ui+* z0FMoNH+q|^Claa3Ys|>A3IJfqshPH`nwx--5FScx4CP$d?+V!DGU?=dnBZeg*E*%n zLNa`AJ>`304rO!sLSSRbk~blDK}FNuFMax6d2AT=E7`bfXNQw}NRBO`&@>gBOOvmZx4>KZ))P$M?4sw~ z%5hH$e(k>`jN*6G`?=A~>fX=tEE9b))A8m~r)$#}u(J3FP@nyA6*30M_@(5wjl`S3 zHW6;+nY=SjS$W-2x;EbMqB)0jM;q z{W*;fG4ZC@0x~virQft$9x7BbuZ252U96O53a_fC)@gC<*Np-H+AE>IMkp)`-vz$1 z(~9h;rf<&*&Y93oE%V77*s6n5%B=KVuR<+4pT3;+8x-#!gWQv&fMFx7*p^NT(KsfX zwAnSr?7cyHad`=kaE8#EAp1mFh3B8C+1RNL#>)Oq%Y58K1R#m|g(YLM?*C-K@xbd z z=)tb^8=tjRk#rt`E_)^E>7TFSMMz8{Oe zB2FT8it9ym%!}CC+uB!q!psWGm(3;&%;nODTCS98LfjIPD}MGp`l;oU7PRhBp@@xy zU?VZzf>oVbs}W`s)mg%1ruUo<$S0!j_(=*^f2IhR76aQ;t(3NP-?7C=i#GcUN)V>4 zl=y%8OHg7e?90NU^YVw&^!Xc+dO~98Axe@^|BC3}&V(@OR2Y}-Q1^?6_nSj>#Z_Ve zFvJ}x4#vq%`!&8r^WD zgkPQ0VgM3EIKqqv%KiMc3)pY~Z8Fri9?Q@`_#*sL!Cn#w0AZ{|8IJWQ?BCdK_#lNF zyexX})Hi!!M33kgPRLg+^+?vAFdk4w@OE(FFFrZ{^;K5?OA|De**xInIj`=Kff6rZ zRNOC+^UbHd!V~P5qS!DL1!H15d~s2iniIP2yRXv-gz4*PXyyTQOOW5qpH5do z*+iDMlh3~YD&XT0ZrP2yD)(2o)AwhSOoV2&mNlPb5`<7lc(E7?`4MOp*$EftJ`8tm zBK*Ym`s795=r2En9gBj(>9%#H=0H7e{L)RAv5yORwt;)5#rOK9Tlq&5h|eWi&#Jv0 zy#T+x?5BlBfms9Td*FWY{vqkowxrUb%`OywvHhuj#g}BMcYWm99tGYWl_kMo|N1^U z#>mceY^LwB=()10YOp|9Ka($kbNhn`;BGFzpx|_q57L-B(N7c1J>+%Z{74m~hUD?6 zekbG5O4h7T=-C?>E&$BsMDaP`1tDWdKXV|Mf8A^K3;?~L>ShtSo=KIU#@qa)hv|YWnd3rA}gf02Go{{Z5!yMnk zQ+ri9+-gP|!t}la{jNAX-wN%oy56t6Pq5FkHy(`>d#nJ4mE979=g~zROs{MHx)gs8 zcP6M90>taLkd@rtPT%(X+dvA<{>tV(M)f1GxL%k7-#X}8@?-`4Oy$M$B)-M(&sCHg z*~?TQtK?#~7(#ax-qfl)v#&}^s2-m%ctDY8d-4n56l?w8n`rct2yG2`FgUhxe!iHJ z@_pjT{rTAXitll6j(m9h$AZ&f=KE|lOCf?h5+cz^&d5-bK^plN-@8uN`&|*so=-?{ zU?^agkzlIu@yq30Tbt|6FL^Iz3oh&F5D#Bm53fE4a1Y6_iG*6T^zE4Rfo@lR5Nk0E zixEA)eD&tCAC}_0cZoq2dfgv^dNzlWT~Bivzp>jk-i+`QaP#ttT+(4V|H86RqC;EC4Gp^- z%NZ{The2u+nE;Xd%c=CE=!rQ6w96RCniseSZfL>>!gmQ zZ&Dh2@DNwE056;X=3WrJZ2e4YP9SCSfIn`_%kxZ$j4fg7_R|J;m^QYc6?YgGr4MRQ z?l4xnv|w=h5g{E%WYDy$|!MX#`4EwCZwe z%F8D|pk{u-vpt{^K-`F*6`B^%a%f)p&7A+?Pvpy3=Zll8-2P)FU}eY_FrjQl$1>FMG;KXn|iA) z4u>h~@^%}#Z$dS+?3y+3D#n9TO{2}+abIo6!S96z+#%_O20hEckHw2pB<{1s(6>Na zsWQ>=f|?8Ax*-%BnhaZYH)OMKJR5hYjS&bNPj~8B4cIr}(b1Mb>3s+DlRZzwtEqx} z^^_(Hf2xF)V}UKw@*@)9rWp4D8)+$t`6=c54aY`^x|c}gIzSjrS8yo)=fg2PAFaKf@Eb;oh2XZ-VS2G zN%$-4kyX3B%RlFea^3u&oXxiO;+)$fXA6DiJ^%Oc%{{AwiO|w1>DGxtr4-Fe6r0D1y1e^p^W7; zCqPYo^1tLBjy3mLBUl6LBxp=lUY~`GQq_Zoku>w70nC4n8Q-2wV<*HQHwU|z0XX3~ zp8kagjN2%8f?v&Kv3NC?=m0N7#(yr@XqKzj#k>oaDGp^($dfYMtk4?s(|2x=>^Rz3 zRo43T>1i!(Ih}nq^UAKh-2!9(ZXj&N|ZX{wJ~#4cb8FnmZ>GB zQ}~+kL(7gB{S~sLT65+9g$5p|x`pI9PC@E_Ve zzmMC@L)mt80r2erCLy7k>wP}y1(&-{5CC#|GbW$38TluVtBx4f#33t0yTS1+-(z^| z%?^W$urBdjHFwMU0IA=W^gZ_d5aiq$L~{vw?{3r&K^ZqL!4KJP6|79no4#yie`0RL@ZK|nQ<94T?2ZPl z35;^5E#0L;hO*7|Xz$jHhC6Dweaz(j zS|`?ddD%}iugd;l^Je<+-+}Q#;qrK>nv{dAF>Is@aB}|!Po7ohPe;5**Iq&ZUzbzS z)~mWhI}g}Lo=dFfmV_nz4Y@0zScAJ_RqNkBM#<;>>I?xGG!q=bo#Xrfw5ynvmn!$? ztG7HCS66F%Z6_u#UYKWq8!TvzjEt_MkoVpM$BWpLFX>ml(L(Fh-2&A2wjkm(H44&k zmKI9~W7CI|12n<;wqD1+aoyVpxXI%_Bv|tW=A>a;AWA8YWRvS;dCXEp5-~L(18mY} zEDB-#V%@o{FC`oLSnDQss|eH5-f>Wd&snTWkBVFL;#Wu$0Cr9f!t;-Mx`xFr46(H5 zef96Hqrf))#OU{?oEi{9E2(#)vrxLYHL z=ADMNjhI``3Y^b#HBA)|zV9WJUWg6mKtn$H7wUuOYr*_YKOFh@r`eBXq`?Q2) zW)G;)Ewdp;WMRM8rh?kU&%JKzF=N0`;$Q}XtL1GpyE%v-evya`$CR#4m;7* zWYq_Chvpy8sv8}=0MQL^4fWm(HLf52jy7^a=SU;w2{X;Eug5-ytlph;Vl#)tGoRY( zY3HoX7S4Op_J_Ra-Y6}4x;!X=m68EBd)F27xb^OsHVl}x8lHFDf{kE$ZO7-_FRncF zBKpcP0C{N6-{~T9VlqT68Um;w3jAMybYh~iKd+l$R&6dGU%cw{<6p;Xkh^rXk2D&QS zvTBP z)2(-$OUWQJ)hv=syv^{ZH+AOrQ2>~;Bl9@+eayAE5LG5MLoj?a-@c;lp6H_Bu5pNu z5K3mL2VEZrl8WKk?qbK2Ne6vBrhNeL7_QY5$%ZSV%ERRDlJGy!xfsM%yDwzRn2b{q2ZmTY=fEuq8d2+eJgCmrt!BGAxc-#!4!X;Ric)bgq}#ef&@zrFC5F|I^+M+@>;T2qLO6L`sGjj=iR$^^@42cVp$0%9{Nrt zbPG()+Z*=psi_Eoj-k?}%PjHpf6f7E6|t^PtV=>u>V;-+A5OcL(P`#9aS~P}T^b*1 zw?A4CByDSN_();7!&bX4AY{V}dJj2%!~{q}O1SDbifegEm1FB?G&s-Qx7F?YPg^_F zfJF(l=1=B0mkr6w7_oIMGj6saRq7Eb6Wk>n14(tI!uVQZ2c;Ei={`DNkaqh%sq;O-c zEew^1;ui?2aywvp!M4Dc!Z8l7i2Bm9_CI-e!-+lW-Fd`i)xKF^Jb7IS9^=`eZOEzI zQ>=9qL>+i^DttiV%rCd2KRFo!Y5(332t!Jtq`?nYU%~kBBO{(;^Jfk95ziIV+#UX! z)SKViR1}nPls$R4OgAc7X5Z)G@~rIFaFwZTj_|2-@K~2Yt+K(fU>Q1 zCfsV>SuHPVv?ppS1R>O!4@rD@(_m*)NdyfUb>k`8A?@KTeZ4X%tGjNoDg|Dt0(iI)wDzl0N_d z@|1~N3O?H(|8Qug?H0CjV`1N$4^Si3~S*T%y!jTyT2t0uhZokPggEZ7pF9aE@&$ukTW4U3q$+ejLrO3L^OFj@k! z0Xr##(ciyvBDSg2mdtSNabs(vKbI*4GmOMzhGn($L>78pO1Z+~M=~SuNtyd$l zJXivAVSLiB4R9Z&+=yNtu+MDJ> zy_9sTEP=DPjvTDojiT!e746$*WGD(tsBXSv!;2*jPxDbtOpDmAV5cWKc<%XgHOiA{ znpguxcc!Hv=eDh;$nio02VcVFLpK!iRptOmDwDHroGXUQU0z~hZxiHpxOysfLl-*+ zk#g_1*V7ynLkznRw}qY&G!R7TIw>^>PsthXXJOu8r; zp<3?t3}L;0OH#xrhFM>vm$-2Qx*Tcs|J_Kjdt{3l)s(If*;kF{*9MQ6B2(xnV{Fsc4-YfH*)1LeH|nebFc| z(c~0qR_nx+qUE;a@v1KuMQEGszXJjcB7Sr2J+&Nc|B6mYquR8 z<&e$2+B)^CL>sU;>15g$d|O(EtBl}Mn<7!7j_*pWzg>WmLKU5kR;S5)El@*VY}rEQ zunf>w<@}Q+a+7}P;g4qJNTOi`V-Ra&)RoJPZeNXHc@Y`W_2)ZX_7YR(H`}^oO;GzA za{=+1I7{-&8?XxzySavq$h~;e?N#~m$7$C0ycY*caT#^^N~h0J^Z)!q+hid^d$XY~ zV|4nMBJ~+R-S3F5Aa5-DA)R-+Nb}grmGC7qU*Ys zP6+`)LK+oG=@1DCNok}(r2Ep1G*W_ehje$Rlt@W;37789?_8hv`wesF%$$98ti9Hr zRpe)G%P-t=^~C6$aN;F}{EO?Fn9|sTf2WllSpEt)F~j+WAh3YyR4;p4&*buR-8Q+( z2Sum7nqfs+?MuhrghV05fOH6=R?Rjgj6q2T%A6{u7`<{7F!e@-NP%)qNig_)59MIN zPsQ*_$h2buP_VrSWYK^d{>jxj^!zInnP6?&edRzul*P7ITgC#`llIF90ms%?;jYx%Y1<`Qk z;dPQv@u%+#$UuSQnynnlbulaKRhVQ9>y|x%koXzplX%@EM?w1=V`g}JY1S`VZZ(WL z?k{SXxId<3dlNk$(@ALg^QYMxBBt)O9Y2_{NDL{w2HdEbFm55pYhlfYnt9F6j%67+ zF~zmz&pZxem50j}&dX_H*@y0A3cF7+FKsV#4LpulF2r8wzV~Bivo@HDHQ6h8$IU)e zk!9u{N^dyT!JHkC@e@DnR0Qu`k^)@qV>TAZgTT?V{Fp~DXtFyIq6b777(~pjVeIur z7;i5KaP?oNc8t#>4(TSfTQWuba6pagW*3}}v>k~gbHb$}KPCQR=-S3MMABSf*5`)V zXyKXbWQvA1Qk8|#Y!zol6`NpXrXTv^7rN8TK$)UrIQ*8wfoQ_*x-4b_4$-(SZ_xKB zeh$($QL~{^dkf}%?pP(yHH%d#`4t(DfG11zrNPa@wiIQkf)PpBzJbYJGk0ZkYkg|n z)?Ey5eL{yP4*cL2XEF_>kshH)K*G_H-PWjO!Rff*g4a0moHJ~jz=uC_W}9hfw5c;h z7D501x;t(N4fdq7MJ)N8AMn*hYeVJ!oHsVy*s~A(RB~Z;h;K7M*M6z&(Dj%{oTbd- zhYmzJ1TfoV#{lIYr|d1}MT&7;L@`z?HGRZP)g)>nMH zZytEH8|V@~KiSYS$TA)N)Ju{;m6cQ2k1)X5!AubMk$W{3YAfQ;a${>Xd?$tC#H{eT zMeZpdk(<0#!8HsZD~kWEOw}?JlK<4>RYyO%D?9Oz6s;%r_REpHSZ z_8KsMz1njJ)6i>w~ZJRj9$$Ds{ zU;98jbXq>pHXRvqW0qd-GSaws*_*Xmby30qACxBV$2U10~mgx1ubiS_SX0QgN;mf09ZBNpXuCu$dE}`XW>(=M@Z#H=!c<$KQvGy5~yiR z*!wz-)9xEv%~b`5_?U12<>+!H88*VoU_$MNc~c zRAECcfm=S(fY&m3d!J8^aXC)OwRLUA=5XF!C%I98_wQeA-Q_+7U7Qd8GZ-$Se|}vb z4e3K%E<8?WvWz^O!mEoqJMZ|o6u=(%^DkY($nE+D?&!~9_^xXLIMh_*iOQ!PZiE== zFfEPErzq{ms=|TOu>*Y!j~}I5e0(B0DuVt6(po?q%mDWQrC_BP6YbH))~D7{sy}&y zUNUOzR-)Ol^|T`+sOx#6X!7fvxB^J<4FX{OU=7p4@3TEO0?AJ%*LzF0)Tcvb;R|=T zwGTK6Sy+9ze@D28h|@e#V{BS|Wd(Kf5BVOP-TgM7@|J&d2dZ0t2jm`};?(f9WRoYh z1|oBnP)PJOFGRr@$DKtGq5lIy52isR2I8&GoBFdmT(M{!+lH1Xx*tEh{!Nwv{u8C$ z0^^aMrv<|JpvBGV)xGxt>1n3TJ*Qzz{GOSipwKSKGx6cHZ{dsG5u6}*{xP*-2gr}^ zfrw-+0?D&q)ni<|XaW?-wgg!2Mng z*>0Yth_%Z_Dx8B;tIbx2FMGGH4eVp>wpdI2b?5_Ge2cXlILnzx)M4i?zM0O32YUy_ z9>1TY!wvxbQeN%{zVe<|@qg-uO}|9EN_swos+51sM!n;i$roWl*FAy^G5lU@Ebf|R zHYp$5jg-o+1Gax#qQVgix~ydYSgHy!Dh&oAw;$-q(_|(_k+PAe z(_Q?J`QRlUN!NU>Et6y#jCvbqNMxFByYi=yXz4;@4Hq*Slt{W|KeW!GikqU`x#w%j zk~chVJd-~go$Hf^H+i{Ifo@dJZpd!RhZ5-1d91gp-U`;*_Z6*!=Vs~Q66Af%=>HbN z+dLNZ$-`=v@07I{&B%C_rFn@lk9B3#@EDQROU4wGqd>r^qX19Y#OuoS-u- z49WJpcLY6N%Y|1yZ|kGq4gKXj{FJLb*Y%-XdBu%^hq)06%P?SX< zk(W}L9ZxAP9r`95PCY$2(R~K!rm14SooHzQTiYGr;N{*#g9yI;h6s7LK(jwb@Ut`u zOF>Vw6Le)DO$}F`U74cF(st0_>*4@eae`2Uyx$c$cv>}(T`mX#4aAaAvh90ZjKp~ zl*Jz;olglP>P8(;H_1*k`eqGQeqKHnjP}tR;9-wT4>BK&nPB|2-b>{8m3RK&mFpr* zH%%m7l1cj{veiH@+4<4s(eC;WQJyGsm=#lQoZtrd@myv8o>k2WPn(U4N02nZHjR^$ zcE#IPHGxvKdU}M9U7euw)ugZsSb1Rb4KHZ+n3kcPm{F9;eg@pSRgvX01kl8&o{ z*~44H^e#|jku@&3tcqsYE_}a0(B4~;gtaUN3#IOmLKNhF9G<6Oe#|+nTR;z6q2&=T z=UH7mzPl!UZ_WVBgZ6I1W*>MFepp+2xrC1hH^LutWuu@1xz)}%b5-o_ zDuhs|`mMhL#a|yDd$(D4cPhquNX{) z49|*3b2t)54hQSjc&FDoXzQGaBPLws^0t1c5gY<(H3Z^yCB=#usG9WpmY8bRFj`~U#{;`0kK+meU*e0$jb@ifbluOCu(;I=H8NgEIRp< zjN|F;uGPW^x=h=WzUm_w9?2XM%X3WIn1H zs|X!NdY;2SA zQx~t4oy99lJ0aHOinzAOozDyAB>>T=idQDQuYEeiN8%GnvAZ37hz60l!ew$!%?))A zjBCS)=vIMymr)UKYHWv_U7Kv+OIUT~;sl-PvIx_Pq~ORht%b(s#Q^==wt_jo1amb` zuK2d01wXH!DpfiCKju%=qYtHv{|4T5AQ`yOA-qj0z7vE5*c^7336MLkMR>8w4}WG) zpDYqO@c3;l&0j0&+IWBaqhSIN9$N9h!Q?S!yyza?b{t*D zm$}wmm^C?=>&}9n{dtFp<{pPejY5d;{Ak?!AtosV0KYsJ^;obzzIdR88aa-wS1bM2 zb=Ml5d?a;$S;}BP(A$DY7OxA|rZd_KKFs{$n%=w(>sA42p6Xk$nVM~pLC)8Q z1=)9GuWfA?7=4!7n*CGEtZr;a%)sD*Y#wI=XQ?gt#ukpw&Wo#`AXw!TKOWzU!3XXM z7;b?^ESiisVd4G!w-H(Fao4+1T~>eut{!Kpory6NB=WwAFF_XwrOOzgHDpXIa*?m# zIib$Gy>vUF+^phC`r2uIvu5(13zH`wv`>LIkgV+37S3*2EF3J7Cx|uHpeKs?Ff+)# z8VAk)26 zZBuPd>Uv!}c*6aTxl)LN(I4{7>A47iE=^-_lC zBqp~n?B0~fK$=MgR%^Col__(LES6LJkS>AZba#kn%R%%KX~dz8EeQI_QMJHJQcx+= zMylzTjdsmACart`EVe-QJtdoDi7{gkL2%pQFH4ylp4pua5F|mm@ZvP@<$dnF6gc#^ zu|}B>L()dr5iAOKQ%naj=oursoKKXGdSr8L#kADilZ!lccwtwdiBl*yb=F!hSkB|~ zoQ3XiIuT{}aA2BJrxcYj+9${OK&EL59Wp<@*Aa5`L-e7dZbM~$zKZ;Owk1OC*V6=B zLl2CxmlOtd!E?^n8B(IkQwDE_4%A!%CR&}q&5I3IIG*Pf-ZvR*I7XH=DGzKgRStHXh>#q4q8Y=iJ5ooXmmn0)=t^MtMHdy=Ec_ zr*jJi1z5d3j?l&N@1$Kx+CAC$H5C=T=A@rl=BqMggT$zmpvUy8RRYpf=?tC*OtIqZ z5}hn(Q{j{XQ&xPWNLU<*1AwFJ&a8LdBEHD z`yLXV;I|H?ejpjC zxq&kxjBhvm0fHOpv_cTn$euWUblDJFqI%%p8-7{28B6k{a!(Iy0{Nsjrcb>r+3eAg zlxVl<4T#_%uUq6Je1U!Ma(=8-A~#*uf;pvM;nwC27`tASQ?p4Z?K#HFqdcFYP$umV zGL=m&PQO+Wk?&tn=O&R9z1Ha1+VqlX)%&WvUaxUJeN`QiV7{8mc;}}!HvQ$U(Y;f8 z!rS@UgwJzTI*m+O=2}D*>)X%~k@KPRIGM{aC>wzEM<)43!hH{_I8Rxkx#J5JW5(4! z;=Y;q?u^Iko$6M|RT7`xYp!}9Fxx++We&zSBeN_?-#x$8))ZoQ~ z#X4z$iGoIlkrHcI2Giueql!$G4p&;k_jd5VvE*WP3KH>>+IBulbO^v1Z^w5Vx7t?E z7JuB$eTG{ZsCx7?cD&HrnyU+gL*Bd2H%jRw2~hO0J`!|&`!k$M@CgIZOPpb_+nw`% z-Z;v9wS%uq<~n{jc{XNO2kFM8@)|Z5aIc{W3goUz*@QrZF-}`2Zod=}KcU)JJ=3}O z##NIk?-fDRe$il;2J~bn1O?HGR=`ByJqmjo)yri$bA~ih8GR}$VhNm5F!3r;VaMg= zgH@9*%nGJ;SvSo3L`Vv-+z3!$T=-gKT4_feHBR z6@Ke;(t_Tn%ZiJ)Sk>MX6R+`=lnK!2{Z{}J)Q}AvFJKGxG@8*5RfI2p!d-awgEAP ziZ*oUz(?IMK=Izk^PaV*COQ!Z(sWD?*3`O5T5QZ!D$;8^9{w#@>FmUwi}U8jW!sH3 zosX}EI@4D58dA7LSeoXAf;H9iO-BnPGPIgayIt1#5!}&FLKU*1WP#M@Vf3ql93a7!iobGr5 zf1$nJaHf;reW(9?4KJ*B=B>%&O_#}b(aBH zT+u|&ue^bq=&#|&iy~7au>iXz_6^y?p&DMa#TD<(SyDEdXZu7xx_;G~5O(gC8GQa- zvKvVrSh!4X-+OaogN8cubjgc747kkbXj}u4l47sW?XIkcOJf(jcEQAScf#^A+`RNP zb5n4fTG)ovCq-=bw@Gw8ZxuCi6_xhZ&U^3=zz9#Vi!U6$qRZSd6U?`B&Qr-g{>6C` zSXBg~+_-r+UB(G`2~1E_N7WmewuR#SQ`@uCqRY{x`^eBkv}vNZLnMvHOYbgz?<`yH zMC8y+U|?4-GE@7HvnP%)CS8EY(zCpYTW>D-wK&Lxg`V-6fFDWQZAyQre=v6s;gmKHb2k2%nqYEQ}iv{Gp>0$R)Dy*np%ZyJcIC(vwf$G`HP?A9e?`IO%vZG6Y; zvA#uT8f9^Hrwhzi@0@rSQ7EPL9Tamvy54N!DsiB~hpxm{Pau1om8f_4Z-|j0Q9b?0 zHsIgxGy06jYn{S#^FrBn`WLeAigZ@XRStlk@G9+Q%@O5g_5A)83Kdw&rHJXb10G5F ztG|e!!qm`bwt3GG{*Bwss%7-qlilAvx<0gUzWBtZ-F!}qA}m_l1mc3^Qi9E`a3uq0 zS0~&b%Sy=1p8UN60=V_W&c0Fb55p-b=JgH+c|c!iKUVA{Vs24{NV3T1|974+iDIOK zPjYGBKq?e1Os^|pDRFDD?GJrRG zOCvw5%95?J4+1d6eRbCF%`>>n;wS;Erx)Nx2|j24flgROud$JhK)SzBegzu#AK*c@ z#B))O-PiIBsEsC51e<2w2iIjQPmKkLP$||0-<%^SyRoL80BMXP3>(l8T~4c8X8YU} zIJJe)w(j%E^ac-P^04!#LuWC4+-;i3Y`hL^0EAq*8W{I}q8QU20HNF6g9rB*kUdpg z3~H^hHQ4Iraj@0GDVRscf&m>0FP|LTJI)FmgMK6rZhR%WXr$CzA>!RXoG#cFTu~p0 zc8-ea3&+gT>Z623xp+A9Rj?`}Q$Mx?_{}B}!+b#O9$;}X!~`UIbIh8zXw>se2S zYId5PSvS;S`RC&KPfYfXG&}Y^l6P)O1FX}-z)v3j3_y1Ce2gl}?DUj&Cu^NSe~1s& zy7|$;^_5<4lh?j_#m&eZy=!#dg?|cop?1D=p(7J}45~qU} z1EdtkujN66==V6w;T?g)hJg0{{t{`|yc=kd1of(gHjR%CO^LIt@f9L%1~+ zGLH*4g4G;|2Uaxa(sAtwLbS)`;8KF9i^=(oMqNPYIM@8CUb}HNi=kNxdEEo)S;mq? zfV6%fZq7un??ylk&O-ipl;M0Sflb*^m-DT@`I^}2Yklpzv39&liqzC$Ln^AWIsZb0 zZe4;?0P^U9JQL7rQ%Gc}EHqqf>6DJAm6grjEf`Kn&%*hRDN-xpiL&o&KL0;rb#8c8 z!rjk5IMS_3nYU&6AL1A|H%+E^_)7&9AUFu(KTY(~3i6-u&lOM3Yt9P_;6(w{ug&lw z9podgloLCb)T?Hk*z_=xjwJDC}TwRQ=@ZeS{Z^dzgkTJr%0)mBwS&6YKtBzGChF9qLZHo;R)f&JW zdY#Z1H*f_V--H(%7=YUTyzO>LK|TkhOFG51i>KK(zduo1Jn;&G!(2^8xWel-AeaEo z%&&!iwFE#uI13T2MXk6H1bAVaOWqt=emDAw3?vYd?^I0-^pOP0n5HvT!vlJ6Nun_U;n?Jg9e<9|c=-p+0ji z93TO}ocdj#MOpyKH(;uE9_z5mO?Id`6C80JK{`rmy@7 z!($D|*Ofi~K}G>k2vb?OyN3;4g)d*n z(E54%-ig@Jl}xAJ(Wkz1x>`0$x-D{oE3Pa*vhqJ2o>%KNY3Wk%qo%KChOVRZ2r^($ zNLoh+>^OivDvs-k*Xam3rRx3`Njj*`-HI;U2iB~2& zt6H!ozJLt)N52l#bIWmoNNeV59YF+8N5Gb^-2b6+s9#c_z5u?Z1s?@e>p*WM<0XU_ z#jZy9J!vK~7LIm&B8m zFMvepi87tSyHy0;U0*JpS_j1fln9RIs1_y8WVO3OBgtakx8U`ejGf=GkrX~$t@(S1 z6Eb?~k{>27L^=)Tor!XkgN}z$M$5&aZCe=|$zzAjKt5uq09J$p1tcYp@RV z5O`lNv(Df;W8Iae9P<7{Cft+4LCXO^=s7fhLT{YV4mY<@+!sbkk&NI~Yi|2f^u8h? z#AyuCBmuu=i z+o;GyC?BBhT!qxoJQh{^I#xEc_51#^!R5VPo__0NHLohBT+$~1IzmSS)H#rFf@API z*D)nYdMeeqJndcU_IEBOrUHox$jp$FdKch-vdQ)Thd|VEn&?y*UrX@c4FCv38dc4s z)RN`rNdRmZB^c9K4-_5!-q2`WG8e z&l~;F{)UV#Nf_461+tb36e94iCCn@O6ZwZn!xk22@*Pqe{9~h11hR z`*NbkPB%C!Vjr4yy(?p}aoj;yW29{VP@WR3hB21{;AS9*uBl4M@Pjjv`Ltmg6hNSX zclnp1g}0R_cnmrdr{+5Xx+`)yY)b0WGyZsJO3HHW z2oXa9o92m-Y{>_8>k6$i2gG7K22I6~^y9UV(E@@>f}uF+1FasWIXo|6zc18u`2ana z_s>B$92Fo7<`B+^6~SLmF;C*aBq>MwJu33HGB5y0hZXSlRM66&Da0W^GifNLz58); z!{>XHC^K-E;gz!Fo#%H8h>P&28N|p*_1Tt{S(E+lZcj7eR8Kk-X`iYv=h*<1FuG~Q zt=My-wGIz$U~UbtKY`=yM37d2kl4)mjnR@70K@|7{ON1pOn$7XNwwLoE&4cA6E*(3 z(<>JGMPag>QR1N#=2g8tgg^-v2Alz*7xxoo{NY(&5AarBD>jkp46b(uvOrYJ2e8k?7-gc&kwp7~r8Xg` zZ}g<4=}qbs{Rb3Vtdo9~KhG!l28%&*LFBuC?@8^y@EA<%!LrEa_dK6{Plo;(mP_-jU@6DNsCvQUK0ANI=Td%&xdW9Ir8JlHL%koy`a{_mc&6 zNfl^nNQoLSKGk=%ZYTH@5|@X9u#1`J=2gMA3{u9mTx*4ip*!clus2ML51=vkvLNRU zbb8=zC?iM^hi&$>`QXZS3Jkw(v6$gYbI6|Xpb#G0^}<)~pRX0_oeNQZxmalN*2bAkta@sfyxXf0<_h+^&Ha_w+(hFWzdLKHjdN25dKsA>Cm2#2r63B^F=Ybj3Vg%&TK z$(G4BL&^(7uYcEv*aDxR3a(gjw*9D&{)*-i!?dU@PME|0$W%o@n2Qb(@MF>V{!oiO zwp!0F4Z7@rqM|jCi|)hY(DV{wjh_NVCPWy;(U|cCf0-!-PisGfuR@1Rx8aZO}~%8$bfxusI3d!t3z~AoF(sI#Hior1{(VXrKI`Nt=JvluAIc z;)5MF(w$xv=*<}S@JUMHT!@ClwXINUlA zLX^O{n-;PbGP+18&XDiLE|&FaDf~*KSj$H63ej$w(HrQuvFb52$a$-~{DOlDE8%b6 zsbUK;gW@%bmpv!qdwoKF?t~@!<;@R}cKf$-;A($Mt7H}kKXvv1?TWFcy`|X4l(kw4 z-AkgR%S8CC<-x5T*m@547Z5IWXjBn+z~?#KS|0B&$ay}1tE2Xe$54fFaO#*=X|oOf z%3}KL3^HTFJ3o*N2%L*!$&t(rc|Cpi;B-mU?9Qs1IT_Q<2dEJ~!}N&%Ba%5)nITc> zqu~zmfdp~%AWyMlr=yzJdTFIQ(`!lM026MDlYO~3K0XwPPd@=QtM%y>oq2?|SPC@E z;ib!gRKn1|kM@LHT|>~PJKpJIQ#SEoiqAf7YF^h!q1}!>JPgN&jPJ_8mtzuUQ=v5M zA<_FF*y7_bloM4M>U2qeJ71#6#^3{$hr#J-Gd;B`eBsat>wohv!-Ip8{M7xr`LdpY zGZocI3D)!HWvw>KNT$}4XP$2YXR2}f`$X9s(baHLFv^jM9cHl5S0xTM0@onO13oz> z)Jcr(2+B(OAuI{eWIRr2h?JdU0jizW`F_8q-_!7(!bnk52!=Lr|g@p_W-;E=B+~V;ODc>U` zo`nc8%F7qXn7(jxuC1ouKb#tF^ZjjDe0GTD!-3lI8egNa5!Jo+$`;Z(PWX;vN^Lq; z8?!Xs{n6BLOT%oF1l&&L3}ukpDQ<8(3DEs&*bC!3aD1t)o(X;QVX_=11?QCpNw6~5 zC-Dmfiq;Y;7?4f#hNS+ec4F!MzcYfVWk^^)2kWhn^g@w=a`dq?9~6z6X6qL~)liZn zNukXoU4}d^#27+|fie{isy^NppU7%w#yWgYc_|z)oy}10lQc?Xc F{|A|uv^f9( From 7902a08862f09a42cd057a2045c3408466252b53 Mon Sep 17 00:00:00 2001 From: jakubmkowalski Date: Tue, 13 Feb 2024 16:36:42 +0100 Subject: [PATCH 03/50] chore(BUX-586): renames bux-server to spv-wallet in README --- README.md | 52 +++++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 859880480..389a2214b 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,22 @@
-# BUX: Server +# SPV Wallet -[![Release](https://img.shields.io/github/release-pre/BuxOrg/bux-server.svg?logo=github&style=flat&v=3)](https://github.com/BuxOrg/bux-server/releases) -[![Build Status](https://img.shields.io/github/actions/workflow/status/BuxOrg/bux-server/run-tests.yml?branch=master&v=3)](https://github.com/BuxOrg/bux-server/actions) -[![Report](https://goreportcard.com/badge/github.com/BuxOrg/bux-server?style=flat&v=3)](https://goreportcard.com/report/github.com/BuxOrg/bux-server) -[![codecov](https://codecov.io/gh/BuxOrg/bux-server/branch/master/graph/badge.svg?v=3)](https://codecov.io/gh/BuxOrg/bux-server) -[![Mergify Status](https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/BuxOrg/bux-server&style=flat&v=3)](https://mergify.io) +[![Release](https://img.shields.io/github/release-pre/BuxOrg/spv-wallet.svg?logo=github&style=flat&v=3)](https://github.com/BuxOrg/spv-wallet/releases) +[![Build Status](https://img.shields.io/github/actions/workflow/status/BuxOrg/spv-wallet/run-tests.yml?branch=master&v=3)](https://github.com/BuxOrg/spv-wallet/actions) +[![Report](https://goreportcard.com/badge/github.com/BuxOrg/spv-wallet?style=flat&v=3)](https://goreportcard.com/report/github.com/BuxOrg/spv-wallet) +[![codecov](https://codecov.io/gh/BuxOrg/spv-wallet/branch/master/graph/badge.svg?v=3)](https://codecov.io/gh/BuxOrg/spv-wallet) +[![Mergify Status](https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/BuxOrg/spv-wallet&style=flat&v=3)](https://mergify.io)
-[![Go](https://img.shields.io/github/go-mod/go-version/BuxOrg/bux-server?v=3)](https://golang.org/) -[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod&v=3)](https://gitpod.io/#https://github.com/BuxOrg/bux-server) +[![Go](https://img.shields.io/github/go-mod/go-version/BuxOrg/spv-wallet?v=3)](https://golang.org/) +[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod&v=3)](https://gitpod.io/#https://github.com/BuxOrg/spv-wallet) [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat&v=3)](https://github.com/RichardLitt/standard-readme) [![Makefile Included](https://img.shields.io/badge/Makefile-Supported%20-brightgreen?=flat&logo=probot&v=3)](Makefile)
-> Get started using [BUX](https://getbux.io) in five minutes - ## Table of Contents - [About](#about) @@ -36,36 +34,36 @@ ## About -[Read more about BUX](https://getbux.io) +Complete stand-alone server using the SPV Wallet engine (UTXOs, xPubs, Paymail & More!)
## Installation -**bux-server** requires a [supported release of Go](https://golang.org/doc/devel/release.html#policy). +**spv-wallet** requires a [supported release of Go](https://golang.org/doc/devel/release.html#policy). ```shell script -go get -u github.com/BuxOrg/bux-server +go get -u github.com/BuxOrg/spv-wallet ``` #### build ```shell script -go build -o bux-server cmd/server/* +go build -o spv-wallet cmd/server/* ``` #### run ```shell script -./bux-server +./spv-wallet ```
## Documentation -View the generated [documentation](https://pkg.go.dev/github.com/BuxOrg/bux-server) +View the generated [documentation](https://pkg.go.dev/github.com/BuxOrg/spv-wallet) -[![GoDoc](https://godoc.org/github.com/BuxOrg/bux-server?status.svg&style=flat&v=3)](https://pkg.go.dev/github.com/BuxOrg/bux-server) +[![GoDoc](https://godoc.org/github.com/BuxOrg/spv-wallet?status.svg&style=flat&v=3)](https://pkg.go.dev/github.com/BuxOrg/spv-wallet)
@@ -195,7 +193,7 @@ vet Run the Go vet application ### Defaults -If you run Bux-Server without editing anything, it will use the default configuration from file [defaults.go](/config/defaults.go). It is set up to use _freecache_, _sqlite_ with enabled _paymail_ with _signing disabled_ and with _beef_. +If you run spv-wallet without editing anything, it will use the default configuration from file [defaults.go](/config/defaults.go). It is set up to use _freecache_, _sqlite_ with enabled _paymail_ with _signing disabled_ and with _beef_. ### Config Variables @@ -228,7 +226,7 @@ go run ./cmd/server/main.go -C /my/config.json #### Environment variables -To override any config variable with ENV, use the "BUX\_" prefix with mapstructure annotation path with "_" as a delimiter in all uppercase. Example: +To override any config variable with ENV, use the "SPV\_" prefix with mapstructure annotation path with "_" as a delimiter in all uppercase. Example: Let's take this fragment of AppConfig from `config.example.yaml`: @@ -240,9 +238,9 @@ auth: signing_disabled: true ``` -To override admin_key in auth config, use the path with "_" as a path delimiter and BUX\_ as prefix. So: +To override admin_key in auth config, use the path with "_" as a path delimiter and SPV\_ as prefix. So: ```bash -BUX_AUTH_ADMIN_KEY="admin_key" +SPV_AUTH_ADMIN_KEY="admin_key" ``` To be able to use TAAL API Key is needed. @@ -265,7 +263,7 @@ To use your API key put key in ``token`` field in ```config.example.yaml``` ### Examples & Tests -All unit tests run via [GitHub Actions](https://github.com/BuxOrg/bux-server/actions) and +All unit tests run via [GitHub Actions](https://github.com/BuxOrg/spv-wallet/actions) and uses [Go version 1.19.x](https://golang.org/doc/go1.19). View the [configuration file](.github/workflows/run-tests.yml).
@@ -297,8 +295,8 @@ make bench ### Docker Compose Quickstart -To get started with development, `bux-server` provides a `start.sh` script -which is using `docker-compose.yml` file to starts up Bux Server with selected database +To get started with development, `spv-wallet` provides a `start.sh` script +which is using `docker-compose.yml` file to starts up SPV Wallet serer with selected database and cache storage. To start, we need to fill the config json which we want to use, for example: `config/envs/development.json`. @@ -306,7 +304,7 @@ Main configuration is done when running the script. There are two way of running this script: 1. with manual configuration - Every option is displayed in terminal and user can choose - which database/cache storage use and configure how to run bux-server. + which database/cache storage use and configure how to run spv-wallet. ```bash ./start.sh ``` @@ -316,7 +314,7 @@ There are two way of running this script: ./start.sh -db postgresql -c redis -bs true -env development -b false ``` -`-l/--load` option add possibility to use previously created `.env.config` file and run bux-server with simple command: +`-l/--load` option add possibility to use previously created `.env.config` file and run spv-wallet with simple command: ```bash ./start.sh -l ``` @@ -338,4 +336,4 @@ View the [contributing guidelines](.github/CODE_STANDARDS.md#3-contributing) and ## License -[![License](https://img.shields.io/github/license/BuxOrg/bux-server.svg?style=flat&v=3)](LICENSE) +[![License](https://img.shields.io/github/license/BuxOrg/spv-wallet.svg?style=flat&v=3)](LICENSE) From a12b2cd3003342fdeeeffe98b1b4b83e0fc46aa9 Mon Sep 17 00:00:00 2001 From: jakubmkowalski Date: Tue, 13 Feb 2024 16:51:13 +0100 Subject: [PATCH 04/50] chore(BUX-586): fixes package comment --- spv_wallet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spv_wallet.go b/spv_wallet.go index b3c97d043..3e5f3b3a6 100644 --- a/spv_wallet.go +++ b/spv_wallet.go @@ -1,4 +1,4 @@ -// Package spv-wallet is a complete stand-alone server using the SPV Wallet Engine +// Package spvwallet is a complete stand-alone server using the SPV Wallet Engine // // Run: go run cmd/server/main.go // From 6b9165c17a4651f0b2b7f23e1115e18b7b1599f3 Mon Sep 17 00:00:00 2001 From: Damian Orzepowski Date: Wed, 14 Feb 2024 10:51:06 +0100 Subject: [PATCH 05/50] deps(BUX-417): update bux; get rid of centrifuge dependency; upgrade other dependencies --- go.mod | 33 +++++++++--------------------- go.sum | 63 +++++++++++++++++----------------------------------------- 2 files changed, 27 insertions(+), 69 deletions(-) diff --git a/go.mod b/go.mod index 6a248c2b6..a10ff064e 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/BuxOrg/bux-server go 1.21.5 require ( - github.com/BuxOrg/bux v0.14.4 + github.com/BuxOrg/bux v0.14.5 github.com/BuxOrg/bux-models v0.3.0 github.com/bitcoin-sv/go-broadcast-client v0.17.2 github.com/go-ozzo/ozzo-validation v3.6.0+incompatible @@ -27,9 +27,8 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/common v0.46.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect ) @@ -45,8 +44,6 @@ require ( github.com/bitcoinsv/bsvutil v0.0.0-20181216182056-1d77cf353ea9 // indirect github.com/bsm/redislock v0.9.4 // indirect github.com/capnm/sysinfo v0.0.0-20130621111458-5909a53897f3 // indirect - github.com/centrifugal/centrifuge-go v0.10.2 // indirect - github.com/centrifugal/protocol v0.11.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/coocood/freecache v1.2.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -55,8 +52,8 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.4 // indirect - github.com/go-openapi/spec v0.20.13 // indirect - github.com/go-openapi/swag v0.22.6 // indirect + github.com/go-openapi/spec v0.20.14 // indirect + github.com/go-openapi/swag v0.22.9 // indirect github.com/go-redis/redis_rate/v9 v9.1.2 // indirect github.com/go-resty/resty/v2 v2.11.0 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect @@ -68,7 +65,6 @@ require ( github.com/gomodule/redigo v2.0.0+incompatible // indirect github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/websocket v1.5.1 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect @@ -79,7 +75,6 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/jpillora/backoff v1.0.0 // indirect github.com/klauspost/compress v1.17.6 // indirect github.com/libsv/go-bc v0.1.26 // indirect github.com/libsv/go-bk v0.1.6 // indirect @@ -105,8 +100,6 @@ require ( github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/segmentio/asm v1.2.0 // indirect - github.com/segmentio/encoding v0.4.0 // indirect github.com/sosodev/duration v1.2.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect @@ -117,9 +110,7 @@ require ( github.com/swaggo/files v1.0.1 // indirect github.com/swaggo/http-swagger v1.3.4 github.com/tonicpow/go-minercraft/v2 v2.0.8 - github.com/tylertreat/BoomFilters v0.0.0-20210315201527-1a82519a3e43 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/vektah/gqlparser/v2 v2.5.11 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -132,15 +123,15 @@ require ( go.mongodb.org/mongo-driver v1.13.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.19.0 // indirect - golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect - golang.org/x/mod v0.14.0 // indirect + golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect + golang.org/x/mod v0.15.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.17.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 // indirect - google.golang.org/grpc v1.61.0 // indirect + golang.org/x/tools v0.18.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 // indirect + google.golang.org/grpc v1.61.1 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -157,11 +148,5 @@ replace github.com/bsm/redislock => github.com/bsm/redislock v0.7.2 // Issue with using wrong version of Redigo replace github.com/gomodule/redigo => github.com/gomodule/redigo v1.8.9 -// Breaking changes - needs a full refactor in WOC and BUX -replace github.com/centrifugal/centrifuge-go => github.com/centrifugal/centrifuge-go v0.8.3 - -// Breaking changes - needs a full refactor in WOC and BUX -replace github.com/centrifugal/protocol => github.com/centrifugal/protocol v0.9.1 - // Issue: go.mongodb.org/mongo-driver/x/bsonx: cannot find module providing package go.mongodb.org/mongo-driver/x/bsonx replace go.mongodb.org/mongo-driver => go.mongodb.org/mongo-driver v1.11.7 diff --git a/go.sum b/go.sum index 354bf1f03..9aba9fac7 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/99designs/gqlgen v0.17.43 h1:I4SYg6ahjowErAQcHFVKy5EcWuwJ3+Xw9z2fLpuFCPo= github.com/99designs/gqlgen v0.17.43/go.mod h1:lO0Zjy8MkZgBdv4T1U91x09r0e0WFOdhVUutlQs1Rsc= -github.com/BuxOrg/bux v0.14.4 h1:gNvfHnmWUpzftD1qLmtTgKyJZu3vaSCcdvJ58GdKlLA= -github.com/BuxOrg/bux v0.14.4/go.mod h1:sDeOTJshHA0rduuag5NGnMUEe0oC/gVKKPuHlPIMpYI= +github.com/BuxOrg/bux v0.14.5 h1:fSgxhesAOBliCv0MwTMssupdmRZ+lfHQElyRE0YpcZk= +github.com/BuxOrg/bux v0.14.5/go.mod h1:je8b1btINOvd4iiVhU5HnbE8MEY8EmINs4vuiWpuVGM= github.com/BuxOrg/bux-models v0.3.0 h1:+pvpDdYaiIgeOhAO847RK07Vzc+vuUvy2ok/xJevQyg= github.com/BuxOrg/bux-models v0.3.0/go.mod h1:JCpoXxVnKZmCy3rg6nOtLQL5AXh6iEo2tBvcD/qAxEY= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= @@ -45,10 +45,6 @@ github.com/bsm/redislock v0.7.2/go.mod h1:kS2g0Yvlymc9Dz8V3iVYAtLAaSVruYbAFdYBDr github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI= github.com/capnm/sysinfo v0.0.0-20130621111458-5909a53897f3 h1:IHZ1Le1ejzkmS7Si7dIzJvYDWe+BIoNmqMnfWHBZSVw= github.com/capnm/sysinfo v0.0.0-20130621111458-5909a53897f3/go.mod h1:M5XHQLu90v2JNm/bW2tdsYar+5vhV0gEcBcmDBNAN1Y= -github.com/centrifugal/centrifuge-go v0.8.3 h1:kZ0AZaaSbrDJuMH+Hp5RiCKZyUsKmUmNv8h0tMlij78= -github.com/centrifugal/centrifuge-go v0.8.3/go.mod h1:HkBuiQLwdLdauVtiqig2wgzLsDKp9As72w01WDat+ow= -github.com/centrifugal/protocol v0.9.1 h1:DCoZvYtblUotGrM7GrpIvKtZYhjT03chvhEN9tyWZMY= -github.com/centrifugal/protocol v0.9.1/go.mod h1:qpYrxz4cDj+rlgC6giSADkf7XDN1K7aFmkkFwt/bayQ= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -59,8 +55,6 @@ github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgk github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M= github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U= -github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -93,10 +87,10 @@ github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbX github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= -github.com/go-openapi/spec v0.20.13 h1:XJDIN+dLH6vqXgafnl5SUIMnzaChQ6QTo0/UPMbkIaE= -github.com/go-openapi/spec v0.20.13/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= -github.com/go-openapi/swag v0.22.6 h1:dnqg1XfHXL9aBxSbktBqFR5CxVyVI+7fYWhAf1JOeTw= -github.com/go-openapi/swag v0.22.6/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= +github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do= +github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw= +github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= +github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= @@ -152,9 +146,6 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -183,8 +174,6 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= @@ -227,8 +216,6 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/goveralls v0.0.6/go.mod h1:h8b4ow6FxSPMQHF6o2ve3qsclnffZjYTNEKmLesRwqw= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= @@ -285,8 +272,8 @@ github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+ github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= -github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= +github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rafaeljusto/redigomock v2.4.0+incompatible h1:d7uo5MVINMxnRr20MxbgDkmZ8QRfevjOVgEa4n0OZyY= @@ -306,13 +293,6 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= -github.com/segmentio/asm v1.1.4/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/segmentio/encoding v0.3.5/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM= -github.com/segmentio/encoding v0.4.0 h1:MEBYvRqiUB2nfR2criEXWqwdY6HJOUrCn5hboVOVmy8= -github.com/segmentio/encoding v0.4.0/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= @@ -372,12 +352,8 @@ github.com/tonicpow/go-minercraft/v2 v2.0.8 h1:gDjHOpmD0P5qRLpgRLUHDcDR39DdT5c/X github.com/tonicpow/go-minercraft/v2 v2.0.8/go.mod h1:mfr1fgOpnu2GkTmPDT4Sanoh4wOfV6kcwOrjVdo8vPk= 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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8= github.com/vektah/gqlparser/v2 v2.5.11/go.mod h1:1rCcfwB2ekJofmluGWXMSEnPMZgbxzwj6FaZ/4OT8Cc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= @@ -424,16 +400,16 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4= -golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -472,8 +448,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -511,16 +485,16 @@ golang.org/x/tools v0.0.0-20200530233709-52effbd89c51/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20240205150955-31a09d347014 h1:FSL3lRCkhaPFxqi0s9o+V4UI2WTzAVOvkgbd4kVV4Wg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014/go.mod h1:SaPjaZGWb0lPqs6Ittu0spdfrOArqji4ZdeP5IC/9N4= -google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= -google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 h1:hZB7eLIaYlW9qXRfCq/qDaPdbeY3757uARz5Vvfv+cY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk= +google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -529,7 +503,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 3653aa4c2ff73fd9d2a73806aeaef29603fed980 Mon Sep 17 00:00:00 2001 From: jakubmkowalski Date: Tue, 13 Feb 2024 15:34:54 +0100 Subject: [PATCH 06/50] chore(BUX-586): renames bux-server to spv-wallet --- .github/CODE_STANDARDS.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 4 +- Makefile | 2 +- actions/access_keys/access_keys_test.go | 2 +- actions/access_keys/count.go | 10 +- actions/access_keys/create.go | 6 +- actions/access_keys/get.go | 6 +- actions/access_keys/revoke.go | 6 +- actions/access_keys/routes.go | 4 +- actions/access_keys/routes_test.go | 2 +- actions/access_keys/search.go | 14 +- actions/actions.go | 2 +- actions/admin/access_keys.go | 20 +-- actions/admin/destinations.go | 16 +-- actions/admin/paymail_addresses.go | 34 ++--- actions/admin/record.go | 8 +- actions/admin/routes.go | 4 +- actions/admin/stats.go | 6 +- actions/admin/status.go | 2 +- actions/admin/transactions.go | 20 +-- actions/admin/utxos.go | 16 +-- actions/admin/xpubs.go | 16 +-- actions/base/base_test.go | 2 +- actions/base/index.go | 2 +- actions/base/routes.go | 4 +- actions/base/routes_test.go | 2 +- actions/destinations/count.go | 10 +- actions/destinations/create.go | 12 +- actions/destinations/destination_test.go | 2 +- actions/destinations/get.go | 10 +- actions/destinations/routes.go | 4 +- actions/destinations/routes_test.go | 2 +- actions/destinations/search.go | 14 +- actions/destinations/update.go | 12 +- actions/methods.go | 8 +- actions/middleware.go | 14 +- actions/paymail/create.go | 8 +- actions/paymail/delete.go | 6 +- actions/paymail/routes.go | 6 +- actions/transactions/broadcast_callback.go | 2 +- actions/transactions/count.go | 10 +- actions/transactions/get.go | 6 +- actions/transactions/new.go | 21 ++- actions/transactions/record.go | 10 +- actions/transactions/routes.go | 4 +- actions/transactions/routes_test.go | 2 +- actions/transactions/search.go | 14 +- actions/transactions/transaction_test.go | 2 +- actions/transactions/update.go | 8 +- actions/utxos/count.go | 10 +- actions/utxos/get.go | 6 +- actions/utxos/routes.go | 4 +- actions/utxos/routes_test.go | 2 +- actions/utxos/search.go | 14 +- actions/utxos/unreserve.go | 4 +- actions/utxos/utxo_test.go | 2 +- actions/xpubs/create.go | 8 +- actions/xpubs/get.go | 8 +- actions/xpubs/routes.go | 4 +- actions/xpubs/routes_test.go | 2 +- actions/xpubs/update.go | 8 +- actions/xpubs/xpubs_test.go | 2 +- cmd/server/main.go | 22 +-- config.example.yaml | 14 +- config/config.go | 24 ++-- config/config_test.go | 2 +- config/defaults.go | 8 +- config/flags.go | 12 +- config/load.go | 4 +- config/load_test.go | 8 +- config/services.go | 32 ++--- config/services_test.go | 4 +- config/task_manager.go | 2 +- go.mod | 2 +- logging/logging.go | 2 +- mappings/access_keys.go | 10 +- mappings/admin.go | 8 +- mappings/common/common.go | 4 +- mappings/destination.go | 14 +- mappings/fee_unit.go | 12 +- mappings/metadata.go | 6 +- mappings/paymail_address.go | 20 +-- mappings/script_output.go | 12 +- mappings/sync_config.go | 12 +- mappings/transaction.go | 152 ++++++++++----------- mappings/utxo.go | 28 ++-- mappings/xpub.go | 10 +- metrics/collector.go | 6 +- metrics/global.go | 4 +- metrics/naming.go | 2 +- server/server.go | 22 +-- server/server_test.go | 2 +- bux_server.go => spv_wallet.go | 4 +- tests/tests.go | 4 +- 94 files changed, 470 insertions(+), 471 deletions(-) rename bux_server.go => spv_wallet.go (65%) diff --git a/.github/CODE_STANDARDS.md b/.github/CODE_STANDARDS.md index 1a5ac2d78..94bd5bb09 100644 --- a/.github/CODE_STANDARDS.md +++ b/.github/CODE_STANDARDS.md @@ -246,7 +246,7 @@ Additional information and guidelines on Conventional Commits can be found [here Good example: ```bash -feat: add possibility to create a new user by admin (#BUX-123) +feat(#123): add possibility to create a new user by admin ``` Bad example: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d9e297ba5..29b954e55 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,8 +5,8 @@ # Pull Request Checklist -- [ ] 📖 I created my PR using provided : [CODE_STANDARDS](https://github.com/BuxOrg/bux-server/blob/main/.github/CODE_STANDARDS.md) -- [ ] 📖 I have read the short Code of Conduct: [CODE_OF_CONDUCT](https://github.com/BuxOrg/bux-server/blob/main/.github/CODE_OF_CONDUCT.md) +- [ ] 📖 I created my PR using provided : [CODE_STANDARDS](https://github.com/BuxOrg/spv-wallet/blob/main/.github/CODE_STANDARDS.md) +- [ ] 📖 I have read the short Code of Conduct: [CODE_OF_CONDUCT](https://github.com/BuxOrg/spv-wallet/blob/main/.github/CODE_OF_CONDUCT.md) - [ ] 🏠 I tested my changes locally. - [ ] ✅ I have provided tests for my changes. - [ ] 📝 I have used conventional commits. diff --git a/Makefile b/Makefile index 800836f2d..f2d70f19e 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ include .make/go.mk ## Not defined? Use default repo name which is the application ifeq ($(REPO_NAME),) - REPO_NAME="bux-server" + REPO_NAME="spv-wallet" endif ## Not defined? Use default repo owner diff --git a/actions/access_keys/access_keys_test.go b/actions/access_keys/access_keys_test.go index 19f42e1b1..444c8a77d 100644 --- a/actions/access_keys/access_keys_test.go +++ b/actions/access_keys/access_keys_test.go @@ -3,7 +3,7 @@ package accesskeys import ( "testing" - "github.com/BuxOrg/bux-server/tests" + "github.com/BuxOrg/spv-wallet/tests" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" diff --git a/actions/access_keys/count.go b/actions/access_keys/count.go index 30123b46a..ade244853 100644 --- a/actions/access_keys/count.go +++ b/actions/access_keys/count.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,14 +20,14 @@ import ( // @Param conditions query string false "conditions" // @Success 200 // @Router /v1/access-key/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -35,7 +35,7 @@ func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Pa // Record a new transaction (get the hex from parameters)a var count int64 - if count, err = a.Services.Bux.GetAccessKeysByXPubIDCount( + if count, err = a.Services.SPV.GetAccessKeysByXPubIDCount( req.Context(), reqXPubID, metadata, diff --git a/actions/access_keys/create.go b/actions/access_keys/create.go index eacc320a5..fd572c241 100644 --- a/actions/access_keys/create.go +++ b/actions/access_keys/create.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -18,7 +18,7 @@ import ( // @Param metadata query string false "metadata" // @Success 201 // @Router /v1/access-key [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPub, _ := bux.GetXpubFromRequest(req) @@ -29,7 +29,7 @@ func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.P metadata := params.GetJSON("metadata") // Create a new accessKey - accessKey, err := a.Services.Bux.NewAccessKey( + accessKey, err := a.Services.SPV.NewAccessKey( req.Context(), reqXPub, bux.WithMetadatas(metadata), diff --git a/actions/access_keys/get.go b/actions/access_keys/get.go index d56cf0943..7fece99f9 100644 --- a/actions/access_keys/get.go +++ b/actions/access_keys/get.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -18,7 +18,7 @@ import ( // @Param id query string true "id" // @Success 200 // @Router /v1/access-key [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -32,7 +32,7 @@ func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Para } // Get access key - accessKey, err := a.Services.Bux.GetAccessKey( + accessKey, err := a.Services.SPV.GetAccessKey( req.Context(), reqXPubID, id, ) if err != nil { diff --git a/actions/access_keys/revoke.go b/actions/access_keys/revoke.go index f021f5d53..3341fdb44 100644 --- a/actions/access_keys/revoke.go +++ b/actions/access_keys/revoke.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -18,7 +18,7 @@ import ( // @Param id query string true "id" // @Success 201 // @Router /v1/access-key [delete] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) revoke(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPub, _ := bux.GetXpubFromRequest(req) @@ -32,7 +32,7 @@ func (a *Action) revoke(w http.ResponseWriter, req *http.Request, _ httprouter.P } // Create a new accessKey - accessKey, err := a.Services.Bux.RevokeAccessKey( + accessKey, err := a.Services.SPV.RevokeAccessKey( req.Context(), reqXPub, id, diff --git a/actions/access_keys/routes.go b/actions/access_keys/routes.go index 3aac282f6..650c638c2 100644 --- a/actions/access_keys/routes.go +++ b/actions/access_keys/routes.go @@ -1,8 +1,8 @@ package accesskeys import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/access_keys/routes_test.go b/actions/access_keys/routes_test.go index b2d62523b..6eb703586 100644 --- a/actions/access_keys/routes_test.go +++ b/actions/access_keys/routes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" ) diff --git a/actions/access_keys/search.go b/actions/access_keys/search.go index aae74e94d..2f7518a9f 100644 --- a/actions/access_keys/search.go +++ b/actions/access_keys/search.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -25,14 +25,14 @@ import ( // @Param conditions query string false "conditions" // @Success 200 // @Router /v1/access-key/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -40,7 +40,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P // Record a new transaction (get the hex from parameters)a var accessKeys []*bux.AccessKey - if accessKeys, err = a.Services.Bux.GetAccessKeysByXPubID( + if accessKeys, err = a.Services.SPV.GetAccessKeysByXPubID( req.Context(), reqXPubID, metadata, @@ -51,7 +51,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P return } - accessKeyContracts := make([]*buxmodels.AccessKey, 0) + accessKeyContracts := make([]*spvwalletmodels.AccessKey, 0) for _, accessKey := range accessKeys { accessKeyContracts = append(accessKeyContracts, mappings.MapToAccessKeyContract(accessKey)) } diff --git a/actions/actions.go b/actions/actions.go index f90968997..16ed0fc34 100644 --- a/actions/actions.go +++ b/actions/actions.go @@ -4,7 +4,7 @@ package actions import ( "net/http" - "github.com/BuxOrg/bux-server/dictionary" + "github.com/BuxOrg/spv-wallet/dictionary" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/admin/access_keys.go b/actions/admin/access_keys.go index 18a575c9c..d9f42e239 100644 --- a/actions/admin/access_keys.go +++ b/actions/admin/access_keys.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -25,19 +25,19 @@ import ( // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/access-keys/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) accessKeysSearch(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var accessKeys []*bux.AccessKey - if accessKeys, err = a.Services.Bux.GetAccessKeys( + if accessKeys, err = a.Services.SPV.GetAccessKeys( req.Context(), metadata, conditions, @@ -47,7 +47,7 @@ func (a *Action) accessKeysSearch(w http.ResponseWriter, req *http.Request, _ ht return } - accessKeyContracts := make([]*buxmodels.AccessKey, 0) + accessKeyContracts := make([]*spvwalletmodels.AccessKey, 0) for _, accessKey := range accessKeys { accessKeyContracts = append(accessKeyContracts, mappings.MapToAccessKeyContract(accessKey)) } @@ -66,19 +66,19 @@ func (a *Action) accessKeysSearch(w http.ResponseWriter, req *http.Request, _ ht // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/access-keys/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) accessKeysCount(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var count int64 - if count, err = a.Services.Bux.GetAccessKeysCount( + if count, err = a.Services.SPV.GetAccessKeysCount( req.Context(), metadata, conditions, diff --git a/actions/admin/destinations.go b/actions/admin/destinations.go index 67f4ecb5c..2763520aa 100644 --- a/actions/admin/destinations.go +++ b/actions/admin/destinations.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -24,19 +24,19 @@ import ( // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/destinations/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) destinationsSearch(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var destinations []*bux.Destination - if destinations, err = a.Services.Bux.GetDestinations( + if destinations, err = a.Services.SPV.GetDestinations( req.Context(), metadata, conditions, @@ -60,19 +60,19 @@ func (a *Action) destinationsSearch(w http.ResponseWriter, req *http.Request, _ // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/destinations/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) destinationsCount(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var count int64 - if count, err = a.Services.Bux.GetDestinationsCount( + if count, err = a.Services.SPV.GetDestinationsCount( req.Context(), metadata, conditions, diff --git a/actions/admin/paymail_addresses.go b/actions/admin/paymail_addresses.go index 88047d22d..2ace7715d 100644 --- a/actions/admin/paymail_addresses.go +++ b/actions/admin/paymail_addresses.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -19,7 +19,7 @@ import ( // @Produce json // @Success 200 // @Router /v1/admin/paymail/get [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) paymailGetAddress(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { params := apirouter.GetParams(req) address := params.GetString("address") @@ -29,9 +29,9 @@ func (a *Action) paymailGetAddress(w http.ResponseWriter, req *http.Request, _ h return } - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() - paymailAddress, err := a.Services.Bux.GetPaymailAddress(req.Context(), address, opts...) + paymailAddress, err := a.Services.SPV.GetPaymailAddress(req.Context(), address, opts...) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -54,19 +54,19 @@ func (a *Action) paymailGetAddress(w http.ResponseWriter, req *http.Request, _ h // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/paymails/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) paymailAddressesSearch(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var paymailAddresses []*bux.PaymailAddress - if paymailAddresses, err = a.Services.Bux.GetPaymailAddresses( + if paymailAddresses, err = a.Services.SPV.GetPaymailAddresses( req.Context(), metadata, conditions, @@ -90,19 +90,19 @@ func (a *Action) paymailAddressesSearch(w http.ResponseWriter, req *http.Request // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/paymails/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) paymailAddressesCount(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var count int64 - if count, err = a.Services.Bux.GetPaymailAddressesCount( + if count, err = a.Services.SPV.GetPaymailAddressesCount( req.Context(), metadata, conditions, @@ -128,7 +128,7 @@ func (a *Action) paymailAddressesCount(w http.ResponseWriter, req *http.Request, // @Produce json // @Success 201 // @Router /v1/admin/paymail/create [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) paymailCreateAddress(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -153,14 +153,14 @@ func (a *Action) paymailCreateAddress(w http.ResponseWriter, req *http.Request, return } - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() if metadata != nil { opts = append(opts, bux.WithMetadatas(*metadata)) } var paymailAddress *bux.PaymailAddress - paymailAddress, err = a.Services.Bux.NewPaymailAddress(req.Context(), xpub, address, publicName, avatar, opts...) + paymailAddress, err = a.Services.SPV.NewPaymailAddress(req.Context(), xpub, address, publicName, avatar, opts...) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -179,7 +179,7 @@ func (a *Action) paymailCreateAddress(w http.ResponseWriter, req *http.Request, // @Produce json // @Success 200 // @Router /v1/admin/paymail/delete [delete] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) paymailDeleteAddress(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -190,10 +190,10 @@ func (a *Action) paymailDeleteAddress(w http.ResponseWriter, req *http.Request, return } - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() // Delete a new paymail address - err := a.Services.Bux.DeletePaymailAddress(req.Context(), address, opts...) + err := a.Services.SPV.DeletePaymailAddress(req.Context(), address, opts...) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return diff --git a/actions/admin/record.go b/actions/admin/record.go index 9d8439753..e04ba74fd 100644 --- a/actions/admin/record.go +++ b/actions/admin/record.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" "github.com/mrz1836/go-datastore" @@ -20,7 +20,7 @@ import ( // @Param hex query string true "Transaction hex" // @Success 201 // @Router /v1/admin/transactions/record [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) transactionRecord(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -31,7 +31,7 @@ func (a *Action) transactionRecord(w http.ResponseWriter, req *http.Request, _ h opts := make([]bux.ModelOps, 0) // Record a new transaction (get the hex from parameters) - transaction, err := a.Services.Bux.RecordRawTransaction( + transaction, err := a.Services.SPV.RecordRawTransaction( req.Context(), hex, opts..., @@ -39,7 +39,7 @@ func (a *Action) transactionRecord(w http.ResponseWriter, req *http.Request, _ h if err != nil { if errors.Is(err, datastore.ErrDuplicateKey) { // already registered, just return the registered transaction - if transaction, err = a.Services.Bux.GetTransactionByHex(req.Context(), hex); err != nil { + if transaction, err = a.Services.SPV.GetTransactionByHex(req.Context(), hex); err != nil { apirouter.ReturnResponse(w, req, http.StatusUnprocessableEntity, err.Error()) return } diff --git a/actions/admin/routes.go b/actions/admin/routes.go index 53cf8d75f..2a499a039 100644 --- a/actions/admin/routes.go +++ b/actions/admin/routes.go @@ -1,8 +1,8 @@ package admin import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/admin/stats.go b/actions/admin/stats.go index a7f0c8a5e..48c258f82 100644 --- a/actions/admin/stats.go +++ b/actions/admin/stats.go @@ -3,7 +3,7 @@ package admin import ( "net/http" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -16,9 +16,9 @@ import ( // @Produce json // @Success 200 // @Router /v1/admin/stats [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) stats(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { - stats, err := a.Services.Bux.GetStats(req.Context()) + stats, err := a.Services.SPV.GetStats(req.Context()) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return diff --git a/actions/admin/status.go b/actions/admin/status.go index 85e0b781c..64e552b44 100644 --- a/actions/admin/status.go +++ b/actions/admin/status.go @@ -15,7 +15,7 @@ import ( // @Produce json // @Success 200 // @Router /v1/admin/status [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) status(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Return response apirouter.ReturnResponse(w, req, http.StatusOK, true) diff --git a/actions/admin/transactions.go b/actions/admin/transactions.go index 084c6cab5..580b41e0d 100644 --- a/actions/admin/transactions.go +++ b/actions/admin/transactions.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -25,19 +25,19 @@ import ( // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/transactions/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) transactionsSearch(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var transactions []*bux.Transaction - if transactions, err = a.Services.Bux.GetTransactions( + if transactions, err = a.Services.SPV.GetTransactions( req.Context(), metadata, conditions, @@ -47,7 +47,7 @@ func (a *Action) transactionsSearch(w http.ResponseWriter, req *http.Request, _ return } - contracts := make([]*buxmodels.Transaction, 0) + contracts := make([]*spvwalletmodels.Transaction, 0) for _, transaction := range transactions { contracts = append(contracts, mappings.MapToTransactionContractForAdmin(transaction)) } @@ -66,19 +66,19 @@ func (a *Action) transactionsSearch(w http.ResponseWriter, req *http.Request, _ // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/transactions/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) transactionsCount(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var count int64 - if count, err = a.Services.Bux.GetTransactionsCount( + if count, err = a.Services.SPV.GetTransactionsCount( req.Context(), metadata, conditions, diff --git a/actions/admin/utxos.go b/actions/admin/utxos.go index 60bf24bee..8b3ee5dac 100644 --- a/actions/admin/utxos.go +++ b/actions/admin/utxos.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -24,19 +24,19 @@ import ( // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/utxos/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) utxosSearch(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var utxos []*bux.Utxo - if utxos, err = a.Services.Bux.GetUtxos( + if utxos, err = a.Services.SPV.GetUtxos( req.Context(), metadata, conditions, @@ -60,19 +60,19 @@ func (a *Action) utxosSearch(w http.ResponseWriter, req *http.Request, _ httprou // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/utxos/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) utxosCount(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var count int64 - if count, err = a.Services.Bux.GetUtxosCount( + if count, err = a.Services.SPV.GetUtxosCount( req.Context(), metadata, conditions, diff --git a/actions/admin/xpubs.go b/actions/admin/xpubs.go index 917ea94bb..c63e92a5a 100644 --- a/actions/admin/xpubs.go +++ b/actions/admin/xpubs.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -24,12 +24,12 @@ import ( // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/xpubs/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) xpubsSearch(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) @@ -37,7 +37,7 @@ func (a *Action) xpubsSearch(w http.ResponseWriter, req *http.Request, _ httprou } var xpubs []*bux.Xpub - if xpubs, err = a.Services.Bux.GetXPubs( + if xpubs, err = a.Services.SPV.GetXPubs( req.Context(), metadata, conditions, @@ -61,19 +61,19 @@ func (a *Action) xpubsSearch(w http.ResponseWriter, req *http.Request, _ httprou // @Param conditions query string false "Conditions filter" // @Success 200 // @Router /v1/admin/xpubs/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) xpubsCount(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return } var count int64 - if count, err = a.Services.Bux.GetXPubsCount( + if count, err = a.Services.SPV.GetXPubsCount( req.Context(), metadata, conditions, diff --git a/actions/base/base_test.go b/actions/base/base_test.go index 6a9ec2e54..a3f397d18 100644 --- a/actions/base/base_test.go +++ b/actions/base/base_test.go @@ -3,7 +3,7 @@ package base import ( "testing" - "github.com/BuxOrg/bux-server/tests" + "github.com/BuxOrg/spv-wallet/tests" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" diff --git a/actions/base/index.go b/actions/base/index.go index 41e58189c..ee757cb36 100644 --- a/actions/base/index.go +++ b/actions/base/index.go @@ -12,6 +12,6 @@ func index(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { apirouter.ReturnResponse( w, req, http.StatusOK, - map[string]interface{}{"message": "Welcome to the Bux Server ✌(◕‿-)✌"}, + map[string]interface{}{"message": "Welcome to the SPV Wallet ✌(◕‿-)✌"}, ) } diff --git a/actions/base/routes.go b/actions/base/routes.go index f0d57558c..1fe439654 100644 --- a/actions/base/routes.go +++ b/actions/base/routes.go @@ -4,8 +4,8 @@ import ( "net/http" "net/http/pprof" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/base/routes_test.go b/actions/base/routes_test.go index 0a2b7470f..438db6f67 100644 --- a/actions/base/routes_test.go +++ b/actions/base/routes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" ) diff --git a/actions/destinations/count.go b/actions/destinations/count.go index 693924171..fb29d8de6 100644 --- a/actions/destinations/count.go +++ b/actions/destinations/count.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,14 +20,14 @@ import ( // @Produce json // @Success 200 // @Router /v1/destination/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) _, metadataModels, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModels) + metadata := mappings.MapToSPVMetadata(metadataModels) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -35,7 +35,7 @@ func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Pa // Record a new transaction (get the hex from parameters) var count int64 - if count, err = a.Services.Bux.GetDestinationsByXpubIDCount( + if count, err = a.Services.SPV.GetDestinationsByXpubIDCount( req.Context(), reqXPubID, metadata, diff --git a/actions/destinations/create.go b/actions/destinations/create.go index 89d01ac24..39e10ed31 100644 --- a/actions/destinations/create.go +++ b/actions/destinations/create.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" "github.com/BuxOrg/bux/utils" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -22,14 +22,14 @@ import ( // @Param metadata query string false "metadata" // @Success 201 // @Router /v1/destination [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) // Get the xPub from the request (via authentication) reqXPub, _ := bux.GetXpubFromRequest(req) - xPub, err := a.Services.Bux.GetXpub(req.Context(), reqXPub) + xPub, err := a.Services.SPV.GetXpub(req.Context(), reqXPub) if err != nil { apirouter.ReturnResponse(w, req, http.StatusUnprocessableEntity, err.Error()) return @@ -53,7 +53,7 @@ func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.P metadata[bux.ReferenceIDField] = referenceID } - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() if metadata != nil { opts = append(opts, bux.WithMetadatas(metadata)) @@ -61,7 +61,7 @@ func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.P // Get a new destination var destination *bux.Destination - if destination, err = a.Services.Bux.NewDestination( + if destination, err = a.Services.SPV.NewDestination( req.Context(), xPub.RawXpub(), uint32(0), // todo: use a constant? protect this? diff --git a/actions/destinations/destination_test.go b/actions/destinations/destination_test.go index 3ae2cba19..ae7c07a86 100644 --- a/actions/destinations/destination_test.go +++ b/actions/destinations/destination_test.go @@ -3,7 +3,7 @@ package destinations import ( "testing" - "github.com/BuxOrg/bux-server/tests" + "github.com/BuxOrg/spv-wallet/tests" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" diff --git a/actions/destinations/get.go b/actions/destinations/get.go index a23087679..e1786efaa 100644 --- a/actions/destinations/get.go +++ b/actions/destinations/get.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,7 +20,7 @@ import ( // @Param locking_script query string false "Destination locking script" // @Success 200 // @Router /v1/destination [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -38,15 +38,15 @@ func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Para var destination *bux.Destination var err error if id != "" { - destination, err = a.Services.Bux.GetDestinationByID( + destination, err = a.Services.SPV.GetDestinationByID( req.Context(), reqXPubID, id, ) } else if address != "" { - destination, err = a.Services.Bux.GetDestinationByAddress( + destination, err = a.Services.SPV.GetDestinationByAddress( req.Context(), reqXPubID, address, ) } else { - destination, err = a.Services.Bux.GetDestinationByLockingScript( + destination, err = a.Services.SPV.GetDestinationByLockingScript( req.Context(), reqXPubID, lockingScript, ) } diff --git a/actions/destinations/routes.go b/actions/destinations/routes.go index 0dab46444..d39651ea2 100644 --- a/actions/destinations/routes.go +++ b/actions/destinations/routes.go @@ -1,8 +1,8 @@ package destinations import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/destinations/routes_test.go b/actions/destinations/routes_test.go index f16108cd6..7f6b68bd1 100644 --- a/actions/destinations/routes_test.go +++ b/actions/destinations/routes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" ) diff --git a/actions/destinations/search.go b/actions/destinations/search.go index ac7e1b776..b0f7b483f 100644 --- a/actions/destinations/search.go +++ b/actions/destinations/search.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -25,14 +25,14 @@ import ( // @Param condition query string false "condition" // @Success 200 // @Router /v1/destination/search [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -40,7 +40,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P // Record a new transaction (get the hex from parameters)a var destinations []*bux.Destination - if destinations, err = a.Services.Bux.GetDestinationsByXpubID( + if destinations, err = a.Services.SPV.GetDestinationsByXpubID( req.Context(), reqXPubID, metadata, @@ -51,7 +51,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P return } - contracts := make([]*buxmodels.Destination, 0) + contracts := make([]*spvwalletmodels.Destination, 0) for _, destination := range destinations { contracts = append(contracts, mappings.MapToDestinationContract(destination)) } diff --git a/actions/destinations/update.go b/actions/destinations/update.go index fe1eb8eec..5a7877940 100644 --- a/actions/destinations/update.go +++ b/actions/destinations/update.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -22,7 +22,7 @@ import ( // @Param metadata body string true "Destination Metadata" // @Success 200 // @Router /v1/destination [patch] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) update(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -41,15 +41,15 @@ func (a *Action) update(w http.ResponseWriter, req *http.Request, _ httprouter.P var destination *bux.Destination var err error if id != "" { - destination, err = a.Services.Bux.UpdateDestinationMetadataByID( + destination, err = a.Services.SPV.UpdateDestinationMetadataByID( req.Context(), reqXPubID, id, metadata, ) } else if address != "" { - destination, err = a.Services.Bux.UpdateDestinationMetadataByAddress( + destination, err = a.Services.SPV.UpdateDestinationMetadataByAddress( req.Context(), reqXPubID, address, metadata, ) } else { - destination, err = a.Services.Bux.UpdateDestinationMetadataByLockingScript( + destination, err = a.Services.SPV.UpdateDestinationMetadataByLockingScript( req.Context(), reqXPubID, lockingScript, metadata, ) } diff --git a/actions/methods.go b/actions/methods.go index 1a73ce1a5..96281fb41 100644 --- a/actions/methods.go +++ b/actions/methods.go @@ -4,8 +4,8 @@ import ( "encoding/json" "net/http" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/dictionary" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/dictionary" "github.com/julienschmidt/httprouter" "github.com/mrz1836/go-datastore" "github.com/mrz1836/go-parameters" @@ -51,7 +51,7 @@ func MethodNotAllowed(w http.ResponseWriter, req *http.Request) { } // GetQueryParameters get all filtering parameters related to the db query -func GetQueryParameters(params *parameters.Params) (*datastore.QueryParams, *buxmodels.Metadata, *map[string]interface{}, error) { +func GetQueryParameters(params *parameters.Params) (*datastore.QueryParams, *spvwalletmodels.Metadata, *map[string]interface{}, error) { var queryParams *datastore.QueryParams jsonQueryParams, ok := params.GetJSONOk("params") if ok { @@ -73,7 +73,7 @@ func GetQueryParameters(params *parameters.Params) (*datastore.QueryParams, *bux } metadataReq := params.GetJSON(MetadataField) - var metadata *buxmodels.Metadata + var metadata *spvwalletmodels.Metadata if len(metadataReq) > 0 { // marshal the metadata into the Metadata model metaJSON, _ := json.Marshal(metadataReq) //nolint:errchkjson // ignore for now diff --git a/actions/middleware.go b/actions/middleware.go index 3cccd92cd..727d5e20c 100644 --- a/actions/middleware.go +++ b/actions/middleware.go @@ -6,8 +6,8 @@ import ( "time" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/config" - "github.com/BuxOrg/bux-server/dictionary" + "github.com/BuxOrg/spv-wallet/config" + "github.com/BuxOrg/spv-wallet/dictionary" "github.com/gofrs/uuid" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" @@ -32,7 +32,7 @@ func (a *Action) RequireAuthentication(fn httprouter.Handle) httprouter.Handle { return func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { // Check the authentication var knownErr dictionary.ErrorMessage - if req, knownErr = CheckAuthentication(a.AppConfig, a.Services.Bux, req, false, true); knownErr.Code > 0 { + if req, knownErr = CheckAuthentication(a.AppConfig, a.Services.SPV, req, false, true); knownErr.Code > 0 { ReturnErrorResponse(w, req, knownErr, "") return } @@ -47,7 +47,7 @@ func (a *Action) RequireBasicAuthentication(fn httprouter.Handle) httprouter.Han return func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { // Check the authentication var knownErr dictionary.ErrorMessage - if req, knownErr = CheckAuthentication(a.AppConfig, a.Services.Bux, req, false, false); knownErr.Code > 0 { + if req, knownErr = CheckAuthentication(a.AppConfig, a.Services.SPV, req, false, false); knownErr.Code > 0 { ReturnErrorResponse(w, req, knownErr, "") return } @@ -62,7 +62,7 @@ func (a *Action) RequireAdminAuthentication(fn httprouter.Handle) httprouter.Han return func(w http.ResponseWriter, req *http.Request, p httprouter.Params) { // Check the authentication var knownErr dictionary.ErrorMessage - if req, knownErr = CheckAuthentication(a.AppConfig, a.Services.Bux, req, true, true); knownErr.Code > 0 { + if req, knownErr = CheckAuthentication(a.AppConfig, a.Services.SPV, req, true, true); knownErr.Code > 0 { ReturnErrorResponse(w, req, knownErr, "") return } @@ -92,7 +92,7 @@ func (a *Action) Request(_ *apirouter.Router, h httprouter.Handle) httprouter.Ha } // CheckAuthentication will check the authentication -func CheckAuthentication(appConfig *config.AppConfig, bux bux.ClientInterface, req *http.Request, +func CheckAuthentication(appConfig *config.AppConfig, spv bux.ClientInterface, req *http.Request, adminRequired bool, requireSigning bool, ) (*http.Request, dictionary.ErrorMessage) { // Bad/Unknown scheme @@ -102,7 +102,7 @@ func CheckAuthentication(appConfig *config.AppConfig, bux bux.ClientInterface, r // AuthenticateFromRequest using the xPub scheme var err error - if req, err = bux.AuthenticateRequest( + if req, err = spv.AuthenticateRequest( req.Context(), req, []string{appConfig.Authentication.AdminKey}, adminRequired, requireSigning && appConfig.Authentication.RequireSigning, diff --git a/actions/paymail/create.go b/actions/paymail/create.go index ef9a6c191..5c37c142d 100644 --- a/actions/paymail/create.go +++ b/actions/paymail/create.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -22,7 +22,7 @@ import ( // @Produce json // @Success 201 // @Router /v1/paymail [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -34,13 +34,13 @@ func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.P avatar := params.GetString("avatar") // the avatar metadata := params.GetJSON("metadata") // optional metadata - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() if metadata != nil { opts = append(opts, bux.WithMetadatas(metadata)) } - paymailAddress, err := a.Services.Bux.NewPaymailAddress(req.Context(), key, address, publicName, avatar, opts...) + paymailAddress, err := a.Services.SPV.NewPaymailAddress(req.Context(), key, address, publicName, avatar, opts...) if err != nil { apirouter.ReturnResponse(w, req, http.StatusUnprocessableEntity, err.Error()) return diff --git a/actions/paymail/delete.go b/actions/paymail/delete.go index e140667bc..192658241 100644 --- a/actions/paymail/delete.go +++ b/actions/paymail/delete.go @@ -16,7 +16,7 @@ import ( // @Produce json // @Success 200 // @Router /v1/paymail [delete] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) delete(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -24,10 +24,10 @@ func (a *Action) delete(w http.ResponseWriter, req *http.Request, _ httprouter.P // params address := params.GetString("address") // the full paymail address - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() // Delete a new paymail address - err := a.Services.Bux.DeletePaymailAddress( + err := a.Services.SPV.DeletePaymailAddress( req.Context(), address, opts..., ) if err != nil { diff --git a/actions/paymail/routes.go b/actions/paymail/routes.go index 8393d6d73..16c10698f 100644 --- a/actions/paymail/routes.go +++ b/actions/paymail/routes.go @@ -1,8 +1,8 @@ package pmail import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) @@ -18,7 +18,7 @@ func RegisterRoutes(router *apirouter.Router, appConfig *config.AppConfig, servi requireAdmin.Use(a.RequireAdminAuthentication) // Register the custom Paymail routes - services.Bux.GetPaymailConfig().RegisterRoutes(router.HTTPRouter) + services.SPV.GetPaymailConfig().RegisterRoutes(router.HTTPRouter) // Create the action action := &Action{actions.Action{AppConfig: a.AppConfig, Services: a.Services}} diff --git a/actions/transactions/broadcast_callback.go b/actions/transactions/broadcast_callback.go index 34042b48c..18f4f002b 100644 --- a/actions/transactions/broadcast_callback.go +++ b/actions/transactions/broadcast_callback.go @@ -32,7 +32,7 @@ func (a *Action) broadcastCallback(w http.ResponseWriter, req *http.Request, _ h } }() - err = a.Services.Bux.UpdateTransaction(req.Context(), resp) + err = a.Services.SPV.UpdateTransaction(req.Context(), resp) if err != nil { a.Services.Logger.Err(err).Msgf("failed to update transaction - tx: %v", resp) apirouter.ReturnResponse(w, req, http.StatusInternalServerError, "") diff --git a/actions/transactions/count.go b/actions/transactions/count.go index c2a730570..9a44205b9 100644 --- a/actions/transactions/count.go +++ b/actions/transactions/count.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,14 +20,14 @@ import ( // @Param conditions query string false "conditions" // @Success 200 // @Router /v1/transaction/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -35,7 +35,7 @@ func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Pa // Record a new transaction (get the hex from parameters)a var count int64 - if count, err = a.Services.Bux.GetTransactionsByXpubIDCount( + if count, err = a.Services.SPV.GetTransactionsByXpubIDCount( req.Context(), reqXPubID, metadata, diff --git a/actions/transactions/get.go b/actions/transactions/get.go index f5b8f39c6..d7fe95f8c 100644 --- a/actions/transactions/get.go +++ b/actions/transactions/get.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -18,7 +18,7 @@ import ( // @Param id query string true "id" // @Success 200 // @Router /v1/transaction [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -27,7 +27,7 @@ func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Para reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Get a transaction by ID - transaction, err := a.Services.Bux.GetTransaction( + transaction, err := a.Services.SPV.GetTransaction( req.Context(), reqXPubID, params.GetString("id"), diff --git a/actions/transactions/new.go b/actions/transactions/new.go index 968bb1854..0de2b0600 100644 --- a/actions/transactions/new.go +++ b/actions/transactions/new.go @@ -5,9 +5,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -22,14 +22,14 @@ import ( // @Param metadata query string false "metadata" // @Success 201 // @Router /v1/transaction [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) newTransaction(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) // Get the xPub from the request (via authentication) reqXPub, _ := bux.GetXpubFromRequest(req) - xPub, err := a.Services.Bux.GetXpub(req.Context(), reqXPub) + xPub, err := a.Services.SPV.GetXpub(req.Context(), reqXPub) if err != nil { apirouter.ReturnResponse(w, req, http.StatusUnprocessableEntity, err.Error()) return @@ -39,8 +39,7 @@ func (a *Action) newTransaction(w http.ResponseWriter, req *http.Request, _ http } // Read transaction config from request body - // TODO: Austin's params package probably has a better way to do this than - // marshal/unmarshal... couldn't figure it out + // TODO: consider using go params package functions instead of marshal/unmarshal configMap, ok := params.GetJSONOk("config") if !ok { apirouter.ReturnResponse(w, req, http.StatusBadRequest, actions.ErrTxConfigNotFound.Error()) @@ -53,23 +52,23 @@ func (a *Action) newTransaction(w http.ResponseWriter, req *http.Request, _ http return } - txContract := buxmodels.TransactionConfig{} + txContract := spvwalletmodels.TransactionConfig{} if err = json.Unmarshal(configBytes, &txContract); err != nil { apirouter.ReturnResponse(w, req, http.StatusBadRequest, actions.ErrBadTxConfig.Error()) return } metadata := params.GetJSON(bux.ModelMetadata.String()) - opts := a.Services.Bux.DefaultModelOptions() + opts := a.Services.SPV.DefaultModelOptions() if metadata != nil { opts = append(opts, bux.WithMetadatas(metadata)) } - txConfig := mappings.MapToTransactionConfigBux(&txContract) + txConfig := mappings.MapToTransactionConfigSPV(&txContract) // Record a new transaction (get the hex from parameters) var transaction *bux.DraftTransaction - if transaction, err = a.Services.Bux.NewTransaction( + if transaction, err = a.Services.SPV.NewTransaction( req.Context(), xPub.RawXpub(), txConfig, diff --git a/actions/transactions/record.go b/actions/transactions/record.go index 7bc5f3c29..337e4dcb9 100644 --- a/actions/transactions/record.go +++ b/actions/transactions/record.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -21,14 +21,14 @@ import ( // @Param metadata query string false "metadata" // @Success 200 // @Router /v1/transaction/record [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) record(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) // Get the xPub from the request (via authentication) reqXPub, _ := bux.GetXpubFromRequest(req) - xPub, err := a.Services.Bux.GetXpub(req.Context(), reqXPub) + xPub, err := a.Services.SPV.GetXpub(req.Context(), reqXPub) if err != nil { apirouter.ReturnResponse(w, req, http.StatusUnprocessableEntity, err.Error()) return @@ -46,7 +46,7 @@ func (a *Action) record(w http.ResponseWriter, req *http.Request, _ httprouter.P // Record a new transaction (get the hex from parameters) var transaction *bux.Transaction - if transaction, err = a.Services.Bux.RecordTransaction( + if transaction, err = a.Services.SPV.RecordTransaction( req.Context(), reqXPub, params.GetString("hex"), diff --git a/actions/transactions/routes.go b/actions/transactions/routes.go index d8a9123e8..0b4b1a6d5 100644 --- a/actions/transactions/routes.go +++ b/actions/transactions/routes.go @@ -1,8 +1,8 @@ package transactions import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/transactions/routes_test.go b/actions/transactions/routes_test.go index 7575d12f6..8e7bde728 100644 --- a/actions/transactions/routes_test.go +++ b/actions/transactions/routes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" ) diff --git a/actions/transactions/search.go b/actions/transactions/search.go index 1f01d7ba0..cbb4132d5 100644 --- a/actions/transactions/search.go +++ b/actions/transactions/search.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -25,14 +25,14 @@ import ( // @Param conditions query string false "conditions" // @Success 200 // @Router /v1/transaction/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) queryParams, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -40,7 +40,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P // Record a new transaction (get the hex from parameters)a var transactions []*bux.Transaction - if transactions, err = a.Services.Bux.GetTransactionsByXpubID( + if transactions, err = a.Services.SPV.GetTransactionsByXpubID( req.Context(), reqXPubID, metadata, @@ -51,7 +51,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P return } - contracts := make([]*buxmodels.Transaction, 0) + contracts := make([]*spvwalletmodels.Transaction, 0) for _, transaction := range transactions { contracts = append(contracts, mappings.MapToTransactionContract(transaction)) } diff --git a/actions/transactions/transaction_test.go b/actions/transactions/transaction_test.go index 33ad55faa..15ee1a404 100644 --- a/actions/transactions/transaction_test.go +++ b/actions/transactions/transaction_test.go @@ -3,7 +3,7 @@ package transactions import ( "testing" - "github.com/BuxOrg/bux-server/tests" + "github.com/BuxOrg/spv-wallet/tests" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" diff --git a/actions/transactions/update.go b/actions/transactions/update.go index 334eae6db..d9adc20d4 100644 --- a/actions/transactions/update.go +++ b/actions/transactions/update.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,7 +20,7 @@ import ( // @Param metadata query string true "metadata" // @Success 200 // @Router /v1/transaction [patch] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) update(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Get the xPub from the request (via authentication) reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -30,7 +30,7 @@ func (a *Action) update(w http.ResponseWriter, req *http.Request, _ httprouter.P metadata := params.GetJSON(actions.MetadataField) // Get a transaction by ID - transaction, err := a.Services.Bux.UpdateTransactionMetadata( + transaction, err := a.Services.SPV.UpdateTransactionMetadata( req.Context(), reqXPubID, params.GetString("id"), diff --git a/actions/utxos/count.go b/actions/utxos/count.go index ea6df106b..ed0f54ecb 100644 --- a/actions/utxos/count.go +++ b/actions/utxos/count.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,14 +20,14 @@ import ( // @Param conditions query string false "conditions" // @Success 200 // @Router /v1/utxo/count [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) _, metadataModel, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(metadataModel) + metadata := mappings.MapToSPVMetadata(metadataModel) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -42,7 +42,7 @@ func (a *Action) count(w http.ResponseWriter, req *http.Request, _ httprouter.Pa // Get a utxo using a xPub var count int64 - if count, err = a.Services.Bux.GetUtxosCount( + if count, err = a.Services.SPV.GetUtxosCount( req.Context(), metadata, &dbConditions, diff --git a/actions/utxos/get.go b/actions/utxos/get.go index 1c0ad93de..cd9fe8b18 100644 --- a/actions/utxos/get.go +++ b/actions/utxos/get.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -19,7 +19,7 @@ import ( // @Param output_index query int true "output_index" // @Success 200 // @Router /v1/utxo [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -29,7 +29,7 @@ func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Para outputIndex := uint32(params.GetUint64("output_index")) // Get a utxo using a xPub - utxo, err := a.Services.Bux.GetUtxo( + utxo, err := a.Services.SPV.GetUtxo( req.Context(), reqXPubID, txID, diff --git a/actions/utxos/routes.go b/actions/utxos/routes.go index 5b092db1f..c358486fc 100644 --- a/actions/utxos/routes.go +++ b/actions/utxos/routes.go @@ -1,8 +1,8 @@ package utxos import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/utxos/routes_test.go b/actions/utxos/routes_test.go index 0c5d464fc..c8b662931 100644 --- a/actions/utxos/routes_test.go +++ b/actions/utxos/routes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" ) diff --git a/actions/utxos/search.go b/actions/utxos/search.go index 0f64038e1..76e2d7a70 100644 --- a/actions/utxos/search.go +++ b/actions/utxos/search.go @@ -4,9 +4,9 @@ import ( "net/http" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -25,14 +25,14 @@ import ( // @Param conditions query string false "conditions" // @Success 200 // @Router /v1/utxo/search [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) // Parse the params params := apirouter.GetParams(req) queryParams, modelMetadata, conditions, err := actions.GetQueryParameters(params) - metadata := mappings.MapToBuxMetadata(modelMetadata) + metadata := mappings.MapToSPVMetadata(modelMetadata) if err != nil { apirouter.ReturnResponse(w, req, http.StatusExpectationFailed, err.Error()) return @@ -40,7 +40,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P // Record a new transaction (get the hex from parameters)a var utxos []*bux.Utxo - if utxos, err = a.Services.Bux.GetUtxosByXpubID( + if utxos, err = a.Services.SPV.GetUtxosByXpubID( req.Context(), reqXPubID, metadata, @@ -51,7 +51,7 @@ func (a *Action) search(w http.ResponseWriter, req *http.Request, _ httprouter.P return } - contracts := make([]*buxmodels.Utxo, 0) + contracts := make([]*spvwalletmodels.Utxo, 0) for _, utxo := range utxos { contracts = append(contracts, mappings.MapToUtxoContract(utxo)) } diff --git a/actions/utxos/unreserve.go b/actions/utxos/unreserve.go index 068aad9f8..96de04f64 100644 --- a/actions/utxos/unreserve.go +++ b/actions/utxos/unreserve.go @@ -16,12 +16,12 @@ import ( // @Param reference_id query string false "draft tx id" // @Success 201 // @Router /v1/utxo/unreserve [patch] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) unreserve(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPubID, _ := bux.GetXpubIDFromRequest(req) params := apirouter.GetParams(req) - err := a.Services.Bux.UnReserveUtxos( + err := a.Services.SPV.UnReserveUtxos( req.Context(), reqXPubID, params.GetString(bux.ReferenceIDField), diff --git a/actions/utxos/utxo_test.go b/actions/utxos/utxo_test.go index fa36a6eab..a916145e1 100644 --- a/actions/utxos/utxo_test.go +++ b/actions/utxos/utxo_test.go @@ -3,7 +3,7 @@ package utxos import ( "testing" - "github.com/BuxOrg/bux-server/tests" + "github.com/BuxOrg/spv-wallet/tests" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" diff --git a/actions/xpubs/create.go b/actions/xpubs/create.go index bcf1218c2..4dba8a9de 100644 --- a/actions/xpubs/create.go +++ b/actions/xpubs/create.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -20,7 +20,7 @@ import ( // @Param metadata query string false "metadata" // @Success 201 // @Router /v1/xpub [post] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { // Parse the params params := apirouter.GetParams(req) @@ -30,7 +30,7 @@ func (a *Action) create(w http.ResponseWriter, req *http.Request, _ httprouter.P metadata := params.GetJSON(actions.MetadataField) // Create a new xPub - xPub, err := a.Services.Bux.NewXpub( + xPub, err := a.Services.SPV.NewXpub( req.Context(), key, bux.WithMetadatas(metadata), ) diff --git a/actions/xpubs/get.go b/actions/xpubs/get.go index 9582ddf55..f67cade49 100644 --- a/actions/xpubs/get.go +++ b/actions/xpubs/get.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -18,7 +18,7 @@ import ( // @Param key query string false "key" // @Success 200 // @Router /v1/xpub [get] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPub, _ := bux.GetXpubFromRequest(req) reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -39,11 +39,11 @@ func (a *Action) get(w http.ResponseWriter, req *http.Request, _ httprouter.Para var xPub *bux.Xpub var err error if key != "" { - xPub, err = a.Services.Bux.GetXpub( + xPub, err = a.Services.SPV.GetXpub( req.Context(), key, ) } else { - xPub, err = a.Services.Bux.GetXpubByID( + xPub, err = a.Services.SPV.GetXpubByID( req.Context(), reqXPubID, ) } diff --git a/actions/xpubs/routes.go b/actions/xpubs/routes.go index 28bf38a8b..899eadb0e 100644 --- a/actions/xpubs/routes.go +++ b/actions/xpubs/routes.go @@ -1,8 +1,8 @@ package xpubs import ( - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/config" apirouter "github.com/mrz1836/go-api-router" ) diff --git a/actions/xpubs/routes_test.go b/actions/xpubs/routes_test.go index ea3dddc26..3f542aa91 100644 --- a/actions/xpubs/routes_test.go +++ b/actions/xpubs/routes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" ) diff --git a/actions/xpubs/update.go b/actions/xpubs/update.go index 7ae938011..94b6045d3 100644 --- a/actions/xpubs/update.go +++ b/actions/xpubs/update.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/actions" - "github.com/BuxOrg/bux-server/mappings" + "github.com/BuxOrg/spv-wallet/actions" + "github.com/BuxOrg/spv-wallet/mappings" "github.com/julienschmidt/httprouter" apirouter "github.com/mrz1836/go-api-router" ) @@ -19,7 +19,7 @@ import ( // @Param metadata query string false "metadata" // @Success 200 // @Router /v1/xpub [patch] -// @Security bux-auth-xpub +// @Security spv-wallet-auth-xpub func (a *Action) update(w http.ResponseWriter, req *http.Request, _ httprouter.Params) { reqXPub, _ := bux.GetXpubFromRequest(req) reqXPubID, _ := bux.GetXpubIDFromRequest(req) @@ -31,7 +31,7 @@ func (a *Action) update(w http.ResponseWriter, req *http.Request, _ httprouter.P // Get an xPub var xPub *bux.Xpub var err error - xPub, err = a.Services.Bux.UpdateXpubMetadata( + xPub, err = a.Services.SPV.UpdateXpubMetadata( req.Context(), reqXPubID, metadata, ) if err != nil { diff --git a/actions/xpubs/xpubs_test.go b/actions/xpubs/xpubs_test.go index e967b0bb8..74b006987 100644 --- a/actions/xpubs/xpubs_test.go +++ b/actions/xpubs/xpubs_test.go @@ -3,7 +3,7 @@ package xpubs import ( "testing" - "github.com/BuxOrg/bux-server/tests" + "github.com/BuxOrg/spv-wallet/tests" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" diff --git a/cmd/server/main.go b/cmd/server/main.go index 5d2924299..6a6127e25 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,5 +1,5 @@ /* -Package main is the core service layer for the BUX Server +Package main is the core service layer for the SPV Wallet */ package main @@ -9,19 +9,19 @@ import ( "os/signal" "time" - "github.com/BuxOrg/bux-server/config" - "github.com/BuxOrg/bux-server/dictionary" - _ "github.com/BuxOrg/bux-server/docs" - "github.com/BuxOrg/bux-server/logging" - "github.com/BuxOrg/bux-server/server" + "github.com/BuxOrg/spv-wallet/config" + "github.com/BuxOrg/spv-wallet/dictionary" + _ "github.com/BuxOrg/spv-wallet/docs" + "github.com/BuxOrg/spv-wallet/logging" + "github.com/BuxOrg/spv-wallet/server" ) -// main method starts everything for the BUX Server -// @title BUX: Server +// main method starts everything for the SPV Wallet +// @title SPV: Wallet // @version v0.12.0 -// @securityDefinitions.apikey bux-auth-xpub +// @securityDefinitions.apikey spv-wallet-auth-xpub // @in header -// @name bux-auth-xpub +// @name spv-wallet-auth-xpub // @securityDefinitions.apikey callback-auth // @in header @@ -49,7 +49,7 @@ func main() { return } - // Try to ping the pulse service if enabled + // Try to ping the block header service if enabled appConfig.CheckPulse(context.Background(), services.Logger) // @mrz New Relic is ready at this point diff --git a/config.example.yaml b/config.example.yaml index b67369a5e..b65773a0b 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -12,7 +12,7 @@ cache: # cluster coordinator - redis/memory coordinator: memory # prefix for channel names - prefix: bux_cluster_ + prefix: spv_cluster_ redis: null # cache engine - freecache/redis engine: freecache @@ -60,7 +60,7 @@ db: tx_timeout: 10s user: postgres sqlite: - database_path: ./bux.db + database_path: ./spv.db debug: false max_connection_idle_time: 0s max_connection_time: 0s @@ -83,7 +83,7 @@ new_relic: license_key: BOGUS-LICENSE-KEY-1234567890987654321234 nodes: # deployment id used annotating api calls in XDeployment-ID header - this value will be randomly generated if not set - _deployment_id: bux-deployment-id + _deployment_id: spv-deployment-id callback: callback_host: https://xyz.com # token to authenticate callback calls - default callback token will be generated from the Admin Key @@ -109,14 +109,14 @@ notifications: webhook_endpoint: "" paymail: beef: - pulse_auth_token: mQZQ6WmxURxWz5ch - # url to pulse, used for merkle root verification - pulse_url: http://localhost:8080/api/v1/chain/merkleroot/verify + bhs_auth_token: mQZQ6WmxURxWz5ch + # url to block header service, used for merkle root verification + bhs_url: http://localhost:8080/api/v1/chain/merkleroot/verify use_beef: false # set is as a default sender paymail if account does not have one default_from_paymail: from@domain.com # default note added into transactions - Deprecated - default_note: bux Address Resolution + default_note: spv Address Resolution # enable paymail domain validation, paymail domain must be in domains list to be valid and that the transaction can be processed domain_validation_enabled: false # list of domains used for paymail domain validation diff --git a/config/config.go b/config/config.go index 4408bea05..72b9f07ae 100644 --- a/config/config.go +++ b/config/config.go @@ -10,28 +10,28 @@ import ( "github.com/mrz1836/go-datastore" ) -// Config constants used for bux-server +// Config constants used for spv-wallet const ( - ApplicationName = "BuxServer" + ApplicationName = "SPVWallet" APIVersion = "v1" DefaultNewRelicShutdown = 10 * time.Second HealthRequestPath = "health" Version = "v0.12.0" ConfigFilePathKey = "config_file" DefaultConfigFilePath = "config.yaml" - ConfigEnvPrefix = "BUX_" + ConfigEnvPrefix = "SPV_" BroadcastCallbackRoute = "/transaction/broadcast/callback" ) // AppConfig is the configuration values and associated env vars type AppConfig struct { - // Authentication is the configuration for keys authentication in bux. + // Authentication is the configuration for keys authentication in spv. Authentication *AuthenticationConfig `json:"auth" mapstructure:"auth"` // Cache is the configuration for cache, memory or redis, and cluster cache settings. Cache *CacheConfig `json:"cache" mapstructure:"cache"` // Db is the configuration for database related settings. Db *DbConfig `json:"db" mapstructure:"db"` - // Debug is a flag for enabling additional information from bux. + // Debug is a flag for enabling additional information from spv. Debug bool `json:"debug" mapstructure:"debug"` // DebugProfiling is a flag for enabling additinal debug profiling. DebugProfiling bool `json:"debug_profiling" mapstructure:"debug_profiling"` @@ -39,7 +39,7 @@ type AppConfig struct { DisableITC bool `json:"disable_itc" mapstructure:"disable_itc"` // ImportBlockHeaders is a URL from where the headers can be downloaded. ImportBlockHeaders string `json:"import_block_headers" mapstructure:"import_block_headers"` - // Logging is the configuration for zerolog used in bux. + // Logging is the configuration for zerolog used in spv. Logging *LoggingConfig `json:"logging" mapstructure:"logging"` // NewRelic is New Relic related settings. NewRelic *NewRelicConfig `json:"new_relic" mapstructure:"new_relic"` @@ -51,11 +51,11 @@ type AppConfig struct { Paymail *PaymailConfig `json:"paymail" mapstructure:"paymail"` // RequestLogging is flag for enabling logging in go-api-router. RequestLogging bool `json:"request_logging" mapstructure:"request_logging"` - // Server is a general configuration for bux-server. + // Server is a general configuration for spv-wallet. Server *ServerConfig `json:"server_config" mapstructure:"server_config"` - // TaskManager is a configuration for Task Manager in bux. + // TaskManager is a configuration for Task Manager in spv. TaskManager *TaskManagerConfig `json:"task_manager" mapstructure:"task_manager"` - // Metrics is a configuration for metrics in bux. + // Metrics is a configuration for metrics in spv. Metrics *MetricsConfig `json:"metrics" mapstructure:"metrics"` } @@ -75,7 +75,7 @@ type AuthenticationConfig struct { type CacheConfig struct { // Engine is the cache engine to use (redis, freecache). Engine cachestore.Engine `json:"engine" mapstructure:"engine"` - // Cluster is the cluster-specific configuration for bux. + // Cluster is the cluster-specific configuration for spv. Cluster *ClusterConfig `json:"cluster" mapstructure:"cluster"` // Redis is a general config for redis if the engine is set to it. Redis *RedisConfig `json:"redis" mapstructure:"redis"` @@ -89,7 +89,7 @@ type CallbackConfig struct { CallbackToken string `json:"callback_token" mapstructure:"callback_token"` } -// ClusterConfig is a configuration for the Bux cluster +// ClusterConfig is a configuration for the SPV cluster type ClusterConfig struct { // Coordinator is a cluster coordinator (redis or memory). Coordinator cluster.Coordinator `json:"coordinator" mapstructure:"coordinator"` @@ -249,5 +249,5 @@ type MetricsConfig struct { // GetUserAgent will return the outgoing user agent func (a *AppConfig) GetUserAgent() string { - return "BUX-Server " + Version + return "SPV-Wallet " + Version } diff --git a/config/config_test.go b/config/config_test.go index 95f81ba2f..0c29b5a87 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/BuxOrg/bux-server/logging" + "github.com/BuxOrg/spv-wallet/logging" "github.com/mrz1836/go-cachestore" "github.com/mrz1836/go-datastore" "github.com/stretchr/testify/assert" diff --git a/config/defaults.go b/config/defaults.go index 61747783e..01a398edb 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -45,7 +45,7 @@ func getCacheDefaults() *CacheConfig { Engine: "freecache", Cluster: &ClusterConfig{ Coordinator: "memory", - Prefix: "bux_cluster_", + Prefix: "spv_cluster_", Redis: nil, }, Redis: &RedisConfig{ @@ -95,7 +95,7 @@ func getDbDefaults() *DbConfig { SslMode: "disable", }, SQLite: &datastore.SQLiteConfig{ - DatabasePath: "./bux.db", + DatabasePath: "./spv.db", ExistingConnection: nil, Shared: true, }, @@ -106,7 +106,7 @@ func getLoggingDefaults() *LoggingConfig { return &LoggingConfig{ Level: "info", Format: "console", - InstanceName: "bux-server", + InstanceName: "spv-wallet", LogOrigin: false, } } @@ -122,7 +122,7 @@ func getNewRelicDefaults() *NewRelicConfig { func getNodesDefaults() *NodesConfig { depIDSufix, _ := uuid.NewUUID() return &NodesConfig{ - DeploymentID: "bux-" + depIDSufix.String(), + DeploymentID: "spv-" + depIDSufix.String(), Protocol: NodesProtocolArc, Callback: getCallbackDefaults(), Apis: []*MinerAPI{ diff --git a/config/flags.go b/config/flags.go index 96fd6c999..8a41fc88f 100644 --- a/config/flags.go +++ b/config/flags.go @@ -20,22 +20,22 @@ func loadFlags() error { } cli := &cliFlags{} - buxFlags := pflag.NewFlagSet("buxFlags", pflag.ContinueOnError) + appFlags := pflag.NewFlagSet("appFlags", pflag.ContinueOnError) - initFlags(buxFlags, cli) + initFlags(appFlags, cli) - err := buxFlags.Parse(os.Args[1:]) + err := appFlags.Parse(os.Args[1:]) if err != nil { fmt.Printf("Flags can't be parsed: %v\n", err) os.Exit(1) } - err = viper.BindPFlag(ConfigFilePathKey, buxFlags.Lookup(ConfigFilePathKey)) + err = viper.BindPFlag(ConfigFilePathKey, appFlags.Lookup(ConfigFilePathKey)) if err != nil { return err } - parseCliFlags(buxFlags, cli) + parseCliFlags(appFlags, cli) return nil } @@ -59,7 +59,7 @@ func parseCliFlags(fs *pflag.FlagSet, cli *cliFlags) { } if cli.showVersion { - fmt.Println("bux-sever", "version", Version) + fmt.Println("spv-wallet", "version", Version) os.Exit(0) } diff --git a/config/load.go b/config/load.go index 3b519ee8a..c89681e25 100644 --- a/config/load.go +++ b/config/load.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - "github.com/BuxOrg/bux-server/dictionary" + "github.com/BuxOrg/spv-wallet/dictionary" "github.com/mitchellh/mapstructure" "github.com/rs/zerolog" "github.com/spf13/viper" @@ -68,7 +68,7 @@ func setDefaults() error { } func envConfig() { - viper.SetEnvPrefix("BUX") + viper.SetEnvPrefix("SPV") viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() } diff --git a/config/load_test.go b/config/load_test.go index e09fba777..0b3b189d9 100644 --- a/config/load_test.go +++ b/config/load_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "github.com/BuxOrg/bux-server/logging" + "github.com/BuxOrg/spv-wallet/logging" "github.com/spf13/viper" "github.com/stretchr/testify/assert" ) @@ -30,9 +30,9 @@ func TestLoadConfig(t *testing.T) { // when // IMPORTANT! If you need to change the name of this variable, it means you're - // making backwards incompatible changes. Please inform all Bux adoptors and + // making backwards incompatible changes. Please inform all SPV adoptors and // update your configs on all servers and scripts. - os.Setenv("BUX_CONFIG_FILE", anotherPath) + os.Setenv("SPV_CONFIG_FILE", anotherPath) _, err := Load(defaultLogger) // then @@ -40,6 +40,6 @@ func TestLoadConfig(t *testing.T) { assert.Error(t, err) // cleanup - os.Unsetenv("BUX_CONFIG_FILE") + os.Unsetenv("SPV_CONFIG_FILE") }) } diff --git a/config/services.go b/config/services.go index 6ea8dc643..bbab03b12 100644 --- a/config/services.go +++ b/config/services.go @@ -9,11 +9,11 @@ import ( "time" "github.com/BuxOrg/bux" - "github.com/BuxOrg/bux-server/logging" - "github.com/BuxOrg/bux-server/metrics" "github.com/BuxOrg/bux/cluster" "github.com/BuxOrg/bux/taskmanager" "github.com/BuxOrg/bux/utils" + "github.com/BuxOrg/spv-wallet/logging" + "github.com/BuxOrg/spv-wallet/metrics" broadcastclient "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client" "github.com/go-redis/redis/v8" "github.com/mrz1836/go-cachestore" @@ -25,7 +25,7 @@ import ( // AppServices is the loaded services via config type ( AppServices struct { - Bux bux.ClientInterface + SPV bux.ClientInterface NewRelic *newrelic.Application Logger *zerolog.Logger } @@ -54,8 +54,8 @@ func (a *AppConfig) LoadServices(ctx context.Context) (*AppServices, error) { _services.Logger = logger - // Load BUX - if err = _services.loadBux(ctx, a, false, logger); err != nil { + // Load SPV Wallet + if err = _services.loadSPV(ctx, a, false, logger); err != nil { return nil, err } @@ -78,8 +78,8 @@ func (a *AppConfig) LoadTestServices(ctx context.Context) (*AppServices, error) txn := _services.NewRelic.StartTransaction("services_load_test") defer txn.End() - // Load bux for testing - if err = _services.loadBux(ctx, a, true, _services.Logger); err != nil { + // Load spv for testing + if err = _services.loadSPV(ctx, a, true, _services.Logger); err != nil { return nil, err } @@ -114,10 +114,10 @@ func (a *AppConfig) loadNewRelic(services *AppServices) (err error) { // CloseAll will close all connections to all services func (s *AppServices) CloseAll(ctx context.Context) { - // Close Bux - if s.Bux != nil { - _ = s.Bux.Close(ctx) - s.Bux = nil + // Close SPV + if s.SPV != nil { + _ = s.SPV.Close(ctx) + s.SPV = nil } // Close new relic @@ -132,8 +132,8 @@ func (s *AppServices) CloseAll(ctx context.Context) { } } -// loadBux will load the bux client (including CacheStore and DataStore) -func (s *AppServices) loadBux(ctx context.Context, appConfig *AppConfig, testMode bool, logger *zerolog.Logger) (err error) { +// loadSPV will load the spv client (including CacheStore and DataStore) +func (s *AppServices) loadSPV(ctx context.Context, appConfig *AppConfig, testMode bool, logger *zerolog.Logger) (err error) { var options []bux.ClientOps if appConfig.NewRelic.Enabled { @@ -148,8 +148,8 @@ func (s *AppServices) loadBux(ctx context.Context, appConfig *AppConfig, testMod options = append(options, bux.WithUserAgent(appConfig.GetUserAgent())) if logger != nil { - buxLogger := logger.With().Str("service", "bux").Logger() - options = append(options, bux.WithLogger(&buxLogger)) + spvLogger := logger.With().Str("service", "spv").Logger() + options = append(options, bux.WithLogger(&spvLogger)) } if appConfig.Debug { @@ -202,7 +202,7 @@ func (s *AppServices) loadBux(ctx context.Context, appConfig *AppConfig, testMod } // Create the new client - s.Bux, err = bux.NewClient(ctx, options...) + s.SPV, err = bux.NewClient(ctx, options...) return } diff --git a/config/services_test.go b/config/services_test.go index 3ea4cb32c..6ed7afc51 100644 --- a/config/services_test.go +++ b/config/services_test.go @@ -34,7 +34,7 @@ func TestAppServices_CloseAll(t *testing.T) { require.NotNil(t, s) s.CloseAll(context.Background()) - assert.Nil(t, s.Bux) + assert.Nil(t, s.SPV) assert.Nil(t, s.NewRelic) }) } @@ -47,6 +47,6 @@ func TestAppConfig_GetUserAgent(t *testing.T) { ac := newTestConfig(t) require.NotNil(t, ac) agent := ac.GetUserAgent() - assert.Equal(t, "BUX-Server "+Version, agent) + assert.Equal(t, "SPV-Wallet "+Version, agent) }) } diff --git a/config/task_manager.go b/config/task_manager.go index 506367381..68ca84431 100644 --- a/config/task_manager.go +++ b/config/task_manager.go @@ -2,5 +2,5 @@ package config // TaskManager defaults const ( - TaskManagerQueueName = "bux_queue" + TaskManagerQueueName = "spv_queue" ) diff --git a/go.mod b/go.mod index a10ff064e..9d503f62d 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/BuxOrg/bux-server +module github.com/BuxOrg/spv-wallet go 1.21.5 diff --git a/logging/logging.go b/logging/logging.go index 3ff5cbade..bae08dc8b 100644 --- a/logging/logging.go +++ b/logging/logging.go @@ -59,7 +59,7 @@ func GetDefaultLogger() *zerolog.Logger { logger := ecszerolog.New(os.Stdout, ecszerolog.Level(zerolog.DebugLevel)). With(). Caller(). - Str("application", "bux-default"). + Str("application", "spv-default"). Logger() return &logger diff --git a/mappings/access_keys.go b/mappings/access_keys.go index a6ce12708..4d139c4b2 100644 --- a/mappings/access_keys.go +++ b/mappings/access_keys.go @@ -5,12 +5,12 @@ import ( "time" "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/mappings/common" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/mappings/common" ) -// MapToAccessKeyContract will map the access key to the bux-models contract -func MapToAccessKeyContract(ac *bux.AccessKey) *buxmodels.AccessKey { +// MapToAccessKeyContract will map the access key to the spv-wallet-models contract +func MapToAccessKeyContract(ac *bux.AccessKey) *spvwalletmodels.AccessKey { if ac == nil { return nil } @@ -20,7 +20,7 @@ func MapToAccessKeyContract(ac *bux.AccessKey) *buxmodels.AccessKey { revokedAt = &ac.RevokedAt.Time } - return &buxmodels.AccessKey{ + return &spvwalletmodels.AccessKey{ Model: *common.MapToContract(&ac.Model), ID: ac.ID, XpubID: ac.XpubID, diff --git a/mappings/admin.go b/mappings/admin.go index e86e9029c..04a28d7a2 100644 --- a/mappings/admin.go +++ b/mappings/admin.go @@ -2,16 +2,16 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" + spvwalletmodels "github.com/BuxOrg/bux-models" ) -// MapToAdminStatsContract will map the model from bux to the bux-models contract -func MapToAdminStatsContract(s *bux.AdminStats) *buxmodels.AdminStats { +// MapToAdminStatsContract will map the model from spv-wallet to the spv-wallet-models contract +func MapToAdminStatsContract(s *bux.AdminStats) *spvwalletmodels.AdminStats { if s == nil { return nil } - return &buxmodels.AdminStats{ + return &spvwalletmodels.AdminStats{ Balance: s.Balance, Destinations: s.Destinations, PaymailAddresses: s.PaymailAddresses, diff --git a/mappings/common/common.go b/mappings/common/common.go index fff1f9a5e..5017b79fa 100644 --- a/mappings/common/common.go +++ b/mappings/common/common.go @@ -6,7 +6,7 @@ import ( "github.com/BuxOrg/bux-models/common" ) -// MapToContract will map the common model to the bux-models contract +// MapToContract will map the common model to the spv-wallet-models contract func MapToContract(m *bux.Model) *common.Model { if m == nil { return nil @@ -20,7 +20,7 @@ func MapToContract(m *bux.Model) *common.Model { } } -// MapToModel will map the bux-models contract to the common bux model +// MapToModel will map the spv-wallet-models contract to the common spv model func MapToModel(m *common.Model) *bux.Model { if m == nil { return nil diff --git a/mappings/destination.go b/mappings/destination.go index 98832aff1..04e6b491f 100644 --- a/mappings/destination.go +++ b/mappings/destination.go @@ -2,17 +2,17 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/mappings/common" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/mappings/common" ) -// MapToDestinationContract will map the bux destination model to the bux-models contract -func MapToDestinationContract(d *bux.Destination) *buxmodels.Destination { +// MapToDestinationContract will map the spv-wallet destination model to the spv-wallet-models contract +func MapToDestinationContract(d *bux.Destination) *spvwalletmodels.Destination { if d == nil { return nil } - return &buxmodels.Destination{ + return &spvwalletmodels.Destination{ Model: *common.MapToContract(&d.Model), ID: d.ID, XpubID: d.XpubID, @@ -25,8 +25,8 @@ func MapToDestinationContract(d *bux.Destination) *buxmodels.Destination { } } -// MapToDestinationBux will map the bux-models destination contract to the bux destination model -func MapToDestinationBux(d *buxmodels.Destination) *bux.Destination { +// MapToDestinationSPV will map the spv-wallet-models destination contract to the spv-wallet destination model +func MapToDestinationSPV(d *spvwalletmodels.Destination) *bux.Destination { if d == nil { return nil } diff --git a/mappings/fee_unit.go b/mappings/fee_unit.go index 516807051..8078b62c9 100644 --- a/mappings/fee_unit.go +++ b/mappings/fee_unit.go @@ -1,24 +1,24 @@ package mappings import ( - buxmodels "github.com/BuxOrg/bux-models" + spvwalletmodels "github.com/BuxOrg/bux-models" "github.com/BuxOrg/bux/utils" ) -// MapToFeeUnitContract will map the fee-unit model from bux to the bux-models contract -func MapToFeeUnitContract(fu *utils.FeeUnit) (fc *buxmodels.FeeUnit) { +// MapToFeeUnitContract will map the fee-unit model from spv-wallet to the spv-wallet-models contract +func MapToFeeUnitContract(fu *utils.FeeUnit) (fc *spvwalletmodels.FeeUnit) { if fu == nil { return nil } - return &buxmodels.FeeUnit{ + return &spvwalletmodels.FeeUnit{ Satoshis: fu.Satoshis, Bytes: fu.Bytes, } } -// MapToFeeUnitBux will map the fee-unit model from bux-models to the bux contract -func MapToFeeUnitBux(fu *buxmodels.FeeUnit) (fc *utils.FeeUnit) { +// MapToFeeUnitSPV will map the fee-unit model from spv-wallet-models to the spv-wallet contract +func MapToFeeUnitSPV(fu *spvwalletmodels.FeeUnit) (fc *utils.FeeUnit) { if fu == nil { return nil } diff --git a/mappings/metadata.go b/mappings/metadata.go index 08c777470..fcc1fca7f 100644 --- a/mappings/metadata.go +++ b/mappings/metadata.go @@ -2,11 +2,11 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" + spvwalletmodels "github.com/BuxOrg/bux-models" ) -// MapToBuxMetadata will map the *buxmodels.Metadata to *bux.Metadata -func MapToBuxMetadata(metadata *buxmodels.Metadata) *bux.Metadata { +// MapToSPVMetadata will map the *spvwalletmodels.Metadata to *spv.Metadata +func MapToSPVMetadata(metadata *spvwalletmodels.Metadata) *bux.Metadata { if metadata == nil { return nil } diff --git a/mappings/paymail_address.go b/mappings/paymail_address.go index 81b06a24f..a52461015 100644 --- a/mappings/paymail_address.go +++ b/mappings/paymail_address.go @@ -2,17 +2,17 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/mappings/common" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/mappings/common" ) -// MapToPaymailContract will map the bux paymail-address model to the bux-models contract -func MapToPaymailContract(pa *bux.PaymailAddress) *buxmodels.PaymailAddress { +// MapToPaymailContract will map the spv-wallet paymail-address model to the spv-wallet-models contract +func MapToPaymailContract(pa *bux.PaymailAddress) *spvwalletmodels.PaymailAddress { if pa == nil { return nil } - return &buxmodels.PaymailAddress{ + return &spvwalletmodels.PaymailAddress{ Model: *common.MapToContract(&pa.Model), ID: pa.ID, XpubID: pa.XpubID, @@ -24,13 +24,13 @@ func MapToPaymailContract(pa *bux.PaymailAddress) *buxmodels.PaymailAddress { } } -// MapToPaymailP4Contract will map the bux-models paymail-address contract to the bux paymail-address model -func MapToPaymailP4Contract(p *bux.PaymailP4) *buxmodels.PaymailP4 { +// MapToPaymailP4Contract will map the spv-wallet-models paymail-address contract to the spv-wallet paymail-address model +func MapToPaymailP4Contract(p *bux.PaymailP4) *spvwalletmodels.PaymailP4 { if p == nil { return nil } - return &buxmodels.PaymailP4{ + return &spvwalletmodels.PaymailP4{ Alias: p.Alias, Domain: p.Domain, FromPaymail: p.FromPaymail, @@ -42,8 +42,8 @@ func MapToPaymailP4Contract(p *bux.PaymailP4) *buxmodels.PaymailP4 { } } -// MapToPaymailP4Bux will map the bux-models paymail-address contract to the bux paymail-address model -func MapToPaymailP4Bux(p *buxmodels.PaymailP4) *bux.PaymailP4 { +// MapToPaymailP4SPV will map the spv-wallet-models paymail-address contract to the spv-wallet paymail-address model +func MapToPaymailP4SPV(p *spvwalletmodels.PaymailP4) *bux.PaymailP4 { if p == nil { return nil } diff --git a/mappings/script_output.go b/mappings/script_output.go index cbe02816c..8daa96dda 100644 --- a/mappings/script_output.go +++ b/mappings/script_output.go @@ -2,16 +2,16 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" + spvwalletmodels "github.com/BuxOrg/bux-models" ) -// MapToScriptOutputContract will map the script-output model from bux to the bux-models contract -func MapToScriptOutputContract(so *bux.ScriptOutput) (sc *buxmodels.ScriptOutput) { +// MapToScriptOutputContract will map the script-output model from spv-wallet to the spv-wallet-models contract +func MapToScriptOutputContract(so *bux.ScriptOutput) (sc *spvwalletmodels.ScriptOutput) { if so == nil { return nil } - return &buxmodels.ScriptOutput{ + return &spvwalletmodels.ScriptOutput{ Address: so.Address, Satoshis: so.Satoshis, Script: so.Script, @@ -19,8 +19,8 @@ func MapToScriptOutputContract(so *bux.ScriptOutput) (sc *buxmodels.ScriptOutput } } -// MapToScriptOutputBux will map the script-output model from bux-models to the bux contract -func MapToScriptOutputBux(so *buxmodels.ScriptOutput) (sc *bux.ScriptOutput) { +// MapToScriptOutputSPV will map the script-output model from spv-wallet-models to the spv-wallet contract +func MapToScriptOutputSPV(so *spvwalletmodels.ScriptOutput) (sc *bux.ScriptOutput) { if so == nil { return nil } diff --git a/mappings/sync_config.go b/mappings/sync_config.go index 322fe3e71..46dbf0a61 100644 --- a/mappings/sync_config.go +++ b/mappings/sync_config.go @@ -2,16 +2,16 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" + spvwalletmodels "github.com/BuxOrg/bux-models" ) -// MapToSyncConfigContract will map the sync-config model from bux to the bux-models contract -func MapToSyncConfigContract(sc *bux.SyncConfig) *buxmodels.SyncConfig { +// MapToSyncConfigContract will map the sync-config model from spv-wallet to the spv-wallet-models contract +func MapToSyncConfigContract(sc *bux.SyncConfig) *spvwalletmodels.SyncConfig { if sc == nil { return nil } - return &buxmodels.SyncConfig{ + return &spvwalletmodels.SyncConfig{ Broadcast: sc.Broadcast, BroadcastInstant: sc.BroadcastInstant, PaymailP2P: sc.PaymailP2P, @@ -19,8 +19,8 @@ func MapToSyncConfigContract(sc *bux.SyncConfig) *buxmodels.SyncConfig { } } -// MapToSyncConfigBux will map the sync-config model from bux-models to the bux contract -func MapToSyncConfigBux(sc *buxmodels.SyncConfig) *bux.SyncConfig { +// MapToSyncConfigSPV will map the sync-config model from spv-wallet-models to the spv-wallet contract +func MapToSyncConfigSPV(sc *spvwalletmodels.SyncConfig) *bux.SyncConfig { if sc == nil { return nil } diff --git a/mappings/transaction.go b/mappings/transaction.go index 14777ccc5..0a447a7de 100644 --- a/mappings/transaction.go +++ b/mappings/transaction.go @@ -2,17 +2,17 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/mappings/common" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/mappings/common" ) -// MapToTransactionContract will map the model from bux to the bux-models contract -func MapToTransactionContract(t *bux.Transaction) *buxmodels.Transaction { +// MapToTransactionContract will map the model from spv-wallet to the spv-wallet-models contract +func MapToTransactionContract(t *bux.Transaction) *spvwalletmodels.Transaction { if t == nil { return nil } - model := buxmodels.Transaction{ + model := spvwalletmodels.Transaction{ Model: *common.MapToContract(&t.Model), ID: t.ID, Hex: t.Hex, @@ -35,13 +35,13 @@ func MapToTransactionContract(t *bux.Transaction) *buxmodels.Transaction { return &model } -// MapToTransactionContractForAdmin will map the model from bux to the bux-models contract for admin -func MapToTransactionContractForAdmin(t *bux.Transaction) *buxmodels.Transaction { +// MapToTransactionContractForAdmin will map the model from spv-wallet to the spv-wallet-models contract for admin +func MapToTransactionContractForAdmin(t *bux.Transaction) *spvwalletmodels.Transaction { if t == nil { return nil } - model := buxmodels.Transaction{ + model := spvwalletmodels.Transaction{ Model: *common.MapToContract(&t.Model), ID: t.ID, Hex: t.Hex, @@ -63,10 +63,10 @@ func MapToTransactionContractForAdmin(t *bux.Transaction) *buxmodels.Transaction return &model } -func processMetadata(t *bux.Transaction, xpubID string, model *buxmodels.Transaction) { +func processMetadata(t *bux.Transaction, xpubID string, model *spvwalletmodels.Transaction) { if len(t.XpubMetadata) > 0 && len(t.XpubMetadata[xpubID]) > 0 { if t.Model.Metadata == nil { - model.Model.Metadata = make(buxmodels.Metadata) + model.Model.Metadata = make(spvwalletmodels.Metadata) } for key, value := range t.XpubMetadata[xpubID] { model.Model.Metadata[key] = value @@ -74,7 +74,7 @@ func processMetadata(t *bux.Transaction, xpubID string, model *buxmodels.Transac } } -func processOutputValue(t *bux.Transaction, xpubID string, model *buxmodels.Transaction) { +func processOutputValue(t *bux.Transaction, xpubID string, model *spvwalletmodels.Transaction) { model.OutputValue = int64(0) if len(t.XpubOutputValue) > 0 && t.XpubOutputValue[xpubID] != 0 { model.OutputValue = t.XpubOutputValue[xpubID] @@ -87,8 +87,8 @@ func processOutputValue(t *bux.Transaction, xpubID string, model *buxmodels.Tran } } -// MapToTransactionBux will map the model from bux-models to the bux contract -func MapToTransactionBux(t *buxmodels.Transaction) *bux.Transaction { +// MapToTransactionSPV will map the model from spv-wallet-models to the spv-wallet contract +func MapToTransactionSPV(t *spvwalletmodels.Transaction) *bux.Transaction { if t == nil { return nil } @@ -111,97 +111,97 @@ func MapToTransactionBux(t *buxmodels.Transaction) *bux.Transaction { } } -// MapToTransactionConfigBux will map the transaction-config model from bux to the bux-models contract -func MapToTransactionConfigBux(tx *buxmodels.TransactionConfig) *bux.TransactionConfig { +// MapToTransactionConfigSPV will map the transaction-config model from spv-wallet to the spv-wallet-models contract +func MapToTransactionConfigSPV(tx *spvwalletmodels.TransactionConfig) *bux.TransactionConfig { if tx == nil { return nil } return &bux.TransactionConfig{ - ChangeDestinations: mapToBuxDestinations(tx), + ChangeDestinations: mapToSPVDestinations(tx), ChangeDestinationsStrategy: bux.ChangeStrategy(tx.ChangeStrategy), ChangeMinimumSatoshis: tx.ChangeMinimumSatoshis, ChangeNumberOfDestinations: tx.ChangeNumberOfDestinations, ChangeSatoshis: tx.ChangeSatoshis, ExpiresIn: tx.ExpiresIn, Fee: tx.Fee, - FeeUnit: MapToFeeUnitBux(tx.FeeUnit), - FromUtxos: mapToBuxFromUtxos(tx), - IncludeUtxos: mapToBuxIncludeUtxos(tx), - Inputs: mapToBuxInputs(tx), - Outputs: mapToBuxOutputs(tx), - SendAllTo: MapToTransactionOutputBux(tx.SendAllTo), - Sync: MapToSyncConfigBux(tx.Sync), + FeeUnit: MapToFeeUnitSPV(tx.FeeUnit), + FromUtxos: mapToSPVFromUtxos(tx), + IncludeUtxos: mapToSPVIncludeUtxos(tx), + Inputs: mapToSPVInputs(tx), + Outputs: mapToSPVOutputs(tx), + SendAllTo: MapToTransactionOutputSPV(tx.SendAllTo), + Sync: MapToSyncConfigSPV(tx.Sync), } } -func mapToBuxOutputs(tx *buxmodels.TransactionConfig) []*bux.TransactionOutput { +func mapToSPVOutputs(tx *spvwalletmodels.TransactionConfig) []*bux.TransactionOutput { if tx.Outputs == nil { return nil } outputs := make([]*bux.TransactionOutput, 0) for _, output := range tx.Outputs { - outputs = append(outputs, MapToTransactionOutputBux(output)) + outputs = append(outputs, MapToTransactionOutputSPV(output)) } return outputs } -func mapToBuxInputs(tx *buxmodels.TransactionConfig) []*bux.TransactionInput { +func mapToSPVInputs(tx *spvwalletmodels.TransactionConfig) []*bux.TransactionInput { if tx.Inputs == nil { return nil } inputs := make([]*bux.TransactionInput, 0) for _, input := range tx.Inputs { - inputs = append(inputs, MapToTransactionInputBux(input)) + inputs = append(inputs, MapToTransactionInputSPV(input)) } return inputs } -func mapToBuxIncludeUtxos(tx *buxmodels.TransactionConfig) []*bux.UtxoPointer { +func mapToSPVIncludeUtxos(tx *spvwalletmodels.TransactionConfig) []*bux.UtxoPointer { if tx.IncludeUtxos == nil { return nil } includeUtxos := make([]*bux.UtxoPointer, 0) for _, utxo := range tx.IncludeUtxos { - includeUtxos = append(includeUtxos, MapToUtxoPointerBux(utxo)) + includeUtxos = append(includeUtxos, MapToUtxoPointerSPV(utxo)) } return includeUtxos } -func mapToBuxFromUtxos(tx *buxmodels.TransactionConfig) []*bux.UtxoPointer { +func mapToSPVFromUtxos(tx *spvwalletmodels.TransactionConfig) []*bux.UtxoPointer { if tx.FromUtxos == nil { return nil } fromUtxos := make([]*bux.UtxoPointer, 0) for _, utxo := range tx.FromUtxos { - fromUtxos = append(fromUtxos, MapToUtxoPointerBux(utxo)) + fromUtxos = append(fromUtxos, MapToUtxoPointerSPV(utxo)) } return fromUtxos } -func mapToBuxDestinations(tx *buxmodels.TransactionConfig) []*bux.Destination { +func mapToSPVDestinations(tx *spvwalletmodels.TransactionConfig) []*bux.Destination { if tx.ChangeDestinations == nil { return nil } destinations := make([]*bux.Destination, 0) for _, destination := range tx.ChangeDestinations { - destinations = append(destinations, MapToDestinationBux(destination)) + destinations = append(destinations, MapToDestinationSPV(destination)) } return destinations } -// MapToTransactionConfigContract will map the transaction-config model from bux-models to the bux contract -func MapToTransactionConfigContract(tx *bux.TransactionConfig) *buxmodels.TransactionConfig { +// MapToTransactionConfigContract will map the transaction-config model from spv-wallet-models to the spv-wallet contract +func MapToTransactionConfigContract(tx *bux.TransactionConfig) *spvwalletmodels.TransactionConfig { if tx == nil { return nil } - return &buxmodels.TransactionConfig{ + return &spvwalletmodels.TransactionConfig{ ChangeDestinations: mapToContractDestinations(tx), ChangeStrategy: string(tx.ChangeDestinationsStrategy), ChangeMinimumSatoshis: tx.ChangeMinimumSatoshis, @@ -218,73 +218,73 @@ func MapToTransactionConfigContract(tx *bux.TransactionConfig) *buxmodels.Transa } } -func mapToContractOutputs(tx *bux.TransactionConfig) []*buxmodels.TransactionOutput { +func mapToContractOutputs(tx *bux.TransactionConfig) []*spvwalletmodels.TransactionOutput { if tx.Outputs == nil { return nil } - outputs := make([]*buxmodels.TransactionOutput, 0) + outputs := make([]*spvwalletmodels.TransactionOutput, 0) for _, output := range tx.Outputs { outputs = append(outputs, MapToTransactionOutputContract(output)) } return outputs } -func mapToContractInputs(tx *bux.TransactionConfig) []*buxmodels.TransactionInput { +func mapToContractInputs(tx *bux.TransactionConfig) []*spvwalletmodels.TransactionInput { if tx.Inputs == nil { return nil } - inputs := make([]*buxmodels.TransactionInput, 0) + inputs := make([]*spvwalletmodels.TransactionInput, 0) for _, input := range tx.Inputs { inputs = append(inputs, MapToTransactionInputContract(input)) } return inputs } -func mapToContractIncludeUtxos(tx *bux.TransactionConfig) []*buxmodels.UtxoPointer { +func mapToContractIncludeUtxos(tx *bux.TransactionConfig) []*spvwalletmodels.UtxoPointer { if tx.IncludeUtxos == nil { return nil } - includeUtxos := make([]*buxmodels.UtxoPointer, 0) + includeUtxos := make([]*spvwalletmodels.UtxoPointer, 0) for _, utxo := range tx.IncludeUtxos { includeUtxos = append(includeUtxos, MapToUtxoPointer(utxo)) } return includeUtxos } -func mapToContractFromUtxos(tx *bux.TransactionConfig) []*buxmodels.UtxoPointer { +func mapToContractFromUtxos(tx *bux.TransactionConfig) []*spvwalletmodels.UtxoPointer { if tx.FromUtxos == nil { return nil } - fromUtxos := make([]*buxmodels.UtxoPointer, 0) + fromUtxos := make([]*spvwalletmodels.UtxoPointer, 0) for _, utxo := range tx.FromUtxos { fromUtxos = append(fromUtxos, MapToUtxoPointer(utxo)) } return fromUtxos } -func mapToContractDestinations(tx *bux.TransactionConfig) []*buxmodels.Destination { +func mapToContractDestinations(tx *bux.TransactionConfig) []*spvwalletmodels.Destination { if tx.ChangeDestinations == nil { return nil } - destinations := make([]*buxmodels.Destination, 0) + destinations := make([]*spvwalletmodels.Destination, 0) for _, destination := range tx.ChangeDestinations { destinations = append(destinations, MapToDestinationContract(destination)) } return destinations } -// MapToDraftTransactionContract will map the transaction-output model from bux to the bux-models contract -func MapToDraftTransactionContract(tx *bux.DraftTransaction) *buxmodels.DraftTransaction { +// MapToDraftTransactionContract will map the transaction-output model from spv-wallet to the spv-wallet-models contract +func MapToDraftTransactionContract(tx *bux.DraftTransaction) *spvwalletmodels.DraftTransaction { if tx == nil { return nil } - return &buxmodels.DraftTransaction{ + return &spvwalletmodels.DraftTransaction{ Model: *common.MapToContract(&tx.Model), ID: tx.ID, Hex: tx.Hex, @@ -294,42 +294,42 @@ func MapToDraftTransactionContract(tx *bux.DraftTransaction) *buxmodels.DraftTra } } -// MapToTransactionInputContract will map the transaction-output model from bux-models to the bux contract -func MapToTransactionInputContract(inp *bux.TransactionInput) *buxmodels.TransactionInput { +// MapToTransactionInputContract will map the transaction-output model from spv-wallet-models to the spv-wallet contract +func MapToTransactionInputContract(inp *bux.TransactionInput) *spvwalletmodels.TransactionInput { if inp == nil { return nil } - return &buxmodels.TransactionInput{ + return &spvwalletmodels.TransactionInput{ Utxo: *MapToUtxoContract(&inp.Utxo), Destination: *MapToDestinationContract(&inp.Destination), } } -// MapToTransactionInputBux will map the transaction-output model from bux to the bux-models contract -func MapToTransactionInputBux(inp *buxmodels.TransactionInput) *bux.TransactionInput { +// MapToTransactionInputSPV will map the transaction-output model from spv-wallet to the spv-wallet-models contract +func MapToTransactionInputSPV(inp *spvwalletmodels.TransactionInput) *bux.TransactionInput { if inp == nil { return nil } return &bux.TransactionInput{ - Utxo: *MapToUtxoBux(&inp.Utxo), - Destination: *MapToDestinationBux(&inp.Destination), + Utxo: *MapToUtxoSPV(&inp.Utxo), + Destination: *MapToDestinationSPV(&inp.Destination), } } -// MapToTransactionOutputContract will map the transaction-output model from bux to the bux-models contract -func MapToTransactionOutputContract(out *bux.TransactionOutput) *buxmodels.TransactionOutput { +// MapToTransactionOutputContract will map the transaction-output model from spv-wallet to the spv-wallet-models contract +func MapToTransactionOutputContract(out *bux.TransactionOutput) *spvwalletmodels.TransactionOutput { if out == nil { return nil } - scriptOutputs := make([]*buxmodels.ScriptOutput, 0) + scriptOutputs := make([]*spvwalletmodels.ScriptOutput, 0) for _, scriptOutput := range out.Scripts { scriptOutputs = append(scriptOutputs, MapToScriptOutputContract(scriptOutput)) } - return &buxmodels.TransactionOutput{ + return &spvwalletmodels.TransactionOutput{ OpReturn: MapToOpReturnContract(out.OpReturn), PaymailP4: MapToPaymailP4Contract(out.PaymailP4), Satoshis: out.Satoshis, @@ -340,20 +340,20 @@ func MapToTransactionOutputContract(out *bux.TransactionOutput) *buxmodels.Trans } } -// MapToTransactionOutputBux will map the transaction-output model from bux-models to the bux contract -func MapToTransactionOutputBux(out *buxmodels.TransactionOutput) *bux.TransactionOutput { +// MapToTransactionOutputSPV will map the transaction-output model from spv-wallet-models to the spv-wallet contract +func MapToTransactionOutputSPV(out *spvwalletmodels.TransactionOutput) *bux.TransactionOutput { if out == nil { return nil } scriptOutputs := make([]*bux.ScriptOutput, 0) for _, scriptOutput := range out.Scripts { - scriptOutputs = append(scriptOutputs, MapToScriptOutputBux(scriptOutput)) + scriptOutputs = append(scriptOutputs, MapToScriptOutputSPV(scriptOutput)) } return &bux.TransactionOutput{ - OpReturn: MapToOpReturnBux(out.OpReturn), - PaymailP4: MapToPaymailP4Bux(out.PaymailP4), + OpReturn: MapToOpReturnSPV(out.OpReturn), + PaymailP4: MapToPaymailP4SPV(out.PaymailP4), Satoshis: out.Satoshis, Script: out.Script, Scripts: scriptOutputs, @@ -362,21 +362,21 @@ func MapToTransactionOutputBux(out *buxmodels.TransactionOutput) *bux.Transactio } } -// MapToMapProtocolContract will map the transaction-output model from bux to the bux-models contract -func MapToMapProtocolContract(mp *bux.MapProtocol) *buxmodels.MapProtocol { +// MapToMapProtocolContract will map the transaction-output model from spv-wallet to the spv-wallet-models contract +func MapToMapProtocolContract(mp *bux.MapProtocol) *spvwalletmodels.MapProtocol { if mp == nil { return nil } - return &buxmodels.MapProtocol{ + return &spvwalletmodels.MapProtocol{ App: mp.App, Keys: mp.Keys, Type: mp.Type, } } -// MapToMapProtocolBux will map the transaction-output model from bux-models to the bux contract -func MapToMapProtocolBux(mp *buxmodels.MapProtocol) *bux.MapProtocol { +// MapToMapProtocolSPV will map the transaction-output model from spv-wallet-models to the spv-wallet contract +func MapToMapProtocolSPV(mp *spvwalletmodels.MapProtocol) *bux.MapProtocol { if mp == nil { return nil } @@ -388,13 +388,13 @@ func MapToMapProtocolBux(mp *buxmodels.MapProtocol) *bux.MapProtocol { } } -// MapToOpReturnContract will map the transaction-output model from bux to the bux-models contract -func MapToOpReturnContract(op *bux.OpReturn) *buxmodels.OpReturn { +// MapToOpReturnContract will map the transaction-output model from spv-wallet to the spv-wallet-models contract +func MapToOpReturnContract(op *bux.OpReturn) *spvwalletmodels.OpReturn { if op == nil { return nil } - return &buxmodels.OpReturn{ + return &spvwalletmodels.OpReturn{ Hex: op.Hex, HexParts: op.HexParts, Map: MapToMapProtocolContract(op.Map), @@ -402,8 +402,8 @@ func MapToOpReturnContract(op *bux.OpReturn) *buxmodels.OpReturn { } } -// MapToOpReturnBux will map the op-return model from bux-models to the bux contract -func MapToOpReturnBux(op *buxmodels.OpReturn) *bux.OpReturn { +// MapToOpReturnSPV will map the op-return model from spv-wallet-models to the spv-wallet contract +func MapToOpReturnSPV(op *spvwalletmodels.OpReturn) *bux.OpReturn { if op == nil { return nil } @@ -411,7 +411,7 @@ func MapToOpReturnBux(op *buxmodels.OpReturn) *bux.OpReturn { return &bux.OpReturn{ Hex: op.Hex, HexParts: op.HexParts, - Map: MapToMapProtocolBux(op.Map), + Map: MapToMapProtocolSPV(op.Map), StringParts: op.StringParts, } } diff --git a/mappings/utxo.go b/mappings/utxo.go index 060a6f12e..e00bf6feb 100644 --- a/mappings/utxo.go +++ b/mappings/utxo.go @@ -2,25 +2,25 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/mappings/common" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/mappings/common" customtypes "github.com/mrz1836/go-datastore/custom_types" ) -// MapToUtxoPointer will map the utxo-pointer model from bux to the bux-models contract -func MapToUtxoPointer(u *bux.UtxoPointer) *buxmodels.UtxoPointer { +// MapToUtxoPointer will map the utxo-pointer model from spv-wallet to the spv-wallet-models contract +func MapToUtxoPointer(u *bux.UtxoPointer) *spvwalletmodels.UtxoPointer { if u == nil { return nil } - return &buxmodels.UtxoPointer{ + return &spvwalletmodels.UtxoPointer{ TransactionID: u.TransactionID, OutputIndex: u.OutputIndex, } } -// MapToUtxoPointerBux will map the utxo-pointer model from bux-models to the bux contract -func MapToUtxoPointerBux(u *buxmodels.UtxoPointer) *bux.UtxoPointer { +// MapToUtxoPointerSPV will map the utxo-pointer model from spv-wallet-models to the spv-wallet contract +func MapToUtxoPointerSPV(u *spvwalletmodels.UtxoPointer) *bux.UtxoPointer { if u == nil { return nil } @@ -31,13 +31,13 @@ func MapToUtxoPointerBux(u *buxmodels.UtxoPointer) *bux.UtxoPointer { } } -// MapToUtxoContract will map the utxo model from bux to the bux-models contract -func MapToUtxoContract(u *bux.Utxo) *buxmodels.Utxo { +// MapToUtxoContract will map the utxo model from spv-wallet to the spv-wallet-models contract +func MapToUtxoContract(u *bux.Utxo) *spvwalletmodels.Utxo { if u == nil { return nil } - return &buxmodels.Utxo{ + return &spvwalletmodels.Utxo{ Model: *common.MapToContract(&u.Model), UtxoPointer: *MapToUtxoPointer(&u.UtxoPointer), ID: u.ID, @@ -51,8 +51,8 @@ func MapToUtxoContract(u *bux.Utxo) *buxmodels.Utxo { } } -// MapToUtxoBux will map the utxo model from bux-models to the bux contract -func MapToUtxoBux(u *buxmodels.Utxo) *bux.Utxo { +// MapToUtxoSPV will map the utxo model from spv-wallet-models to the spv-wallet contract +func MapToUtxoSPV(u *spvwalletmodels.Utxo) *bux.Utxo { if u == nil { return nil } @@ -65,7 +65,7 @@ func MapToUtxoBux(u *buxmodels.Utxo) *bux.Utxo { return &bux.Utxo{ Model: *common.MapToModel(&u.Model), - UtxoPointer: *MapToUtxoPointerBux(&u.UtxoPointer), + UtxoPointer: *MapToUtxoPointerSPV(&u.UtxoPointer), ID: u.ID, XpubID: u.XpubID, Satoshis: u.Satoshis, @@ -73,6 +73,6 @@ func MapToUtxoBux(u *buxmodels.Utxo) *bux.Utxo { Type: u.Type, DraftID: draftID, SpendingTxID: spendingTxID, - Transaction: MapToTransactionBux(u.Transaction), + Transaction: MapToTransactionSPV(u.Transaction), } } diff --git a/mappings/xpub.go b/mappings/xpub.go index 8af5b1ce1..4d985b133 100644 --- a/mappings/xpub.go +++ b/mappings/xpub.go @@ -2,17 +2,17 @@ package mappings import ( "github.com/BuxOrg/bux" - buxmodels "github.com/BuxOrg/bux-models" - "github.com/BuxOrg/bux-server/mappings/common" + spvwalletmodels "github.com/BuxOrg/bux-models" + "github.com/BuxOrg/spv-wallet/mappings/common" ) -// MapToXpubContract will map the xpub model from bux to the bux-models contract -func MapToXpubContract(xpub *bux.Xpub) *buxmodels.Xpub { +// MapToXpubContract will map the xpub model from spv-wallet to the spv-wallet-models contract +func MapToXpubContract(xpub *bux.Xpub) *spvwalletmodels.Xpub { if xpub == nil { return nil } - return &buxmodels.Xpub{ + return &spvwalletmodels.Xpub{ Model: *common.MapToContract(&xpub.Model), ID: xpub.ID, CurrentBalance: xpub.CurrentBalance, diff --git a/metrics/collector.go b/metrics/collector.go index f6bff1128..c21b20ec5 100644 --- a/metrics/collector.go +++ b/metrics/collector.go @@ -1,17 +1,17 @@ package metrics import ( - buxmetrics "github.com/BuxOrg/bux/metrics" + spvwalletmodels "github.com/BuxOrg/bux/metrics" "github.com/prometheus/client_golang/prometheus" ) -// PrometheusCollector is a collector for Prometheus metrics. It should implement buxmetrics.Collector. +// PrometheusCollector is a collector for Prometheus metrics. It should implement spvwalletmodels.Collector. type PrometheusCollector struct { reg prometheus.Registerer } // NewPrometheusCollector creates a new PrometheusCollector. -func NewPrometheusCollector(reg prometheus.Registerer) buxmetrics.Collector { +func NewPrometheusCollector(reg prometheus.Registerer) spvwalletmodels.Collector { return &PrometheusCollector{reg: reg} } diff --git a/metrics/global.go b/metrics/global.go index a37372d44..2eafc1dd6 100644 --- a/metrics/global.go +++ b/metrics/global.go @@ -1,13 +1,13 @@ package metrics import ( - buxmetrics "github.com/BuxOrg/bux/metrics" + spvmetrics "github.com/BuxOrg/bux/metrics" ) var metrics *Metrics // EnableMetrics will enable the metrics for the application -func EnableMetrics() buxmetrics.Collector { +func EnableMetrics() spvmetrics.Collector { metrics = newMetrics() return NewPrometheusCollector(metrics.registerer) } diff --git a/metrics/naming.go b/metrics/naming.go index 7f33b99af..e0cb053d8 100644 --- a/metrics/naming.go +++ b/metrics/naming.go @@ -1,3 +1,3 @@ package metrics -const appName = "bux-server" +const appName = "spv-wallet" diff --git a/server/server.go b/server/server.go index 6524b284b..5ab9daf16 100644 --- a/server/server.go +++ b/server/server.go @@ -1,4 +1,4 @@ -// Package server is for all the BUX server settings and HTTP server +// Package server is for all the SPV wallet settings and HTTP server package server import ( @@ -7,16 +7,16 @@ import ( "net/http" "strconv" - accessKeys "github.com/BuxOrg/bux-server/actions/access_keys" - "github.com/BuxOrg/bux-server/actions/admin" - "github.com/BuxOrg/bux-server/actions/base" - "github.com/BuxOrg/bux-server/actions/destinations" - pmail "github.com/BuxOrg/bux-server/actions/paymail" - "github.com/BuxOrg/bux-server/actions/transactions" - "github.com/BuxOrg/bux-server/actions/utxos" - "github.com/BuxOrg/bux-server/actions/xpubs" - "github.com/BuxOrg/bux-server/config" - "github.com/BuxOrg/bux-server/metrics" + accessKeys "github.com/BuxOrg/spv-wallet/actions/access_keys" + "github.com/BuxOrg/spv-wallet/actions/admin" + "github.com/BuxOrg/spv-wallet/actions/base" + "github.com/BuxOrg/spv-wallet/actions/destinations" + pmail "github.com/BuxOrg/spv-wallet/actions/paymail" + "github.com/BuxOrg/spv-wallet/actions/transactions" + "github.com/BuxOrg/spv-wallet/actions/utxos" + "github.com/BuxOrg/spv-wallet/actions/xpubs" + "github.com/BuxOrg/spv-wallet/config" + "github.com/BuxOrg/spv-wallet/metrics" apirouter "github.com/mrz1836/go-api-router" "github.com/newrelic/go-agent/v3/integrations/nrhttprouter" httpSwagger "github.com/swaggo/http-swagger" diff --git a/server/server_test.go b/server/server_test.go index 6c497d282..3dae11bf2 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -3,7 +3,7 @@ package server import ( "testing" - "github.com/BuxOrg/bux-server/config" + "github.com/BuxOrg/spv-wallet/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/bux_server.go b/spv_wallet.go similarity index 65% rename from bux_server.go rename to spv_wallet.go index 76ca3544c..b3c97d043 100644 --- a/bux_server.go +++ b/spv_wallet.go @@ -1,4 +1,4 @@ -// Package buxserver is a complete stand-alone server using the BUX Engine +// Package spv-wallet is a complete stand-alone server using the SPV Wallet Engine // // Run: go run cmd/server/main.go // @@ -6,4 +6,4 @@ // this GitHub repository! // // By BuxOrg (https://github.com/BuxOrg) -package buxserver +package spvwallet diff --git a/tests/tests.go b/tests/tests.go index 7cc75d1ef..8c864ab47 100644 --- a/tests/tests.go +++ b/tests/tests.go @@ -5,8 +5,8 @@ import ( "context" "os" - "github.com/BuxOrg/bux-server/config" - "github.com/BuxOrg/bux-server/logging" + "github.com/BuxOrg/spv-wallet/config" + "github.com/BuxOrg/spv-wallet/logging" apirouter "github.com/mrz1836/go-api-router" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" From 91cd6cb9a74d977f49d3b7ec30e71ad8bc92dbe3 Mon Sep 17 00:00:00 2001 From: jakubmkowalski Date: Tue, 13 Feb 2024 15:42:15 +0100 Subject: [PATCH 07/50] chore(BUX-586): removes unnecessary files --- .all-contributorsrc | 49 ------------------------------ .github/FUNDING.yml | 4 --- .github/IMAGES/go-share-image.png | Bin 30104 -> 0 bytes 3 files changed, 53 deletions(-) delete mode 100644 .all-contributorsrc delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/IMAGES/go-share-image.png diff --git a/.all-contributorsrc b/.all-contributorsrc deleted file mode 100644 index 374d6df58..000000000 --- a/.all-contributorsrc +++ /dev/null @@ -1,49 +0,0 @@ -{ - "projectName": "bux-server", - "projectOwner": "BuxOrg", - "repoType": "github", - "repoHost": "https://github.com", - "files": [ - "README.md" - ], - "imageSize": 100, - "commit": false, - "commitConvention": "none", - "contributorsPerLine": 7, - "contributorsSortAlphabetically": false, - "contributors": [ - { - "login": "mrz1836", - "name": "Mr. Z", - "avatar_url": "https://avatars.githubusercontent.com/u/3743002?v=4", - "profile": "https://mrz1818.com", - "contributions": [ - "infra", - "code", - "maintenance", - "security" - ] - }, - { - "login": "icellan", - "name": "Siggi", - "avatar_url": "https://avatars.githubusercontent.com/u/4411176?v=4", - "profile": "https://github.com/icellan", - "contributions": [ - "infra", - "code", - "security" - ] - }, - { - "login": "galt-tr", - "name": "Dylan", - "avatar_url": "https://avatars.githubusercontent.com/u/64976002?v=4", - "profile": "https://github.com/galt-tr", - "contributions": [ - "infra", - "code" - ] - } - ] -} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index da6889a3c..000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,4 +0,0 @@ -# These are supported funding model platforms - -github: BuxOrg -custom: https://gobitcoinsv.com/?utm_source=github&utm_medium=sponsor-link&utm_campaign=bux-server&utm_term=bux-server&utm_content=bux-server diff --git a/.github/IMAGES/go-share-image.png b/.github/IMAGES/go-share-image.png deleted file mode 100644 index 00a14d3ebc1c4761cb3c1228259565e7f8511f64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30104 zcmeEt^;cWp6K#UKyF)3Z#i3Y>(-vBwP}~Xbn&Oh+#icD?w8fp`?i4NVkl^kRJTLvc z_b0p`ziW}TZo&!NJ7;Fko;`CS)IKT@;?dy&006@Gif_~b05m870F=SOLVdzLGV<>i zxGsu%U;qH0{ND=*NXwuB0Ng*keuI*srB-^P{86)#6q)=tK(Lh?Tla& zIULrI&jhAS+7$J&ojMbOZ&W=B8)>nq?QRLqRMba1aEY>-h4C zl*Kszz^ep`#xhTmYC%x*bsMNj3?bS+qj-bsH0mxwKd=2F<=~hy6ANN zrOv&PuA8Cn0>H@rIMh8Zt-yQ^ME?C<$A9aOZZ~HWG=wODssk-VV?D<}#h~V+H@x?} zG&$G+j;T@{H>b`+!n_h^QHx~iAQ>TgQq+CN{QKsMR*LR}jYY~QzTz|zlSsah4Wz+^ z23BSFUwLAg<6Hg)HA{V((S!=zuFWe}n%!X0zbdlvC@e1uMq|+v%>Wy=-*bHCF(TU# z1IRK5d)|w7&KrJzh6i=QzBus(`FM1mtr*v`;f!{Qhi|w&S&&BSJw!K0yCnZ--@$)3 zz>)qQjE<(c=L1H*hTnKz%EXu2lg;a-P+kkDCM*oxRW|HUwDCZw8 zX~4ftOwR@=&Wv32=oRkvM($;$_%mikAaO?f4CWRb`GXWpG2WSa+i7B$hRbq%+1Xpf zH_48A5%ez)**qnw{^+KUHNSy|QPtwi6#VVgtl2d>be5H;mB`x_kbi8v)A@w|0Rgb| zN79(Xk|J*Qx2rgt;g_DQbG7KzLp8bU|I%2UU@;)kUBLb3buLG89 zOafD`4~ko(KxNI$*oM<^xoJ`V~vZ$wL3{HdoNmF{d z-Px!`TG?~3AD`3~HGPlO6)KLiCz@l?Mm#uO-6N)AF!=)8U-4t9-XF( zA!vy1O^B#ra{%61hi}Ky>kfcov}~2~#uV?*q^+F%Ll&IMM3w;>bnN)|ye#RF^4}=l z+=at)EVobu!9kt^@F(q$t)9i?Y2-;i@XG>0!{s_LwO{L*|8BoZRdPD zMCsEv*lB1^!^7XHmx4u@g}WF8^62$R>G@IVg@i`E7vDwUS79*X%Z~ly+{S;Q=0N`v zC!{pBcaEKEyIlL?VI6i7_wcIW(r$5)Y8-tG<}5dlbmbq5mBWm4HFn3ST)r5>gUo;;W=O~AXyWV}GKb;nhUnNJ zSE8ZJw-_@oGF}A=;M~X-5fJbuzq4TIwt z%kwY{zJS|3jV^i8SqvY&aVNzfeRN@#8wY-2^N(Xg*{)U%Fs!t#+*6IUHTkNK8r+cf zH-1o$I$-x39gdgwEW=wn**MU&%e=w>vlpMpne{E{D?JFWwi7BwV}gAwIO zwDIMdgV-8(kKOm_4p7LnvIk z<+--0IQUwLa zu{qS*I}iLzzB$;;7~9iPK^b6|QBoWPD@oNXJypnAZQQ>s6*DUUj0eZg=1*LIlDgQv z`LOy9JO4Xpz`jH^l&K=b^Oz764q4Z((bdn8b=X5~{=J6I97}Hm5VwG#2u{p z{G24{3Fv037nt%#G$#hhXHoHpD<)V1Arb!4hHd(JnRvukT3MrF5ruwe?DH{-VMxU>})HtN@a3}7OQjqZ7PX2oS^X^F3k_1u09>xe^zjO?Uw+K-7e>bkLXdp9|`FI z=q;^?8V3qAJPkX2OGqBU;NwRYONd!nG1SUik!6@sgaaXZdi5i}n|Yo0n#dn$*5P!8 zDbIReWQ`=B>a8d{%M?}BF@e5pgLaIx;t((>_}DVdPq8V>m!v5DeXIcpe}$&KmvQq( z1&aA*WI(A!ceZu-?$zI2QEyH|o z&~{@=v)3MHR|lSYPYvUqpQ>wFMp9%pBZr-G$B^9ANYdXmS>a75t2&V@cp@=1R=V!G11o@HY)@S}0Z}nt#j+q+#S#dN;QP5uvtkVKSTYmOKOpulHE@(ji z#!Oqe61}D1XK;o4KQZN;*b~JsNoCJ3Z%H)~&TBbd&5{`q(W(9V%ag?g>B+*Gu$gb- z#CpEIDrdE)s`EPz4NdJffJyt7?e~-h*8&w_V9@Af`LRDfSK;smyOvw<4^4F{#%CT4 zcX!f`)SqecZjEr_OBRxO;HRaub4e5>n@_9jC3!o!=H>q?26;QkUCwJq$@+5=H(It{ zY;?Hfx+~7gRWjA!`}@+U-h4hSLP(GgmDw%!$kFqq?sj1Qn=|r<`Naudf6~;Vv2riw zw+K!@1u0_7V?N4&tuuyT>uqOQiZK0^%<|UgrC&%~@U99@5Kiy9!cR}pT>m=~C{SDy zTQ_Vu+9*T-ivx#0hJkAP8NVxBi`{J2G!3h693g;8z3FpF$LJ050w)M+YJsY6!JKL* zB?=&dG+X#k#hCf;*uzfMb5B9a3N?CUxc{Oq=h4HLhv=rBnMYdo3daTXFf-zMo2mB(8_V`-nt z>L>og9O(Y|pyovqwwkG=M!cls8cRdoA+F$eC784}o8uLP<9-;jc3QEc`_IvR>Kp(Y z8RoBkt#di|fpq88&#xWXpFuM+N7e?-h@?FYC}e*<3_-!MQxk^MFdnOqvcrDNEfMu$ zu9$;?CS2U7eeulGew!uy!$UlWI=fl{$h zE+MFNeiDB7ce4D*i4U?3qP<=0C#ML zP9SUe^B>Ux(3%nw4G5;KE%tEF5dT9x3v~db_qp}Wt`mw<{7l}0Uxkw3AW)^nr29t{ zHJf&51PqAxd-C$GT_`6xec|e%kssZ-zKg>B3FbIrBKU3iK4#3g{MB4wQ2{H@51Zm! zY`__;AHV?>ui0~rUFs1X|2SE}f|cD5!O@wOgt7z?N)dGENvCv!&z-w#uSptMWRJW> zxp@ft`~a%I@AZis_aJgBm%xGXyeoBghWM~Y*11b*F1?!&{7I52XPM!Qkd#;GlAkfG zI*9T)%W<~ME7KyMr0YZ12=kgDG>IFz`tWNrtZC`|6i!)e=ptJq1}jMgXaHVKK_ zC}GA5AK!xm05pd2lAc}Q6HgWdBMPjfWtH?mn@PBi_Z8WySu#$izLj2Jts%ES`ZfUK z5n|>n)NbXv*L)n7FKOT_UB=R|@3E8Pn;kBVF&Z)m?HBiwYJ2AsMiNQ%yB30CMPJO7 z+<9;NQZWrxY~r8am%Zqtc@qdf^rkd{*aO3+uU`YV8I+=gwC)NbQfFb2{=ar-lO^kA z2@d&irM8B_j^WY(*Z^y?KU3e*5Fn{L>wDU}qqFtD))0cBfj}@m2n_`+7|8Jo7MG1r z4SDTzGxl5gncT|2ovD+JAIQqb06Zn=sei+k6#m%sYG{TS(52);Hac)K7~VB(`7^Pq z?kWydxVV=3l2@9#{(M=(uT`#OcJjw;d7Il7tL3p-E-D^-m6+p;%OxW=Sb;Tzm<7cbpTnY*Ult#ib67EXC9 zmB_?On5`P)u_m*E1nBmc>qM^ww1TSve>5OzeShAN79G zQEpg*K@)tKc7#UWa9V(AmcK4LZ!E2cDH-?Mv>E0l_eSsWlsNl~Ha7ca0i zo|)HF@6T$Nh+zz%O*3W9(No-1_H8vC?~+_r;{T-g@KwkhCboF7|D}tiXD1=iQFoUM zHc)y^^JWHne6ps+Q6sp#P486ftBU+>Oqq8PAU^3 z3ylzbg9u2m0|Q-Xv4_9LQ-75E0HI0ES;V(a2ghb4l{ZfOC&vz?VRwk;Pk3#)k!9pr zVa5qyB>>qDXkWVJho(-sNps>s_Ejlp2%^>()XPtV-jab_VU-Y44jM7*r;{oC4`hZ_ z6{1ED=z#HBd`<(b5$?1?@241+G-el^P`}xhVKQzeEy!41dVG76Z{9u{2;z07`gOgc@UtM}1 zORuG?1&UjVnL2x*TpFa0VgPY(EQT_bLT)d_JUsu)_tvHfBW!?TVDM9FF;UIz4-%G3 zn6u?sSVQ5`L>(#OKq1e@&Dq}<&wX!1eTS})5{IJGf?Pp{h+s-|c^fwy)NXQBm0=a>*L~-juTeHo&ePZD)*UCeS79X5@F{bkC zUIEnzRy4o(5&-atJ0R=SrcYNw8b1?3akprL?6-sCu~wa>*!*5955xC2Ywkfsb~eh z8c7vkpqmd&2;9*>B+~ zofz9C%9E70Gem3V9v@JCEd(gFQ*|7;K?49Pxm~!L6OAwdUrMoqUPs>NGQg66m$gW2 zc76pH zLXp|I`p#-?wf1I^K>1>>#FHNj=yB~po1A^=|FZqh=3W-d;Fk-6x<==JnZAiySnDlR|{4mOx$U)|HhVpCN-v$rCwyKCVhh$u`N<5(2 z3E^U}PCWBemCGOzWOT?3&+gW_>Y`7T?K}^H<&+AFmL+-GaOH=w5{PUro3)GwHoit~ z`(1uAaqxb<@kM$Gy$|D7@?ZJLW={oZmYCK_fH5@Kdh!`2>NZ5A=7$FR z-s#ivD9;dY%P3B8S*gMSJyTI2pM8HMLsFaOnI{Jh$;NktGda3w(r~&pZ)W+e*eHR; z_9K0C>NcJ~{ja~K1Fx9@^82!yAW|_d1eLM;4`;_GhES$gcvd{b_ke?h_D40o5u^f& z)b!5$#7z+h3(UqxGyjf`btYP2UVqT{XHqAJp2LrcT22Zx%(m<%CI-yHoxYP;CUb_>M)PtFCyA4ZYiZy3u(1>*J%ygU=ICgE+Dk=Bdl4K9{-4=n&)_~E_-Jtt zFj~r?*vF0j%*K)sM%FC)89FUtF>H&#C!BgTXEbyB0x+ru$+emtSGj;n8SZCmkuOX? zQ0C|ph?lr^bo;;=T%s6^$GblE_COb9;v72BK$M3ql3&mi(+SLJX?3c}1UZ8BEGyr= z#^@8M&?yRYCDNb`$v6a3>*IeQ(rC3)P45bkgGdVay(U5GD=jAcq=&7XbMXu5uwXhv ztIy>_4_)QMEr+##0O$Z1Ora#d6%GXfRMvmD+lfafd6?sNn0f5RtYAr)T)ss&gf75$ zCo2=on|w1aV@FAd@P9I|RSa6T|NcW!@TYsk>{exA8YO&mje+THUu*qA*q<9Z!bjNL zwy;Y_!4noYoMET5$&!#)lV~*SI71w4)e#2w1hijG)xWxkJeA+xPev2RZb=L^czD%h z#L<$gRTyh4jP|@9@D|oEpVb{B#po*EVep>X$_>qqBP(CP#xTS#LM>$T1Is$q@3yRP zICE%9BZ1DnbH{vdQk?^xIcz~9rJ%ktS$#t!yEix07a<*V*9WoFAxUUQ`;qW8mq2Gs z!IWD9f3gKD>M2kcWLO=%K2X4dSV0#XLkIE1)YdZmSOS-RHkNnlR5K8JHX-@fM=IB6y+dQm* z5!EC+_o?PQNj~c?_za$*!Uvkeaxie3XGv?uL(Q5$njc$#OdG?ymRH95n*3!ECE8jg zO|uN~7_p{Vv5UdtDGacs%wOgFIj%9Y9#^Zl{GaA~9>%#>J~p3dtG|nVEwonq%<@CZ z^fHa+1SiJeJq!MjCpJnR=kE$15aWd=^yP;*%r$$_a*dIdko8ZH?Yuztk6G8QY*W`I*odi&z)4<65Uq*>(hzWM zDi{g1Vd1vrRDDSRNJezUU$NG_Mtd%aCH}|`YQ@srm)3ogbxp6>yrNU+HkczH-+df} zs)JRIO}5V3v3SwZqU7Rl;d{O~nFbJQ<@gR2-xw2)NRl_qFeV=Fat)r1=uYKU%=HD& z>|dF7C__{uc@(zXtb9$ZQwUDmlQle%*Oa}lIY*%~r zLy4EA^q+qrk<%V(;^r*=LU7DTv@U-qA0Jv<3Yt^NKE|<)*S!N4r;z6>kN#a3JasX_ zlKLZ8D{Q&gEB^1jOmbwlP3KxEzrg7S{ytD(Czrk#OIj~)+_;CvYjY@W*fGx)s}tvq z)R#^AAszki^f?L;v|V%)DANNq=)SLMab|rih+e$8>-%c)5;CyvYI040=GM0GE0S{f zC8!4ya>3OeITh6oa9m4=p$apGBwJ`YR)ggN(iQ*qKxn||UT|U^nOioSbjLh+EX`Ny zWHv0hdU1YP9uD$xR&c#uUy^I^y&JnLNWE-lAI{gYcBerRraIv=z$;Y4Bx60_nD5G? z@pSO=rE!ZL-}5?s6@3nDC)^#H%S}+JtWcR`;nzTcSi~Ex`-c{6@fwe>4mI3FZ;UvO z_SHL6jBfNUv0O(%F0r0rS{Y;%8LC4zeBnYg8StJpqKm;7PAA-^;apLXS1Q&c`jaxe zx8(e2qH-UMpx?ZJ$`C5hFH!LFD}3`?y#|lg`bzc;8rsm(7WAg8yt-JuZ@0idm|Wtq z2TKr4fQvb9xe>8^`xQ;KHH3O;5Q+enl+AO?0w4gE2wxU=X5Is7Y*3G?tdhiv z<~iT`6MnS3UnKzmR5uFMZm9D=uh1?)Lk%-nT$U+elIRloL=Lz(a6*v}S4`L6HyZ&u zL10+6bhyF2IEb=1pjK>JrXB?MALmvEy$#k8!_!uJ(@z^MSFy2yW^J1 zRgB@rue>dBy|PBsQhXGobUOdUct~_lx#xf`km&{>=nElEU54N)dDHyrA2d~#`gDQD z;C9Xbty5uP@jbu{Mj}QI;x^*ES61_3;m7hn^4@8jHKn1>-VPuzx+5i6H(Z1h0}LOK z^gnmT6Q3?dm#7BSY}KVk1z;rYhgEwcX}ZRZ7Nzb{Z9+ZBJ{5rRqcH#4_tU6)_q^F> z-TJbTYYaP|l%sDig&DIy-KG&pjdwA28n)TZwk@KG<3v{s1qfNiu?46mM^-wNyVx`2 zE%W+a&lv5jg7I&$oKY!N1KBH%{q13Z=t1Q7qco=H14arZ2EYbvf~}+BF=_i~q4}8F=afsgHMm>Z9iDR*SNh~b~zKjYUF;843lAz5ehc&@D18IV(x@J zK5@BYb9u*3t%Lak@dJoz^Rd<3nXn37^us3ZKze#9RS{k0hn+0paHhx0x+^~42e9t# z(d+oAhnVv{&reC zt2X-)N6s1}%O_X{Ril-*x$mOu;%_rVvdffKn<>^D;cc>PD(shg=JY&uS%QthYoB74 zE#-^X%Xd|M!E60JG}{Er$T6wfR`7exQ~&#zC=kdS_+7I=Y?}lF*T2~bnxCIwoc2#> z&QB;QH50(<6Xzb2wYXZwv)cPRiR(q355s@Q8hQ0awdhX7%k%z~;3Q;aK|n6FyjA_| z>qR<}CP)IO+pra|v-`;YbH#H3@`pL^dmEnny=t0|6e?VT>b35dHm{zL92wOA-?i*vCqOr&$(3s zWTuY5BJd;fvr1^G+WZ4Zsn>sTZAHJKIKbtiIlPofpo37W+Htgbyb61S+IJW9juYTp zTh*u$;*Y8ReDNr`N9EeE8r~USayY(dZ6)-VAoeTybBhu~W`)~UoaSHoTM?yKnT)%L zwInI$-p3!Pf40=3e-^;FO3&Rg>ZjkYqspgwFxkmO>~X!BzcNN$!NE0}u0ZDU`EOi8 zA$=g#iJKN(DiZd4Z7+N)zts$uV`bRE-t8$YG&w}0rh+z-6Mm>^yzDe@)v zKNyyK%L4Y>=u=!K(L3BSI&OSF(nD>WTGl)kV@wWU@;7fI@j>m42s_4Tf&0CUV9X?V zq+)t*uT2|&JI2ojf|J);oBZByOC7C3S?f6NE<%2J=jxB59S4&%b;Y$A{7chYzXK2j zwYgpQgZ*#mEMM<@J6-Tgu%yG)`P7FmPwD0FFAAg|c4L+T@z3=PqwRTAu^c;@uE$GK zY%EVOP~5!#bT1APJ-g~fp3EEbq`(#jBRKxo3|_8>27kODCVTYOEZj2MyuQDKD0y1@ zlJD!tTJ5Y!f4XyK-+cWT+mDak`{$iyQx%5aj$fl^gF(ErxA#znjzC^fw#>clXi8yg zp9j)wIZ2YY)XEy+wmVw#Eaq37_CwXEcF+Jjbq5c5wKc%ZhGy%4*i^4-#^ZC$m#&Yf zb`DW_rf1-0fj>N278S?4RjX*a4^obRj#9>fx6D|E=ucOf*!5(8T60QM{=GUzVgsH% zQ|B*TSba;3xI5VtHPaoOuAo4Z5wwPv&2u~L6&Xc%pW2bl(~XR>na`PQZ}|%3ulW?; zj;M-?JMEo*yH`%Y*dQp`0s~+NEs^xOPF_aS&S?eZ1*f;eZaZlsG$$X>ZteYm2KeRz zlqTY>(0S!``{p@^zK%q@8OZ`@E8Q^COET;n-)`v6!&J7!S863w!~V$ggHRRMPS@m{k z-1dj!3R9-tZ0&=#-L6=ZMG60*QjLXPUvm2y<^68f!?O*y#>9v1jCCEzrq0Q!$vx&5 zGw^B)%{q-6y;!>41BaO_(TIcJ*$@A(mYAL80U^1s6AUxnqIwx(6&{)AU|csM$>g_w zM=z4rCUM>o%QfzLcCOy4q|96BMBjU8{nQ%stBM-mvA!yx8&h6znSDX74!-8JpS#i+wMZc14)| zz_TbT9(Wy&=4Jn}%$H!*=XI>=%BT5*tf}GR@0nl8i}KUsbnLkE*V3K)7Xu!58y%Ln zF6~s}%3cYyg&1dPUJCEI3pCKoLaW|%4_2Sg`FdjWlf>J%&4%{(O%;#i)VR0dh%E`P zOqPAlM77CPR@(kJs;%z|*2GA;cwDN6|B$AWP6fv@x1QmxTpnSc%$fvHgqhBfqZv?z zg@>P_CVSwSzDJ$NQ~UNX)rWIPtcBqGMVpJ0>Tm8kF*-4JVTh1FKV#^i4O>;{GF7AOOB2ss`fz^d|}MRgDexb^y?lh`ml9 z!JIO&DY2E5q5vtn*45f3bWNdz0aND&S9_(hgu@YKyS8o<+Kjq2Z?^Qsuw+`R$ui+* z0FMoNH+q|^Claa3Ys|>A3IJfqshPH`nwx--5FScx4CP$d?+V!DGU?=dnBZeg*E*%n zLNa`AJ>`304rO!sLSSRbk~blDK}FNuFMax6d2AT=E7`bfXNQw}NRBO`&@>gBOOvmZx4>KZ))P$M?4sw~ z%5hH$e(k>`jN*6G`?=A~>fX=tEE9b))A8m~r)$#}u(J3FP@nyA6*30M_@(5wjl`S3 zHW6;+nY=SjS$W-2x;EbMqB)0jM;q z{W*;fG4ZC@0x~virQft$9x7BbuZ252U96O53a_fC)@gC<*Np-H+AE>IMkp)`-vz$1 z(~9h;rf<&*&Y93oE%V77*s6n5%B=KVuR<+4pT3;+8x-#!gWQv&fMFx7*p^NT(KsfX zwAnSr?7cyHad`=kaE8#EAp1mFh3B8C+1RNL#>)Oq%Y58K1R#m|g(YLM?*C-K@xbd z z=)tb^8=tjRk#rt`E_)^E>7TFSMMz8{Oe zB2FT8it9ym%!}CC+uB!q!psWGm(3;&%;nODTCS98LfjIPD}MGp`l;oU7PRhBp@@xy zU?VZzf>oVbs}W`s)mg%1ruUo<$S0!j_(=*^f2IhR76aQ;t(3NP-?7C=i#GcUN)V>4 zl=y%8OHg7e?90NU^YVw&^!Xc+dO~98Axe@^|BC3}&V(@OR2Y}-Q1^?6_nSj>#Z_Ve zFvJ}x4#vq%`!&8r^WD zgkPQ0VgM3EIKqqv%KiMc3)pY~Z8Fri9?Q@`_#*sL!Cn#w0AZ{|8IJWQ?BCdK_#lNF zyexX})Hi!!M33kgPRLg+^+?vAFdk4w@OE(FFFrZ{^;K5?OA|De**xInIj`=Kff6rZ zRNOC+^UbHd!V~P5qS!DL1!H15d~s2iniIP2yRXv-gz4*PXyyTQOOW5qpH5do z*+iDMlh3~YD&XT0ZrP2yD)(2o)AwhSOoV2&mNlPb5`<7lc(E7?`4MOp*$EftJ`8tm zBK*Ym`s795=r2En9gBj(>9%#H=0H7e{L)RAv5yORwt;)5#rOK9Tlq&5h|eWi&#Jv0 zy#T+x?5BlBfms9Td*FWY{vqkowxrUb%`OywvHhuj#g}BMcYWm99tGYWl_kMo|N1^U z#>mceY^LwB=()10YOp|9Ka($kbNhn`;BGFzpx|_q57L-B(N7c1J>+%Z{74m~hUD?6 zekbG5O4h7T=-C?>E&$BsMDaP`1tDWdKXV|Mf8A^K3;?~L>ShtSo=KIU#@qa)hv|YWnd3rA}gf02Go{{Z5!yMnk zQ+ri9+-gP|!t}la{jNAX-wN%oy56t6Pq5FkHy(`>d#nJ4mE979=g~zROs{MHx)gs8 zcP6M90>taLkd@rtPT%(X+dvA<{>tV(M)f1GxL%k7-#X}8@?-`4Oy$M$B)-M(&sCHg z*~?TQtK?#~7(#ax-qfl)v#&}^s2-m%ctDY8d-4n56l?w8n`rct2yG2`FgUhxe!iHJ z@_pjT{rTAXitll6j(m9h$AZ&f=KE|lOCf?h5+cz^&d5-bK^plN-@8uN`&|*so=-?{ zU?^agkzlIu@yq30Tbt|6FL^Iz3oh&F5D#Bm53fE4a1Y6_iG*6T^zE4Rfo@lR5Nk0E zixEA)eD&tCAC}_0cZoq2dfgv^dNzlWT~Bivzp>jk-i+`QaP#ttT+(4V|H86RqC;EC4Gp^- z%NZ{The2u+nE;Xd%c=CE=!rQ6w96RCniseSZfL>>!gmQ zZ&Dh2@DNwE056;X=3WrJZ2e4YP9SCSfIn`_%kxZ$j4fg7_R|J;m^QYc6?YgGr4MRQ z?l4xnv|w=h5g{E%WYDy$|!MX#`4EwCZwe z%F8D|pk{u-vpt{^K-`F*6`B^%a%f)p&7A+?Pvpy3=Zll8-2P)FU}eY_FrjQl$1>FMG;KXn|iA) z4u>h~@^%}#Z$dS+?3y+3D#n9TO{2}+abIo6!S96z+#%_O20hEckHw2pB<{1s(6>Na zsWQ>=f|?8Ax*-%BnhaZYH)OMKJR5hYjS&bNPj~8B4cIr}(b1Mb>3s+DlRZzwtEqx} z^^_(Hf2xF)V}UKw@*@)9rWp4D8)+$t`6=c54aY`^x|c}gIzSjrS8yo)=fg2PAFaKf@Eb;oh2XZ-VS2G zN%$-4kyX3B%RlFea^3u&oXxiO;+)$fXA6DiJ^%Oc%{{AwiO|w1>DGxtr4-Fe6r0D1y1e^p^W7; zCqPYo^1tLBjy3mLBUl6LBxp=lUY~`GQq_Zoku>w70nC4n8Q-2wV<*HQHwU|z0XX3~ zp8kagjN2%8f?v&Kv3NC?=m0N7#(yr@XqKzj#k>oaDGp^($dfYMtk4?s(|2x=>^Rz3 zRo43T>1i!(Ih}nq^UAKh-2!9(ZXj&N|ZX{wJ~#4cb8FnmZ>GB zQ}~+kL(7gB{S~sLT65+9g$5p|x`pI9PC@E_Ve zzmMC@L)mt80r2erCLy7k>wP}y1(&-{5CC#|GbW$38TluVtBx4f#33t0yTS1+-(z^| z%?^W$urBdjHFwMU0IA=W^gZ_d5aiq$L~{vw?{3r&K^ZqL!4KJP6|79no4#yie`0RL@ZK|nQ<94T?2ZPl z35;^5E#0L;hO*7|Xz$jHhC6Dweaz(j zS|`?ddD%}iugd;l^Je<+-+}Q#;qrK>nv{dAF>Is@aB}|!Po7ohPe;5**Iq&ZUzbzS z)~mWhI}g}Lo=dFfmV_nz4Y@0zScAJ_RqNkBM#<;>>I?xGG!q=bo#Xrfw5ynvmn!$? ztG7HCS66F%Z6_u#UYKWq8!TvzjEt_MkoVpM$BWpLFX>ml(L(Fh-2&A2wjkm(H44&k zmKI9~W7CI|12n<;wqD1+aoyVpxXI%_Bv|tW=A>a;AWA8YWRvS;dCXEp5-~L(18mY} zEDB-#V%@o{FC`oLSnDQss|eH5-f>Wd&snTWkBVFL;#Wu$0Cr9f!t;-Mx`xFr46(H5 zef96Hqrf))#OU{?oEi{9E2(#)vrxLYHL z=ADMNjhI``3Y^b#HBA)|zV9WJUWg6mKtn$H7wUuOYr*_YKOFh@r`eBXq`?Q2) zW)G;)Ewdp;WMRM8rh?kU&%JKzF=N0`;$Q}XtL1GpyE%v-evya`$CR#4m;7* zWYq_Chvpy8sv8}=0MQL^4fWm(HLf52jy7^a=SU;w2{X;Eug5-ytlph;Vl#)tGoRY( zY3HoX7S4Op_J_Ra-Y6}4x;!X=m68EBd)F27xb^OsHVl}x8lHFDf{kE$ZO7-_FRncF zBKpcP0C{N6-{~T9VlqT68Um;w3jAMybYh~iKd+l$R&6dGU%cw{<6p;Xkh^rXk2D&QS zvTBP z)2(-$OUWQJ)hv=syv^{ZH+AOrQ2>~;Bl9@+eayAE5LG5MLoj?a-@c;lp6H_Bu5pNu z5K3mL2VEZrl8WKk?qbK2Ne6vBrhNeL7_QY5$%ZSV%ERRDlJGy!xfsM%yDwzRn2b{q2ZmTY=fEuq8d2+eJgCmrt!BGAxc-#!4!X;Ric)bgq}#ef&@zrFC5F|I^+M+@>;T2qLO6L`sGjj=iR$^^@42cVp$0%9{Nrt zbPG()+Z*=psi_Eoj-k?}%PjHpf6f7E6|t^PtV=>u>V;-+A5OcL(P`#9aS~P}T^b*1 zw?A4CByDSN_();7!&bX4AY{V}dJj2%!~{q}O1SDbifegEm1FB?G&s-Qx7F?YPg^_F zfJF(l=1=B0mkr6w7_oIMGj6saRq7Eb6Wk>n14(tI!uVQZ2c;Ei={`DNkaqh%sq;O-c zEew^1;ui?2aywvp!M4Dc!Z8l7i2Bm9_CI-e!-+lW-Fd`i)xKF^Jb7IS9^=`eZOEzI zQ>=9qL>+i^DttiV%rCd2KRFo!Y5(332t!Jtq`?nYU%~kBBO{(;^Jfk95ziIV+#UX! z)SKViR1}nPls$R4OgAc7X5Z)G@~rIFaFwZTj_|2-@K~2Yt+K(fU>Q1 zCfsV>SuHPVv?ppS1R>O!4@rD@(_m*)NdyfUb>k`8A?@KTeZ4X%tGjNoDg|Dt0(iI)wDzl0N_d z@|1~N3O?H(|8Qug?H0CjV`1N$4^Si3~S*T%y!jTyT2t0uhZokPggEZ7pF9aE@&$ukTW4U3q$+ejLrO3L^OFj@k! z0Xr##(ciyvBDSg2mdtSNabs(vKbI*4GmOMzhGn($L>78pO1Z+~M=~SuNtyd$l zJXivAVSLiB4R9Z&+=yNtu+MDJ> zy_9sTEP=DPjvTDojiT!e746$*WGD(tsBXSv!;2*jPxDbtOpDmAV5cWKc<%XgHOiA{ znpguxcc!Hv=eDh;$nio02VcVFLpK!iRptOmDwDHroGXUQU0z~hZxiHpxOysfLl-*+ zk#g_1*V7ynLkznRw}qY&G!R7TIw>^>PsthXXJOu8r; zp<3?t3}L;0OH#xrhFM>vm$-2Qx*Tcs|J_Kjdt{3l)s(If*;kF{*9MQ6B2(xnV{Fsc4-YfH*)1LeH|nebFc| z(c~0qR_nx+qUE;a@v1KuMQEGszXJjcB7Sr2J+&Nc|B6mYquR8 z<&e$2+B)^CL>sU;>15g$d|O(EtBl}Mn<7!7j_*pWzg>WmLKU5kR;S5)El@*VY}rEQ zunf>w<@}Q+a+7}P;g4qJNTOi`V-Ra&)RoJPZeNXHc@Y`W_2)ZX_7YR(H`}^oO;GzA za{=+1I7{-&8?XxzySavq$h~;e?N#~m$7$C0ycY*caT#^^N~h0J^Z)!q+hid^d$XY~ zV|4nMBJ~+R-S3F5Aa5-DA)R-+Nb}grmGC7qU*Ys zP6+`)LK+oG=@1DCNok}(r2Ep1G*W_ehje$Rlt@W;37789?_8hv`wesF%$$98ti9Hr zRpe)G%P-t=^~C6$aN;F}{EO?Fn9|sTf2WllSpEt)F~j+WAh3YyR4;p4&*buR-8Q+( z2Sum7nqfs+?MuhrghV05fOH6=R?Rjgj6q2T%A6{u7`<{7F!e@-NP%)qNig_)59MIN zPsQ*_$h2buP_VrSWYK^d{>jxj^!zInnP6?&edRzul*P7ITgC#`llIF90ms%?;jYx%Y1<`Qk z;dPQv@u%+#$UuSQnynnlbulaKRhVQ9>y|x%koXzplX%@EM?w1=V`g}JY1S`VZZ(WL z?k{SXxId<3dlNk$(@ALg^QYMxBBt)O9Y2_{NDL{w2HdEbFm55pYhlfYnt9F6j%67+ zF~zmz&pZxem50j}&dX_H*@y0A3cF7+FKsV#4LpulF2r8wzV~Bivo@HDHQ6h8$IU)e zk!9u{N^dyT!JHkC@e@DnR0Qu`k^)@qV>TAZgTT?V{Fp~DXtFyIq6b777(~pjVeIur z7;i5KaP?oNc8t#>4(TSfTQWuba6pagW*3}}v>k~gbHb$}KPCQR=-S3MMABSf*5`)V zXyKXbWQvA1Qk8|#Y!zol6`NpXrXTv^7rN8TK$)UrIQ*8wfoQ_*x-4b_4$-(SZ_xKB zeh$($QL~{^dkf}%?pP(yHH%d#`4t(DfG11zrNPa@wiIQkf)PpBzJbYJGk0ZkYkg|n z)?Ey5eL{yP4*cL2XEF_>kshH)K*G_H-PWjO!Rff*g4a0moHJ~jz=uC_W}9hfw5c;h z7D501x;t(N4fdq7MJ)N8AMn*hYeVJ!oHsVy*s~A(RB~Z;h;K7M*M6z&(Dj%{oTbd- zhYmzJ1TfoV#{lIYr|d1}MT&7;L@`z?HGRZP)g)>nMH zZytEH8|V@~KiSYS$TA)N)Ju{;m6cQ2k1)X5!AubMk$W{3YAfQ;a${>Xd?$tC#H{eT zMeZpdk(<0#!8HsZD~kWEOw}?JlK<4>RYyO%D?9Oz6s;%r_REpHSZ z_8KsMz1njJ)6i>w~ZJRj9$$Ds{ zU;98jbXq>pHXRvqW0qd-GSaws*_*Xmby30qACxBV$2U10~mgx1ubiS_SX0QgN;mf09ZBNpXuCu$dE}`XW>(=M@Z#H=!c<$KQvGy5~yiR z*!wz-)9xEv%~b`5_?U12<>+!H88*VoU_$MNc~c zRAECcfm=S(fY&m3d!J8^aXC)OwRLUA=5XF!C%I98_wQeA-Q_+7U7Qd8GZ-$Se|}vb z4e3K%E<8?WvWz^O!mEoqJMZ|o6u=(%^DkY($nE+D?&!~9_^xXLIMh_*iOQ!PZiE== zFfEPErzq{ms=|TOu>*Y!j~}I5e0(B0DuVt6(po?q%mDWQrC_BP6YbH))~D7{sy}&y zUNUOzR-)Ol^|T`+sOx#6X!7fvxB^J<4FX{OU=7p4@3TEO0?AJ%*LzF0)Tcvb;R|=T zwGTK6Sy+9ze@D28h|@e#V{BS|Wd(Kf5BVOP-TgM7@|J&d2dZ0t2jm`};?(f9WRoYh z1|oBnP)PJOFGRr@$DKtGq5lIy52isR2I8&GoBFdmT(M{!+lH1Xx*tEh{!Nwv{u8C$ z0^^aMrv<|JpvBGV)xGxt>1n3TJ*Qzz{GOSipwKSKGx6cHZ{dsG5u6}*{xP*-2gr}^ zfrw-+0?D&q)ni<|XaW?-wgg!2Mng z*>0Yth_%Z_Dx8B;tIbx2FMGGH4eVp>wpdI2b?5_Ge2cXlILnzx)M4i?zM0O32YUy_ z9>1TY!wvxbQeN%{zVe<|@qg-uO}|9EN_swos+51sM!n;i$roWl*FAy^G5lU@Ebf|R zHYp$5jg-o+1Gax#qQVgix~ydYSgHy!Dh&oAw;$-q(_|(_k+PAe z(_Q?J`QRlUN!NU>Et6y#jCvbqNMxFByYi=yXz4;@4Hq*Slt{W|KeW!GikqU`x#w%j zk~chVJd-~go$Hf^H+i{Ifo@dJZpd!RhZ5-1d91gp-U`;*_Z6*!=Vs~Q66Af%=>HbN z+dLNZ$-`=v@07I{&B%C_rFn@lk9B3#@EDQROU4wGqd>r^qX19Y#OuoS-u- z49WJpcLY6N%Y|1yZ|kGq4gKXj{FJLb*Y%-XdBu%^hq)06%P?SX< zk(W}L9ZxAP9r`95PCY$2(R~K!rm14SooHzQTiYGr;N{*#g9yI;h6s7LK(jwb@Ut`u zOF>Vw6Le)DO$}F`U74cF(st0_>*4@eae`2Uyx$c$cv>}(T`mX#4aAaAvh90ZjKp~ zl*Jz;olglP>P8(;H_1*k`eqGQeqKHnjP}tR;9-wT4>BK&nPB|2-b>{8m3RK&mFpr* zH%%m7l1cj{veiH@+4<4s(eC;WQJyGsm=#lQoZtrd@myv8o>k2WPn(U4N02nZHjR^$ zcE#IPHGxvKdU}M9U7euw)ugZsSb1Rb4KHZ+n3kcPm{F9;eg@pSRgvX01kl8&o{ z*~44H^e#|jku@&3tcqsYE_}a0(B4~;gtaUN3#IOmLKNhF9G<6Oe#|+nTR;z6q2&=T z=UH7mzPl!UZ_WVBgZ6I1W*>MFepp+2xrC1hH^LutWuu@1xz)}%b5-o_ zDuhs|`mMhL#a|yDd$(D4cPhquNX{) z49|*3b2t)54hQSjc&FDoXzQGaBPLws^0t1c5gY<(H3Z^yCB=#usG9WpmY8bRFj`~U#{;`0kK+meU*e0$jb@ifbluOCu(;I=H8NgEIRp< zjN|F;uGPW^x=h=WzUm_w9?2XM%X3WIn1H zs|X!NdY;2SA zQx~t4oy99lJ0aHOinzAOozDyAB>>T=idQDQuYEeiN8%GnvAZ37hz60l!ew$!%?))A zjBCS)=vIMymr)UKYHWv_U7Kv+OIUT~;sl-PvIx_Pq~ORht%b(s#Q^==wt_jo1amb` zuK2d01wXH!DpfiCKju%=qYtHv{|4T5AQ`yOA-qj0z7vE5*c^7336MLkMR>8w4}WG) zpDYqO@c3;l&0j0&+IWBaqhSIN9$N9h!Q?S!yyza?b{t*D zm$}wmm^C?=>&}9n{dtFp<{pPejY5d;{Ak?!AtosV0KYsJ^;obzzIdR88aa-wS1bM2 zb=Ml5d?a;$S;}BP(A$DY7OxA|rZd_KKFs{$n%=w(>sA42p6Xk$nVM~pLC)8Q z1=)9GuWfA?7=4!7n*CGEtZr;a%)sD*Y#wI=XQ?gt#ukpw&Wo#`AXw!TKOWzU!3XXM z7;b?^ESiisVd4G!w-H(Fao4+1T~>eut{!Kpory6NB=WwAFF_XwrOOzgHDpXIa*?m# zIib$Gy>vUF+^phC`r2uIvu5(13zH`wv`>LIkgV+37S3*2EF3J7Cx|uHpeKs?Ff+)# z8VAk)26 zZBuPd>Uv!}c*6aTxl)LN(I4{7>A47iE=^-_lC zBqp~n?B0~fK$=MgR%^Col__(LES6LJkS>AZba#kn%R%%KX~dz8EeQI_QMJHJQcx+= zMylzTjdsmACart`EVe-QJtdoDi7{gkL2%pQFH4ylp4pua5F|mm@ZvP@<$dnF6gc#^ zu|}B>L()dr5iAOKQ%naj=oursoKKXGdSr8L#kADilZ!lccwtwdiBl*yb=F!hSkB|~ zoQ3XiIuT{}aA2BJrxcYj+9${OK&EL59Wp<@*Aa5`L-e7dZbM~$zKZ;Owk1OC*V6=B zLl2CxmlOtd!E?^n8B(IkQwDE_4%A!%CR&}q&5I3IIG*Pf-ZvR*I7XH=DGzKgRStHXh>#q4q8Y=iJ5ooXmmn0)=t^MtMHdy=Ec_ zr*jJi1z5d3j?l&N@1$Kx+CAC$H5C=T=A@rl=BqMggT$zmpvUy8RRYpf=?tC*OtIqZ z5}hn(Q{j{XQ&xPWNLU<*1AwFJ&a8LdBEHD z`yLXV;I|H?ejpjC zxq&kxjBhvm0fHOpv_cTn$euWUblDJFqI%%p8-7{28B6k{a!(Iy0{Nsjrcb>r+3eAg zlxVl<4T#_%uUq6Je1U!Ma(=8-A~#*uf;pvM;nwC27`tASQ?p4Z?K#HFqdcFYP$umV zGL=m&PQO+Wk?&tn=O&R9z1Ha1+VqlX)%&WvUaxUJeN`QiV7{8mc;}}!HvQ$U(Y;f8 z!rS@UgwJzTI*m+O=2}D*>)X%~k@KPRIGM{aC>wzEM<)43!hH{_I8Rxkx#J5JW5(4! z;=Y;q?u^Iko$6M|RT7`xYp!}9Fxx++We&zSBeN_?-#x$8))ZoQ~ z#X4z$iGoIlkrHcI2Giueql!$G4p&;k_jd5VvE*WP3KH>>+IBulbO^v1Z^w5Vx7t?E z7JuB$eTG{ZsCx7?cD&HrnyU+gL*Bd2H%jRw2~hO0J`!|&`!k$M@CgIZOPpb_+nw`% z-Z;v9wS%uq<~n{jc{XNO2kFM8@)|Z5aIc{W3goUz*@QrZF-}`2Zod=}KcU)JJ=3}O z##NIk?-fDRe$il;2J~bn1O?HGR=`ByJqmjo)yri$bA~ih8GR}$VhNm5F!3r;VaMg= zgH@9*%nGJ;SvSo3L`Vv-+z3!$T=-gKT4_feHBR z6@Ke;(t_Tn%ZiJ)Sk>MX6R+`=lnK!2{Z{}J)Q}AvFJKGxG@8*5RfI2p!d-awgEAP ziZ*oUz(?IMK=Izk^PaV*COQ!Z(sWD?*3`O5T5QZ!D$;8^9{w#@>FmUwi}U8jW!sH3 zosX}EI@4D58dA7LSeoXAf;H9iO-BnPGPIgayIt1#5!}&FLKU*1WP#M@Vf3ql93a7!iobGr5 zf1$nJaHf;reW(9?4KJ*B=B>%&O_#}b(aBH zT+u|&ue^bq=&#|&iy~7au>iXz_6^y?p&DMa#TD<(SyDEdXZu7xx_;G~5O(gC8GQa- zvKvVrSh!4X-+OaogN8cubjgc747kkbXj}u4l47sW?XIkcOJf(jcEQAScf#^A+`RNP zb5n4fTG)ovCq-=bw@Gw8ZxuCi6_xhZ&U^3=zz9#Vi!U6$qRZSd6U?`B&Qr-g{>6C` zSXBg~+_-r+UB(G`2~1E_N7WmewuR#SQ`@uCqRY{x`^eBkv}vNZLnMvHOYbgz?<`yH zMC8y+U|?4-GE@7HvnP%)CS8EY(zCpYTW>D-wK&Lxg`V-6fFDWQZAyQre=v6s;gmKHb2k2%nqYEQ}iv{Gp>0$R)Dy*np%ZyJcIC(vwf$G`HP?A9e?`IO%vZG6Y; zvA#uT8f9^Hrwhzi@0@rSQ7EPL9Tamvy54N!DsiB~hpxm{Pau1om8f_4Z-|j0Q9b?0 zHsIgxGy06jYn{S#^FrBn`WLeAigZ@XRStlk@G9+Q%@O5g_5A)83Kdw&rHJXb10G5F ztG|e!!qm`bwt3GG{*Bwss%7-qlilAvx<0gUzWBtZ-F!}qA}m_l1mc3^Qi9E`a3uq0 zS0~&b%Sy=1p8UN60=V_W&c0Fb55p-b=JgH+c|c!iKUVA{Vs24{NV3T1|974+iDIOK zPjYGBKq?e1Os^|pDRFDD?GJrRG zOCvw5%95?J4+1d6eRbCF%`>>n;wS;Erx)Nx2|j24flgROud$JhK)SzBegzu#AK*c@ z#B))O-PiIBsEsC51e<2w2iIjQPmKkLP$||0-<%^SyRoL80BMXP3>(l8T~4c8X8YU} zIJJe)w(j%E^ac-P^04!#LuWC4+-;i3Y`hL^0EAq*8W{I}q8QU20HNF6g9rB*kUdpg z3~H^hHQ4Iraj@0GDVRscf&m>0FP|LTJI)FmgMK6rZhR%WXr$CzA>!RXoG#cFTu~p0 zc8-ea3&+gT>Z623xp+A9Rj?`}Q$Mx?_{}B}!+b#O9$;}X!~`UIbIh8zXw>se2S zYId5PSvS;S`RC&KPfYfXG&}Y^l6P)O1FX}-z)v3j3_y1Ce2gl}?DUj&Cu^NSe~1s& zy7|$;^_5<4lh?j_#m&eZy=!#dg?|cop?1D=p(7J}45~qU} z1EdtkujN66==V6w;T?g)hJg0{{t{`|yc=kd1of(gHjR%CO^LIt@f9L%1~+ zGLH*4g4G;|2Uaxa(sAtwLbS)`;8KF9i^=(oMqNPYIM@8CUb}HNi=kNxdEEo)S;mq? zfV6%fZq7un??ylk&O-ipl;M0Sflb*^m-DT@`I^}2Yklpzv39&liqzC$Ln^AWIsZb0 zZe4;?0P^U9JQL7rQ%Gc}EHqqf>6DJAm6grjEf`Kn&%*hRDN-xpiL&o&KL0;rb#8c8 z!rjk5IMS_3nYU&6AL1A|H%+E^_)7&9AUFu(KTY(~3i6-u&lOM3Yt9P_;6(w{ug&lw z9podgloLCb)T?Hk*z_=xjwJDC}TwRQ=@ZeS{Z^dzgkTJr%0)mBwS&6YKtBzGChF9qLZHo;R)f&JW zdY#Z1H*f_V--H(%7=YUTyzO>LK|TkhOFG51i>KK(zduo1Jn;&G!(2^8xWel-AeaEo z%&&!iwFE#uI13T2MXk6H1bAVaOWqt=emDAw3?vYd?^I0-^pOP0n5HvT!vlJ6Nun_U;n?Jg9e<9|c=-p+0ji z93TO}ocdj#MOpyKH(;uE9_z5mO?Id`6C80JK{`rmy@7 z!($D|*Ofi~K}G>k2vb?OyN3;4g)d*n z(E54%-ig@Jl}xAJ(Wkz1x>`0$x-D{oE3Pa*vhqJ2o>%KNY3Wk%qo%KChOVRZ2r^($ zNLoh+>^OivDvs-k*Xam3rRx3`Njj*`-HI;U2iB~2& zt6H!ozJLt)N52l#bIWmoNNeV59YF+8N5Gb^-2b6+s9#c_z5u?Z1s?@e>p*WM<0XU_ z#jZy9J!vK~7LIm&B8m zFMvepi87tSyHy0;U0*JpS_j1fln9RIs1_y8WVO3OBgtakx8U`ejGf=GkrX~$t@(S1 z6Eb?~k{>27L^=)Tor!XkgN}z$M$5&aZCe=|$zzAjKt5uq09J$p1tcYp@RV z5O`lNv(Df;W8Iae9P<7{Cft+4LCXO^=s7fhLT{YV4mY<@+!sbkk&NI~Yi|2f^u8h? z#AyuCBmuu=i z+o;GyC?BBhT!qxoJQh{^I#xEc_51#^!R5VPo__0NHLohBT+$~1IzmSS)H#rFf@API z*D)nYdMeeqJndcU_IEBOrUHox$jp$FdKch-vdQ)Thd|VEn&?y*UrX@c4FCv38dc4s z)RN`rNdRmZB^c9K4-_5!-q2`WG8e z&l~;F{)UV#Nf_461+tb36e94iCCn@O6ZwZn!xk22@*Pqe{9~h11hR z`*NbkPB%C!Vjr4yy(?p}aoj;yW29{VP@WR3hB21{;AS9*uBl4M@Pjjv`Ltmg6hNSX zclnp1g}0R_cnmrdr{+5Xx+`)yY)b0WGyZsJO3HHW z2oXa9o92m-Y{>_8>k6$i2gG7K22I6~^y9UV(E@@>f}uF+1FasWIXo|6zc18u`2ana z_s>B$92Fo7<`B+^6~SLmF;C*aBq>MwJu33HGB5y0hZXSlRM66&Da0W^GifNLz58); z!{>XHC^K-E;gz!Fo#%H8h>P&28N|p*_1Tt{S(E+lZcj7eR8Kk-X`iYv=h*<1FuG~Q zt=My-wGIz$U~UbtKY`=yM37d2kl4)mjnR@70K@|7{ON1pOn$7XNwwLoE&4cA6E*(3 z(<>JGMPag>QR1N#=2g8tgg^-v2Alz*7xxoo{NY(&5AarBD>jkp46b(uvOrYJ2e8k?7-gc&kwp7~r8Xg` zZ}g<4=}qbs{Rb3Vtdo9~KhG!l28%&*LFBuC?@8^y@EA<%!LrEa_dK6{Plo;(mP_-jU@6DNsCvQUK0ANI=Td%&xdW9Ir8JlHL%koy`a{_mc&6 zNfl^nNQoLSKGk=%ZYTH@5|@X9u#1`J=2gMA3{u9mTx*4ip*!clus2ML51=vkvLNRU zbb8=zC?iM^hi&$>`QXZS3Jkw(v6$gYbI6|Xpb#G0^}<)~pRX0_oeNQZxmalN*2bAkta@sfyxXf0<_h+^&Ha_w+(hFWzdLKHjdN25dKsA>Cm2#2r63B^F=Ybj3Vg%&TK z$(G4BL&^(7uYcEv*aDxR3a(gjw*9D&{)*-i!?dU@PME|0$W%o@n2Qb(@MF>V{!oiO zwp!0F4Z7@rqM|jCi|)hY(DV{wjh_NVCPWy;(U|cCf0-!-PisGfuR@1Rx8aZO}~%8$bfxusI3d!t3z~AoF(sI#Hior1{(VXrKI`Nt=JvluAIc z;)5MF(w$xv=*<}S@JUMHT!@ClwXINUlA zLX^O{n-;PbGP+18&XDiLE|&FaDf~*KSj$H63ej$w(HrQuvFb52$a$-~{DOlDE8%b6 zsbUK;gW@%bmpv!qdwoKF?t~@!<;@R}cKf$-;A($Mt7H}kKXvv1?TWFcy`|X4l(kw4 z-AkgR%S8CC<-x5T*m@547Z5IWXjBn+z~?#KS|0B&$ay}1tE2Xe$54fFaO#*=X|oOf z%3}KL3^HTFJ3o*N2%L*!$&t(rc|Cpi;B-mU?9Qs1IT_Q<2dEJ~!}N&%Ba%5)nITc> zqu~zmfdp~%AWyMlr=yzJdTFIQ(`!lM026MDlYO~3K0XwPPd@=QtM%y>oq2?|SPC@E z;ib!gRKn1|kM@LHT|>~PJKpJIQ#SEoiqAf7YF^h!q1}!>JPgN&jPJ_8mtzuUQ=v5M zA<_FF*y7_bloM4M>U2qeJ71#6#^3{$hr#J-Gd;B`eBsat>wohv!-Ip8{M7xr`LdpY zGZocI3D)!HWvw>KNT$}4XP$2YXR2}f`$X9s(baHLFv^jM9cHl5S0xTM0@onO13oz> z)Jcr(2+B(OAuI{eWIRr2h?JdU0jizW`F_8q-_!7(!bnk52!=Lr|g@p_W-;E=B+~V;ODc>U` zo`nc8%F7qXn7(jxuC1ouKb#tF^ZjjDe0GTD!-3lI8egNa5!Jo+$`;Z(PWX;vN^Lq; z8?!Xs{n6BLOT%oF1l&&L3}ukpDQ<8(3DEs&*bC!3aD1t)o(X;QVX_=11?QCpNw6~5 zC-Dmfiq;Y;7?4f#hNS+ec4F!MzcYfVWk^^)2kWhn^g@w=a`dq?9~6z6X6qL~)liZn zNukXoU4}d^#27+|fie{isy^NppU7%w#yWgYc_|z)oy}10lQc?Xc F{|A|uv^f9( From 98d7e66af08e10d2a38cf94e09c5aa6456f0e12a Mon Sep 17 00:00:00 2001 From: jakubmkowalski Date: Tue, 13 Feb 2024 16:36:42 +0100 Subject: [PATCH 08/50] chore(BUX-586): renames bux-server to spv-wallet in README --- README.md | 52 +++++++++++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 859880480..389a2214b 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,22 @@
-# BUX: Server +# SPV Wallet -[![Release](https://img.shields.io/github/release-pre/BuxOrg/bux-server.svg?logo=github&style=flat&v=3)](https://github.com/BuxOrg/bux-server/releases) -[![Build Status](https://img.shields.io/github/actions/workflow/status/BuxOrg/bux-server/run-tests.yml?branch=master&v=3)](https://github.com/BuxOrg/bux-server/actions) -[![Report](https://goreportcard.com/badge/github.com/BuxOrg/bux-server?style=flat&v=3)](https://goreportcard.com/report/github.com/BuxOrg/bux-server) -[![codecov](https://codecov.io/gh/BuxOrg/bux-server/branch/master/graph/badge.svg?v=3)](https://codecov.io/gh/BuxOrg/bux-server) -[![Mergify Status](https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/BuxOrg/bux-server&style=flat&v=3)](https://mergify.io) +[![Release](https://img.shields.io/github/release-pre/BuxOrg/spv-wallet.svg?logo=github&style=flat&v=3)](https://github.com/BuxOrg/spv-wallet/releases) +[![Build Status](https://img.shields.io/github/actions/workflow/status/BuxOrg/spv-wallet/run-tests.yml?branch=master&v=3)](https://github.com/BuxOrg/spv-wallet/actions) +[![Report](https://goreportcard.com/badge/github.com/BuxOrg/spv-wallet?style=flat&v=3)](https://goreportcard.com/report/github.com/BuxOrg/spv-wallet) +[![codecov](https://codecov.io/gh/BuxOrg/spv-wallet/branch/master/graph/badge.svg?v=3)](https://codecov.io/gh/BuxOrg/spv-wallet) +[![Mergify Status](https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/BuxOrg/spv-wallet&style=flat&v=3)](https://mergify.io)
-[![Go](https://img.shields.io/github/go-mod/go-version/BuxOrg/bux-server?v=3)](https://golang.org/) -[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod&v=3)](https://gitpod.io/#https://github.com/BuxOrg/bux-server) +[![Go](https://img.shields.io/github/go-mod/go-version/BuxOrg/spv-wallet?v=3)](https://golang.org/) +[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod&v=3)](https://gitpod.io/#https://github.com/BuxOrg/spv-wallet) [![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat&v=3)](https://github.com/RichardLitt/standard-readme) [![Makefile Included](https://img.shields.io/badge/Makefile-Supported%20-brightgreen?=flat&logo=probot&v=3)](Makefile)
-> Get started using [BUX](https://getbux.io) in five minutes - ## Table of Contents - [About](#about) @@ -36,36 +34,36 @@ ## About -[Read more about BUX](https://getbux.io) +Complete stand-alone server using the SPV Wallet engine (UTXOs, xPubs, Paymail & More!)
## Installation -**bux-server** requires a [supported release of Go](https://golang.org/doc/devel/release.html#policy). +**spv-wallet** requires a [supported release of Go](https://golang.org/doc/devel/release.html#policy). ```shell script -go get -u github.com/BuxOrg/bux-server +go get -u github.com/BuxOrg/spv-wallet ``` #### build ```shell script -go build -o bux-server cmd/server/* +go build -o spv-wallet cmd/server/* ``` #### run ```shell script -./bux-server +./spv-wallet ```
## Documentation -View the generated [documentation](https://pkg.go.dev/github.com/BuxOrg/bux-server) +View the generated [documentation](https://pkg.go.dev/github.com/BuxOrg/spv-wallet) -[![GoDoc](https://godoc.org/github.com/BuxOrg/bux-server?status.svg&style=flat&v=3)](https://pkg.go.dev/github.com/BuxOrg/bux-server) +[![GoDoc](https://godoc.org/github.com/BuxOrg/spv-wallet?status.svg&style=flat&v=3)](https://pkg.go.dev/github.com/BuxOrg/spv-wallet)
@@ -195,7 +193,7 @@ vet Run the Go vet application ### Defaults -If you run Bux-Server without editing anything, it will use the default configuration from file [defaults.go](/config/defaults.go). It is set up to use _freecache_, _sqlite_ with enabled _paymail_ with _signing disabled_ and with _beef_. +If you run spv-wallet without editing anything, it will use the default configuration from file [defaults.go](/config/defaults.go). It is set up to use _freecache_, _sqlite_ with enabled _paymail_ with _signing disabled_ and with _beef_. ### Config Variables @@ -228,7 +226,7 @@ go run ./cmd/server/main.go -C /my/config.json #### Environment variables -To override any config variable with ENV, use the "BUX\_" prefix with mapstructure annotation path with "_" as a delimiter in all uppercase. Example: +To override any config variable with ENV, use the "SPV\_" prefix with mapstructure annotation path with "_" as a delimiter in all uppercase. Example: Let's take this fragment of AppConfig from `config.example.yaml`: @@ -240,9 +238,9 @@ auth: signing_disabled: true ``` -To override admin_key in auth config, use the path with "_" as a path delimiter and BUX\_ as prefix. So: +To override admin_key in auth config, use the path with "_" as a path delimiter and SPV\_ as prefix. So: ```bash -BUX_AUTH_ADMIN_KEY="admin_key" +SPV_AUTH_ADMIN_KEY="admin_key" ``` To be able to use TAAL API Key is needed. @@ -265,7 +263,7 @@ To use your API key put key in ``token`` field in ```config.example.yaml``` ### Examples & Tests -All unit tests run via [GitHub Actions](https://github.com/BuxOrg/bux-server/actions) and +All unit tests run via [GitHub Actions](https://github.com/BuxOrg/spv-wallet/actions) and uses [Go version 1.19.x](https://golang.org/doc/go1.19). View the [configuration file](.github/workflows/run-tests.yml).
@@ -297,8 +295,8 @@ make bench ### Docker Compose Quickstart -To get started with development, `bux-server` provides a `start.sh` script -which is using `docker-compose.yml` file to starts up Bux Server with selected database +To get started with development, `spv-wallet` provides a `start.sh` script +which is using `docker-compose.yml` file to starts up SPV Wallet serer with selected database and cache storage. To start, we need to fill the config json which we want to use, for example: `config/envs/development.json`. @@ -306,7 +304,7 @@ Main configuration is done when running the script. There are two way of running this script: 1. with manual configuration - Every option is displayed in terminal and user can choose - which database/cache storage use and configure how to run bux-server. + which database/cache storage use and configure how to run spv-wallet. ```bash ./start.sh ``` @@ -316,7 +314,7 @@ There are two way of running this script: ./start.sh -db postgresql -c redis -bs true -env development -b false ``` -`-l/--load` option add possibility to use previously created `.env.config` file and run bux-server with simple command: +`-l/--load` option add possibility to use previously created `.env.config` file and run spv-wallet with simple command: ```bash ./start.sh -l ``` @@ -338,4 +336,4 @@ View the [contributing guidelines](.github/CODE_STANDARDS.md#3-contributing) and ## License -[![License](https://img.shields.io/github/license/BuxOrg/bux-server.svg?style=flat&v=3)](LICENSE) +[![License](https://img.shields.io/github/license/BuxOrg/spv-wallet.svg?style=flat&v=3)](LICENSE) From a2e832bcc20f4565ed45788ce1253a653bdb4167 Mon Sep 17 00:00:00 2001 From: jakubmkowalski Date: Tue, 13 Feb 2024 16:51:13 +0100 Subject: [PATCH 09/50] chore(BUX-586): fixes package comment --- spv_wallet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spv_wallet.go b/spv_wallet.go index b3c97d043..3e5f3b3a6 100644 --- a/spv_wallet.go +++ b/spv_wallet.go @@ -1,4 +1,4 @@ -// Package spv-wallet is a complete stand-alone server using the SPV Wallet Engine +// Package spvwallet is a complete stand-alone server using the SPV Wallet Engine // // Run: go run cmd/server/main.go // From 714213e57cdf1a869ecafc5b84be6b9f84b2eb49 Mon Sep 17 00:00:00 2001 From: jakubmkowalski Date: Fri, 16 Feb 2024 12:32:23 +0100 Subject: [PATCH 10/50] feat(BUX-603): adds models --- go.mod | 2 + models/.dockerignore | 71 +++ models/.editorconfig | 87 ++++ models/.gitattributes | 30 ++ models/.github/CODEOWNERS | 7 + models/.github/CODE_OF_CONDUCT.md | 39 ++ models/.github/CODE_STANDARDS.md | 334 ++++++++++++++ .../.github/ISSUE_TEMPLATE/feature_request.md | 23 + .../.github/ISSUE_TEMPLATE/issue-template.md | 41 ++ models/.github/PULL_REQUEST_TEMPLATE.md | 37 ++ models/.github/dependabot.yml | 33 ++ models/.github/labels.yml | 54 +++ models/.github/mergify.yml | 218 +++++++++ models/.github/workflows/codeql-analysis.yml | 78 ++++ models/.github/workflows/release.yml | 37 ++ models/.github/workflows/run-tests.yml | 67 +++ models/.github/workflows/stale.yml | 27 ++ models/.github/workflows/sync-labels.yml | 19 + models/.gitignore | 34 ++ models/.golangci.yml | 431 ++++++++++++++++++ models/.goreleaser.yml | 71 +++ models/.make/common.mk | 99 ++++ models/.make/go.mk | 146 ++++++ models/.yamllint.yml | 36 ++ models/LICENSE | 22 + models/Makefile | 41 ++ models/README.md | 214 +++++++++ models/access_key.go | 23 + models/admin_stats.go | 21 + models/auth_payload.go | 19 + models/authentication.go | 26 ++ models/block_header.go | 32 ++ models/codecov.yml | 42 ++ models/common/model.go | 16 + models/destination.go | 32 ++ models/draft_transaction.go | 42 ++ models/fee_unit.go | 9 + models/go.mod | 11 + models/go.sum | 10 + models/metadata.go | 7 + models/models_test.go | 33 ++ models/paymail_address.go | 24 + models/spvwalleterrors/errors_def.go | 13 + models/sync_config.go | 13 + models/sync_result.go | 23 + models/sync_transaction.go | 28 ++ models/transaction.go | 39 ++ models/transaction_config.go | 115 +++++ models/utxo.go | 42 ++ models/xpub.go | 18 + 50 files changed, 2936 insertions(+) create mode 100644 models/.dockerignore create mode 100644 models/.editorconfig create mode 100644 models/.gitattributes create mode 100644 models/.github/CODEOWNERS create mode 100644 models/.github/CODE_OF_CONDUCT.md create mode 100644 models/.github/CODE_STANDARDS.md create mode 100644 models/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 models/.github/ISSUE_TEMPLATE/issue-template.md create mode 100644 models/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 models/.github/dependabot.yml create mode 100644 models/.github/labels.yml create mode 100644 models/.github/mergify.yml create mode 100644 models/.github/workflows/codeql-analysis.yml create mode 100644 models/.github/workflows/release.yml create mode 100644 models/.github/workflows/run-tests.yml create mode 100644 models/.github/workflows/stale.yml create mode 100644 models/.github/workflows/sync-labels.yml create mode 100644 models/.gitignore create mode 100644 models/.golangci.yml create mode 100644 models/.goreleaser.yml create mode 100644 models/.make/common.mk create mode 100644 models/.make/go.mk create mode 100644 models/.yamllint.yml create mode 100644 models/LICENSE create mode 100644 models/Makefile create mode 100644 models/README.md create mode 100644 models/access_key.go create mode 100644 models/admin_stats.go create mode 100644 models/auth_payload.go create mode 100644 models/authentication.go create mode 100644 models/block_header.go create mode 100644 models/codecov.yml create mode 100644 models/common/model.go create mode 100644 models/destination.go create mode 100644 models/draft_transaction.go create mode 100644 models/fee_unit.go create mode 100644 models/go.mod create mode 100644 models/go.sum create mode 100644 models/metadata.go create mode 100644 models/models_test.go create mode 100644 models/paymail_address.go create mode 100644 models/spvwalleterrors/errors_def.go create mode 100644 models/sync_config.go create mode 100644 models/sync_result.go create mode 100644 models/sync_transaction.go create mode 100644 models/transaction.go create mode 100644 models/transaction_config.go create mode 100644 models/utxo.go create mode 100644 models/xpub.go diff --git a/go.mod b/go.mod index 9d503f62d..49a45b69d 100644 --- a/go.mod +++ b/go.mod @@ -150,3 +150,5 @@ replace github.com/gomodule/redigo => github.com/gomodule/redigo v1.8.9 // Issue: go.mongodb.org/mongo-driver/x/bsonx: cannot find module providing package go.mongodb.org/mongo-driver/x/bsonx replace go.mongodb.org/mongo-driver => go.mongodb.org/mongo-driver v1.11.7 + +replace github.com/bitcoin-sv/spv-wallet/models => ./models diff --git a/models/.dockerignore b/models/.dockerignore new file mode 100644 index 000000000..98a7dcd07 --- /dev/null +++ b/models/.dockerignore @@ -0,0 +1,71 @@ +## +## Specific to .dockerignore +## + +.git/ +Dockerfile +contrib/ + +## +## Common with .gitignore +## + +# Temporary files +*~ +*# +.#* + +# Vendors +node_modules/ +vendor/ + +# Binaries for programs and plugins +dist/ +gin-bin +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Byte-compiled / optimized / DLL files +__pycache__/ +**/__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.py[cod] +*$py.class +.pytest_cache/ +..mypy_cache/ + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Virtual environments +.venv +../venv +.DS_Store +.AppleDouble +.LSOverride +._* + +# Custom repo notes +todo.md + +# Temporary directories in the project +bin +tmp +.all-contributorsrc +.gitpod.yml +.golangci.yml +.goreleaser.yml +.yamllint.yml +.editorconfig +codecov.yml +LICENSE +README.md diff --git a/models/.editorconfig b/models/.editorconfig new file mode 100644 index 000000000..67d6d324e --- /dev/null +++ b/models/.editorconfig @@ -0,0 +1,87 @@ +# Check http://editorconfig.org for more information +# This is the main config file for this project: + +root = true + +[*] +charset = utf-8 + +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +indent_style = space +indent_size = 4 + +[*.mod] +indent_style = tab + +[{Makefile,**.mk}] +indent_style = tab + +[*.go] +indent_style = tab + +[*.css] +indent_size = 2 + +[*.proto] +indent_size = 2 + +[*.ftl] +indent_size = 2 + +[*.toml] +indent_size = 2 + +[*.swift] +indent_size = 4 + +[*.tmpl] +indent_size = 2 + +[*.js] +indent_size = 2 +block_comment_start = /* +block_comment_end = */ + +[*.{html,htm}] +indent_size = 2 + +[*.bat] +end_of_line = crlf + +[*.{yml,yaml}] +indent_size = 2 + +[*.json] +indent_size = 2 + +[.{babelrc,eslintrc,prettierrc}] +indent_size = 2 + +[{Fastfile,.buckconfig,BUCK}] +indent_size = 2 + +[*.diff] +indent_size = 1 + +[*.{diff,patch}] +trim_trailing_whitespace = false + +[*.m] +indent_size = 1 +indent_style = space +block_comment_start = /** +block_comment = * +block_comment_end = */ + +[*.java] +indent_size = 4 +indent_style = space +block_comment_start = /** +block_comment = * +block_comment_end = */ + +[*.{md,rst}] +trim_trailing_whitespace = false diff --git a/models/.gitattributes b/models/.gitattributes new file mode 100644 index 000000000..fe46d96e9 --- /dev/null +++ b/models/.gitattributes @@ -0,0 +1,30 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Collapse generated and vendored files on GitHub +AUTHORS linguist-generated merge=union +*.gen.* linguist-generated merge=ours +*.pb.go linguist-generated merge=ours +*.pb.gw.go linguist-generated merge=ours +go.sum linguist-generated merge=ours +go.mod linguist-generated +gen.sum linguist-generated merge=ours +depaware.txt linguist-generated linguist-vendored merge=union +vendor/* linguist-vendored +rules.mk linguist-vendored +*/vendor/* linguist-vendored + +# doc +docs/* linguist-documentation +docs/Makefile linguist-documentation=false + +# Reduce conflicts on markdown files +*.md merge=union + +# A set of files you probably don't want in distribution +/.github export-ignore +/.githooks export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.gitmodules export-ignore +/tool/lint export-ignore diff --git a/models/.github/CODEOWNERS b/models/.github/CODEOWNERS new file mode 100644 index 000000000..d29ac98b9 --- /dev/null +++ b/models/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# CODEOWNERS file for bitcoin-sv + +# This file is used to designate code owners for files and directories in this repository. +# The code owner will automatically be requested for review on pull requests that modify the corresponding files. + +* @bitcoin-sv/sr-engineers +* @bitcoin-sv/developers diff --git a/models/.github/CODE_OF_CONDUCT.md b/models/.github/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..ceef82baa --- /dev/null +++ b/models/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,39 @@ +# Code of Conduct + +## 1. Purpose + +The primary goal of this project is to foster an inclusive, respectful, and open community for everyone, regardless of their background or identity. This Code of Conduct outlines our expectations for participant behavior and the consequences for unacceptable behavior. + +## 2. Open Discussions & Respectful Feedback + +- **Encourage Diverse Ideas:** Everyone brings a unique perspective. Encourage different viewpoints, and listen openly to each other’s ideas. + +- **Constructive Feedback:** Focus on providing constructive feedback rather than criticizing individuals. Discuss ideas, not the person presenting them. + +- **Avoid Harmful Language:** Refrain from using offensive or harmful language, including but not limited to sexist, racist, homophobic, transphobic, ableist, or discriminatory remarks. + +## 3. No Politics or Off-topic Discussions + +- **Stay On Topic:** Keep discussions focused on the project and avoid bringing in off-topic or political discussions. + +- **Respectful Discourse:** If discussions become heated, maintain a level of respect and understanding, and work towards a compromise. + +## 4. Reporting & Enforcement + +- **Report Violations:** If you observe a violation of this Code of Conduct, please report it by contacting the project team members. + +- **Consequences:** Violations of this Code of Conduct may result in temporary or permanent banning from the project community. + +## 5. Inclusion & Diversity + +- **Welcome Everyone:** Foster an environment where everyone feels welcome, regardless of their background, identity, or level of experience. + +- **Help Newcomers:** Offer help and guidance to newcomers to make them feel welcome in our community. + +## 6. Be Kind & Courteous + +- **Respect Time & Effort:** Recognize and respect the time and effort put in by contributors and maintainers. + +- **Courtesy:** Be courteous and polite. Treat others as you would like to be treated. + +By participating in this project, you agree to abide by this Code of Conduct. Let’s work together to make this community respectful and inclusive for everyone. diff --git a/models/.github/CODE_STANDARDS.md b/models/.github/CODE_STANDARDS.md new file mode 100644 index 000000000..071e3175e --- /dev/null +++ b/models/.github/CODE_STANDARDS.md @@ -0,0 +1,334 @@ +# Code Standards & Contributing Guidelines + +- [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.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) + - [As a Developer](#as-a-developer) + - [As a PR Reviewer](#as-a-pr-reviewer) + - [2.2 Tests](#22-tests) + - [Principle](#principle) + - [Guidelines for Writing Tests](#guidelines-for-writing-tests) + - [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) + - [Include Issue Number](#include-issue-number) + - [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.1 Overview](#41-overview) + - [4.2 Principles](#42-principles) + - [4.3 Feature Documentation](#43-feature-documentation) + - [Necessity](#necessity) + - [Examples](#examples) + - [4.4 External Features](#44-external-features) + - [4.5 Markdown usage](#45-markdown-usage) + - [4.6 Conclusion](#46-conclusion) + +## Most important rules - Quick Checklist + +- [ ] Follow official Go guidelines for style and formatting. +- [ ] Write self-documenting code and minimize comments. +- [ ] Ensure comprehensive test coverage including happy and error paths. +- [ ] Provide meaningful and constructive code reviews. +- [ ] Adhere to Conventional Commits for commit messages and PR naming. +- [ ] Document every feature adequately, especially for open-source projects. +- [ ] 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.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://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 + + > Our current linter configuration is in the `.golangci.yml` file. + +#### Additional useful resources with GO recommendations, best practices and the common mistakes + +- [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://go.dev/wiki/CommonMistakes) - Common Mistakes in Go + +## 2 Code Rules + +### 2.1. Self-documenting code + +#### As a Developer + +- Refactoring tasks should be strategically undertaken; not every piece of code warrants modification. If a segment of code, despite comments, remains untouched and unproblematic, it is likely fulfilling its purpose effectively. +- When embarking on feature additions or bug fixes, and encountering commented code, consider extracting functions and creating a PR before proceeding with the primary task. This helps maintain code clarity and function. +- Should you come across a superfluous comment during your changes, do take the initiative to remove it and create a PR for that. This practice contributes to keeping the repository neat and well-maintained. +- Endeavor to minimize the addition of comments when making any changes to the code. + +#### As a PR Reviewer + +- 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 + +#### Principle + +Developers are required to diligently cover their changes with tests and organize these tests with caution. A well-structured set of tests serves as a safety net, facilitating the swift identification and resolution of issues. + +#### Guidelines for Writing Tests + +1. **Readability**: Maintain test readability through the use of descriptive test names. When possible, group cases into table-driven tests, exercising discretion to avoid excessiveness. Avoid code branching in tests; instead, split differing scenarios into separate test cases. + +2. **Comprehensive Coverage**: Ensure comprehensive test coverage, including error paths. Prioritize adding use cases for well-known errors that could be returned from your code. + +3. **Structural Consistency**: Adopt the "Given-When-Then" structure in automatic tests to enhance readability and consistency. This structure assists team members in following the test flow and understanding its purpose, thus facilitating smoother onboarding for new members and maintaining uniformity across tests. + + - **Given**: Set the stage by preparing inputs, mocks, and application state. + - **When**: Trigger the action or function under test. + - **Then**: Verify if the outcomes match the expectations. + + ```go + func TestSomethingVeryUsefulIsHappening(t *testing.T) { + //given + ... // prepare inputs, mocks, application state + + //when + ... // call the function you're actually testing + + //then + ... // check expectations about the function output + } + ``` + +4. **Test Isolation**: Ensure test isolation by avoiding the use of global variables and shared state. Each test should be independent and not rely on the execution of other tests. If a test requires a shared state, use a setup function to create the state before each test. + +5. **Test Data**: Avoid random data in tests. Instead, use predefined data to ensure test consistency and reproducibility. If random data is required, use a seed to ensure the same data is generated each time the test is run. + +6. **Test Cases**: If you are writing a public functions - it should be covered by tests. We should have test cases for all possible scenarios. That means that **we should have tests for all possible errors** that can be returned from the function. +Of course not only error paths should be covered - **we should highlight the happy path as well**. + +7. **Testing private (unexported) functions**: When testing private functions, we should test them through the public (exported) functions that use them. The exception is when the private function is too complex to be tested through the public function. Good example is for example a function that is implementing a complex algorithm. In this case we should test the private function directly. + +### 2.3 Code Review + +#### Guidelines for Code Review + +1. **Constructive Feedback**: Reviewers should provide constructive feedback, highlighting both strengths and areas of improvement. Comments should be clear, concise, and related to code structure, functionality, or style. + +2. **Coding Standards**: Both authors and reviewers should ensure the code adheres to established coding standards, including formatting, naming conventions, and best practices as outlined in official Golang guidelines. + +3. **Testing**: Reviewers should ensure that the submitted code is accompanied by adequate tests, covering both happy paths and error paths. Check the readability, descriptiveness, and structure of tests. + +4. **Error Handling**: Pay special attention to error handling within the code. Ensure that errors are not ignored, logged appropriately, and presented to the user in a user-friendly format when applicable. + +5. **Performance**: Review the code for any potential performance issues, such as inefficient loops, unnecessary allocations, or misuse of concurrency. + +6. **Dependency Management**: Ensure that any new dependencies are necessary, appropriately versioned, and have been vetted for performance and security. + +7. **Security Practices**: Ensure that the code follows secure coding practices and avoids common vulnerabilities. + Good checklist for security practices can be found [here](https://owasp.org/www-project-top-ten/). + +8. **Documentation**: Confirm that the code is well-documented, including comments, function/method descriptions, and module-level documentation as necessary. + +9. **Efficiency and Readability**: The code should be efficient and readable. Reviewers should look for any code smells, overly complex functions, and ensure the use of idiomatic Go patterns. + +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. + +#### 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? +- [ ] Is error handling comprehensive and user-friendly? +- [ ] Are there any performance concerns in the code? +- [ ] Have new dependencies been appropriately vetted? +- [ ] Does the code follow secure coding practices and avoid common vulnerabilities? +- [ ] Is the code well(self)-documented, with clear variable, function naming? +- [ ] Is the code efficient, readable, and free of code smells? +- [ ] Have all review comments been addressed in a timely manner? +- [ ] Do you understand the code and it's purpose? + +This checklist serves as a guide to both authors and reviewers to ensure a thorough and effective code review process. + +## 3 Contributing + +### 3.1 Pull Requests && Issues + +We have separate templates for Pull Requests and Issues. Please use them when creating a new PR or Issue. + +### 3.2 Conventional Commits & Pull Requests Naming + +#### 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. + +#### Structure + +Conventional Commits follow a structured format: `type(scope): description`, where: + +- `type`: Represents the nature of the commit (e.g., feat, fix, chore). +- `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`. + +#### Types + +- `feat`: Utilized when introducing a new feature to the codebase. +- `fix`: Employed when resolving a bug or issue in the code. +- `docs`: Designated for commits involving documentation changes, such as updating README files or adding comments. +- `style`: Applied to commits focusing on code style and formatting, without altering the code's functionality. +- `refactor`: Used for code changes that neither introduce new features nor fix bugs, but improve the code structure or design. +- `test`: Assigned to commits pertaining to the addition, modification, or refactoring of tests. +- `chore`: For changes related to build processes, local development, or other maintenance tasks. +- `perf`: Employed when enhancing the performance of the codebase. +- `revert`: Marked for commits that revert a previous change. +- `ci`: Applied to changes concerning the Continuous Integration (CI) configuration or scripts. +- `deps`: Used when updating or modifying dependencies. + +#### 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.`
+`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`
+`fix(#125): fix a bug - 2.1.0 -> 2.1.1` + +Given a version number MAJOR.MINOR.PATCH, increment the: + +MAJOR version when you make incompatible API changes +MINOR version when you add functionality in a backward compatible manner +PATCH version when you make backward compatible bug fixes +Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. + +More about Semantic Versioning can be found [here](https://semver.org/). + +#### 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. + +#### Further Reading + +Additional information and guidelines on Conventional Commits can be found [here](https://www.conventionalcommits.org/en/v1.0.0/). + +#### Examples + +##### Commit message with scope + +Good example: + +```bash +feat: add possibility to create a new user by admin (#123) +``` + +Bad example: + +```bash +debugo feature - checkpoint full work +``` + +##### Pull request title with a scope and task number + +> feat(#123): add new feature + +### 3.3 Branching + +#### Choosing branch names + +- Choose consistent naming conventions. Common practices include: + - `feature/feature-name` + - `bugfix/issue-or-bug-name` + - `hotfix/hotfix-name` + - `chore/task-name` + - `refactor/refactor-name` + +#### 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`. + +#### Include Issue Number + +- If applicable, include the issue number in the branch name for easy tracking, e.g.,
+`feature/123-add-login-button`. + +#### Deleting Branches After Merging + +#### Remove Remote Branches + +- Once a PR has been merged, delete the remote branch to keep the repository clean. +- GitHub provides a button to delete the branch once the PR is merged. + +#### 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`. + +## 4 Documentation Code Standards + +### 4.1 Overview + +A well-documented codebase is pivotal for both internal development and external contributions, especially for open-source projects that expose functionalities for public use. Comprehensive documentation, supplemented with examples where necessary, ensures that every feature is easily understandable, usable, and maintainable. + +### 4.2 Principles + +- **Clarity and Conciseness**: Documentation should be clear, concise, and focused, providing necessary information without unnecessary complexity or verbosity. +- **Accessibility**: Documentation should be accessible to developers of varying skill levels, enabling both novices and experts to understand the codebase and its features. +- **Up-to-Date**: Documentation must be kept current, reflecting the latest changes and developments in the codebase to avoid misinformation and confusion. + +### 4.3 Feature Documentation + +#### 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. + +#### 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. + +### 4.4 External Features + +For projects exposing external features: + +- **Comprehensive Guides**: Ensure the creation of comprehensive guides detailing the utilization of exposed features, their benefits, and any potential configurations or customizations. +- **Community Engagement**: Encourage community members to contribute to documentation by providing feedback, suggestions, and improvements. This collaborative approach enriches the documentation quality and breadth. + +### 4.5 Markdown usage + +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](https://markdownguide.org) - Comprehensive guide to Markdown syntax. + +### 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/models/.github/ISSUE_TEMPLATE/feature_request.md b/models/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..91c49f65d --- /dev/null +++ b/models/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for a new feature or improvement +title: "[FEATURE]" +labels: enhancement +assignees: '' + +--- + +## Desired Solution +A clear and concise description of what you want to happen. Describe how the new feature should work and what problems it should solve. If possible, including mockups, diagrams, or sample code can be helpful. + +## Suggested Implementation +In this section, you can present ideas on how the new feature could be implemented. Suggest where in the code changes should be made and what libraries or tools might be useful. + +## Alternatives Considered +Describe alternative solutions or features that have been considered. Why was this particular solution chosen? What are the pros and cons of the alternative options? + +## Additional Context +Add any other context or screenshots about the feature request here. If the proposal involves visual elements, it’s good to attach screenshots, videos, or other materials illustrating the proposed changes. + +## Categorize Feature +If it's possible try categorize the request - if it's UX Improvement, Security Improvement or maybe the changed is related to the application performance. diff --git a/models/.github/ISSUE_TEMPLATE/issue-template.md b/models/.github/ISSUE_TEMPLATE/issue-template.md new file mode 100644 index 000000000..dff68919d --- /dev/null +++ b/models/.github/ISSUE_TEMPLATE/issue-template.md @@ -0,0 +1,41 @@ +--- +name: Issue Template +about: Create a report to help us fix bugs/inaccuracies +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**TL;DR** +Check if your issue answers those question: + +- [ ] Does my issue clearly describe what exactly is not working properly? +- [ ] Does my issue clearly describe what are my expectations and what is the current output? +- [ ] Did I attach logs/screenshots/errors which ocured? +- [ ] Did I provide information how I run the code/library/application and on which platform/browser? + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior/error. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +If you have any logs connected with your problem - please attach them. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Used environment (please complete the following information):** + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/models/.github/PULL_REQUEST_TEMPLATE.md b/models/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..a2e7e4e76 --- /dev/null +++ b/models/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,37 @@ + + +# Pull Request Checklist + +- [ ] 📖 I created my PR using provided : [CODE_STANDARDS](https://github.com/bitcoin-sv/spv-wallet/models/blob/main/.github/CODE_STANDARDS.md) +- [ ] 📖 I have read the short Code of Conduct: [CODE_OF_CONDUCT](https://github.com/bitcoin-sv/spv-wallet/models/blob/main/.github/CODE_OF_CONDUCT.md) +- [ ] 🏠 I tested my changes locally. +- [ ] ✅ I have provided tests for my changes. +- [ ] 📝 I have used conventional commits. +- [ ] 📗 I have updated any related documentation. +- [ ] 💾 PR was issued based on the GitHub or Jira issue. + + + + + + diff --git a/models/.github/dependabot.yml b/models/.github/dependabot.yml new file mode 100644 index 000000000..81421f986 --- /dev/null +++ b/models/.github/dependabot.yml @@ -0,0 +1,33 @@ +# Basic dependabot.yml to update gomod and GitHub Actions +version: 2 +updates: + # Maintain dependencies for the core library + - package-ecosystem: "gomod" + target-branch: "master" + directory: "/" + schedule: + interval: "daily" + time: "10:00" + timezone: "UTC" + reviewers: + - "mrz1836" + assignees: + - "mrz1836" + labels: + - "chore" + open-pull-requests-limit: 10 + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + target-branch: "master" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + reviewers: + - "mrz1836" + assignees: + - "mrz1836" + labels: + - "chore" + open-pull-requests-limit: 10 diff --git a/models/.github/labels.yml b/models/.github/labels.yml new file mode 100644 index 000000000..df916752a --- /dev/null +++ b/models/.github/labels.yml @@ -0,0 +1,54 @@ +- color: 0075ca + description: "Improvements or additions to documentation" + name: "documentation" +- color: b23128 + description: "Highest rated bug or issue, affects all" + name: "bug-P1" +- color: de3d32 + description: "Medium rated bug, affects a few" + name: "bug-P2" +- color: f44336 + description: "Lowest rated bug, affects nearly none or low-impact" + name: "bug-P3" +- color: 0e8a16 + description: "Any new significant addition" + name: "feature" +- color: b60205 + description: "Urgent or important fix/patch" + name: "hot-fix" +- color: cccccc + description: "Any idea, suggestion" + name: "idea" +- color: d4c5f9 + description: "Experimental - can break!" + name: "prototype" +- color: cc317c + description: "Any question or concern" + name: "question" +- color: c2e0c6 + description: "Unit tests, mocking, integration testing" + name: "test" +- color: fbca04 + description: "Anything GUI related" + name: "ui-ux" +- color: 006b75 + description: "Simple updates or version bumps" + name: "chore" +- color: 006b75 + description: "Dependency updates" + name: "dependencies" +- color: 006b75 + description: "General updates" + name: "update" +- color: FFA500 + description: "Any significant refactoring" + name: "refactor" +- color: FEF2C0 + description: "Used for automatic merging" + name: "automerge" +- color: FBCA04 + description: "Used for denoting a WIP, stops auto-merge" + name: "work-in-progress" +- color: c2e0c6 + description: "Old, unused, stale" + name: "stale" \ No newline at end of file diff --git a/models/.github/mergify.yml b/models/.github/mergify.yml new file mode 100644 index 000000000..9d6fc8c64 --- /dev/null +++ b/models/.github/mergify.yml @@ -0,0 +1,218 @@ +pull_request_rules: + + # =============================================================================== + # DEPENDABOT + # =============================================================================== + + - name: Automatic Merge for Dependabot Minor Version Pull Requests + conditions: + - -draft + - author~=^dependabot(|-preview)\[bot\]$ + - check-success='test (1.18.x, ubuntu-latest)' + - check-success='test (1.19.x, ubuntu-latest)' + - check-success='Analyze (go)' + actions: + review: + type: APPROVE + message: Automatically approving dependabot pull request + merge: + method: merge + # =============================================================================== + # AUTOMATIC MERGE (APPROVALS) + # =============================================================================== + + - name: Automatic Merge ⬇️ on Approval ✔ + conditions: + - "#approved-reviews-by>=1" + - "#review-requested=0" + - "#changes-requested-reviews-by=0" + - check-success='test (1.18.x, ubuntu-latest)' + - check-success='test (1.19.x, ubuntu-latest)' + - check-success='Analyze (go)' + - -title~=(?i)wip + - label!=work-in-progress + - -draft + actions: + merge: + method: merge + + # =============================================================================== + # AUTHOR + # =============================================================================== + + - name: Auto-Assign Author + conditions: + - "#assignee=0" + actions: + assign: + users: ["mrz1836"] + + # =============================================================================== + # ALERTS + # =============================================================================== + + - name: Notify on merge + conditions: + - merged + - label=automerge + actions: + comment: + message: "✅ @{{author}}: **{{title}}** has been merged successfully." + - name: Alert on merge conflict + conditions: + - conflict + - label=automerge + actions: + comment: + message: "🆘 @{{author}}: `{{head}}` has conflicts with `{{base}}` that must be resolved." + - name: Alert on tests failure for automerge + conditions: + - label=automerge + - status-failure=commit + actions: + comment: + message: "🆘 @{{author}}: unable to merge due to CI failure." + + # =============================================================================== + # LABELS + # =============================================================================== + # Automatically add labels when PRs match certain patterns + # + # NOTE: + # - single quotes for regex to avoid accidental escapes + # - Mergify leverages Python regular expressions to match rules. + # + # Semantic commit messages + # - chore: updating grunt tasks etc.; no production code change + # - docs: changes to the documentation + # - feat: feature or story + # - feature: new feature or story + # - fix: bug fix for the user, not a fix to a build script + # - idea: general idea or suggestion + # - question: question regarding code + # - test: test related changes + # - wip: work in progress PR + # =============================================================================== + + - name: Work in Progress + conditions: + - "head~=(?i)^wip" # if the PR branch starts with wip/ + actions: + label: + add: ["work-in-progress"] + - name: Hotfix label + conditions: + - "head~=(?i)^hotfix" # if the PR branch starts with hotfix/ + actions: + label: + add: ["hot-fix"] + - name: Bug / Fix label + conditions: + - "head~=(?i)^(bug)?fix" # if the PR branch starts with (bug)?fix/ + actions: + label: + add: ["bug-P3"] + - name: Documentation label + conditions: + - "head~=(?i)^docs" # if the PR branch starts with docs/ + actions: + label: + add: ["documentation"] + - name: Feature label + conditions: + - "head~=(?i)^feat(ure)?" # if the PR branch starts with feat(ure)?/ + actions: + label: + add: ["feature"] + - name: Chore label + conditions: + - "head~=(?i)^chore" # if the PR branch starts with chore/ + actions: + label: + add: ["update"] + - name: Question label + conditions: + - "head~=(?i)^question" # if the PR branch starts with question/ + actions: + label: + add: ["question"] + - name: Test label + conditions: + - "head~=(?i)^test" # if the PR branch starts with test/ + actions: + label: + add: ["test"] + - name: Idea label + conditions: + - "head~=(?i)^idea" # if the PR branch starts with idea/ + actions: + label: + add: ["idea"] + + # =============================================================================== + # CONTRIBUTORS + # =============================================================================== + + - name: Welcome New Contributors + conditions: + - and: + - author!=dependabot[bot] + - author!=mergify[bot] + - author!=allcontributors[bot] + - author!=mrz1836 + - author!=icellan + - author!=dorzepowski + - author!=pawellewandowski98 + - author!=arkadiuszos4chain + - author!=wregulski + actions: + comment: + message: "Welcome to our open-source project @{{author}}! 💘" + + # =============================================================================== + # STALE BRANCHES + # =============================================================================== + + - name: Close stale pull request + conditions: + - base=master + - -closed + - updated-at<21 days ago + actions: + close: + message: | + This pull request looks stale. Feel free to reopen it if you think it's a mistake. + label: + add: ["stale"] + + # =============================================================================== + # BRANCHES + # =============================================================================== + + - name: Delete head branch after merge + conditions: + - merged + actions: + delete_head_branch: + + # =============================================================================== + # CONVENTION + # =============================================================================== + # https://www.conventionalcommits.org/en/v1.0.0/ + # Premium feature only + + #- name: Conventional Commit + # conditions: + # - "title~=^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\\(.+\\))?:" + # actions: + # post_check: + # title: | + # {% if check_succeed %} + # Title follows Conventional Commit + # {% else %} + # Title does not follow Conventional Commit + # {% endif %} + # summary: | + # {% if not check_succeed %} + # Your pull request title must follow [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/). + # {% endif %} diff --git a/models/.github/workflows/codeql-analysis.yml b/models/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..8af4d0d6b --- /dev/null +++ b/models/.github/workflows/codeql-analysis.yml @@ -0,0 +1,78 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + # schedule: + # - cron: '0 23 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['go'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can check out the head. + fetch-depth: 2 + + # GH Actions runner uses go1.20 by default, so we need to install our own version. + # https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087 + - name: Install Go from go.mod + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + # - run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/models/.github/workflows/release.yml b/models/.github/workflows/release.yml new file mode 100644 index 000000000..82d54bbd0 --- /dev/null +++ b/models/.github/workflows/release.yml @@ -0,0 +1,37 @@ +# From: https://goreleaser.com/ci/actions/#usage +name: release + +env: + GO111MODULE: on + +on: + push: + tags: + - '*' + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5.0.0 + with: + distribution: goreleaser + version: latest + args: release --rm-dist --debug + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + - name: Syndicate to GoDocs + run: make godocs diff --git a/models/.github/workflows/run-tests.yml b/models/.github/workflows/run-tests.yml new file mode 100644 index 000000000..bd6440eeb --- /dev/null +++ b/models/.github/workflows/run-tests.yml @@ -0,0 +1,67 @@ +# See more at: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions +name: run-go-tests + +env: + GO111MODULE: on + +on: + pull_request: + branches: + - "*" + push: + branches: + - "*" + +jobs: + yamllint: + name: Run yaml linter + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Run yaml linter + uses: ibiqlik/action-yamllint@v3.1 + asknancy: + name: Ask Nancy (check dependencies) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Write go list + run: go list -json -m all > go.list + - name: Ask Nancy + uses: sonatype-nexus-community/nancy-github-action@v1.0.3 + # continue-on-error: true + test: + needs: [yamllint, asknancy] + strategy: + matrix: + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Go from go.mod + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Cache code + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod # Module download cache + ~/.cache/go-build # Build cache (Linux) + ~/Library/Caches/go-build # Build cache (Mac) + '%LocalAppData%\go-build' # Build cache (Windows) + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Run linter and tests + run: make test-ci + - name: Update code coverage + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: unittests + fail_ci_if_error: true # optional (default = false) + verbose: true # optional (default = false) diff --git a/models/.github/workflows/stale.yml b/models/.github/workflows/stale.yml new file mode 100644 index 000000000..da755cf22 --- /dev/null +++ b/models/.github/workflows/stale.yml @@ -0,0 +1,27 @@ +# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/actions/stale +name: stale-check + +on: + schedule: + - cron: '32 8 * * *' + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue has become stale due to in-activity.' + stale-pr-message: 'This pull-request has become stale due to in-activity.' + stale-issue-label: 'stale' + stale-pr-label: 'stale' diff --git a/models/.github/workflows/sync-labels.yml b/models/.github/workflows/sync-labels.yml new file mode 100644 index 000000000..da0514af7 --- /dev/null +++ b/models/.github/workflows/sync-labels.yml @@ -0,0 +1,19 @@ +# Workflow: https://github.com/micnncim/action-label-syncer +# Export your labels: https://github.com/micnncim/label-exporter +name: sync-labels +on: + push: + branches: + - master + paths: + - .github/labels.yml +jobs: + sync-labels: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: micnncim/action-label-syncer@v1.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + manifest: .github/labels.yml diff --git a/models/.gitignore b/models/.gitignore new file mode 100644 index 000000000..4165044d3 --- /dev/null +++ b/models/.gitignore @@ -0,0 +1,34 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Go List +go.list + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# OS files +*.db +*.DS_Store + +# Jetbrains +.idea/ + +# Eclipse +.project + +# Notes +todo.md + +# Distribution +dist + +# Code coverage +coverage.txt diff --git a/models/.golangci.yml b/models/.golangci.yml new file mode 100644 index 000000000..877cec42a --- /dev/null +++ b/models/.golangci.yml @@ -0,0 +1,431 @@ +# This file contains all available configuration options +# with their default values. + +# options for analysis running +run: + # default concurrency is an available CPU number + concurrency: 4 + + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 5m + + # exit code when at least one issue was found, default is 1 + issues-exit-code: 1 + + # include test files or not, default is true + tests: true + + # list of build tags, all linters use it. Default is empty list. + build-tags: + - mytag + + # which dirs to skip: issues from them won't be reported; + # can use regexp here: generated.*, regexp is applied on full path; + # default value is empty list, but default dirs are skipped independently + # of this option's value (see skip-dirs-use-default). + # "/" will be replaced by current OS file path separator to properly work + # on Windows. + skip-dirs: + - .github + - .make + - dist + + # default is true. Enables skipping of directories: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + skip-dirs-use-default: true + + # which files to skip: they will be analyzed, but issues from them + # won't be reported. Default value is empty list, but there is + # no need to include all autogenerated files, we confidently recognize + # autogenerated files. If it's not please let us know. + # "/" will be replaced by current OS file path separator to properly work + # on Windows. + skip-files: + - ".*\\.my\\.go$" + - lib/bad.go + + # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the implicit + # automatic updating of go.mod described above. Instead, it fails when any changes + # to go.mod are needed. This setting is most useful to check that go.mod does + # not need updates, such as in a continuous integration and testing system. + # If invoked with -mod=vendor, the go command assumes that the vendor + # directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. + #modules-download-mode: readonly|release|vendor + + # Allow multiple parallel golangci-lint instances running. + # If false (default) - golangci-lint acquires file lock on start. + allow-parallel-runners: false + + +# output configuration options +output: + # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" + format: colored-line-number + + # print lines of code with issue, default is true + print-issued-lines: true + + # print linter name in the end of issue text, default is true + print-linter-name: true + + # make issues output unique by line, default is true + uniq-by-line: true + + # add a prefix to the output file references; default is no prefix + path-prefix: "" + + +# all available settings of specific linters +linters-settings: + dogsled: + # checks assignments with too many blank identifiers; default is 2 + max-blank-identifiers: 2 + dupl: + # tokens count to trigger issue, 150 by default + threshold: 100 + errcheck: + # report about not checking of errors in type assertions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: false + + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; + # default is false: such cases aren't reported by default. + check-blank: false + + # [deprecated] comma-separated list of pairs of the form pkg:regex + # the regex is used to ignore names within pkg. (default "fmt:.*"). + # see https://github.com/kisielk/errcheck#the-deprecated-method for details + ignore: fmt:.*,io/ioutil:^Read.* + + # path to a file containing a list of functions to exclude from checking + # see https://github.com/kisielk/errcheck#excluding-functions for details + #exclude: /path/to/file.txt + exhaustive: + # indicates that switch statements are to be considered exhaustive if a + # 'default' case is present, even if all enum members aren't listed in the + # switch + default-signifies-exhaustive: false + funlen: + lines: 60 + statements: 40 + gci: + # put imports beginning with prefix after 3rd-party packages; + # only support one prefix + # if not set, use goimports.local-prefixes + local-prefixes: github.com/org/project + gocognit: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 10 + nestif: + # minimal complexity of if statements to report, 5 by default + min-complexity: 4 + goconst: + # minimal length of string constant, 3 by default + min-len: 3 + # minimal occurrences count to trigger, 3 by default + min-occurrences: 3 + gocritic: + # Which checks should be enabled; can't be combined with 'disabled-checks'; + # See https://go-critic.github.io/overview#checks-overview + # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` + # By default list of stable checks is used. + #enabled-checks: + # - rangeValCopy + + # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty + disabled-checks: + - regexpMust + + # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. + # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". + enabled-tags: + - performance + disabled-tags: + - experimental + + settings: # settings passed to gocritic + captLocal: # must be valid enabled check name + paramsOnly: true + rangeValCopy: + sizeThreshold: 32 + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 10 + godot: + # check all top-level comments, not only declarations + check-all: false + godox: + # report any comments starting with keywords, this is useful for TODO or FIXME comments that + # might be left in the code accidentally and should be resolved before merging + keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting + - NOTE + - OPTIMIZE # marks code that should be optimized before merging + - HACK # marks hack-arounds that should be removed before merging + gofmt: + # simplify code: gofmt with `-s` option, true by default + simplify: true + goimports: + # put imports beginning with prefix after 3rd-party packages; + # it's a comma-separated list of prefixes + local-prefixes: github.com/org/project + gomnd: + settings: + mnd: + # the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description. + checks: + - argument + - case + - condition + - operation + - return + - assign + govet: + # report about shadowed variables + check-shadowing: true + + # settings per analyzer + settings: + printf: # analyzer name, run `go tool vet help` to see all analyzers + funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + + # enable or disable analyzers by name + enable: + - atomicalign + enable-all: false + disable-all: false + depguard: + list-type: blacklist + include-go-root: false + packages: + - github.com/sirupsen/logrus + packages-with-error-message: + # specify an error message to output when a blacklisted package is used + - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" + lll: + # max line length, lines longer will be reported. Default is 120. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option + line-length: 120 + # tab width in spaces. Default to 1. + tab-width: 1 + maligned: + # print struct with more effective memory layout or not, false by default + suggest-new: true + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + locale: US + ignore-words: + - bsv + - bitcoin + nakedret: + # make an issue if func has more lines of code than this setting, and it has naked returns; default is 30 + max-func-lines: 30 + prealloc: + # XXX: we don't recommend using this linter before doing performance profiling. + # For most programs usage of prealloc will be a premature optimization. + + # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. + # True by default. + simple: true + range-loops: true # Report preallocation suggestions on range loops, true by default + for-loops: false # Report preallocation suggestions on for loops, false by default + nolintlint: + # Enable to ensure that nolint directives are all used. Default is true. + allow-unused: false + # Disable to ensure that nolint directives don't have a leading space. Default is true. + allow-leading-space: true + # Exclude following linters from requiring an explanation. Default is []. + allow-no-explanation: [] + # Enable to require an explanation of nonzero length after each nolint directive. Default is false. + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. + require-specific: true + rowserrcheck: + packages: + - github.com/jmoiron/sqlx + testpackage: + # regexp pattern to skip files + skip-regexp: (export|internal)_test\.go + unparam: + # Inspect exported functions, default is false. Set to true if no external program/library imports your code. + # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: + # if it's called for sub-dir of a project it can't find external interfaces. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + unused: + # treat code as a program (not a library) and report unused exported identifiers; default is false. + # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: + # if it's called for sub-dir of a project it can't find function usages. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + whitespace: + multi-if: false # Enforces newlines (or comments) after every multi-line if statement + multi-func: false # Enforces newlines (or comments) after every multi-line function signature + wsl: + # If true append is only allowed to be cuddled if appending value is + # matching variables, fields or types online above. Default is true. + strict-append: true + # Allow calls and assignments to be cuddled as long as the lines have any + # matching variables, fields or types. Default is true. + allow-assign-and-call: true + # Allow multiline assignments to be cuddled. Default is true. + allow-multiline-assign: true + # Allow declarations (var) to be cuddled. + allow-cuddle-declarations: true + # Allow trailing comments in ending of blocks + allow-trailing-comment: false + # Force newlines in end of case at this limit (0 = never). + force-case-trailing-whitespace: 0 + # Force cuddling of err checks with err var assignment + force-err-cuddling: false + # Allow leading comments to be separated with empty liens + allow-separated-leading-comment: false + gofumpt: + # Choose whether to use the extra rules that are disabled + # by default + extra-rules: false + + # The custom section can be used to define linter plugins to be loaded at runtime. See README doc + # for more info. + custom: + # Each custom linter should have a unique name. + #example: + # The path to the plugin *.so. Can be absolute or local. Required for each custom linter + #path: /path/to/example.so + # The description of the linter. Optional, just for documentation purposes. + #description: This is an example usage of a plugin linter. + # Intended to point to the repo location of the linter. Optional, just for documentation purposes. + #original-url: github.com/golangci/example-linter + +linters: + enable: + - megacheck + - govet + - gosec + - bodyclose + - revive + - unconvert + - dupl + - misspell + - ineffassign + - dogsled + - prealloc + - exportloopref + - exhaustive + - sqlclosecheck + - nolintlint + - gci + - goconst + disable: + - gocritic # use this for very opinionated linting + - gochecknoglobals + - whitespace + - wsl + - goerr113 + - godot + - testpackage + - nestif + - nlreturn + disable-all: false + presets: + - bugs + - unused + fast: false + + +issues: + # List of regexps of issue texts to exclude, empty list by default. + # But independently of this option we use default exclude patterns, + # it can be disabled by `exclude-use-default: false`. To list all + # excluded by default patterns execute `golangci-lint run --help` + exclude: + - Using the variable on range scope .* in function literal + + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec + + # Exclude known linters from partially "hard-vendored" code, + # which is impossible to exclude via "nolint" comments. + - path: internal/hmac/ + text: "weak cryptographic primitive" + linters: + - gosec + + # Exclude some "staticcheck" messages + - linters: + - staticcheck + text: "SA1019:" + + # Exclude lll issues for long lines with go:generate + - linters: + - lll + source: "^//go:generate " + + # Independently of option `exclude` we use default exclude patterns, + # it can be disabled by this option. To list all + # excluded by default patterns execute `golangci-lint run --help`. + # Default value for this option is true. + exclude-use-default: false + + # The default value is false. If set to true exclude and exclude-rules + # regular expressions become case-sensitive. + exclude-case-sensitive: false + + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-issues-per-linter: 0 + + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-same-issues: 0 + + # Show only new issues: if there are "un-staged" changes or untracked files, + # only those changes are analyzed, else only changes in HEAD~ are analyzed. + # It's a super-useful option for integration of golangci-lint into existing + # large codebase. It's not practical to fix all existing issues at the moment + # of integration: much better don't allow issues in new code. + # Default is false. + new: false + + # Show only new issues created after git revision `REV` + new-from-rev: "" + + # Show only new issues created in git patch with set file path. + #new-from-patch: path/to/patch/file + +severity: + # Default value is empty string. + # Set the default severity for issues. If severity rules are defined and the issues + # do not match or no severity is provided to the rule this will be the default + # severity applied. Severities should match the supported severity names of the + # selected out format. + # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity + # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity + # - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message + default-severity: error + + # The default value is false. + # If set to true severity-rules regular expressions become case-sensitive. + case-sensitive: false + + # Default value is empty list. + # When a list of severity rules are provided, severity information will be added to lint + # issues. Severity rules have the same filtering capability as exclude rules except you + # are allowed to specify one matcher per severity rule. + # Only affects out formats that support setting severity information. + rules: + - linters: + - dupl + severity: info diff --git a/models/.goreleaser.yml b/models/.goreleaser.yml new file mode 100644 index 000000000..35057655a --- /dev/null +++ b/models/.goreleaser.yml @@ -0,0 +1,71 @@ +# Make sure to check the documentation at http://goreleaser.com +# --------------------------- +# General +# --------------------------- +before: + hooks: + - make all +snapshot: + name_template: "{{ .Tag }}" +changelog: + sort: asc + filters: + exclude: + - '^.github:' + - '^.vscode:' + - '^test:' + +# --------------------------- +# Builder +# --------------------------- +build: + skip: true + +# --------------------------- +# GitHub Release +# --------------------------- +release: + prerelease: true + name_template: "Release v{{.Version}}" + +# --------------------------- +# Announce +# --------------------------- +announce: + + # See more at: https://goreleaser.com/customization/announce/#slack + slack: + enabled: false + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Changelog: https://github.com/bitcoin-sv/{{ .ProjectName }}/releases/tag/{{ .Tag }}' + channel: '#test_slack' + # username: '' + # icon_emoji: '' + # icon_url: '' + + # See more at: https://goreleaser.com/customization/announce/#twitter + twitter: + enabled: false + message_template: '{{ .ProjectName }} {{ .Tag }} is out!' + + # See more at: https://goreleaser.com/customization/announce/#discord + discord: + enabled: false + message_template: '{{ .ProjectName }} {{ .Tag }} is out!' + # Defaults to `GoReleaser` + author: '' + # Defaults to `3888754` - the grey-ish from goreleaser + color: '' + # Defaults to `https://goreleaser.com/static/avatar.png` + icon_url: '' + + # See more at: https://goreleaser.com/customization/announce/#reddit + reddit: + enabled: false + # Application ID for Reddit Application + application_id: "" + # Username for your Reddit account + username: "" + # Defaults to `{{ .GitURL }}/releases/tag/{{ .Tag }}` + # url_template: 'https://github.com/bitcoin-sv/{{ .ProjectName }}/releases/tag/{{ .Tag }}' + # Defaults to `{{ .ProjectName }} {{ .Tag }} is out!` + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' diff --git a/models/.make/common.mk b/models/.make/common.mk new file mode 100644 index 000000000..d0d1fdf75 --- /dev/null +++ b/models/.make/common.mk @@ -0,0 +1,99 @@ +## Default repository domain name +ifndef GIT_DOMAIN + override GIT_DOMAIN=github.com +endif + +## Set if defined (alias variable for ease of use) +ifdef branch + override REPO_BRANCH=$(branch) + export REPO_BRANCH +endif + +## Do we have git available? +HAS_GIT := $(shell command -v git 2> /dev/null) + +ifdef HAS_GIT + ## Do we have a repo? + HAS_REPO := $(shell git rev-parse --is-inside-work-tree 2> /dev/null) + ifdef HAS_REPO + ## Automatically detect the repo owner and repo name (for local use with Git) + REPO_NAME=$(shell basename "$(shell git rev-parse --show-toplevel 2> /dev/null)") + OWNER=$(shell git config --get remote.origin.url | sed 's/git@$(GIT_DOMAIN)://g' | sed 's/\/$(REPO_NAME).git//g') + REPO_OWNER=$(shell echo $(OWNER) | tr A-Z a-z) + VERSION_SHORT=$(shell git describe --tags --always --abbrev=0) + export REPO_NAME, REPO_OWNER, VERSION_SHORT + endif +endif + +## Set the distribution folder +ifndef DISTRIBUTIONS_DIR + override DISTRIBUTIONS_DIR=./dist +endif +export DISTRIBUTIONS_DIR + +.PHONY: diff +diff: ## Show the git diff + $(call print-target) + git diff --exit-code + RES=$$(git status --porcelain) ; if [ -n "$$RES" ]; then echo $$RES && exit 1 ; fi + +.PHONY: help +help: ## Show this help message + @egrep -h '^(.+)\:\ ##\ (.+)' ${MAKEFILE_LIST} | column -t -c 2 -s ':#' + +.PHONY: install-releaser +install-releaser: ## Install the GoReleaser application + @echo "installing GoReleaser..." + @curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh + +.PHONY: release +release:: ## Full production release (creates release in GitHub) + @echo "releasing..." + @test $(github_token) + @export GITHUB_TOKEN=$(github_token) && goreleaser --rm-dist + +.PHONY: release-test +release-test: ## Full production test release (everything except deploy) + @echo "creating a release test..." + @goreleaser --skip-publish --rm-dist + +.PHONY: release-snap +release-snap: ## Test the full release (build binaries) + @echo "creating a release snapshot..." + @goreleaser --snapshot --skip-publish --rm-dist + +.PHONY: replace-version +replace-version: ## Replaces the version in HTML/JS (pre-deploy) + @echo "replacing version..." + @test $(version) + @test "$(path)" + @find $(path) -name "*.html" -type f -exec sed -i '' -e "s/{{version}}/$(version)/g" {} \; + @find $(path) -name "*.js" -type f -exec sed -i '' -e "s/{{version}}/$(version)/g" {} \; + +.PHONY: tag +tag: ## Generate a new tag and push (tag version=0.0.0) + @echo "creating new tag..." + @test $(version) + @git tag -a v$(version) -m "Pending full release..." + @git push origin v$(version) + @git fetch --tags -f + +.PHONY: tag-remove +tag-remove: ## Remove a tag if found (tag-remove version=0.0.0) + @echo "removing tag..." + @test $(version) + @git tag -d v$(version) + @git push --delete origin v$(version) + @git fetch --tags + +.PHONY: tag-update +tag-update: ## Update an existing tag to current commit (tag-update version=0.0.0) + @echo "updating tag to new commit..." + @test $(version) + @git push --force origin HEAD:refs/tags/v$(version) + @git fetch --tags -f + +.PHONY: update-releaser +update-releaser: ## Update the goreleaser application + @echo "updating GoReleaser application..." + @$(MAKE) install-releaser diff --git a/models/.make/go.mk b/models/.make/go.mk new file mode 100644 index 000000000..34848111f --- /dev/null +++ b/models/.make/go.mk @@ -0,0 +1,146 @@ +## Default to the repo name if empty +ifndef BINARY_NAME + override BINARY_NAME=app +endif + +## Define the binary name +ifdef CUSTOM_BINARY_NAME + override BINARY_NAME=$(CUSTOM_BINARY_NAME) +endif + +## Set the binary release names +DARWIN=$(BINARY_NAME)-darwin +LINUX=$(BINARY_NAME)-linux +WINDOWS=$(BINARY_NAME)-windows.exe + +## Define the binary name +TAGS= +ifdef GO_BUILD_TAGS + override TAGS=-tags $(GO_BUILD_TAGS) +endif + +.PHONY: bench +bench: ## Run all benchmarks in the Go application + @echo "running benchmarks..." + @go test -bench=. -benchmem $(TAGS) + +.PHONY: build-go +build-go: ## Build the Go application (locally) + @echo "building go app..." + @go build -o bin/$(BINARY_NAME) $(TAGS) + +.PHONY: clean-mods +clean-mods: ## Remove all the Go mod cache + @echo "cleaning mods..." + @go clean -modcache + +.PHONY: coverage +coverage: ## Shows the test coverage + @echo "creating coverage report..." + @go test -coverprofile=coverage.out ./... $(TAGS) && go tool cover -func=coverage.out $(TAGS) + +.PHONY: generate +generate: ## Runs the go generate command in the base of the repo + @echo "generating files..." + @go generate -v $(TAGS) + +.PHONY: godocs +godocs: ## Sync the latest tag with GoDocs + @echo "syndicating to GoDocs..." + @test $(GIT_DOMAIN) + @test $(REPO_OWNER) + @test $(REPO_NAME) + @test $(VERSION_SHORT) + @curl https://proxy.golang.org/$(GIT_DOMAIN)/$(REPO_OWNER)/$(REPO_NAME)/@v/$(VERSION_SHORT).info + +.PHONY: install +install: ## Install the application + @echo "installing binary..." + @go build -o $$GOPATH/bin/$(BINARY_NAME) $(TAGS) + +.PHONY: install-go +install-go: ## Install the application (Using Native Go) + @echo "installing package..." + @go install $(GIT_DOMAIN)/$(REPO_OWNER)/$(REPO_NAME) $(TAGS) + +.PHONY: lint +lint: ## Run the golangci-lint application (install if not found) + @echo "installing golangci-lint..." + @#Travis (has sudo) + @if [ "$(shell command -v golangci-lint)" = "" ] && [ $(TRAVIS) ]; then curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.54.2 && sudo cp ./bin/golangci-lint $(go env GOPATH)/bin/; fi; + @#AWS CodePipeline + @if [ "$(shell command -v golangci-lint)" = "" ] && [ "$(CODEBUILD_BUILD_ID)" != "" ]; then curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2; fi; + @#GitHub Actions + @if [ "$(shell command -v golangci-lint)" = "" ] && [ "$(GITHUB_WORKFLOW)" != "" ]; then curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b $(go env GOPATH)/bin v1.54.2; fi; + @#Brew - MacOS + @if [ "$(shell command -v golangci-lint)" = "" ] && [ "$(shell command -v brew)" != "" ]; then brew install golangci-lint; fi; + @#MacOS Vanilla + @if [ "$(shell command -v golangci-lint)" = "" ] && [ "$(shell command -v brew)" != "" ]; then curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- v1.54.2; fi; + @echo "running golangci-lint..." + @golangci-lint run --verbose + +.PHONY: test +test: ## Runs lint and ALL tests + @$(MAKE) lint + @echo "running tests..." + @go test ./... -v $(TAGS) + +.PHONY: test-unit +test-unit: ## Runs tests and outputs coverage + @echo "running unit tests..." + @go test ./... -race -coverprofile=coverage.txt -covermode=atomic $(TAGS) + +.PHONY: test-short +test-short: ## Runs vet, lint and tests (excludes integration tests) + @$(MAKE) lint + @echo "running tests (short)..." + @go test ./... -v -test.short $(TAGS) + +.PHONY: test-ci +test-ci: ## Runs all tests via CI (exports coverage) + @$(MAKE) lint + @echo "running tests (CI)..." + @go test ./... -race -coverprofile=coverage.txt -covermode=atomic $(TAGS) + +.PHONY: test-ci-no-race +test-ci-no-race: ## Runs all tests via CI (no race) (exports coverage) + @$(MAKE) lint + @echo "running tests (CI - no race)..." + @go test ./... -coverprofile=coverage.txt -covermode=atomic $(TAGS) + +.PHONY: test-ci-short +test-ci-short: ## Runs unit tests via CI (exports coverage) + @$(MAKE) lint + @echo "running tests (CI - unit tests only)..." + @go test ./... -test.short -race -coverprofile=coverage.txt -covermode=atomic $(TAGS) + +.PHONY: test-no-lint +test-no-lint: ## Runs just tests + @echo "running tests..." + @go test ./... -v $(TAGS) + +.PHONY: uninstall +uninstall: ## Uninstall the application (and remove files) + @echo "uninstalling go application..." + @test $(BINARY_NAME) + @test $(GIT_DOMAIN) + @test $(REPO_OWNER) + @test $(REPO_NAME) + @go clean -i $(GIT_DOMAIN)/$(REPO_OWNER)/$(REPO_NAME) + @rm -rf $$GOPATH/src/$(GIT_DOMAIN)/$(REPO_OWNER)/$(REPO_NAME) + @rm -rf $$GOPATH/bin/$(BINARY_NAME) + +.PHONY: update +update: ## Update all project dependencies + @echo "updating dependencies..." + @go get -u ./... && go mod tidy + +.PHONY: update-linter +update-linter: ## Update the golangci-lint package (macOS only) + @echo "upgrading golangci-lint..." + @brew upgrade golangci-lint + +.PHONY: vet +vet: ## Run the Go vet application + @echo "running go vet..." + @go vet -v ./... $(TAGS) diff --git a/models/.yamllint.yml b/models/.yamllint.yml new file mode 100644 index 000000000..c310c9629 --- /dev/null +++ b/models/.yamllint.yml @@ -0,0 +1,36 @@ +--- + +yaml-files: + - '*.yaml' + - '*.yml' + - '.yamllint' + +ignore: | + dist/ + +rules: + braces: enable + brackets: enable + colons: enable + commas: enable + comments: + level: warning + comments-indentation: + level: warning + document-end: disable + document-start: + level: warning + empty-lines: enable + empty-values: disable + hyphens: enable + indentation: enable + key-duplicates: enable + key-ordering: disable + line-length: disable + new-line-at-end-of-file: disable + new-lines: enable + octal-values: disable + quoted-strings: disable + trailing-spaces: enable + truthy: + level: warning diff --git a/models/LICENSE b/models/LICENSE new file mode 100644 index 000000000..6994ce061 --- /dev/null +++ b/models/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 BSV Blockchain Association ("BA") +Copyright (c) 2023 @BugOrg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/models/Makefile b/models/Makefile new file mode 100644 index 000000000..6e1c4eaf7 --- /dev/null +++ b/models/Makefile @@ -0,0 +1,41 @@ +# Common makefile commands & variables between projects +include .make/common.mk + +# Common Golang makefile commands & variables between projects +include .make/go.mk + +## Not defined? Use default repo name which is the application +ifeq ($(REPO_NAME),) + REPO_NAME="spv-wallet-models" +endif + +## Not defined? Use default repo owner +ifeq ($(REPO_OWNER),) + REPO_OWNER="bitcoin-sv" +endif + +.PHONY: all +all: ## Runs multiple commands + @$(MAKE) test + +.PHONY: clean +clean: ## Remove previous builds and any cached data + @echo "cleaning local cache..." + @go clean -cache -testcache -i -r + @$(MAKE) clean-mods + @test $(DISTRIBUTIONS_DIR) + @if [ -d $(DISTRIBUTIONS_DIR) ]; then rm -r $(DISTRIBUTIONS_DIR); fi + +.PHONY: install-all-contributors +install-all-contributors: ## Installs all contributors locally + @echo "installing all-contributors cli tool..." + @yarn global add all-contributors-cli + +.PHONY: release +release:: ## Runs common.release then runs godocs + @$(MAKE) godocs + +.PHONY: update-contributors +update-contributors: ## Regenerates the contributors html/list + @echo "generating contributor html..." + @all-contributors generate diff --git a/models/README.md b/models/README.md new file mode 100644 index 000000000..691aee373 --- /dev/null +++ b/models/README.md @@ -0,0 +1,214 @@ +
+ +# SPV Wallet: Models + +[![Release](https://img.shields.io/github/release-pre/bitcoin-sv/spv-wallet-models.svg?logo=github&style=flat&v=1)](https://github.com/bitcoin-sv/spv-wallet/models/releases) +[![Build Status](https://img.shields.io/github/actions/workflow/status/bitcoin-sv/spv-wallet/models/run-tests.yml?branch=master&logo=github&v=1)](https://github.com/bitcoin-sv/spv-wallet/models/actions) +[![Report](https://goreportcard.com/badge/github.com/bitcoin-sv/spv-wallet-models?style=flat&v=1)](https://goreportcard.com/report/github.com/bitcoin-sv/spv-wallet-models) +[![Mergify Status](https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/bitcoin-sv/spv-wallet-models&style=flat&v=1)](https://mergify.io) +
+ +[![Go](https://img.shields.io/github/go-mod/go-version/bitcoin-sv/spv-wallet-models?v=1)](https://golang.org/) +[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat&v=1)](https://github.com/RichardLitt/standard-readme) +[![Makefile Included](https://img.shields.io/badge/Makefile-Supported%20-brightgreen?=flat&logo=probot&v=1)](Makefile) + +
+
+ +## Table of Contents +- [About](#about) +- [Installation](#installation) +- [Documentation](#documentation) +- [Usage](#usage) + - [Examples & Tests](#examples--tests) + - [Benchmarks](#benchmarks) +- [Code Standards](#code-standards) +- [Contributing](#contributing) +- [License](#license) + +
+ +## About +SPV Wallet internal models & structs + +
+ +## Installation + +**go-template** requires a [supported release of Go](https://golang.org/doc/devel/release.html#policy). +```shell script +go get -u github.com/bitcoin-sv/spv-wallet-models +``` + +
+ +## Documentation +View the generated [documentation](https://pkg.go.dev/github.com/bitcoin-sv/spv-wallet-models) + +[![GoDoc](https://godoc.org/github.com/bitcoin-sv/spv-wallet-models?status.svg&style=flat&v=1)](https://pkg.go.dev/github.com/bitcoin-sv/spv-wallet-models) + +
+ +
+Repository Features +
+ +This repository was created using [MrZ's `go-template`](https://github.com/mrz1836/go-template#about) + +### Built-in Features +- Continuous integration via [GitHub Actions](https://github.com/features/actions) +- Build automation via [Make](https://www.gnu.org/software/make) +- Dependency management using [Go Modules](https://github.com/golang/go/wiki/Modules) +- Code formatting using [gofumpt](https://github.com/mvdan/gofumpt) and linting with [golangci-lint](https://github.com/golangci/golangci-lint) and [yamllint](https://yamllint.readthedocs.io/en/stable/index.html) +- Unit testing with [testify](https://github.com/stretchr/testify), [race detector](https://blog.golang.org/race-detector), code coverage [HTML report](https://blog.golang.org/cover) and [Codecov report](https://codecov.io/) +- Releasing using [GoReleaser](https://github.com/goreleaser/goreleaser) on [new Tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) +- Dependency scanning and updating thanks to [Dependabot](https://dependabot.com) and [Nancy](https://github.com/sonatype-nexus-community/nancy) +- Security code analysis using [CodeQL Action](https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/about-code-scanning) +- Automatic syndication to [pkg.go.dev](https://pkg.go.dev/) on every release +- Generic templates for [Issues and Pull Requests](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository) in GitHub +- All standard GitHub files such as `LICENSE`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, and `SECURITY.md` +- Code [ownership configuration](.github/CODEOWNERS) for GitHub +- All your ignore files for [vs-code](.editorconfig), [docker](.dockerignore) and [git](.gitignore) +- Automatic sync for [labels](.github/labels.yml) into GitHub using a predefined [configuration](.github/labels.yml) +- Built-in powerful merging rules using [Mergify](https://mergify.io/) +- Welcome [new contributors](.github/mergify.yml) on their first Pull-Request +- Follows the [standard-readme](https://github.com/RichardLitt/standard-readme/blob/master/spec.md) specification +- [Visual Studio Code](https://code.visualstudio.com) configuration with [Go](https://code.visualstudio.com/docs/languages/go) +- (Optional) [Slack](https://slack.com), [Discord](https://discord.com) or [Twitter](https://twitter.com) announcements on new GitHub Releases +- (Optional) Easily add [contributors](https://allcontributors.org/docs/en/bot/installation) in any Issue or Pull-Request + +
+ +
+Package Dependencies +
+ +- [stretchr/testify](https://github.com/stretchr/testify) +
+ +
+Library Deployment +
+ +Releases are automatically created when you create a new [git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging)! + +If you want to manually make releases, please install GoReleaser: + +[goreleaser](https://github.com/goreleaser/goreleaser) for easy binary or library deployment to GitHub and can be installed: +- **using make:** `make install-releaser` +- **using brew:** `brew install goreleaser` + +The [.goreleaser.yml](.goreleaser.yml) file is used to configure [goreleaser](https://github.com/goreleaser/goreleaser). + +
+ +### Automatic Releases on Tag Creation (recommended) +Automatic releases via [GitHub Actions](.github/workflows/release.yml) from creating a new tag: +```shell +make tag version=1.2.3 +``` + +
+ +### Manual Releases (optional) +Use `make release-snap` to create a snapshot version of the release, and finally `make release` to ship to production (manually). + +
+ +
+ +
+Makefile Commands +
+ +View all `makefile` commands +```shell script +make help +``` + +List of all current commands: +```text +all Runs multiple commands +clean Remove previous builds and any cached data +clean-mods Remove all the Go mod cache +coverage Shows the test coverage +diff Show the git diff +generate Runs the go generate command in the base of the repo +godocs Sync the latest tag with GoDocs +help Show this help message +install Install the application +install-all-contributors Installs all contributors locally +install-go Install the application (Using Native Go) +install-releaser Install the GoReleaser application +lint Run the golangci-lint application (install if not found) +release Full production release (creates release in GitHub) +release Runs common.release then runs godocs +release-snap Test the full release (build binaries) +release-test Full production test release (everything except deploy) +replace-version Replaces the version in HTML/JS (pre-deploy) +tag Generate a new tag and push (tag version=0.0.0) +tag-remove Remove a tag if found (tag-remove version=0.0.0) +tag-update Update an existing tag to current commit (tag-update version=0.0.0) +test Runs lint and ALL tests +test-ci Runs all tests via CI (exports coverage) +test-ci-no-race Runs all tests via CI (no race) (exports coverage) +test-ci-short Runs unit tests via CI (exports coverage) +test-no-lint Runs just tests +test-short Runs vet, lint and tests (excludes integration tests) +test-unit Runs tests and outputs coverage +uninstall Uninstall the application (and remove files) +update-contributors Regenerates the contributors html/list +update-linter Update the golangci-lint package (macOS only) +vet Run the Go vet application +``` +
+ +
+ +## Usage + +### Examples & Tests +All unit tests run via [GitHub Actions](https://github.com/bitcoin-sv/spv-wallet/models/actions) and +uses [Go version 1.18.x](https://golang.org/doc/go1.18). View the [configuration file](.github/workflows/run-tests.yml). + +
+ +Run all tests (including integration tests) +```shell script +make test +``` + +
+ +Run tests (excluding integration tests) +```shell script +make test-short +``` + +
+ +### Benchmarks +Run the Go benchmarks: +```shell script +make bench +``` + +
+ +## Code Standards +Read more about this Go project's [code standards](.github/CODE_STANDARDS.md). + +
+ +## Contributing +All kinds of contributions are welcome! +
+To get started, take a look at [code standards](.github/CODE_STANDARDS.md). +
+View the [contributing guidelines](.github/CODE_STANDARDS.md#3-contributing) and follow the [code of conduct](.github/CODE_OF_CONDUCT.md). + +
+ +## License + +[![License](https://img.shields.io/github/license/bitcoin-sv/spv-wallet-models.svg?style=flat&v=1)](LICENSE) diff --git a/models/access_key.go b/models/access_key.go new file mode 100644 index 000000000..11bbc8524 --- /dev/null +++ b/models/access_key.go @@ -0,0 +1,23 @@ +// Package models contains all models (contracts) between spv-wallet api and other spv-wallet solutions +package models + +import ( + "time" + + "github.com/bitcoin-sv/spv-wallet/models/common" +) + +// AccessKey is a model that represents an access key. +type AccessKey struct { + // Model is a common model that contains common fields for all models. + common.Model + + // ID is an access key id. + ID string `json:"id"` + // XpubID is an access key's xpub related id. + XpubID string `json:"xpub_id"` + // RevokedAt is a time when access key was revoked. + RevokedAt *time.Time `json:"revoked_at,omitempty"` + // Key is a string representation of an access key. + Key string `json:"key,omitempty"` +} diff --git a/models/admin_stats.go b/models/admin_stats.go new file mode 100644 index 000000000..87b8332eb --- /dev/null +++ b/models/admin_stats.go @@ -0,0 +1,21 @@ +package models + +// AdminStats is a model that represents admin stats. +type AdminStats struct { + // Balance is a total balance of all xpubs. + Balance int64 `json:"balance"` + // Destinations is a total number of destinations. + Destinations int64 `json:"destinations"` + // PaymailAddresses is a total number of paymail addresses. + PaymailAddresses int64 `json:"paymail_addresses"` + // Transactions is a total number of committed transactions. + Transactions int64 `json:"transactions"` + // TransactionsPerDay is a total number of committed transactions per day. + TransactionsPerDay map[string]interface{} `json:"transactions_per_day"` + // Utxos is a total number of utxos. + Utxos int64 `json:"utxos"` + // UtxosPerType are utxos grouped by type. + UtxosPerType map[string]interface{} `json:"utxos_per_type"` + // Xpubs is a total number of xpubs. + XPubs int64 `json:"xpubs"` +} diff --git a/models/auth_payload.go b/models/auth_payload.go new file mode 100644 index 000000000..33ef17fcc --- /dev/null +++ b/models/auth_payload.go @@ -0,0 +1,19 @@ +package models + +// AuthPayload is the struct that is used to create the signature for the API call +type AuthPayload struct { + // AuthHash is the hash of the body contents + AuthHash string `json:"auth_hash"` + // AuthNonce is a random string + AuthNonce string `json:"auth_nonce"` + // AuthTime is the current time in milliseconds + AuthTime int64 `json:"auth_time"` + // BodyContents is the body of the request + BodyContents string `json:"body_contents"` + // Signature is the signature of the body contents + Signature string `json:"signature"` + // XPub is the xpub of the account + XPub string `json:"xpub"` + // AccessKey is the access key of the account + AccessKey string `json:"access_key"` +} diff --git a/models/authentication.go b/models/authentication.go new file mode 100644 index 000000000..7d9046025 --- /dev/null +++ b/models/authentication.go @@ -0,0 +1,26 @@ +package models + +import "time" + +const ( + // AuthHeader is the header to use for authentication (raw xPub) + AuthHeader = "spv-wallet-auth-xpub" + + // AuthAccessKey is the header to use for access key authentication (access public key) + AuthAccessKey = "spv-wallet-auth-key" + + // AuthSignature is the given signature (body + timestamp) + AuthSignature = "spv-wallet-auth-signature" + + // AuthHeaderHash hash of the body coming from the request + AuthHeaderHash = "spv-wallet-auth-hash" + + // AuthHeaderNonce random nonce for the request + AuthHeaderNonce = "spv-wallet-auth-nonce" + + // AuthHeaderTime the time of the request, only valid for 30 seconds + AuthHeaderTime = "spv-wallet-auth-time" + + // AuthSignatureTTL is the max TTL for a signature to be valid + AuthSignatureTTL = 20 * time.Second +) diff --git a/models/block_header.go b/models/block_header.go new file mode 100644 index 000000000..f2fb29993 --- /dev/null +++ b/models/block_header.go @@ -0,0 +1,32 @@ +package models + +import ( + "time" + + "github.com/bitcoin-sv/spv-wallet/models/common" +) + +// BlockHeader is a model that represents a BSV block header. +type BlockHeader struct { + // Model is a common model that contains common fields for all models. + common.Model + + // ID is a block header id (hash). + ID string `json:"id"` + // Height is a block header height. + Height uint32 `json:"height"` + // Time is a block header time (timestamp). + Time uint32 `json:"time"` + // Nonce is a block header nonce. + Nonce uint32 `json:"nonce"` + // Version is a block header version. + Version uint32 `json:"version"` + // HashPreviousBlock is a block header hash of previous block. + HashPreviousBlock string `json:"hash_previous_block"` + // HashMerkleRoot is a block header hash merkle tree root. + HashMerkleRoot string `json:"hash_merkle_root"` + // Bits contains BSV block header bits no. + Bits string `json:"bits"` + // Synec is a time when block header was synced. + Synced time.Time `json:"synced"` +} diff --git a/models/codecov.yml b/models/codecov.yml new file mode 100644 index 000000000..21745834b --- /dev/null +++ b/models/codecov.yml @@ -0,0 +1,42 @@ +# Reference: https://docs.codecov.com/docs/codecovyml-reference +# ---------------------- +codecov: + require_ci_to_pass: true + +# Coverage configuration +# ---------------------- +coverage: + status: + patch: false + range: 70..90 # First number represents red, and second represents green + # (default is 70..100) + round: down # up, down, or nearest + precision: 2 # Number of decimal places, between 0 and 5 + +# Ignoring Paths +# -------------- +# which folders/files to ignore +ignore: + - "*/.make/.*" + - "*/.github/.*" + - "*/examples/.*" + - "*/tools/.*" + +# Parsers +# -------------- +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +# Pull request comments: +# ---------------------- +# Diff is the Coverage Diff of the pull request. +# Files are the files impacted by the pull request +comment: + layout: "reach,diff,flags,files,footer" + behavior: default + require_changes: false \ No newline at end of file diff --git a/models/common/model.go b/models/common/model.go new file mode 100644 index 000000000..7c989055b --- /dev/null +++ b/models/common/model.go @@ -0,0 +1,16 @@ +// Package common is a package that contains common models used by all other packages. +package common + +import "time" + +// Model is a common model that contains common fields for all models. +type Model struct { + // CreatedAt is a time when outer model was created. + CreatedAt time.Time `json:"created_at"` + // UpdatedAt is a time when outer model was updated. + UpdatedAt time.Time `json:"updated_at"` + // DeletedAt is a time when outer model was deleted. + DeletedAt time.Time `json:"deleted_at"` + // Metadata is a metadata map of outer model. + Metadata map[string]interface{} `json:"metadata"` +} diff --git a/models/destination.go b/models/destination.go new file mode 100644 index 000000000..8104df899 --- /dev/null +++ b/models/destination.go @@ -0,0 +1,32 @@ +package models + +import ( + "time" + + "github.com/bitcoin-sv/spv-wallet/models/common" +) + +// Destination is a model that represents a destination - registered in a spv-wallet with xpub. +type Destination struct { + // Model is a common model that contains common fields for all models. + common.Model + + // ID is a destination id. + ID string `json:"id"` + // XpubID is a destination's xpub related id used to register destination. + XpubID string `json:"xpub_id"` + // LockingScript is a destination's locking script. + LockingScript string `json:"locking_script"` + // Type is a destination's type. + Type string `json:"type"` + // Chain is a destination's chain representation. + Chain uint32 `json:"chain"` + // Num is a destination's num representation. + Num uint32 `json:"num"` + // Address is a destination's address. + Address string `json:"address"` + // DraftID is a destination's draft id. + DraftID string `json:"draft_id"` + // Monitor is a time when destination was monitored. + Monitor time.Time `json:"monitor"` +} diff --git a/models/draft_transaction.go b/models/draft_transaction.go new file mode 100644 index 000000000..4c293785f --- /dev/null +++ b/models/draft_transaction.go @@ -0,0 +1,42 @@ +package models + +import ( + "time" + + "github.com/bitcoin-sv/spv-wallet/models/common" +) + +const ( + // DraftStatusDraft is when the transaction is a draft + DraftStatusDraft string = "draft" + + // DraftStatusCanceled is when the draft is canceled + DraftStatusCanceled string = "canceled" + + // DraftStatusExpired is when the draft has expired + DraftStatusExpired string = "expired" + + // DraftStatusComplete is when the draft transaction is complete + DraftStatusComplete string = "complete" +) + +// DraftTransaction is a model that represents a draft transaction. +type DraftTransaction struct { + // Model is a common model that contains common fields for all models. + common.Model + + // ID is a draft transaction id. + ID string `json:"id"` + // Hex is a draft transaction hex. + Hex string `json:"hex"` + // XpubID is a draft transaction's xpub used to sign transaction. + XpubID string `json:"xpub_id"` + // ExpiresAt is a time when draft transaction expired. + ExpiresAt time.Time `json:"expires_at"` + // Configuration contains draft transaction configuration. + Configuration TransactionConfig `json:"configuration"` + // Status is a draft transaction lastly monitored status. + Status string `json:"status"` + // FinalTxID is a final transaction id. + FinalTxID string `json:"final_tx_id"` +} diff --git a/models/fee_unit.go b/models/fee_unit.go new file mode 100644 index 000000000..a4b2e8251 --- /dev/null +++ b/models/fee_unit.go @@ -0,0 +1,9 @@ +package models + +// FeeUnit is a model that represents a fee unit (simplified version of fee unit from go-bt). +type FeeUnit struct { + // Satoshis is a fee unit satoshis amount. + Satoshis int `json:"satoshis"` + // Bytes is a fee unit bytes representation. + Bytes int `json:"bytes"` +} diff --git a/models/go.mod b/models/go.mod new file mode 100644 index 000000000..c8268c4b3 --- /dev/null +++ b/models/go.mod @@ -0,0 +1,11 @@ +module github.com/bitcoin-sv/spv-wallet/models + +go 1.21 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/models/go.sum b/models/go.sum new file mode 100644 index 000000000..fa4b6e682 --- /dev/null +++ b/models/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/models/metadata.go b/models/metadata.go new file mode 100644 index 000000000..81c65db69 --- /dev/null +++ b/models/metadata.go @@ -0,0 +1,7 @@ +package models + +// Metadata is a SPV wallet ecosystem metadata model. +type Metadata map[string]interface{} + +// XpubMetadata is a SPV wallet ecosystem xpub metadata model. +type XpubMetadata map[string]Metadata diff --git a/models/models_test.go b/models/models_test.go new file mode 100644 index 000000000..4722a13e9 --- /dev/null +++ b/models/models_test.go @@ -0,0 +1,33 @@ +package models + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// TestAccessKey tests AccessKey model. +func TestAccessKey(t *testing.T) { + ac := new(AccessKey) + ac.Model.UpdatedAt = time.Now().UTC() + ac.Model.CreatedAt = time.Now().UTC() + ac.Model.DeletedAt = time.Now().UTC() + ac.XpubID = "123" + ac.ID = "123" + + require.Equal(t, "123", ac.ID) +} + +// ExampleAccessKey is an example for AccessKey model. +func ExampleAccessKey() { + ac := new(AccessKey) + ac.Model.UpdatedAt = time.Now().UTC() + ac.Model.CreatedAt = time.Now().UTC() + ac.Model.DeletedAt = time.Now().UTC() + ac.XpubID = "123" + ac.ID = "123" + fmt.Printf("%s", ac.ID) + // Output: 123 +} diff --git a/models/paymail_address.go b/models/paymail_address.go new file mode 100644 index 000000000..be0efd0d2 --- /dev/null +++ b/models/paymail_address.go @@ -0,0 +1,24 @@ +package models + +import "github.com/bitcoin-sv/spv-wallet/models/common" + +// PaymailAddress is a model that represents a paymail address. +type PaymailAddress struct { + // Model is a common model that contains common fields for all models. + common.Model + + // ID is a paymail address id. + ID string `json:"id"` + // XpubID is a paymail address's xpub related id used to register paymail address. + XpubID string `json:"xpub_id"` + // Alias is a paymail address's alias (first part of paymail). + Alias string `json:"alias"` + // Domain is a paymail address's domain (second part of paymail). + Domain string `json:"domain"` + // PublicName is a paymail address's public name. + PublicName string `json:"public_name"` + // Avatar is a paymail address's avatar. + Avatar string `json:"avatar"` + // ExternalXpubKey is a paymail address's external xpub key. + ExternalXpubKey string `json:"external_xpub_key"` +} diff --git a/models/spvwalleterrors/errors_def.go b/models/spvwalleterrors/errors_def.go new file mode 100644 index 000000000..b9d546738 --- /dev/null +++ b/models/spvwalleterrors/errors_def.go @@ -0,0 +1,13 @@ +// Package spvwalleterrors contains errors that can be returned by spv-wallet api +package spvwalleterrors + +import "errors" + +// ErrDraftNotFound is when the requested draft transaction was not found +var ErrDraftNotFound = errors.New("corresponding draft transaction not found") + +// ErrMissingXPriv is when the xPriv is missing +var ErrMissingXPriv = errors.New("missing xPriv key") + +// ErrMissingAccessKey is when the access key is missing +var ErrMissingAccessKey = errors.New("missing access key") diff --git a/models/sync_config.go b/models/sync_config.go new file mode 100644 index 000000000..45316a012 --- /dev/null +++ b/models/sync_config.go @@ -0,0 +1,13 @@ +package models + +// SyncConfig contains sync configuration flags. +type SyncConfig struct { + // Broadcast is a flag that indicates whether to broadcast transaction or not. + Broadcast bool `json:"broadcast"` + // BroadcastInstant is a flag that indicates whether to broadcast transaction instantly or not. + BroadcastInstant bool `json:"broadcast_instant"` + // PaymailP2P is a flag that indicates whether to use paymail p2p or not. + PaymailP2P bool `json:"paymail_p2p"` + // SyncOnChain is a flag that indicates whether to sync transaction on chain or not. + SyncOnChain bool `json:"sync_on_chain"` +} diff --git a/models/sync_result.go b/models/sync_result.go new file mode 100644 index 000000000..eb87cc183 --- /dev/null +++ b/models/sync_result.go @@ -0,0 +1,23 @@ +package models + +import "time" + +// SyncResults is a model that represents a sync results. +type SyncResults struct { + // LastMessage is a last message received during sync. + LastMessage string `json:"last_message"` + // Results is a slice of sync results. + Results []*SyncResult `json:"results"` +} + +// SyncResult is a model that represents a single sync result. +type SyncResult struct { + // Action type broadcast, sync etc + Action string `json:"action"` + // ExecutedAt contains time when action was executed. + ExecutedAt time.Time `json:"executed_at"` + // Provider field is used for attempts(s). + Provider string `json:"provider,omitempty"` + // StatusMessage contains success or failure messages. + StatusMessage string `json:"status_message"` +} diff --git a/models/sync_transaction.go b/models/sync_transaction.go new file mode 100644 index 000000000..97137c6af --- /dev/null +++ b/models/sync_transaction.go @@ -0,0 +1,28 @@ +package models + +import ( + "time" + + "github.com/bitcoin-sv/spv-wallet/models/common" +) + +// SyncTransaction is a model that represents a sync transaction specific fields. +type SyncTransaction struct { + // Model is a common model that contains common fields for all models. + common.Model + + // ID is a sync transaction id. + ID string `json:"id"` + // Configuration contains sync transaction configuration. + Configuration SyncConfig `json:"configuration"` + // LastAttempt contains last attempt time. + LastAttempt time.Time `json:"last_attempt"` + // Results contains sync transaction results. + Results SyncResults `json:"results"` + // BroadcastStatus contains broadcast status. + BroadcastStatus string `json:"broadcast_status"` + // P2PStatus contains p2p status. + P2PStatus string `json:"p2p_status"` + // SyncStatus contains sync status. + SyncStatus string `json:"sync_status"` +} diff --git a/models/transaction.go b/models/transaction.go new file mode 100644 index 000000000..a65751eab --- /dev/null +++ b/models/transaction.go @@ -0,0 +1,39 @@ +package models + +import "github.com/bitcoin-sv/spv-wallet/models/common" + +// Transaction is a model that represents a transaction. +type Transaction struct { + // Model is a common model that contains common fields for all models. + common.Model + // ID is a transaction id. + ID string `json:"id"` + // Hex is a transaction hex. + Hex string `json:"hex"` + // XpubInIDs is a slice of xpub input ids. + XpubInIDs []string `json:"xpub_in_ids"` + // XpubOutIDs is a slice of xpub output ids. + XpubOutIDs []string `json:"xpub_out_ids"` + // BlockHash is a block hash that transaction is in. + BlockHash string `json:"block_hash"` + // BlockHeight is a block height that transaction is in. + BlockHeight uint64 `json:"block_height"` + // Fee is a transaction fee. + Fee uint64 `json:"fee"` + // NumberOfInputs is a number of transaction inputs. + NumberOfInputs uint32 `json:"number_of_inputs"` + // NumberOfOutputs is a number of transaction outputs. + NumberOfOutputs uint32 `json:"number_of_outputs"` + // DraftID is a transaction related draft id. + DraftID string `json:"draft_id"` + // TotalValue is a total input value. + TotalValue uint64 `json:"total_value"` + // OutputValue is a total output value. + OutputValue int64 `json:"output_value,omitempty"` + // Outputs represents all spv-wallet-transaction outputs. Will be shown only for admin. + Outputs map[string]int64 `json:"outputs,omitempty"` + // Status is a transaction status. + Status string `json:"status"` + // TransactionDirection is a transaction direction (inbound/outbound). + TransactionDirection string `json:"direction"` +} diff --git a/models/transaction_config.go b/models/transaction_config.go new file mode 100644 index 000000000..aca7d71d1 --- /dev/null +++ b/models/transaction_config.go @@ -0,0 +1,115 @@ +package models + +import "time" + +// TransactionConfig is a model that represents a transaction config. +type TransactionConfig struct { + // ChangeDestinations is a slice of change destinations. + ChangeDestinations []*Destination `json:"change_destinations"` + // ChangeStrategy is a change strategy. + ChangeStrategy string `json:"change_destinations_strategy"` + // ChangeMinimumSatoshis is a minimum satoshis for change. + ChangeMinimumSatoshis uint64 `json:"change_minimum_satoshis"` + // ChangeNumberOfDestinations is a number of change destinations. + ChangeNumberOfDestinations int `json:"change_number_of_destinations"` + // ChangeSatoshis is a change satoshis. + ChangeSatoshis uint64 `json:"change_satoshis"` + // ExpiresAt is a time when transaction expires. + ExpiresIn time.Duration `json:"expires_in"` + // Fee is a fee amount. + Fee uint64 `json:"fee"` + // FeeUnit is a pointer to a fee unit object. + FeeUnit *FeeUnit `json:"fee_unit"` + // FromUtxos is a slice of from utxos used to build transaction. + FromUtxos []*UtxoPointer `json:"from_utxos"` + // IncludeUtxos is a slice of utxos to include in transaction. + IncludeUtxos []*UtxoPointer `json:"include_utxos"` + // Inputs is a slice of transaction inputs. + Inputs []*TransactionInput `json:"inputs"` + // Outputs is a slice of transaction outputs. + Outputs []*TransactionOutput `json:"outputs"` + // SendAllTo is a pointer to a transaction output object. + SendAllTo *TransactionOutput `json:"send_all_to"` + // Sync contains sync configuration. + Sync *SyncConfig `json:"sync"` +} + +// TransactionInput is a model that represents a transaction input. +type TransactionInput struct { + // Utxo is a pointer to a utxo object. + Utxo `json:",inline"` + // Destination is a pointer to a destination object. + Destination Destination `json:"destination"` +} + +// TransactionOutput is a model that represents a transaction output. +type TransactionOutput struct { + // OpReturn is a pointer to a op return object. + OpReturn *OpReturn `json:"op_return,omitempty"` + // PaymailP4 is a pointer to a paymail p4 object. + PaymailP4 *PaymailP4 `json:"paymail_p4,omitempty"` + // Satoshis is a satoshis amount. + Satoshis uint64 `json:"satoshis"` + // Script is a transaction output string representation of script. + Script string `json:"script"` + // ScriptType is a transaction output script type. + Scripts []*ScriptOutput `json:"scripts,omitempty"` + // To is a transaction output destination address. + To string `json:"to"` + // UseForChange is a flag that indicates if this output should be used for change. + UseForChange bool `json:"use_for_change"` +} + +// MapProtocol is a model that represents a map protocol. +type MapProtocol struct { + // App is a map protocol app. + App string `json:"app,omitempty"` + // Keys is a map protocol keys. + Keys map[string]interface{} `json:"keys,omitempty"` + // Type is a map protocol type. + Type string `json:"type,omitempty"` +} + +// OpReturn is a model that represents a op return. +type OpReturn struct { + // Hex is a full hex of op return. + Hex string `json:"hex,omitempty"` + // HexParts is a slice of splitted hex parts. + HexParts []string `json:"hex_parts,omitempty"` + // Map is a pointer to a map protocol object. + Map *MapProtocol `json:"map,omitempty"` + // StringParts is a slice of string parts. + StringParts []string `json:"string_parts,omitempty"` +} + +// PaymailP4 is a model that represents a paymail p4. +type PaymailP4 struct { + // Alias is a paymail p4 alias. + Alias string `json:"alias,omitempty"` + // Domain is a paymail p4 domain. + Domain string `json:"domain,omitempty"` + // FromPaymail is a paymail p4 from paymail. + FromPaymail string `json:"from_paymail,omitempty"` + // Note is a paymail p4 note. + Note string `json:"note,omitempty"` + // PubKey is a paymail p4 pub key. + PubKey string `json:"pub_key,omitempty"` + // ReceiveEndpoint is a paymail p4 receive endpoint. + ReceiveEndpoint string `json:"receive_endpoint,omitempty"` + // ReferenceID is a paymail p4 reference id. + ReferenceID string `json:"reference_id,omitempty"` + // ResolutionType is a paymail p4 resolution type. + ResolutionType string `json:"resolution_type,omitempty"` +} + +// ScriptOutput is a model that represents a script output. +type ScriptOutput struct { + // Address is a script output address. + Address string `json:"address,omitempty"` + // Satoshis is a script output satoshis. + Satoshis uint64 `json:"satoshis,omitempty"` + // Script is a script output script. + Script string `json:"script"` + // ScriptType is a script output script type. + ScriptType string `json:"script_type"` +} diff --git a/models/utxo.go b/models/utxo.go new file mode 100644 index 000000000..af1285815 --- /dev/null +++ b/models/utxo.go @@ -0,0 +1,42 @@ +package models + +import ( + "time" + + "github.com/bitcoin-sv/spv-wallet/models/common" +) + +// UtxoPointer is a pointer model that represents a utxo. +type UtxoPointer struct { + // TransactionID is a transaction id that utxo points to. + TransactionID string `json:"transaction_id"` + // OutputIndex is a output index that utxo points to. + OutputIndex uint32 `json:"output_index"` +} + +// Utxo is a model that represents a utxo. +type Utxo struct { + // Model is a common model that contains common fields for all models. + common.Model + // UtxoPointer is a pointer to a utxo object. + UtxoPointer `json:",inline"` + + // ID is a utxo id. + ID string `json:"id"` + // XpubID is a utxo related xpub id. + XpubID string `json:"xpub_id"` + // Satoshis is a utxo satoshis amount. + Satoshis uint64 `json:"satoshis"` + // ScriptPubKey is a utxo script pub key. + ScriptPubKey string `json:"script_pub_key"` + // Type is a utxo type. + Type string `json:"type"` + // DraftID is a utxo transaction related draft id. + DraftID string `json:"draft_id"` + // ReservedAt is a time utxo was reserved at. + ReservedAt time.Time `json:"reserved_at"` + // SpendingTxID is a spending transaction id - null if not spent yet. + SpendingTxID string `json:"spending_tx_id"` + // Transaction is a transaction pointer that utxo points to. + Transaction *Transaction `json:"transaction"` +} diff --git a/models/xpub.go b/models/xpub.go new file mode 100644 index 000000000..460d44e9e --- /dev/null +++ b/models/xpub.go @@ -0,0 +1,18 @@ +package models + +import "github.com/bitcoin-sv/spv-wallet/models/common" + +// Xpub is a model that represents a xpub. +type Xpub struct { + // Model is a common model that contains common fields for all models. + common.Model + + // ID is a xpub id. + ID string `json:"id"` + // CurrentBalance is a xpub's current balance. + CurrentBalance uint64 `json:"current_balance"` + // NextInternalNum is a next internal num. + NextInternalNum uint32 `json:"next_internal_num"` + // NextExternalNum is a next external num. + NextExternalNum uint32 `json:"next_external_num"` +} From ca323cf49631b14697dc93e7710ae2727af02bb2 Mon Sep 17 00:00:00 2001 From: jakubmkowalski Date: Fri, 16 Feb 2024 12:33:19 +0100 Subject: [PATCH 11/50] feat(BUX-602): adds engine --- engine/.dockerignore | 71 + engine/.editorconfig | 87 + engine/.gitattributes | 30 + engine/.github/CODEOWNERS | 7 + engine/.github/CODE_OF_CONDUCT.md | 39 + engine/.github/CODE_STANDARDS.md | 334 ++++ engine/.github/FUNDING.yml | 4 + .../.github/ISSUE_TEMPLATE/feature_request.md | 23 + .../.github/ISSUE_TEMPLATE/issue-template.md | 41 + engine/.github/PULL_REQUEST_TEMPLATE.md | 37 + engine/.github/dependabot.yml | 48 + engine/.github/labels.yml | 57 + engine/.github/mergify.yml | 219 +++ engine/.github/workflows/codeql-analysis.yml | 78 + engine/.github/workflows/on-pull-request.yml | 15 + engine/.github/workflows/release.yml | 41 + engine/.github/workflows/run-tests.yml | 75 + engine/.github/workflows/sync-labels.yml | 19 + engine/.gitignore | 37 + engine/.gitpod.yml | 3 + engine/.golangci.yml | 433 +++++ engine/.goreleaser.yml | 78 + engine/.make/common.mk | 90 + engine/.make/go.mk | 146 ++ engine/.yamllint.yml | 36 + engine/LICENSE | 191 ++ engine/Makefile | 54 + engine/README.md | 295 +++ engine/action_access_key.go | 213 +++ engine/action_destination.go | 282 +++ engine/action_destination_test.go | 385 ++++ engine/action_draft_transaction.go | 61 + engine/action_paymails.go | 220 +++ engine/action_paymails_test.go | 213 +++ engine/action_transaction.go | 431 +++++ engine/action_transaction_test.go | 430 +++++ engine/action_utxo.go | 152 ++ engine/action_xpub.go | 124 ++ engine/action_xpub_test.go | 217 +++ engine/admin_actions_stats.go | 101 ++ engine/authentication.go | 282 +++ engine/authentication_internal.go | 181 ++ engine/authentication_test.go | 686 +++++++ engine/beef_bump.go | 174 ++ engine/beef_fixtures.go | 9 + engine/beef_tx.go | 89 + engine/beef_tx_bytes.go | 85 + engine/beef_tx_mock.go | 29 + engine/beef_tx_sorting.go | 79 + engine/beef_tx_sorting_test.go | 131 ++ engine/beef_tx_test.go | 352 ++++ engine/bux.go | 7 + engine/bux_suite_database_test.go | 15 + engine/bux_suite_mocks_test.go | 60 + engine/bux_suite_test.go | 364 ++++ engine/bux_test.go | 279 +++ engine/chainstate/broadcast.go | 144 ++ engine/chainstate/broadcast_client_init.go | 45 + engine/chainstate/broadcast_providers.go | 118 ++ engine/chainstate/broadcast_test.go | 144 ++ engine/chainstate/broadcast_utils.go | 76 + engine/chainstate/chainstate.go | 90 + engine/chainstate/chainstate_test.go | 43 + engine/chainstate/client.go | 203 +++ engine/chainstate/client_options.go | 184 ++ engine/chainstate/client_options_test.go | 282 +++ engine/chainstate/client_test.go | 124 ++ engine/chainstate/definitions.go | 147 ++ engine/chainstate/errors.go | 24 + engine/chainstate/filters/filters.go | 2 + engine/chainstate/filters/metanet.go | 23 + engine/chainstate/filters/metanet_test.go | 42 + engine/chainstate/filters/planaria-b.go | 26 + engine/chainstate/filters/planaria-b_test.go | 59 + engine/chainstate/filters/planaria-d.go | 26 + engine/chainstate/filters/pubkeyhash.go | 19 + engine/chainstate/filters/rarecandy.go | 23 + engine/chainstate/interface.go | 54 + engine/chainstate/merkle_root.go | 31 + engine/chainstate/merkle_root_provider.go | 109 ++ engine/chainstate/merkle_root_test.go | 127 ++ engine/chainstate/minercraft_default.go | 28 + engine/chainstate/minercraft_init.go | 181 ++ engine/chainstate/mock_const.go | 33 + engine/chainstate/mock_minercraft.go | 534 ++++++ engine/chainstate/network.go | 30 + engine/chainstate/network_test.go | 39 + engine/chainstate/requirements.go | 38 + engine/chainstate/requirements_test.go | 83 + engine/chainstate/transaction.go | 145 ++ engine/chainstate/transaction_info.go | 28 + engine/chainstate/transaction_test.go | 321 ++++ engine/chainstate/types.go | 19 + engine/client.go | 425 +++++ engine/client_datastore.go | 161 ++ engine/client_internal.go | 183 ++ engine/client_options.go | 685 +++++++ engine/client_options_test.go | 860 +++++++++ engine/client_paymail.go | 39 + engine/client_test.go | 251 +++ engine/cluster/client.go | 81 + engine/cluster/client_options.go | 66 + engine/cluster/cluster.go | 2 + engine/cluster/interface.go | 35 + engine/cluster/memory-pub-sub.go | 56 + engine/cluster/redis-pub-sub.go | 80 + engine/codecov.yml | 42 + engine/cron_job_declarations.go | 69 + engine/cron_job_definitions.go | 128 ++ engine/db_model_transactions.go | 228 +++ engine/definitions.go | 170 ++ engine/errors.go | 191 ++ engine/errors_test.go | 9 + .../broadcast_miners/broadcast_miners.go | 41 + .../examples/client/chainstate/chainstate.go | 25 + .../client/custom_cron/custom_cron.go | 29 + .../client/custom_models/custom_models.go | 26 + .../client/custom_models/example_model.go | 69 + .../client/custom_rates/custom_rates.go | 58 + .../custom_user_agent/custom_user_agent.go | 24 + engine/examples/client/debugging/debugging.go | 24 + .../examples/client/encryption/encryption.go | 26 + engine/examples/client/logging/logging.go | 25 + engine/examples/client/mysql/mysql.go | 48 + engine/examples/client/new/new.go | 23 + engine/examples/client/new_relic/new_relic.go | 34 + .../client/paymail_support/paymail_support.go | 28 + engine/examples/client/redis/redis.go | 30 + engine/go.mod | 143 ++ engine/go.sum | 486 +++++ engine/interface.go | 191 ++ engine/locks.go | 34 + engine/logging/adapters.go | 185 ++ engine/logging/logging.go | 20 + engine/metrics/interface.go | 10 + engine/metrics/metrics.go | 84 + engine/metrics/naming.go | 18 + engine/metrics/stats.go | 26 + engine/mock_chainstate_test.go | 153 ++ engine/model_access_keys.go | 197 ++ engine/model_access_keys_test.go | 173 ++ engine/model_bump.go | 422 +++++ engine/model_bump_test.go | 931 ++++++++++ engine/model_destinations.go | 447 +++++ engine/model_destinations_test.go | 535 ++++++ engine/model_draft_transactions.go | 876 +++++++++ engine/model_draft_transactions_test.go | 1582 +++++++++++++++++ engine/model_get.go | 189 ++ engine/model_ids.go | 54 + engine/model_ids_test.go | 77 + engine/model_metadata.go | 187 ++ engine/model_metadata_test.go | 242 +++ engine/model_options.go | 84 + engine/model_options_test.go | 113 ++ engine/model_paymail_addresses.go | 293 +++ engine/model_paymail_addresses_test.go | 111 ++ engine/model_save.go | 145 ++ engine/model_save_test.go | 1 + engine/model_sync_config.go | 48 + engine/model_sync_results.go | 55 + engine/model_sync_status.go | 70 + engine/model_sync_transactions.go | 141 ++ engine/model_sync_transactions_test.go | 136 ++ engine/model_transaction_config.go | 408 +++++ engine/model_transaction_config_test.go | 474 +++++ engine/model_transaction_status.go | 50 + engine/model_transactions.go | 325 ++++ engine/model_transactions_output.go | 50 + engine/model_transactions_output_test.go | 63 + engine/model_transactions_service.go | 26 + engine/model_transactions_test.go | 971 ++++++++++ engine/model_utxos.go | 484 +++++ engine/model_utxos_test.go | 429 +++++ engine/model_xpubs.go | 324 ++++ engine/model_xpubs_test.go | 452 +++++ engine/models.go | 103 ++ engine/models_internal.go | 221 +++ engine/models_internal_test.go | 112 ++ engine/models_test.go | 180 ++ engine/notifications/client.go | 80 + engine/notifications/client_options.go | 54 + engine/notifications/client_options_test.go | 62 + engine/notifications/client_test.go | 29 + engine/notifications/interface.go | 22 + engine/notifications/notifications.go | 60 + engine/notifications/notifications_test.go | 94 + engine/paymail.go | 161 ++ engine/paymail_mocks_test.go | 50 + engine/paymail_service_provider.go | 426 +++++ engine/paymail_test.go | 370 ++++ engine/record_tx.go | 120 ++ ...record_tx_strategy_external_incoming_tx.go | 146 ++ ...record_tx_strategy_internal_incoming_tx.go | 109 ++ engine/record_tx_strategy_outgoing_tx.go | 230 +++ engine/sync_tx_repository.go | 183 ++ engine/sync_tx_service.go | 425 +++++ engine/taskmanager/cron_jobs.go | 54 + engine/taskmanager/cron_jobs_test.go | 96 + engine/taskmanager/factory.go | 21 + engine/taskmanager/factory_test.go | 30 + engine/taskmanager/interface.go | 20 + engine/taskmanager/options.go | 34 + engine/taskmanager/options_test.go | 78 + engine/taskmanager/taskmanager.go | 137 ++ engine/taskmanager/taskq.go | 183 ++ engine/taskmanager/taskq_test.go | 185 ++ engine/tester/cache.go | 67 + engine/tester/cache_test.go | 44 + engine/tester/database.go | 109 ++ engine/tester/database_test.go | 33 + engine/tester/embedded_test.go | 82 + engine/tester/errors.go | 9 + engine/tester/new_relic.go | 37 + engine/tester/new_relic_test.go | 44 + engine/tester/paymail.go | 55 + engine/tester/paymail_test.go | 22 + engine/tester/tester.go | 15 + engine/tester/tester_test.go | 22 + .../tests/model_draft_transactions_test.json | 243 +++ engine/tx_repository.go | 209 +++ engine/tx_service.go | 194 ++ engine/utils.go | 31 + engine/utils/byte_array.go | 27 + engine/utils/byte_array_test.go | 57 + engine/utils/destination_types.go | 240 +++ engine/utils/destination_types_test.go | 242 +++ engine/utils/encrypt.go | 30 + engine/utils/encrypt_test.go | 78 + engine/utils/errors.go | 20 + engine/utils/fees.go | 84 + engine/utils/fees_test.go | 121 ++ engine/utils/interface.go | 8 + engine/utils/keys.go | 147 ++ engine/utils/keys_test.go | 89 + engine/utils/outputs.go | 30 + engine/utils/outputs_test.go | 41 + engine/utils/scripts.go | 33 + engine/utils/scripts_test.go | 2 + engine/utils/tokens.go | 20 + engine/utils/tokens_test.go | 30 + engine/utils/utils.go | 122 ++ engine/utils/utils_test.go | 322 ++++ go.mod | 2 + 243 files changed, 35705 insertions(+) create mode 100644 engine/.dockerignore create mode 100644 engine/.editorconfig create mode 100644 engine/.gitattributes create mode 100644 engine/.github/CODEOWNERS create mode 100644 engine/.github/CODE_OF_CONDUCT.md create mode 100644 engine/.github/CODE_STANDARDS.md create mode 100644 engine/.github/FUNDING.yml create mode 100644 engine/.github/ISSUE_TEMPLATE/feature_request.md create mode 100644 engine/.github/ISSUE_TEMPLATE/issue-template.md create mode 100644 engine/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 engine/.github/dependabot.yml create mode 100644 engine/.github/labels.yml create mode 100644 engine/.github/mergify.yml create mode 100644 engine/.github/workflows/codeql-analysis.yml create mode 100644 engine/.github/workflows/on-pull-request.yml create mode 100644 engine/.github/workflows/release.yml create mode 100644 engine/.github/workflows/run-tests.yml create mode 100644 engine/.github/workflows/sync-labels.yml create mode 100644 engine/.gitignore create mode 100644 engine/.gitpod.yml create mode 100644 engine/.golangci.yml create mode 100644 engine/.goreleaser.yml create mode 100644 engine/.make/common.mk create mode 100644 engine/.make/go.mk create mode 100644 engine/.yamllint.yml create mode 100644 engine/LICENSE create mode 100644 engine/Makefile create mode 100644 engine/README.md create mode 100644 engine/action_access_key.go create mode 100644 engine/action_destination.go create mode 100644 engine/action_destination_test.go create mode 100644 engine/action_draft_transaction.go create mode 100644 engine/action_paymails.go create mode 100644 engine/action_paymails_test.go create mode 100644 engine/action_transaction.go create mode 100644 engine/action_transaction_test.go create mode 100644 engine/action_utxo.go create mode 100644 engine/action_xpub.go create mode 100644 engine/action_xpub_test.go create mode 100644 engine/admin_actions_stats.go create mode 100644 engine/authentication.go create mode 100644 engine/authentication_internal.go create mode 100644 engine/authentication_test.go create mode 100644 engine/beef_bump.go create mode 100644 engine/beef_fixtures.go create mode 100644 engine/beef_tx.go create mode 100644 engine/beef_tx_bytes.go create mode 100644 engine/beef_tx_mock.go create mode 100644 engine/beef_tx_sorting.go create mode 100644 engine/beef_tx_sorting_test.go create mode 100644 engine/beef_tx_test.go create mode 100644 engine/bux.go create mode 100644 engine/bux_suite_database_test.go create mode 100644 engine/bux_suite_mocks_test.go create mode 100644 engine/bux_suite_test.go create mode 100644 engine/bux_test.go create mode 100644 engine/chainstate/broadcast.go create mode 100644 engine/chainstate/broadcast_client_init.go create mode 100644 engine/chainstate/broadcast_providers.go create mode 100644 engine/chainstate/broadcast_test.go create mode 100644 engine/chainstate/broadcast_utils.go create mode 100644 engine/chainstate/chainstate.go create mode 100644 engine/chainstate/chainstate_test.go create mode 100644 engine/chainstate/client.go create mode 100644 engine/chainstate/client_options.go create mode 100644 engine/chainstate/client_options_test.go create mode 100644 engine/chainstate/client_test.go create mode 100644 engine/chainstate/definitions.go create mode 100644 engine/chainstate/errors.go create mode 100644 engine/chainstate/filters/filters.go create mode 100644 engine/chainstate/filters/metanet.go create mode 100644 engine/chainstate/filters/metanet_test.go create mode 100644 engine/chainstate/filters/planaria-b.go create mode 100644 engine/chainstate/filters/planaria-b_test.go create mode 100644 engine/chainstate/filters/planaria-d.go create mode 100644 engine/chainstate/filters/pubkeyhash.go create mode 100644 engine/chainstate/filters/rarecandy.go create mode 100644 engine/chainstate/interface.go create mode 100644 engine/chainstate/merkle_root.go create mode 100644 engine/chainstate/merkle_root_provider.go create mode 100644 engine/chainstate/merkle_root_test.go create mode 100644 engine/chainstate/minercraft_default.go create mode 100644 engine/chainstate/minercraft_init.go create mode 100644 engine/chainstate/mock_const.go create mode 100644 engine/chainstate/mock_minercraft.go create mode 100644 engine/chainstate/network.go create mode 100644 engine/chainstate/network_test.go create mode 100644 engine/chainstate/requirements.go create mode 100644 engine/chainstate/requirements_test.go create mode 100644 engine/chainstate/transaction.go create mode 100644 engine/chainstate/transaction_info.go create mode 100644 engine/chainstate/transaction_test.go create mode 100644 engine/chainstate/types.go create mode 100644 engine/client.go create mode 100644 engine/client_datastore.go create mode 100644 engine/client_internal.go create mode 100644 engine/client_options.go create mode 100644 engine/client_options_test.go create mode 100644 engine/client_paymail.go create mode 100644 engine/client_test.go create mode 100644 engine/cluster/client.go create mode 100644 engine/cluster/client_options.go create mode 100644 engine/cluster/cluster.go create mode 100644 engine/cluster/interface.go create mode 100644 engine/cluster/memory-pub-sub.go create mode 100644 engine/cluster/redis-pub-sub.go create mode 100644 engine/codecov.yml create mode 100644 engine/cron_job_declarations.go create mode 100644 engine/cron_job_definitions.go create mode 100644 engine/db_model_transactions.go create mode 100644 engine/definitions.go create mode 100644 engine/errors.go create mode 100644 engine/errors_test.go create mode 100644 engine/examples/client/broadcast_miners/broadcast_miners.go create mode 100644 engine/examples/client/chainstate/chainstate.go create mode 100644 engine/examples/client/custom_cron/custom_cron.go create mode 100644 engine/examples/client/custom_models/custom_models.go create mode 100644 engine/examples/client/custom_models/example_model.go create mode 100644 engine/examples/client/custom_rates/custom_rates.go create mode 100644 engine/examples/client/custom_user_agent/custom_user_agent.go create mode 100644 engine/examples/client/debugging/debugging.go create mode 100644 engine/examples/client/encryption/encryption.go create mode 100644 engine/examples/client/logging/logging.go create mode 100644 engine/examples/client/mysql/mysql.go create mode 100644 engine/examples/client/new/new.go create mode 100644 engine/examples/client/new_relic/new_relic.go create mode 100644 engine/examples/client/paymail_support/paymail_support.go create mode 100644 engine/examples/client/redis/redis.go create mode 100644 engine/go.mod create mode 100644 engine/go.sum create mode 100644 engine/interface.go create mode 100644 engine/locks.go create mode 100644 engine/logging/adapters.go create mode 100644 engine/logging/logging.go create mode 100644 engine/metrics/interface.go create mode 100644 engine/metrics/metrics.go create mode 100644 engine/metrics/naming.go create mode 100644 engine/metrics/stats.go create mode 100644 engine/mock_chainstate_test.go create mode 100644 engine/model_access_keys.go create mode 100644 engine/model_access_keys_test.go create mode 100644 engine/model_bump.go create mode 100644 engine/model_bump_test.go create mode 100644 engine/model_destinations.go create mode 100644 engine/model_destinations_test.go create mode 100644 engine/model_draft_transactions.go create mode 100644 engine/model_draft_transactions_test.go create mode 100644 engine/model_get.go create mode 100644 engine/model_ids.go create mode 100644 engine/model_ids_test.go create mode 100644 engine/model_metadata.go create mode 100644 engine/model_metadata_test.go create mode 100644 engine/model_options.go create mode 100644 engine/model_options_test.go create mode 100644 engine/model_paymail_addresses.go create mode 100644 engine/model_paymail_addresses_test.go create mode 100644 engine/model_save.go create mode 100644 engine/model_save_test.go create mode 100644 engine/model_sync_config.go create mode 100644 engine/model_sync_results.go create mode 100644 engine/model_sync_status.go create mode 100644 engine/model_sync_transactions.go create mode 100644 engine/model_sync_transactions_test.go create mode 100644 engine/model_transaction_config.go create mode 100644 engine/model_transaction_config_test.go create mode 100644 engine/model_transaction_status.go create mode 100644 engine/model_transactions.go create mode 100644 engine/model_transactions_output.go create mode 100644 engine/model_transactions_output_test.go create mode 100644 engine/model_transactions_service.go create mode 100644 engine/model_transactions_test.go create mode 100644 engine/model_utxos.go create mode 100644 engine/model_utxos_test.go create mode 100644 engine/model_xpubs.go create mode 100644 engine/model_xpubs_test.go create mode 100644 engine/models.go create mode 100644 engine/models_internal.go create mode 100644 engine/models_internal_test.go create mode 100644 engine/models_test.go create mode 100644 engine/notifications/client.go create mode 100644 engine/notifications/client_options.go create mode 100644 engine/notifications/client_options_test.go create mode 100644 engine/notifications/client_test.go create mode 100644 engine/notifications/interface.go create mode 100644 engine/notifications/notifications.go create mode 100644 engine/notifications/notifications_test.go create mode 100644 engine/paymail.go create mode 100644 engine/paymail_mocks_test.go create mode 100644 engine/paymail_service_provider.go create mode 100644 engine/paymail_test.go create mode 100644 engine/record_tx.go create mode 100644 engine/record_tx_strategy_external_incoming_tx.go create mode 100644 engine/record_tx_strategy_internal_incoming_tx.go create mode 100644 engine/record_tx_strategy_outgoing_tx.go create mode 100644 engine/sync_tx_repository.go create mode 100644 engine/sync_tx_service.go create mode 100644 engine/taskmanager/cron_jobs.go create mode 100644 engine/taskmanager/cron_jobs_test.go create mode 100644 engine/taskmanager/factory.go create mode 100644 engine/taskmanager/factory_test.go create mode 100644 engine/taskmanager/interface.go create mode 100644 engine/taskmanager/options.go create mode 100644 engine/taskmanager/options_test.go create mode 100644 engine/taskmanager/taskmanager.go create mode 100644 engine/taskmanager/taskq.go create mode 100644 engine/taskmanager/taskq_test.go create mode 100644 engine/tester/cache.go create mode 100644 engine/tester/cache_test.go create mode 100644 engine/tester/database.go create mode 100644 engine/tester/database_test.go create mode 100644 engine/tester/embedded_test.go create mode 100644 engine/tester/errors.go create mode 100644 engine/tester/new_relic.go create mode 100644 engine/tester/new_relic_test.go create mode 100644 engine/tester/paymail.go create mode 100644 engine/tester/paymail_test.go create mode 100644 engine/tester/tester.go create mode 100644 engine/tester/tester_test.go create mode 100644 engine/tests/model_draft_transactions_test.json create mode 100644 engine/tx_repository.go create mode 100644 engine/tx_service.go create mode 100644 engine/utils.go create mode 100644 engine/utils/byte_array.go create mode 100644 engine/utils/byte_array_test.go create mode 100644 engine/utils/destination_types.go create mode 100644 engine/utils/destination_types_test.go create mode 100644 engine/utils/encrypt.go create mode 100644 engine/utils/encrypt_test.go create mode 100644 engine/utils/errors.go create mode 100644 engine/utils/fees.go create mode 100644 engine/utils/fees_test.go create mode 100644 engine/utils/interface.go create mode 100644 engine/utils/keys.go create mode 100644 engine/utils/keys_test.go create mode 100644 engine/utils/outputs.go create mode 100644 engine/utils/outputs_test.go create mode 100644 engine/utils/scripts.go create mode 100644 engine/utils/scripts_test.go create mode 100644 engine/utils/tokens.go create mode 100644 engine/utils/tokens_test.go create mode 100644 engine/utils/utils.go create mode 100644 engine/utils/utils_test.go diff --git a/engine/.dockerignore b/engine/.dockerignore new file mode 100644 index 000000000..98a7dcd07 --- /dev/null +++ b/engine/.dockerignore @@ -0,0 +1,71 @@ +## +## Specific to .dockerignore +## + +.git/ +Dockerfile +contrib/ + +## +## Common with .gitignore +## + +# Temporary files +*~ +*# +.#* + +# Vendors +node_modules/ +vendor/ + +# Binaries for programs and plugins +dist/ +gin-bin +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Byte-compiled / optimized / DLL files +__pycache__/ +**/__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +*.py[cod] +*$py.class +.pytest_cache/ +..mypy_cache/ + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Virtual environments +.venv +../venv +.DS_Store +.AppleDouble +.LSOverride +._* + +# Custom repo notes +todo.md + +# Temporary directories in the project +bin +tmp +.all-contributorsrc +.gitpod.yml +.golangci.yml +.goreleaser.yml +.yamllint.yml +.editorconfig +codecov.yml +LICENSE +README.md diff --git a/engine/.editorconfig b/engine/.editorconfig new file mode 100644 index 000000000..67d6d324e --- /dev/null +++ b/engine/.editorconfig @@ -0,0 +1,87 @@ +# Check http://editorconfig.org for more information +# This is the main config file for this project: + +root = true + +[*] +charset = utf-8 + +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +indent_style = space +indent_size = 4 + +[*.mod] +indent_style = tab + +[{Makefile,**.mk}] +indent_style = tab + +[*.go] +indent_style = tab + +[*.css] +indent_size = 2 + +[*.proto] +indent_size = 2 + +[*.ftl] +indent_size = 2 + +[*.toml] +indent_size = 2 + +[*.swift] +indent_size = 4 + +[*.tmpl] +indent_size = 2 + +[*.js] +indent_size = 2 +block_comment_start = /* +block_comment_end = */ + +[*.{html,htm}] +indent_size = 2 + +[*.bat] +end_of_line = crlf + +[*.{yml,yaml}] +indent_size = 2 + +[*.json] +indent_size = 2 + +[.{babelrc,eslintrc,prettierrc}] +indent_size = 2 + +[{Fastfile,.buckconfig,BUCK}] +indent_size = 2 + +[*.diff] +indent_size = 1 + +[*.{diff,patch}] +trim_trailing_whitespace = false + +[*.m] +indent_size = 1 +indent_style = space +block_comment_start = /** +block_comment = * +block_comment_end = */ + +[*.java] +indent_size = 4 +indent_style = space +block_comment_start = /** +block_comment = * +block_comment_end = */ + +[*.{md,rst}] +trim_trailing_whitespace = false diff --git a/engine/.gitattributes b/engine/.gitattributes new file mode 100644 index 000000000..fe46d96e9 --- /dev/null +++ b/engine/.gitattributes @@ -0,0 +1,30 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Collapse generated and vendored files on GitHub +AUTHORS linguist-generated merge=union +*.gen.* linguist-generated merge=ours +*.pb.go linguist-generated merge=ours +*.pb.gw.go linguist-generated merge=ours +go.sum linguist-generated merge=ours +go.mod linguist-generated +gen.sum linguist-generated merge=ours +depaware.txt linguist-generated linguist-vendored merge=union +vendor/* linguist-vendored +rules.mk linguist-vendored +*/vendor/* linguist-vendored + +# doc +docs/* linguist-documentation +docs/Makefile linguist-documentation=false + +# Reduce conflicts on markdown files +*.md merge=union + +# A set of files you probably don't want in distribution +/.github export-ignore +/.githooks export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.gitmodules export-ignore +/tool/lint export-ignore diff --git a/engine/.github/CODEOWNERS b/engine/.github/CODEOWNERS new file mode 100644 index 000000000..d29ac98b9 --- /dev/null +++ b/engine/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# CODEOWNERS file for bitcoin-sv + +# This file is used to designate code owners for files and directories in this repository. +# The code owner will automatically be requested for review on pull requests that modify the corresponding files. + +* @bitcoin-sv/sr-engineers +* @bitcoin-sv/developers diff --git a/engine/.github/CODE_OF_CONDUCT.md b/engine/.github/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..ceef82baa --- /dev/null +++ b/engine/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,39 @@ +# Code of Conduct + +## 1. Purpose + +The primary goal of this project is to foster an inclusive, respectful, and open community for everyone, regardless of their background or identity. This Code of Conduct outlines our expectations for participant behavior and the consequences for unacceptable behavior. + +## 2. Open Discussions & Respectful Feedback + +- **Encourage Diverse Ideas:** Everyone brings a unique perspective. Encourage different viewpoints, and listen openly to each other’s ideas. + +- **Constructive Feedback:** Focus on providing constructive feedback rather than criticizing individuals. Discuss ideas, not the person presenting them. + +- **Avoid Harmful Language:** Refrain from using offensive or harmful language, including but not limited to sexist, racist, homophobic, transphobic, ableist, or discriminatory remarks. + +## 3. No Politics or Off-topic Discussions + +- **Stay On Topic:** Keep discussions focused on the project and avoid bringing in off-topic or political discussions. + +- **Respectful Discourse:** If discussions become heated, maintain a level of respect and understanding, and work towards a compromise. + +## 4. Reporting & Enforcement + +- **Report Violations:** If you observe a violation of this Code of Conduct, please report it by contacting the project team members. + +- **Consequences:** Violations of this Code of Conduct may result in temporary or permanent banning from the project community. + +## 5. Inclusion & Diversity + +- **Welcome Everyone:** Foster an environment where everyone feels welcome, regardless of their background, identity, or level of experience. + +- **Help Newcomers:** Offer help and guidance to newcomers to make them feel welcome in our community. + +## 6. Be Kind & Courteous + +- **Respect Time & Effort:** Recognize and respect the time and effort put in by contributors and maintainers. + +- **Courtesy:** Be courteous and polite. Treat others as you would like to be treated. + +By participating in this project, you agree to abide by this Code of Conduct. Let’s work together to make this community respectful and inclusive for everyone. diff --git a/engine/.github/CODE_STANDARDS.md b/engine/.github/CODE_STANDARDS.md new file mode 100644 index 000000000..071e3175e --- /dev/null +++ b/engine/.github/CODE_STANDARDS.md @@ -0,0 +1,334 @@ +# Code Standards & Contributing Guidelines + +- [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.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) + - [As a Developer](#as-a-developer) + - [As a PR Reviewer](#as-a-pr-reviewer) + - [2.2 Tests](#22-tests) + - [Principle](#principle) + - [Guidelines for Writing Tests](#guidelines-for-writing-tests) + - [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) + - [Include Issue Number](#include-issue-number) + - [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.1 Overview](#41-overview) + - [4.2 Principles](#42-principles) + - [4.3 Feature Documentation](#43-feature-documentation) + - [Necessity](#necessity) + - [Examples](#examples) + - [4.4 External Features](#44-external-features) + - [4.5 Markdown usage](#45-markdown-usage) + - [4.6 Conclusion](#46-conclusion) + +## Most important rules - Quick Checklist + +- [ ] Follow official Go guidelines for style and formatting. +- [ ] Write self-documenting code and minimize comments. +- [ ] Ensure comprehensive test coverage including happy and error paths. +- [ ] Provide meaningful and constructive code reviews. +- [ ] Adhere to Conventional Commits for commit messages and PR naming. +- [ ] Document every feature adequately, especially for open-source projects. +- [ ] 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.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://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 + + > Our current linter configuration is in the `.golangci.yml` file. + +#### Additional useful resources with GO recommendations, best practices and the common mistakes + +- [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://go.dev/wiki/CommonMistakes) - Common Mistakes in Go + +## 2 Code Rules + +### 2.1. Self-documenting code + +#### As a Developer + +- Refactoring tasks should be strategically undertaken; not every piece of code warrants modification. If a segment of code, despite comments, remains untouched and unproblematic, it is likely fulfilling its purpose effectively. +- When embarking on feature additions or bug fixes, and encountering commented code, consider extracting functions and creating a PR before proceeding with the primary task. This helps maintain code clarity and function. +- Should you come across a superfluous comment during your changes, do take the initiative to remove it and create a PR for that. This practice contributes to keeping the repository neat and well-maintained. +- Endeavor to minimize the addition of comments when making any changes to the code. + +#### As a PR Reviewer + +- 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 + +#### Principle + +Developers are required to diligently cover their changes with tests and organize these tests with caution. A well-structured set of tests serves as a safety net, facilitating the swift identification and resolution of issues. + +#### Guidelines for Writing Tests + +1. **Readability**: Maintain test readability through the use of descriptive test names. When possible, group cases into table-driven tests, exercising discretion to avoid excessiveness. Avoid code branching in tests; instead, split differing scenarios into separate test cases. + +2. **Comprehensive Coverage**: Ensure comprehensive test coverage, including error paths. Prioritize adding use cases for well-known errors that could be returned from your code. + +3. **Structural Consistency**: Adopt the "Given-When-Then" structure in automatic tests to enhance readability and consistency. This structure assists team members in following the test flow and understanding its purpose, thus facilitating smoother onboarding for new members and maintaining uniformity across tests. + + - **Given**: Set the stage by preparing inputs, mocks, and application state. + - **When**: Trigger the action or function under test. + - **Then**: Verify if the outcomes match the expectations. + + ```go + func TestSomethingVeryUsefulIsHappening(t *testing.T) { + //given + ... // prepare inputs, mocks, application state + + //when + ... // call the function you're actually testing + + //then + ... // check expectations about the function output + } + ``` + +4. **Test Isolation**: Ensure test isolation by avoiding the use of global variables and shared state. Each test should be independent and not rely on the execution of other tests. If a test requires a shared state, use a setup function to create the state before each test. + +5. **Test Data**: Avoid random data in tests. Instead, use predefined data to ensure test consistency and reproducibility. If random data is required, use a seed to ensure the same data is generated each time the test is run. + +6. **Test Cases**: If you are writing a public functions - it should be covered by tests. We should have test cases for all possible scenarios. That means that **we should have tests for all possible errors** that can be returned from the function. +Of course not only error paths should be covered - **we should highlight the happy path as well**. + +7. **Testing private (unexported) functions**: When testing private functions, we should test them through the public (exported) functions that use them. The exception is when the private function is too complex to be tested through the public function. Good example is for example a function that is implementing a complex algorithm. In this case we should test the private function directly. + +### 2.3 Code Review + +#### Guidelines for Code Review + +1. **Constructive Feedback**: Reviewers should provide constructive feedback, highlighting both strengths and areas of improvement. Comments should be clear, concise, and related to code structure, functionality, or style. + +2. **Coding Standards**: Both authors and reviewers should ensure the code adheres to established coding standards, including formatting, naming conventions, and best practices as outlined in official Golang guidelines. + +3. **Testing**: Reviewers should ensure that the submitted code is accompanied by adequate tests, covering both happy paths and error paths. Check the readability, descriptiveness, and structure of tests. + +4. **Error Handling**: Pay special attention to error handling within the code. Ensure that errors are not ignored, logged appropriately, and presented to the user in a user-friendly format when applicable. + +5. **Performance**: Review the code for any potential performance issues, such as inefficient loops, unnecessary allocations, or misuse of concurrency. + +6. **Dependency Management**: Ensure that any new dependencies are necessary, appropriately versioned, and have been vetted for performance and security. + +7. **Security Practices**: Ensure that the code follows secure coding practices and avoids common vulnerabilities. + Good checklist for security practices can be found [here](https://owasp.org/www-project-top-ten/). + +8. **Documentation**: Confirm that the code is well-documented, including comments, function/method descriptions, and module-level documentation as necessary. + +9. **Efficiency and Readability**: The code should be efficient and readable. Reviewers should look for any code smells, overly complex functions, and ensure the use of idiomatic Go patterns. + +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. + +#### 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? +- [ ] Is error handling comprehensive and user-friendly? +- [ ] Are there any performance concerns in the code? +- [ ] Have new dependencies been appropriately vetted? +- [ ] Does the code follow secure coding practices and avoid common vulnerabilities? +- [ ] Is the code well(self)-documented, with clear variable, function naming? +- [ ] Is the code efficient, readable, and free of code smells? +- [ ] Have all review comments been addressed in a timely manner? +- [ ] Do you understand the code and it's purpose? + +This checklist serves as a guide to both authors and reviewers to ensure a thorough and effective code review process. + +## 3 Contributing + +### 3.1 Pull Requests && Issues + +We have separate templates for Pull Requests and Issues. Please use them when creating a new PR or Issue. + +### 3.2 Conventional Commits & Pull Requests Naming + +#### 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. + +#### Structure + +Conventional Commits follow a structured format: `type(scope): description`, where: + +- `type`: Represents the nature of the commit (e.g., feat, fix, chore). +- `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`. + +#### Types + +- `feat`: Utilized when introducing a new feature to the codebase. +- `fix`: Employed when resolving a bug or issue in the code. +- `docs`: Designated for commits involving documentation changes, such as updating README files or adding comments. +- `style`: Applied to commits focusing on code style and formatting, without altering the code's functionality. +- `refactor`: Used for code changes that neither introduce new features nor fix bugs, but improve the code structure or design. +- `test`: Assigned to commits pertaining to the addition, modification, or refactoring of tests. +- `chore`: For changes related to build processes, local development, or other maintenance tasks. +- `perf`: Employed when enhancing the performance of the codebase. +- `revert`: Marked for commits that revert a previous change. +- `ci`: Applied to changes concerning the Continuous Integration (CI) configuration or scripts. +- `deps`: Used when updating or modifying dependencies. + +#### 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.`
+`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`
+`fix(#125): fix a bug - 2.1.0 -> 2.1.1` + +Given a version number MAJOR.MINOR.PATCH, increment the: + +MAJOR version when you make incompatible API changes +MINOR version when you add functionality in a backward compatible manner +PATCH version when you make backward compatible bug fixes +Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. + +More about Semantic Versioning can be found [here](https://semver.org/). + +#### 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. + +#### Further Reading + +Additional information and guidelines on Conventional Commits can be found [here](https://www.conventionalcommits.org/en/v1.0.0/). + +#### Examples + +##### Commit message with scope + +Good example: + +```bash +feat: add possibility to create a new user by admin (#123) +``` + +Bad example: + +```bash +debugo feature - checkpoint full work +``` + +##### Pull request title with a scope and task number + +> feat(#123): add new feature + +### 3.3 Branching + +#### Choosing branch names + +- Choose consistent naming conventions. Common practices include: + - `feature/feature-name` + - `bugfix/issue-or-bug-name` + - `hotfix/hotfix-name` + - `chore/task-name` + - `refactor/refactor-name` + +#### 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`. + +#### Include Issue Number + +- If applicable, include the issue number in the branch name for easy tracking, e.g.,
+`feature/123-add-login-button`. + +#### Deleting Branches After Merging + +#### Remove Remote Branches + +- Once a PR has been merged, delete the remote branch to keep the repository clean. +- GitHub provides a button to delete the branch once the PR is merged. + +#### 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`. + +## 4 Documentation Code Standards + +### 4.1 Overview + +A well-documented codebase is pivotal for both internal development and external contributions, especially for open-source projects that expose functionalities for public use. Comprehensive documentation, supplemented with examples where necessary, ensures that every feature is easily understandable, usable, and maintainable. + +### 4.2 Principles + +- **Clarity and Conciseness**: Documentation should be clear, concise, and focused, providing necessary information without unnecessary complexity or verbosity. +- **Accessibility**: Documentation should be accessible to developers of varying skill levels, enabling both novices and experts to understand the codebase and its features. +- **Up-to-Date**: Documentation must be kept current, reflecting the latest changes and developments in the codebase to avoid misinformation and confusion. + +### 4.3 Feature Documentation + +#### 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. + +#### 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. + +### 4.4 External Features + +For projects exposing external features: + +- **Comprehensive Guides**: Ensure the creation of comprehensive guides detailing the utilization of exposed features, their benefits, and any potential configurations or customizations. +- **Community Engagement**: Encourage community members to contribute to documentation by providing feedback, suggestions, and improvements. This collaborative approach enriches the documentation quality and breadth. + +### 4.5 Markdown usage + +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](https://markdownguide.org) - Comprehensive guide to Markdown syntax. + +### 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/engine/.github/FUNDING.yml b/engine/.github/FUNDING.yml new file mode 100644 index 000000000..a0ae0d39b --- /dev/null +++ b/engine/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: bitcoin-sv +custom: https://getbux.io/?utm_source=github&utm_medium=sponsor-link&utm_campaign=go-buxclient&utm_term=go-buxclient&utm_content=go-buxclient diff --git a/engine/.github/ISSUE_TEMPLATE/feature_request.md b/engine/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..91c49f65d --- /dev/null +++ b/engine/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for a new feature or improvement +title: "[FEATURE]" +labels: enhancement +assignees: '' + +--- + +## Desired Solution +A clear and concise description of what you want to happen. Describe how the new feature should work and what problems it should solve. If possible, including mockups, diagrams, or sample code can be helpful. + +## Suggested Implementation +In this section, you can present ideas on how the new feature could be implemented. Suggest where in the code changes should be made and what libraries or tools might be useful. + +## Alternatives Considered +Describe alternative solutions or features that have been considered. Why was this particular solution chosen? What are the pros and cons of the alternative options? + +## Additional Context +Add any other context or screenshots about the feature request here. If the proposal involves visual elements, it’s good to attach screenshots, videos, or other materials illustrating the proposed changes. + +## Categorize Feature +If it's possible try categorize the request - if it's UX Improvement, Security Improvement or maybe the changed is related to the application performance. diff --git a/engine/.github/ISSUE_TEMPLATE/issue-template.md b/engine/.github/ISSUE_TEMPLATE/issue-template.md new file mode 100644 index 000000000..dff68919d --- /dev/null +++ b/engine/.github/ISSUE_TEMPLATE/issue-template.md @@ -0,0 +1,41 @@ +--- +name: Issue Template +about: Create a report to help us fix bugs/inaccuracies +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**TL;DR** +Check if your issue answers those question: + +- [ ] Does my issue clearly describe what exactly is not working properly? +- [ ] Does my issue clearly describe what are my expectations and what is the current output? +- [ ] Did I attach logs/screenshots/errors which ocured? +- [ ] Did I provide information how I run the code/library/application and on which platform/browser? + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior/error. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +If you have any logs connected with your problem - please attach them. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Used environment (please complete the following information):** + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/engine/.github/PULL_REQUEST_TEMPLATE.md b/engine/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..29b115f8c --- /dev/null +++ b/engine/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,37 @@ + + +# Pull Request Checklist + +- [ ] 📖 I created my PR using provided : [CODE_STANDARDS](https://github.com/bitcoin-sv/spv-wallet/engine/blob/main/.github/CODE_STANDARDS.md) +- [ ] 📖 I have read the short Code of Conduct: [CODE_OF_CONDUCT](https://github.com/bitcoin-sv/spv-wallet/engine/blob/main/.github/CODE_OF_CONDUCT.md) +- [ ] 🏠 I tested my changes locally. +- [ ] ✅ I have provided tests for my changes. +- [ ] 📝 I have used conventional commits. +- [ ] 📗 I have updated any related documentation. +- [ ] 💾 PR was issued based on the Github or Jira issue. + + + + + + diff --git a/engine/.github/dependabot.yml b/engine/.github/dependabot.yml new file mode 100644 index 000000000..3c6932a54 --- /dev/null +++ b/engine/.github/dependabot.yml @@ -0,0 +1,48 @@ +# Basic dependabot.yml to update gomod, Github Actions and Docker +version: 2 +updates: + # Maintain dependencies for the core library + - package-ecosystem: "gomod" + target-branch: "master" + directory: "/" + schedule: + interval: "daily" + time: "10:00" + timezone: "UTC" + reviewers: + - "mrz1836" + assignees: + - "mrz1836" + labels: + - "chore" + open-pull-requests-limit: 10 + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + target-branch: "master" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + reviewers: + - "mrz1836" + assignees: + - "mrz1836" + labels: + - "chore" + open-pull-requests-limit: 10 + + # Maintain dependencies for Docker (if used) + - package-ecosystem: "docker" + target-branch: "master" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + reviewers: + - "mrz1836" + assignees: + - "mrz1836" + labels: + - "chore" + open-pull-requests-limit: 10 diff --git a/engine/.github/labels.yml b/engine/.github/labels.yml new file mode 100644 index 000000000..01acb0c46 --- /dev/null +++ b/engine/.github/labels.yml @@ -0,0 +1,57 @@ +- color: 0075ca + description: "Improvements or additions to documentation" + name: "documentation" +- color: b23128 + description: "Highest rated bug or issue, affects all" + name: "bug-P1" +- color: de3d32 + description: "Medium rated bug, affects a few" + name: "bug-P2" +- color: f44336 + description: "Lowest rated bug, affects nearly none or low-impact" + name: "bug-P3" +- color: 0e8a16 + description: "Any new significant addition" + name: "feature" +- color: b60205 + description: "Urgent or important fix/patch" + name: "hot-fix" +- color: cccccc + description: "Any idea, suggestion, opinion, etc" + name: "idea" +- color: d4c5f9 + description: "Experimental - can break!" + name: "prototype" +- color: cc317c + description: "Any question or concern" + name: "question" +- color: c2e0c6 + description: "Unit tests, mocking, integration testing" + name: "test" +- color: fbca04 + description: "Anything GUI related" + name: "ui-ux" +- color: 006b75 + description: "Simple updates or version bumps" + name: "chore" +- color: 006b75 + description: "Dependency updates" + name: "dependencies" +- color: 006b75 + description: "General updates" + name: "update" +- color: FFA500 + description: "Any significant refactoring" + name: "refactor" +- color: FEF2C0 + description: "Used for automatic merging" + name: "automerge" +- color: FBCA04 + description: "Used for denoting a WIP, stops auto-merge" + name: "work-in-progress" +- color: c2e0c6 + description: "Old, unused, stale" + name: "stale" +- color: 50e061 + description: "PR was tested by a team member" + name: "tested" diff --git a/engine/.github/mergify.yml b/engine/.github/mergify.yml new file mode 100644 index 000000000..c5e4faa99 --- /dev/null +++ b/engine/.github/mergify.yml @@ -0,0 +1,219 @@ +pull_request_rules: + + # =============================================================================== + # DEPENDABOT + # =============================================================================== + + - name: Automatic Merge for Dependabot Minor Version Pull Requests + conditions: + - -draft + - author~=^dependabot(|-preview)\[bot\]$ + - check-success='test (1.19.x, ubuntu-latest)' + - check-success='Analyze (go)' + actions: + review: + type: APPROVE + message: Automatically approving dependabot pull request + merge: + method: merge + + # =============================================================================== + # AUTOMATIC MERGE (APPROVALS) + # =============================================================================== + + - name: Automatic Merge ⬇️ on Approval ✔ + conditions: + - "#approved-reviews-by>=1" + - "#review-requested=0" + - "#changes-requested-reviews-by=0" + - check-success='test (1.19.x, ubuntu-latest)' + - check-success='Analyze (go)' + - -title~=(?i)wip + - label!=work-in-progress + - -draft + actions: + merge: + method: merge + + # =============================================================================== + # AUTHOR + # =============================================================================== + + - name: Auto-Assign Author + conditions: + - "#assignee=0" + actions: + assign: + users: ["arkadiuszos4chain"] + + # =============================================================================== + # ALERTS + # =============================================================================== + + - name: Notify on merge + conditions: + - merged + - label=automerge + actions: + comment: + message: "✅ @{{author}}: **{{title}}** has been merged successfully." + - name: Alert on merge conflict + conditions: + - conflict + - label=automerge + actions: + comment: + message: "🆘 @{{author}}: `{{head}}` has conflicts with `{{base}}` that must be resolved." + - name: Alert on tests failure for automerge + conditions: + - label=automerge + - status-failure=commit + actions: + comment: + message: "🆘 @{{author}}: unable to merge due to CI failure." + + # =============================================================================== + # LABELS + # =============================================================================== + # Automatically add labels when PRs match certain patterns + # + # NOTE: + # - single quotes for regex to avoid accidental escapes + # - Mergify leverages Python regular expressions to match rules. + # + # Semantic commit messages + # - chore: updating grunt tasks etc.; no production code change + # - docs: changes to the documentation + # - feat: feature or story + # - feature: new feature or story + # - fix: bug fix for the user, not a fix to a build script + # - idea: general idea or suggestion + # - question: question regarding code + # - test: test related changes + # - wip: work in progress PR + # =============================================================================== + + - name: Work in Progress + conditions: + - "head~=(?i)^wip" # if the PR branch starts with wip/ + actions: + label: + add: ["work-in-progress"] + - name: Hotfix label + conditions: + - "head~=(?i)^hotfix" # if the PR branch starts with hotfix/ + actions: + label: + add: ["hot-fix"] + - name: Bug / Fix label + conditions: + - "head~=(?i)^(bug)?fix" # if the PR branch starts with (bug)?fix/ + actions: + label: + add: ["bug-P3"] + - name: Documentation label + conditions: + - "head~=(?i)^docs" # if the PR branch starts with docs/ + actions: + label: + add: ["documentation"] + - name: Feature label + conditions: + - "head~=(?i)^feat(ure)?" # if the PR branch starts with feat(ure)?/ + actions: + label: + add: ["feature"] + - name: Chore label + conditions: + - "head~=(?i)^chore" # if the PR branch starts with chore/ + actions: + label: + add: ["update"] + - name: Question label + conditions: + - "head~=(?i)^question" # if the PR branch starts with question/ + actions: + label: + add: ["question"] + - name: Test label + conditions: + - "head~=(?i)^test" # if the PR branch starts with test/ + actions: + label: + add: ["test"] + - name: Idea label + conditions: + - "head~=(?i)^idea" # if the PR branch starts with idea/ + actions: + label: + add: ["idea"] + + # =============================================================================== + # CONTRIBUTORS + # =============================================================================== + + - name: Welcome New Contributors + conditions: + - and: + - author!=dependabot[bot] + - author!=mergify[bot] + - author!=allcontributors[bot] + - author!=mrz1836 + - author!=icellan + - author!=galt-tr + - author!=dorzepowski + - author!=pawellewandowski98 + - author!=vitalibalashka + - author!=arkadiuszos4chain + - author!=wregulski + actions: + comment: + message: "Welcome to our open-source project @{{author}}! 💘" + + # =============================================================================== + # STALE BRANCHES + # =============================================================================== + + - name: Close stale pull request + conditions: + - base=master + - -closed + - updated-at<21 days ago + actions: + close: + message: | + This pull request looks stale. Feel free to reopen it if you think it's a mistake. + label: + add: ["stale"] + + # =============================================================================== + # BRANCHES + # =============================================================================== + + - name: Delete head branch after merge + conditions: + - merged + actions: + delete_head_branch: + + # =============================================================================== + # CONVENTION + # =============================================================================== + # https://www.conventionalcommits.org/en/v1.0.0/ + # Premium feature only + + #- name: Conventional Commit + # conditions: + # - "title~=^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\\(.+\\))?:" + # actions: + # post_check: + # title: | + # {% if check_succeed %} + # Title follows Conventional Commit + # {% else %} + # Title does not follow Conventional Commit + # {% endif %} + # summary: | + # {% if not check_succeed %} + # Your pull request title must follow [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/). + # {% endif %} diff --git a/engine/.github/workflows/codeql-analysis.yml b/engine/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..8af4d0d6b --- /dev/null +++ b/engine/.github/workflows/codeql-analysis.yml @@ -0,0 +1,78 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + # schedule: + # - cron: '0 23 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['go'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can check out the head. + fetch-depth: 2 + + # GH Actions runner uses go1.20 by default, so we need to install our own version. + # https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087 + - name: Install Go from go.mod + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + # - run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/engine/.github/workflows/on-pull-request.yml b/engine/.github/workflows/on-pull-request.yml new file mode 100644 index 000000000..94324b209 --- /dev/null +++ b/engine/.github/workflows/on-pull-request.yml @@ -0,0 +1,15 @@ +name: "On pull request" + +on: + pull_request_target: + types: + - labeled + - unlabeled + - opened + +permissions: + pull-requests: write + +jobs: + on-pr: + uses: bactions/workflows/.github/workflows/on-pull-request.yml@main diff --git a/engine/.github/workflows/release.yml b/engine/.github/workflows/release.yml new file mode 100644 index 000000000..b1f143aa7 --- /dev/null +++ b/engine/.github/workflows/release.yml @@ -0,0 +1,41 @@ +# From: https://goreleaser.com/ci/actions/#usage +name: release + +env: + GO111MODULE: on + +on: + push: + tags: + - '*' + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Start Redis + uses: supercharge/redis-github-action@1.8.0 + with: + redis-version: 6 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5.0.0 + with: + distribution: goreleaser + version: latest + args: release --rm-dist --debug + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + - name: Syndicate to GoDocs + run: make godocs diff --git a/engine/.github/workflows/run-tests.yml b/engine/.github/workflows/run-tests.yml new file mode 100644 index 000000000..efee0910b --- /dev/null +++ b/engine/.github/workflows/run-tests.yml @@ -0,0 +1,75 @@ +# See more at: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions +name: run-go-tests + +env: + GO111MODULE: on + +on: + push: + branches-ignore: + - main + - master + +jobs: + yamllint: + name: Run yaml linter + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Run yaml linter + uses: ibiqlik/action-yamllint@v3.1 + asknancy: + name: Ask Nancy (check dependencies) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + # GH Actions runner uses go1.20 by default, so we need to install our own version. + # https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087 + - name: Install Go from go.mod + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Write go list + run: go list -json -m all > go.list + - name: Ask Nancy + uses: sonatype-nexus-community/nancy-github-action@v1.0.3 + continue-on-error: true + test: + needs: [yamllint, asknancy] + strategy: + matrix: + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Go from go.mod + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Start Redis + uses: supercharge/redis-github-action@1.8.0 + with: + redis-version: 6 + - name: Cache code + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod # Module download cache + ~/.cache/go-build # Build cache (Linux) + ~/Library/Caches/go-build # Build cache (Mac) + '%LocalAppData%\go-build' # Build cache (Windows) + key: ${{ runner.os }}-go-bux-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-bux- + - name: Run linter and tests + run: make test-all-db-ci + - name: Update code coverage + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: unittests + fail_ci_if_error: false # optional (default = false) + verbose: true # optional (default = false) diff --git a/engine/.github/workflows/sync-labels.yml b/engine/.github/workflows/sync-labels.yml new file mode 100644 index 000000000..da0514af7 --- /dev/null +++ b/engine/.github/workflows/sync-labels.yml @@ -0,0 +1,19 @@ +# Workflow: https://github.com/micnncim/action-label-syncer +# Export your labels: https://github.com/micnncim/label-exporter +name: sync-labels +on: + push: + branches: + - master + paths: + - .github/labels.yml +jobs: + sync-labels: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: micnncim/action-label-syncer@v1.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + manifest: .github/labels.yml diff --git a/engine/.gitignore b/engine/.gitignore new file mode 100644 index 000000000..8c56f94cb --- /dev/null +++ b/engine/.gitignore @@ -0,0 +1,37 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Go List +go.list + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# OS files +*.db +*.DS_Store + +# Jetbrains +.idea/ + +#VSCode +.vscode/ + +# Eclipse +.project + +# Notes +todo.md + +# Distribution +dist + +# Code coverage +coverage.txt diff --git a/engine/.gitpod.yml b/engine/.gitpod.yml new file mode 100644 index 000000000..48d5f5234 --- /dev/null +++ b/engine/.gitpod.yml @@ -0,0 +1,3 @@ +tasks: + - init: go get && go build ./... + command: go run diff --git a/engine/.golangci.yml b/engine/.golangci.yml new file mode 100644 index 000000000..3e5fde9cd --- /dev/null +++ b/engine/.golangci.yml @@ -0,0 +1,433 @@ +# This file contains all available configuration options +# with their default values. + +# options for analysis running +run: + # default concurrency is an available CPU number + concurrency: 4 + + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 5m + + # exit code when at least one issue was found, default is 1 + issues-exit-code: 1 + + # include test files or not, default is true + tests: true + + # list of build tags, all linters use it. Default is empty list. + build-tags: + - mytag + + # which dirs to skip: issues from them won't be reported; + # can use regexp here: generated.*, regexp is applied on full path; + # default value is empty list, but default dirs are skipped independently + # of this option's value (see skip-dirs-use-default). + # "/" will be replaced by current OS file path separator to properly work + # on Windows. + skip-dirs: + - .github + - .make + - dist + + # default is true. Enables skipping of directories: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + skip-dirs-use-default: true + + # which files to skip: they will be analyzed, but issues from them + # won't be reported. Default value is empty list, but there is + # no need to include all autogenerated files, we confidently recognize + # autogenerated files. If it's not please let us know. + # "/" will be replaced by current OS file path separator to properly work + # on Windows. + skip-files: + - ".*\\.my\\.go$" + - lib/bad.go + + # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the implicit + # automatic updating of go.mod described above. Instead, it fails when any changes + # to go.mod are needed. This setting is most useful to check that go.mod does + # not need updates, such as in a continuous integration and testing system. + # If invoked with -mod=vendor, the go command assumes that the vendor + # directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. + #modules-download-mode: readonly|release|vendor + + # Allow multiple parallel golangci-lint instances running. + # If false (default) - golangci-lint acquires file lock on start. + allow-parallel-runners: false + + +# output configuration options +output: + # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" + format: colored-line-number + + # print lines of code with issue, default is true + print-issued-lines: true + + # print linter name in the end of issue text, default is true + print-linter-name: true + + # make issues output unique by line, default is true + uniq-by-line: true + + # add a prefix to the output file references; default is no prefix + path-prefix: "" + + +# all available settings of specific linters +linters-settings: + dogsled: + # checks assignments with too many blank identifiers; default is 2 + max-blank-identifiers: 2 + dupl: + # tokens count to trigger issue, 150 by default + threshold: 100 + errcheck: + # report about not checking of errors in type assertions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: false + + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; + # default is false: such cases aren't reported by default. + check-blank: false + + # [deprecated] comma-separated list of pairs of the form pkg:regex + # the regex is used to ignore names within pkg. (default "fmt:.*"). + # see https://github.com/kisielk/errcheck#the-deprecated-method for details + ignore: fmt:.*,io/ioutil:^Read.* + + # path to a file containing a list of functions to exclude from checking + # see https://github.com/kisielk/errcheck#excluding-functions for details + #exclude: /path/to/file.txt + exhaustive: + # indicates that switch statements are to be considered exhaustive if a + # 'default' case is present, even if all enum members aren't listed in the + # switch + default-signifies-exhaustive: false + funlen: + lines: 60 + statements: 40 + gci: + # put imports beginning with prefix after 3rd-party packages; + # only support one prefix + # if not set, use goimports.local-prefixes + local-prefixes: github.com/org/project + gocognit: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 10 + nestif: + # minimal complexity of if statements to report, 5 by default + min-complexity: 4 + goconst: + # minimal length of string constant, 3 by default + min-len: 3 + # minimal occurrences count to trigger, 3 by default + min-occurrences: 3 + gocritic: + # Which checks should be enabled; can't be combined with 'disabled-checks'; + # See https://go-critic.github.io/overview#checks-overview + # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` + # By default list of stable checks is used. + #enabled-checks: + # - rangeValCopy + + # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty + disabled-checks: + - regexpMust + + # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. + # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". + enabled-tags: + - performance + disabled-tags: + - experimental + + settings: # settings passed to gocritic + captLocal: # must be valid enabled check name + paramsOnly: true + rangeValCopy: + sizeThreshold: 32 + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 10 + godot: + # check all top-level comments, not only declarations + check-all: false + godox: + # report any comments starting with keywords, this is useful for TODO or FIXME comments that + # might be left in the code accidentally and should be resolved before merging + keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting + - NOTE + - OPTIMIZE # marks code that should be optimized before merging + - HACK # marks hack-arounds that should be removed before merging + gofmt: + # simplify code: gofmt with `-s` option, true by default + simplify: true + goimports: + # put imports beginning with prefix after 3rd-party packages; + # it's a comma-separated list of prefixes + local-prefixes: github.com/org/project + gomnd: + settings: + mnd: + # the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description. + checks: + - argument + - case + - condition + - operation + - return + - assign + govet: + # report about shadowed variables + check-shadowing: true + + # settings per analyzer + settings: + printf: # analyzer name, run `go tool vet help` to see all analyzers + funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + + # enable or disable analyzers by name + enable: + - atomicalign + enable-all: false + disable-all: false + depguard: + list-type: blacklist + include-go-root: false + packages: + - github.com/sirupsen/logrus + packages-with-error-message: + # specify an error message to output when a blacklisted package is used + - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" + lll: + # max line length, lines longer will be reported. Default is 120. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option + line-length: 120 + # tab width in spaces. Default to 1. + tab-width: 1 + maligned: + # print struct with more effective memory layout or not, false by default + suggest-new: true + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + locale: US + ignore-words: + - bsv + - bitcoin + nakedret: + # make an issue if func has more lines of code than this setting, and it has naked returns; default is 30 + max-func-lines: 30 + prealloc: + # XXX: we don't recommend using this linter before doing performance profiling. + # For most programs usage of prealloc will be a premature optimization. + + # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. + # True by default. + simple: true + range-loops: true # Report preallocation suggestions on range loops, true by default + for-loops: false # Report preallocation suggestions on for loops, false by default + nolintlint: + # Enable to ensure that nolint directives are all used. Default is true. + allow-unused: false + # Disable to ensure that nolint directives don't have a leading space. Default is true. + allow-leading-space: true + # Exclude following linters from requiring an explanation. Default is []. + allow-no-explanation: [] + # Enable to require an explanation of nonzero length after each nolint directive. Default is false. + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. + require-specific: true + rowserrcheck: + packages: + - github.com/jmoiron/sqlx + testpackage: + # regexp pattern to skip files + skip-regexp: (export|internal)_test\.go + unparam: + # Inspect exported functions, default is false. Set to true if no external program/library imports your code. + # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: + # if it's called for sub-dir of a project it can't find external interfaces. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + unused: + # treat code as a program (not a library) and report unused exported identifiers; default is false. + # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: + # if it's called for sub-dir of a project it can't find function usages. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + whitespace: + multi-if: false # Enforces newlines (or comments) after every multi-line if statement + multi-func: false # Enforces newlines (or comments) after every multi-line function signature + wsl: + # If true append is only allowed to be cuddled if appending value is + # matching variables, fields or types online above. Default is true. + strict-append: true + # Allow calls and assignments to be cuddled as long as the lines have any + # matching variables, fields or types. Default is true. + allow-assign-and-call: true + # Allow multiline assignments to be cuddled. Default is true. + allow-multiline-assign: true + # Allow declarations (var) to be cuddled. + allow-cuddle-declarations: true + # Allow trailing comments in ending of blocks + allow-trailing-comment: false + # Force newlines in end of case at this limit (0 = never). + force-case-trailing-whitespace: 0 + # Force cuddling of err checks with err var assignment + force-err-cuddling: false + # Allow leading comments to be separated with empty liens + allow-separated-leading-comment: false + gofumpt: + # Choose whether to use the extra rules that are disabled + # by default + extra-rules: false + + # The custom section can be used to define linter plugins to be loaded at runtime. See README doc + # for more info. + custom: + # Each custom linter should have a unique name. + #example: + # The path to the plugin *.so. Can be absolute or local. Required for each custom linter + #path: /path/to/example.so + # The description of the linter. Optional, just for documentation purposes. + #description: This is an example usage of a plugin linter. + # Intended to point to the repo location of the linter. Optional, just for documentation purposes. + #original-url: github.com/golangci/example-linter + +linters: + enable: + - megacheck + - govet + - gosec + - bodyclose + - revive + - unconvert + - misspell + - ineffassign + - dogsled + - prealloc + - exportloopref + - exhaustive + - sqlclosecheck + - nolintlint + - gci + disable: + - gocritic # use this for very opinionated linting + - gochecknoglobals + - whitespace + - wsl + - goerr113 + - contextcheck + - godot + - testpackage + - nestif + - nlreturn + - dupl + - goconst + - testifylint + disable-all: false + presets: + - bugs + - unused + fast: false + + +issues: + # List of regexps of issue texts to exclude, empty list by default. + # But independently of this option we use default exclude patterns, + # it can be disabled by `exclude-use-default: false`. To list all + # excluded by default patterns execute `golangci-lint run --help` + exclude: + - Using the variable on range scope .* in function literal + + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec + + # Exclude known linters from partially "hard-vendored" code, + # which is impossible to exclude via "nolint" comments. + - path: internal/hmac/ + text: "weak cryptographic primitive" + linters: + - gosec + + # Exclude some "staticcheck" messages + - linters: + - staticcheck + text: "SA1019:" + + # Exclude lll issues for long lines with go:generate + - linters: + - lll + source: "^//go:generate " + + # Independently of option `exclude` we use default exclude patterns, + # it can be disabled by this option. To list all + # excluded by default patterns execute `golangci-lint run --help`. + # Default value for this option is true. + exclude-use-default: false + + # The default value is false. If set to true exclude and exclude-rules + # regular expressions become case-sensitive. + exclude-case-sensitive: false + + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-issues-per-linter: 0 + + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-same-issues: 0 + + # Show only new issues: if there are "un-staged" changes or untracked files, + # only those changes are analyzed, else only changes in HEAD~ are analyzed. + # It's a super-useful option for integration of golangci-lint into existing + # large codebase. It's not practical to fix all existing issues at the moment + # of integration: much better don't allow issues in new code. + # Default is false. + new: false + + # Show only new issues created after git revision `REV` + new-from-rev: "" + + # Show only new issues created in git patch with set file path. + #new-from-patch: path/to/patch/file + +severity: + # Default value is empty string. + # Set the default severity for issues. If severity rules are defined and the issues + # do not match or no severity is provided to the rule this will be the default + # severity applied. Severities should match the supported severity names of the + # selected out format. + # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity + # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity + # - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message + default-severity: error + + # The default value is false. + # If set to true severity-rules regular expressions become case-sensitive. + case-sensitive: false + + # Default value is empty list. + # When a list of severity rules are provided, severity information will be added to lint + # issues. Severity rules have the same filtering capability as exclude rules except you + # are allowed to specify one matcher per severity rule. + # Only affects out formats that support setting severity information. + rules: + - linters: + - dupl + severity: info diff --git a/engine/.goreleaser.yml b/engine/.goreleaser.yml new file mode 100644 index 000000000..21165c897 --- /dev/null +++ b/engine/.goreleaser.yml @@ -0,0 +1,78 @@ +# Make sure to check the documentation at http://goreleaser.com +# --------------------------- +# General +# --------------------------- +before: + hooks: + - make all +snapshot: + name_template: "{{ .Tag }}" +changelog: + sort: asc + filters: + exclude: + - '^.github:' + - '^.vscode:' + - '^test:' + +# --------------------------- +# Publishers +# --------------------------- +# publishers: +# - name: "Publish GoDocs" +# cmd: make godocs + +# --------------------------- +# Builder +# --------------------------- +build: + skip: true + +# --------------------------- +# Github Release +# --------------------------- +release: + prerelease: auto + name_template: "Release v{{.Version}}" + +# --------------------------- +# Announce +# --------------------------- +announce: + + # See more at: https://goreleaser.com/customization/announce/#slack + slack: + enabled: true + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Changelog: https://github.com/bitcoin-sv/{{ .ProjectName }}/releases/tag/{{ .Tag }}' + channel: '#ba-bux' + # username: '' + # icon_emoji: '' + # icon_url: '' + + # See more at: https://goreleaser.com/customization/announce/#twitter + twitter: + enabled: false + message_template: '{{ .ProjectName }} {{ .Tag }} is out!' + + # See more at: https://goreleaser.com/customization/announce/#discord + discord: + enabled: false + message_template: '{{ .ProjectName }} {{ .Tag }} is out!' + # Defaults to `GoReleaser` + author: '' + # Defaults to `3888754` - the grey-ish from goreleaser + color: '' + # Defaults to `https://goreleaser.com/static/avatar.png` + icon_url: '' + + # See more at: https://goreleaser.com/customization/announce/#reddit + reddit: + enabled: false + # Application ID for Reddit Application + application_id: "" + # Username for your Reddit account + username: "" + # Defaults to `{{ .GitURL }}/releases/tag/{{ .Tag }}` + # url_template: 'https://github.com/bitcoin-sv/{{ .ProjectName }}/releases/tag/{{ .Tag }}' + # Defaults to `{{ .ProjectName }} {{ .Tag }} is out!` + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' diff --git a/engine/.make/common.mk b/engine/.make/common.mk new file mode 100644 index 000000000..5d92c0aaf --- /dev/null +++ b/engine/.make/common.mk @@ -0,0 +1,90 @@ +## Default repository domain name +ifndef GIT_DOMAIN + override GIT_DOMAIN=github.com +endif + +## Set if defined (alias variable for ease of use) +ifdef branch + override REPO_BRANCH=$(branch) + export REPO_BRANCH +endif + +## Do we have git available? +HAS_GIT := $(shell command -v git 2> /dev/null) + +ifdef HAS_GIT + ## Do we have a repo? + HAS_REPO := $(shell git rev-parse --is-inside-work-tree 2> /dev/null) + ifdef HAS_REPO + ## Automatically detect the repo owner and repo name (for local use with Git) + REPO_NAME=$(shell basename "$(shell git rev-parse --show-toplevel 2> /dev/null)") + OWNER=$(shell git config --get remote.origin.url | sed 's/git@$(GIT_DOMAIN)://g' | sed 's/\/$(REPO_NAME).git//g') + REPO_OWNER=$(shell echo $(OWNER) | tr A-Z a-z) + VERSION_SHORT=$(shell git describe --tags --always --abbrev=0) + export REPO_NAME, REPO_OWNER, VERSION_SHORT + endif +endif + +## Set the distribution folder +ifndef DISTRIBUTIONS_DIR + override DISTRIBUTIONS_DIR=./dist +endif +export DISTRIBUTIONS_DIR + +.PHONY: diff + +diff: ## Show the git diff + $(call print-target) + git diff --exit-code + RES=$$(git status --porcelain) ; if [ -n "$$RES" ]; then echo $$RES && exit 1 ; fi + +help: ## Show this help message + @egrep -h '^(.+)\:\ ##\ (.+)' ${MAKEFILE_LIST} | column -t -c 2 -s ':#' + +install-releaser: ## Install the GoReleaser application + @echo "installing GoReleaser..." + @curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh + +release:: ## Full production release (creates release in Github) + @echo "releasing..." + @test $(github_token) + @export GITHUB_TOKEN=$(github_token) && goreleaser --rm-dist + +release-test: ## Full production test release (everything except deploy) + @echo "creating a release test..." + @goreleaser --skip-publish --rm-dist + +release-snap: ## Test the full release (build binaries) + @echo "creating a release snapshot..." + @goreleaser --snapshot --skip-publish --rm-dist + +replace-version: ## Replaces the version in HTML/JS (pre-deploy) + @echo "replacing version..." + @test $(version) + @test "$(path)" + @find $(path) -name "*.html" -type f -exec sed -i '' -e "s/{{version}}/$(version)/g" {} \; + @find $(path) -name "*.js" -type f -exec sed -i '' -e "s/{{version}}/$(version)/g" {} \; + +tag: ## Generate a new tag and push (tag version=0.0.0) + @echo "creating new tag..." + @test $(version) + @git tag -a v$(version) -m "Pending full release..." + @git push origin v$(version) + @git fetch --tags -f + +tag-remove: ## Remove a tag if found (tag-remove version=0.0.0) + @echo "removing tag..." + @test $(version) + @git tag -d v$(version) + @git push --delete origin v$(version) + @git fetch --tags + +tag-update: ## Update an existing tag to current commit (tag-update version=0.0.0) + @echo "updating tag to new commit..." + @test $(version) + @git push --force origin HEAD:refs/tags/v$(version) + @git fetch --tags -f + +update-releaser: ## Update the goreleaser application + @echo "updating GoReleaser application..." + @$(MAKE) install-releaser diff --git a/engine/.make/go.mk b/engine/.make/go.mk new file mode 100644 index 000000000..67f619392 --- /dev/null +++ b/engine/.make/go.mk @@ -0,0 +1,146 @@ +## Default to the repo name if empty +ifndef BINARY_NAME + override BINARY_NAME=app +endif + +## Define the binary name +ifdef CUSTOM_BINARY_NAME + override BINARY_NAME=$(CUSTOM_BINARY_NAME) +endif + +## Set the binary release names +DARWIN=$(BINARY_NAME)-darwin +LINUX=$(BINARY_NAME)-linux +WINDOWS=$(BINARY_NAME)-windows.exe + +## Define the binary name +TAGS= +ifdef GO_BUILD_TAGS + override TAGS=-tags $(GO_BUILD_TAGS) +endif + +.PHONY: bench +bench: ## Run all benchmarks in the Go application + @echo "running benchmarks..." + @go test -bench=. -benchmem $(TAGS) + +.PHONY: build-go +build-go: ## Build the Go application (locally) + @echo "building go app..." + @go build -o bin/$(BINARY_NAME) $(TAGS) + +.PHONY: clean-mods +clean-mods: ## Remove all the Go mod cache + @echo "cleaning mods..." + @go clean -modcache + +.PHONY: coverage +coverage: ## Shows the test coverage + @echo "creating coverage report..." + @go test -coverprofile=coverage.out ./... $(TAGS) && go tool cover -func=coverage.out $(TAGS) + +.PHONY: generate +generate: ## Runs the go generate command in the base of the repo + @echo "generating files..." + @go generate -v $(TAGS) + +.PHONY: godocs +godocs: ## Sync the latest tag with GoDocs + @echo "syndicating to GoDocs..." + @test $(GIT_DOMAIN) + @test $(REPO_OWNER) + @test $(REPO_NAME) + @test $(VERSION_SHORT) + @curl https://proxy.golang.org/$(GIT_DOMAIN)/$(REPO_OWNER)/$(REPO_NAME)/@v/$(VERSION_SHORT).info + +.PHONY: install +install: ## Install the application + @echo "installing binary..." + @go build -o $$GOPATH/bin/$(BINARY_NAME) $(TAGS) + +.PHONY: install-go +install-go: ## Install the application (Using Native Go) + @echo "installing package..." + @go install $(GIT_DOMAIN)/$(REPO_OWNER)/$(REPO_NAME) $(TAGS) + +.PHONY: lint +lint: ## Run the golangci-lint application (install if not found) + @echo "installing golangci-lint..." + @#Travis (has sudo) + @if [ "$(shell command -v golangci-lint)" = "" ] && [ $(TRAVIS) ]; then curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.55.1 && sudo cp ./bin/golangci-lint $(go env GOPATH)/bin/; fi; + @#AWS CodePipeline + @if [ "$(shell command -v golangci-lint)" = "" ] && [ "$(CODEBUILD_BUILD_ID)" != "" ]; then curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.1; fi; + @#GitHub Actions + @if [ "$(shell command -v golangci-lint)" = "" ] && [ "$(GITHUB_WORKFLOW)" != "" ]; then curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b $(go env GOPATH)/bin v1.55.1; fi; + @#Brew - MacOS + @if [ "$(shell command -v golangci-lint)" = "" ] && [ "$(shell command -v brew)" != "" ]; then brew install golangci-lint; fi; + @#MacOS Vanilla + @if [ "$(shell command -v golangci-lint)" = "" ] && [ "$(shell command -v brew)" != "" ]; then curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- v1.55.1; fi; + @echo "running golangci-lint..." + @golangci-lint run --verbose + +.PHONY: test +test: ## Runs lint and ALL tests + @$(MAKE) lint + @echo "running tests..." + @go test ./... -v $(TAGS) + +.PHONY: test-unit +test-unit: ## Runs tests and outputs coverage + @echo "running unit tests..." + @go test ./... -race -coverprofile=coverage.txt -covermode=atomic $(TAGS) + +.PHONY: test-short +test-short: ## Runs vet, lint and tests (excludes integration tests) + @$(MAKE) lint + @echo "running tests (short)..." + @go test ./... -v -test.short $(TAGS) + +.PHONY: test-ci +test-ci: ## Runs all tests via CI (exports coverage) + @$(MAKE) lint + @echo "running tests (CI)..." + @go test ./... -race -coverprofile=coverage.txt -covermode=atomic $(TAGS) + +.PHONY: test-ci-no-race +test-ci-no-race: ## Runs all tests via CI (no race) (exports coverage) + @$(MAKE) lint + @echo "running tests (CI - no race)..." + @go test ./... -coverprofile=coverage.txt -covermode=atomic $(TAGS) + +.PHONY: test-ci-short +test-ci-short: ## Runs unit tests via CI (exports coverage) + @$(MAKE) lint + @echo "running tests (CI - unit tests only)..." + @go test ./... -test.short -race -coverprofile=coverage.txt -covermode=atomic $(TAGS) + +.PHONY: test-no-lint +test-no-lint: ## Runs just tests + @echo "running tests..." + @go test ./... -v $(TAGS) + +.PHONY: uninstall +uninstall: ## Uninstall the application (and remove files) + @echo "uninstalling go application..." + @test $(BINARY_NAME) + @test $(GIT_DOMAIN) + @test $(REPO_OWNER) + @test $(REPO_NAME) + @go clean -i $(GIT_DOMAIN)/$(REPO_OWNER)/$(REPO_NAME) + @rm -rf $$GOPATH/src/$(GIT_DOMAIN)/$(REPO_OWNER)/$(REPO_NAME) + @rm -rf $$GOPATH/bin/$(BINARY_NAME) + +.PHONY: update +update: ## Update all project dependencies + @echo "updating dependencies..." + @go get -u ./... && go mod tidy + +.PHONY: update-linter +update-linter: ## Update the golangci-lint package (macOS only) + @echo "upgrading golangci-lint..." + @brew upgrade golangci-lint + +.PHONY: vet +vet: ## Run the Go vet application + @echo "running go vet..." + @go vet -v ./... $(TAGS) diff --git a/engine/.yamllint.yml b/engine/.yamllint.yml new file mode 100644 index 000000000..c310c9629 --- /dev/null +++ b/engine/.yamllint.yml @@ -0,0 +1,36 @@ +--- + +yaml-files: + - '*.yaml' + - '*.yml' + - '.yamllint' + +ignore: | + dist/ + +rules: + braces: enable + brackets: enable + colons: enable + commas: enable + comments: + level: warning + comments-indentation: + level: warning + document-end: disable + document-start: + level: warning + empty-lines: enable + empty-values: disable + hyphens: enable + indentation: enable + key-duplicates: enable + key-ordering: disable + line-length: disable + new-line-at-end-of-file: disable + new-lines: enable + octal-values: disable + quoted-strings: disable + trailing-spaces: enable + truthy: + level: warning diff --git a/engine/LICENSE b/engine/LICENSE new file mode 100644 index 000000000..27747d29f --- /dev/null +++ b/engine/LICENSE @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2023 BSV Blockchain Association ("BA") + Copyright 2021-2022 @BuxOrg + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/engine/Makefile b/engine/Makefile new file mode 100644 index 000000000..d62ad5e1c --- /dev/null +++ b/engine/Makefile @@ -0,0 +1,54 @@ +# Set the custom build tags +GO_BUILD_TAGS=json1 + +# Common makefile commands & variables between projects +include .make/common.mk + +# Common Golang makefile commands & variables between projects +include .make/go.mk + +## Not defined? Use default repo name which is the application +ifeq ($(REPO_NAME),) + REPO_NAME="bux" +endif + +## Not defined? Use default repo owner +ifeq ($(REPO_OWNER),) + REPO_OWNER="bitcoin-sv" +endif + +.PHONY: all +all: ## Runs multiple commands + @$(MAKE) test-all-db + +.PHONY: clean +clean: ## Remove previous builds and any cached data + @echo "cleaning local cache..." + @go clean -cache -testcache -i -r + @$(MAKE) clean-mods + @test $(DISTRIBUTIONS_DIR) + @if [ -d $(DISTRIBUTIONS_DIR) ]; then rm -r $(DISTRIBUTIONS_DIR); fi + +.PHONY: install-all-contributors +install-all-contributors: ## Installs all contributors locally + @echo "installing all-contributors cli tool..." + @yarn global add all-contributors-cli + +.PHONY: release +release:: ## Runs common.release then runs godocs + @$(MAKE) godocs + +.PHONY: test-all-db +test-all-db: ## Runs all tests including embedded database tests + @echo "running all tests including embedded database tests..." + @go test ./... -v -tags="$(GO_BUILD_TAGS) database_tests" + +.PHONY: test-all-db-ci +test-all-db-ci: ## Runs all tests including embedded database tests (CI) + @echo "running all tests including embedded database tests..." + @go test ./... -coverprofile=coverage.txt -covermode=atomic -tags="$(GO_BUILD_TAGS) database_tests" + +.PHONE: update-contributors +update-contributors: ## Regenerates the contributors html/list + @echo "generating contributor html..." + @all-contributors generate diff --git a/engine/README.md b/engine/README.md new file mode 100644 index 000000000..1619c9807 --- /dev/null +++ b/engine/README.md @@ -0,0 +1,295 @@ +
+ +# SPV Wallet Engine + +[![Release](https://img.shields.io/github/release-pre/bitcoin-sv/spv-wallet/engine.svg?logo=github&style=flat&v=2)](https://github.com/bitcoin-sv/spv-wallet/engine/releases) +[![Build Status](https://img.shields.io/github/actions/workflow/status/bitcoin-sv/spv-wallet/engine/run-tests.yml?branch=master&v=2)](https://github.com/bitcoin-sv/spv-wallet/engine/actions) +[![Report](https://goreportcard.com/badge/github.com/bitcoin-sv/spv-wallet/engine?style=flat&v=2)](https://goreportcard.com/report/github.com/bitcoin-sv/spv-wallet/engine) +[![codecov](https://codecov.io/gh/bitcoin-sv/spv-wallet/engine/branch/master/graph/badge.svg?v=2)](https://codecov.io/gh/bitcoin-sv/spv-wallet/engine) +[![Mergify Status](https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/bitcoin-sv/spv-wallet/engine&style=flat&v=2)](https://mergify.com) +
+ +[![Go](https://img.shields.io/github/go-mod/go-version/bitcoin-sv/spv-wallet/engine?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/bitcoin-sv/spv-wallet/engine) +[![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) +
+
+ +> Bitcoin UTXO & xPub Management Engine + +## Table of Contents +- [About](#about) +- [Installation](#installation) +- [Documentation](#documentation) +- [Usage](#usage) + - [Examples & Tests](#examples--tests) + - [Benchmarks](#benchmarks) +- [Code Standards](#code-standards) +- [Contributing](#contributing) +- [License](#license) + +
+ +## About + +> **TLDR;** +> +>Application developers should focus on their applications and should not be bogged down with managing UTXOs or XPubs. Developers should be able to use an open-source, easy to install solution to rapidly build full-featured Bitcoin applications. + +
+ +---- +#### DISCLAIMER +> SPV Wallet Engine is still considered _"ALPHA"_ and should not be used in production until a major v1.0.0 is released. +---- +
+ +#### SPV Wallet Engine: Out-of-the-box Features: +- xPub & UTXO State Management (state, balance, utxos, destinations) +- Bring your own Database ([MySQL](https://www.mysql.com/), [PostgreSQL](https://www.postgresql.org/), [SQLite](https://www.sqlite.org), [Mongo](https://www.mongodb.com/) or [interface](https://github.com/mrz1836/go-datastore/blob/master/interface.go) your own) +- Caching ([FreeCache](https://github.com/github.com/coocood/freecache), [Redis](https://redis.io/) or [interface](https://github.com/mrz1836/go-cachestore/blob/master/interface.go) your own) +- Task Management ([TaskQ](https://github.com/vmihailenco/taskq) or [interface](taskmanager/interface.go) your own) +- Transaction Syncing (queue, broadcast, push to mempool or on-chain, or [interface](chainstate/interface.go) your own) +- Future plugins using [BRFC standards](http://bsvalias.org/01-brfc-specifications.html) + +#### **Project Assumptions: MVP** +- _No private keys are used_, only the xPub (or access key) is given to spv wallet engine +- (BYOX) `Bring your own xPub` +- Signing a transaction is outside this application (IE: [spv-wallet](https://github.com/bitcoin-sv/spv-wallet) or [spv-wallet-client](https://github.com/bitcoin-sv/spv-wallet-go-client)) +- All transactions need to be submitted to the spv wallet service to effectively track utxo states +- Database can be backed up, but not regenerated from chain + - Certain data is not on chain, plus re-scanning an xPub is expensive and not easily possible with 3rd party limitations + + +
+ +## Installation + +**spv-wallet/engine** requires a [supported release of Go](https://golang.org/doc/devel/release.html#policy). +```shell script +go get -u github.com/bitcoin-sv/spv-wallet/engine +``` + +
+ +## Documentation +View the generated [documentation](https://pkg.go.dev/github.com/bitcoin-sv/spv-wallet/engine) + +[![GoDoc](https://godoc.org/github.com/bitcoin-sv/spv-wallet/engine?status.svg&style=flat&v=2)](https://pkg.go.dev/github.com/bitcoin-sv/spv-wallet/engine) + +
+ +
+Repository Features +
+ +This repository was created using [MrZ's `go-template`](https://github.com/mrz1836/go-template#about) + +#### Built-in Features +- Continuous integration via [GitHub Actions](https://github.com/features/actions) +- Build automation via [Make](https://www.gnu.org/software/make) +- Dependency management using [Go Modules](https://github.com/golang/go/wiki/Modules) +- Code formatting using [gofumpt](https://github.com/mvdan/gofumpt) and linting with [golangci-lint](https://github.com/golangci/golangci-lint) and [yamllint](https://yamllint.readthedocs.io/en/stable/index.html) +- Unit testing with [testify](https://github.com/stretchr/testify), [race detector](https://blog.golang.org/race-detector), code coverage [HTML report](https://blog.golang.org/cover) and [Codecov report](https://codecov.io/) +- Releasing using [GoReleaser](https://github.com/goreleaser/goreleaser) on [new Tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) +- Dependency scanning and updating thanks to [Dependabot](https://dependabot.com) and [Nancy](https://github.com/sonatype-nexus-community/nancy) +- Security code analysis using [CodeQL Action](https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/about-code-scanning) +- Automatic syndication to [pkg.go.dev](https://pkg.go.dev/) on every release +- Generic templates for [Issues and Pull Requests](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository) in GitHub +- All standard GitHub files such as `LICENSE`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, and `SECURITY.md` +- Code [ownership configuration](.github/CODEOWNERS) for GitHub +- All your ignore files for [vs-code](.editorconfig), [docker](.dockerignore) and [git](.gitignore) +- Automatic sync for [labels](.github/labels.yml) into GitHub using a pre-defined [configuration](.github/labels.yml) +- Built-in powerful merging rules using [Mergify](https://mergify.io/) +- Welcome [new contributors](.github/mergify.yml) on their first Pull-Request +- Follows the [standard-readme](https://github.com/RichardLitt/standard-readme/blob/master/spec.md) specification +- [Visual Studio Code](https://code.visualstudio.com) configuration with [Go](https://code.visualstudio.com/docs/languages/go) +- (Optional) [Slack](https://slack.com), [Discord](https://discord.com) or [Twitter](https://twitter.com) announcements on new GitHub Releases +- (Optional) Easily add [contributors](https://allcontributors.org/docs/en/bot/installation) in any Issue or Pull-Request + +
+ +
+Package Dependencies +
+ +- [bitcoinschema/go-bitcoin](https://github.com/bitcoinschema/go-bitcoin) +- [bitcoinschema/go-map](https://github.com/bitcoinschema/go-map) +- [coocood/freecache](https://github.com/coocood/freecache) +- [gorm.io/gorm](https://gorm.io/gorm) +- [libsv/go-bk](https://github.com/libsv/go-bk) +- [libsv/go-bt](https://github.com/libsv/go-bt) +- [mrz1836/go-cache](https://github.com/mrz1836/go-cache) +- [mrz1836/go-cachestore](https://github.com/mrz1836/go-cachestore) +- [mrz1836/go-datastore](https://github.com/mrz1836/go-datastore) +- [mrz1836/go-logger](https://github.com/mrz1836/go-logger) +- [newrelic/go-agent](https://github.com/newrelic/go-agent) +- [robfig/cron](https://github.com/robfig/cron) +- [stretchr/testify](https://github.com/stretchr/testify) +- [tonicpow/go-minercraft](https://github.com/tonicpow/go-minercraft) +- [bitcoin-sv/go-paymail](https://github.com/bitcoin-sv/go-paymail) +- [vmihailenco/taskq](https://github.com/vmihailenco/taskq) +
+ +
+Library Deployment +
+ +Releases are automatically created when you create a new [git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging)! + +If you want to manually make releases, please install GoReleaser: + +[goreleaser](https://github.com/goreleaser/goreleaser) for easy binary or library deployment to GitHub and can be installed: +- **using make:** `make install-releaser` +- **using brew:** `brew install goreleaser` + +The [.goreleaser.yml](.goreleaser.yml) file is used to configure [goreleaser](https://github.com/goreleaser/goreleaser). + +
+ +### Automatic Releases on Tag Creation (recommended) +Automatic releases via [GitHub Actions](.github/workflows/release.yml) from creating a new tag: +```shell +make tag version=1.2.3 +``` + +
+ +### Manual Releases (optional) +Use `make release-snap` to create a snapshot version of the release, and finally `make release` to ship to production (manually). + +
+ +
+ +
+Makefile Commands +
+ +View all `makefile` commands +```shell script +make help +``` + +List of all current commands: +```text +all Runs multiple commands +clean Remove previous builds and any cached data +clean-mods Remove all the Go mod cache +coverage Shows the test coverage +diff Show the git diff +generate Runs the go generate command in the base of the repo +godocs Sync the latest tag with GoDocs +help Show this help message +install Install the application +install-all-contributors Installs all contributors locally +install-go Install the application (Using Native Go) +install-releaser Install the GoReleaser application +lint Run the golangci-lint application (install if not found) +release Full production release (creates release in GitHub) +release Runs common.release then runs godocs +release-snap Test the full release (build binaries) +release-test Full production test release (everything except deploy) +replace-version Replaces the version in HTML/JS (pre-deploy) +tag Generate a new tag and push (tag version=0.0.0) +tag-remove Remove a tag if found (tag-remove version=0.0.0) +tag-update Update an existing tag to current commit (tag-update version=0.0.0) +test Runs lint and ALL tests +test-all-db Runs all tests including embedded database tests +test-all-db-ci Runs all tests including embedded database tests (CI) +test-ci Runs all tests via CI (exports coverage) +test-ci-no-race Runs all tests via CI (no race) (exports coverage) +test-ci-short Runs unit tests via CI (exports coverage) +test-no-lint Runs just tests +test-short Runs vet, lint and tests (excludes integration tests) +test-unit Runs tests and outputs coverage +uninstall Uninstall the application (and remove files) +update-contributors Regenerates the contributors html/list +update-linter Update the golangci-lint package (macOS only) +vet Run the Go vet application +``` +
+ +
+ +## Usage + +### Examples & Tests + +Checkout all the [examples](examples)! + +All unit tests and [examples](examples) run via [GitHub Actions](https://github.com/bitcoin-sv/spv-wallet/engine/actions) and +uses [Go version 1.19.x](https://golang.org/doc/go1.19). View the [configuration file](.github/workflows/run-tests.yml). + +
+ +Run all unit tests (excluding database tests) +```shell script +make test +``` + +
+ +Run database integration tests +```shell script +make test-all-db +``` + +
+ +Run tests (excluding integration tests) +```shell script +make test-short +``` + +
+ +### Benchmarks +Run the Go benchmarks: +```shell script +make bench +``` + +
+ +## Code Standards +Read more about this Go project's [code standards](.github/CODE_STANDARDS.md). + +
+ +## Usage + +``` +func main() { + client, err := engine.NewClient( + context.Background(), // Set context + ) + if err != nil { + log.Fatalln("error: " + err.Error()) + } + + defer func() { + _ = client.Close(context.Background()) + }() + + log.Println("client loaded!", client.UserAgent()) +} +``` + +Checkout all the [examples](examples)! + +
+ +## Contributing +All kinds of contributions are welcome! +
+To get started, take a look at [code standards](.github/CODE_STANDARDS.md). +
+View the [contributing guidelines](.github/CODE_STANDARDS.md#3-contributing) and follow the [code of conduct](.github/CODE_OF_CONDUCT.md). + +
+ +## License + +[![License](https://img.shields.io/github/license/bitcoin-sv/spv-wallet/engine.svg?style=flat&v=2)](LICENSE) diff --git a/engine/action_access_key.go b/engine/action_access_key.go new file mode 100644 index 000000000..9ae96bae7 --- /dev/null +++ b/engine/action_access_key.go @@ -0,0 +1,213 @@ +package engine + +import ( + "context" + "time" + + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/mrz1836/go-datastore" +) + +// NewAccessKey will create a new access key for the given xpub +// +// opts are options and can include "metadata" +func (c *Client) NewAccessKey(ctx context.Context, rawXpubKey string, opts ...ModelOps) (*AccessKey, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "new_access_key") + + // Validate that the value is an xPub + _, err := utils.ValidateXPub(rawXpubKey) + if err != nil { + return nil, err + } + + // Get the xPub (by key - converts to id) + var xPub *Xpub + if xPub, err = getXpubWithCache( + ctx, c, rawXpubKey, "", // Pass the context and key everytime (for now) + c.DefaultModelOptions()..., // Passing down the Datastore and client information into the model + ); err != nil { + return nil, err + } else if xPub == nil { + return nil, ErrMissingXpub + } + + // Create the model & set the default options (gives options from client->model) + accessKey := newAccessKey( + xPub.ID, c.DefaultModelOptions(append(opts, New())...)..., + ) + + // Save the model + if err = accessKey.Save(ctx); err != nil { + return nil, err + } + + // Return the created model + return accessKey, nil +} + +// GetAccessKey will get an existing access key from the Datastore +func (c *Client) GetAccessKey(ctx context.Context, xPubID, id string) (*AccessKey, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_access_key") + + // Get the access key + accessKey, err := getAccessKey( + ctx, id, + c.DefaultModelOptions()..., + ) + if err != nil { + return nil, err + } else if accessKey == nil { + return nil, ErrAccessKeyNotFound + } + + // make sure this is the correct accessKey + if accessKey.XpubID != xPubID { + return nil, utils.ErrXpubNoMatch + } + + // Return the model + return accessKey, nil +} + +// GetAccessKeys will get all the access keys from the Datastore +func (c *Client) GetAccessKeys(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, queryParams *datastore.QueryParams, opts ...ModelOps, +) ([]*AccessKey, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_access_keys") + + // Get the access keys + accessKeys, err := getAccessKeys( + ctx, metadataConditions, conditions, queryParams, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return nil, err + } + + return accessKeys, nil +} + +// GetAccessKeysCount will get a count of all the access keys from the Datastore +func (c *Client) GetAccessKeysCount(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, opts ...ModelOps, +) (int64, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_access_keys_count") + + // Get the access keys count + count, err := getAccessKeysCount( + ctx, metadataConditions, conditions, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return 0, err + } + + return count, nil +} + +// GetAccessKeysByXPubID will get all existing access keys from the Datastore +// +// metadataConditions is the metadata to match to the access keys being returned +func (c *Client) GetAccessKeysByXPubID(ctx context.Context, xPubID string, metadataConditions *Metadata, + conditions *map[string]interface{}, queryParams *datastore.QueryParams, opts ...ModelOps, +) ([]*AccessKey, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_access_keys") + + // Get the access key + accessKeys, err := getAccessKeysByXPubID( + ctx, + xPubID, + metadataConditions, + conditions, + queryParams, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return nil, err + } else if accessKeys == nil { + return nil, datastore.ErrNoResults + } + + // Return the models + return accessKeys, nil +} + +// GetAccessKeysByXPubIDCount will get a count of all existing access keys from the Datastore +func (c *Client) GetAccessKeysByXPubIDCount(ctx context.Context, xPubID string, metadataConditions *Metadata, + conditions *map[string]interface{}, opts ...ModelOps, +) (int64, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_access_keys") + + // Get the access key + count, err := getAccessKeysByXPubIDCount( + ctx, + xPubID, + metadataConditions, + conditions, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return 0, err + } + + // Return the models + return count, nil +} + +// RevokeAccessKey will revoke an access key by its id +// +// opts are options and can include "metadata" +func (c *Client) RevokeAccessKey(ctx context.Context, rawXpubKey, id string, opts ...ModelOps) (*AccessKey, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "new_access_key") + + // Validate that the value is an xPub + _, err := utils.ValidateXPub(rawXpubKey) + if err != nil { + return nil, err + } + + // Get the xPub (by key - converts to id) + var xPub *Xpub + if xPub, err = getXpubWithCache( + ctx, c, rawXpubKey, "", // Pass the context and key everytime (for now) + c.DefaultModelOptions()..., // Passing down the Datastore and client information into the model + ); err != nil { + return nil, err + } else if xPub == nil { + return nil, ErrMissingXpub + } + + var accessKey *AccessKey + if accessKey, err = getAccessKey( + ctx, id, c.DefaultModelOptions(opts...)..., + ); err != nil { + return nil, err + } + if accessKey == nil { + return nil, ErrMissingAccessKey + } + + // make sure this is the correct accessKey + xPubID := utils.Hash(rawXpubKey) + if accessKey.XpubID != xPubID { + return nil, utils.ErrXpubNoMatch + } + + accessKey.RevokedAt.Valid = true + accessKey.RevokedAt.Time = time.Now() + + // Save the model + if err = accessKey.Save(ctx); err != nil { + return nil, err + } + + // Return the updated model + return accessKey, nil +} diff --git a/engine/action_destination.go b/engine/action_destination.go new file mode 100644 index 000000000..d6df07a76 --- /dev/null +++ b/engine/action_destination.go @@ -0,0 +1,282 @@ +package engine + +import ( + "context" + + "github.com/mrz1836/go-datastore" +) + +// NewDestination will get a new destination for an existing xPub +// +// xPubKey is the raw public xPub +func (c *Client) NewDestination(ctx context.Context, xPubKey string, chain uint32, + destinationType string, opts ...ModelOps, +) (*Destination, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "new_destination") + + // Get the xPub (by key - converts to id) + var xPub *Xpub + var err error + if xPub, err = getXpubWithCache( + ctx, c, xPubKey, "", // Get the xPub by xPubID + c.DefaultModelOptions()..., // Passing down the Datastore and client information into the model + ); err != nil { + return nil, err + } else if xPub == nil { + return nil, ErrMissingXpub + } + + // Get/create a new destination + var destination *Destination + if destination, err = xPub.getNewDestination( + ctx, chain, destinationType, + append(opts, c.DefaultModelOptions()...)..., // Passing down the Datastore and client information into the model + ); err != nil { + return nil, err + } + + // Save the destination + if err = destination.Save(ctx); err != nil { + return nil, err + } + + // Return the model + return destination, nil +} + +// NewDestinationForLockingScript will create a new destination based on a locking script +func (c *Client) NewDestinationForLockingScript(ctx context.Context, xPubID, lockingScript string, + opts ...ModelOps, +) (*Destination, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "new_destination_for_locking_script") + + // Ensure locking script isn't empty + if len(lockingScript) == 0 { + return nil, ErrMissingLockingScript + } + + // Start the new destination - will detect type + destination := newDestination( + xPubID, lockingScript, + append(opts, c.DefaultModelOptions()...)..., // Passing down the Datastore and client information into the model + ) + + if destination.Type == "" { + return nil, ErrUnknownLockingScript + } + + // Save the destination + if err := destination.Save(ctx); err != nil { + return nil, err + } + + // Return the model + return destination, nil +} + +// GetDestinations will get all the destinations from the Datastore +func (c *Client) GetDestinations(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, queryParams *datastore.QueryParams, opts ...ModelOps, +) ([]*Destination, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_destinations") + + // Get the destinations + destinations, err := getDestinations( + ctx, metadataConditions, conditions, queryParams, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return nil, err + } + + return destinations, nil +} + +// GetDestinationsCount will get a count of all the destinations from the Datastore +func (c *Client) GetDestinationsCount(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, opts ...ModelOps, +) (int64, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_destinations_count") + + // Get the destinations count + count, err := getDestinationsCount( + ctx, metadataConditions, conditions, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return 0, err + } + + return count, nil +} + +// GetDestinationsByXpubID will get destinations based on an xPub +// +// metadataConditions are the search criteria used to find destinations +func (c *Client) GetDestinationsByXpubID(ctx context.Context, xPubID string, metadataConditions *Metadata, + conditions *map[string]interface{}, queryParams *datastore.QueryParams, +) ([]*Destination, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_destinations") + + // Get the destinations + destinations, err := getDestinationsByXpubID( + ctx, xPubID, metadataConditions, conditions, queryParams, c.DefaultModelOptions()..., + ) + if err != nil { + return nil, err + } + + return destinations, nil +} + +// GetDestinationsByXpubIDCount will get a count of all destinations based on an xPub +func (c *Client) GetDestinationsByXpubIDCount(ctx context.Context, xPubID string, metadataConditions *Metadata, + conditions *map[string]interface{}, +) (int64, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_destinations") + + // Get the count + count, err := getDestinationsCountByXPubID( + ctx, xPubID, metadataConditions, conditions, c.DefaultModelOptions()..., + ) + if err != nil { + return 0, err + } + + return count, nil +} + +// GetDestinationByID will get a destination by id +func (c *Client) GetDestinationByID(ctx context.Context, xPubID, id string) (*Destination, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_destination_by_id") + + // Get the destination + destination, err := getDestinationWithCache( + ctx, c, id, "", "", c.DefaultModelOptions()..., + ) + if err != nil { + return nil, err + } + + // Check that the id matches + if destination.XpubID != xPubID { + return nil, ErrXpubIDMisMatch + } + + return destination, nil +} + +// GetDestinationByLockingScript will get a destination for a locking script +func (c *Client) GetDestinationByLockingScript(ctx context.Context, xPubID, lockingScript string) (*Destination, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_destination_by_locking_script") + + // Get the destination + destination, err := getDestinationWithCache( + ctx, c, "", "", lockingScript, c.DefaultModelOptions()..., + ) + if err != nil { + return nil, err + } + + // Check that the id matches + if destination.XpubID != xPubID { + return nil, ErrXpubIDMisMatch + } + + return destination, nil +} + +// GetDestinationByAddress will get a destination for an address +func (c *Client) GetDestinationByAddress(ctx context.Context, xPubID, address string) (*Destination, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_destination_by_address") + + // Get the destination + destination, err := getDestinationWithCache( + ctx, c, "", address, "", c.DefaultModelOptions()..., + ) + if err != nil { + return nil, err + } + + // Check that the id matches + if destination.XpubID != xPubID { + return nil, ErrXpubIDMisMatch + } + + return destination, nil +} + +// UpdateDestinationMetadataByID will update the metadata in an existing destination by id +func (c *Client) UpdateDestinationMetadataByID(ctx context.Context, xPubID, id string, + metadata Metadata, +) (*Destination, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "update_destination_by_id") + + // Get the destination + destination, err := c.GetDestinationByID(ctx, xPubID, id) + if err != nil { + return nil, err + } + + // Update and save the model + destination.UpdateMetadata(metadata) + if err = destination.Save(ctx); err != nil { + return nil, err + } + + return destination, nil +} + +// UpdateDestinationMetadataByLockingScript will update the metadata in an existing destination by locking script +func (c *Client) UpdateDestinationMetadataByLockingScript(ctx context.Context, xPubID, + lockingScript string, metadata Metadata, +) (*Destination, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "update_destination_by_locking_script") + + // Get the destination + destination, err := c.GetDestinationByLockingScript(ctx, xPubID, lockingScript) + if err != nil { + return nil, err + } + + // Update and save the metadata + destination.UpdateMetadata(metadata) + if err = destination.Save(ctx); err != nil { + return nil, err + } + + return destination, nil +} + +// UpdateDestinationMetadataByAddress will update the metadata in an existing destination by address +func (c *Client) UpdateDestinationMetadataByAddress(ctx context.Context, xPubID, address string, + metadata Metadata, +) (*Destination, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "update_destination_by_address") + + // Get the destination + destination, err := c.GetDestinationByAddress(ctx, xPubID, address) + if err != nil { + return nil, err + } + + // Update and save the metadata + destination.UpdateMetadata(metadata) + if err = destination.Save(ctx); err != nil { + return nil, err + } + + return destination, nil +} diff --git a/engine/action_destination_test.go b/engine/action_destination_test.go new file mode 100644 index 000000000..b556c49ea --- /dev/null +++ b/engine/action_destination_test.go @@ -0,0 +1,385 @@ +package engine + +import ( + "context" + "testing" + + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestClient_NewDestination will test the method NewDestination() +func (ts *EmbeddedDBTestSuite) TestClient_NewDestination() { + for _, testCase := range dbTestCases { + + ts.T().Run(testCase.name+" - valid", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + ctx := context.Background() + + _, err := tc.client.NewXpub(ctx, testXPub, tc.client.DefaultModelOptions()...) + assert.NoError(t, err) + + metadata := map[string]interface{}{ + "test-key": "test-value", + } + opts := append(tc.client.DefaultModelOptions(), WithMetadatas(metadata)) + + var destination *Destination + destination, err = tc.client.NewDestination( + ctx, testXPub, utils.ChainExternal, utils.ScriptTypePubKeyHash, opts..., + ) + assert.NoError(t, err) + assert.Equal(t, "fc1e635d98151c6008f29908ee2928c60c745266f9853e945c917b1baa05973e", destination.ID) + assert.Equal(t, testXPubID, destination.XpubID) + assert.Equal(t, utils.ScriptTypePubKeyHash, destination.Type) + assert.Equal(t, utils.ChainExternal, destination.Chain) + assert.Equal(t, uint32(0), destination.Num) + assert.Equal(t, testExternalAddress, destination.Address) + assert.Equal(t, "test-value", destination.Metadata["test-key"]) + + destination2, err2 := tc.client.NewDestination( + ctx, testXPub, utils.ChainExternal, utils.ScriptTypePubKeyHash, opts..., + ) + assert.NoError(t, err2) + assert.Equal(t, testXPubID, destination2.XpubID) + // assert.Equal(t, "1234567", destination2.Metadata[ReferenceIDField]) + assert.Equal(t, utils.ScriptTypePubKeyHash, destination2.Type) + assert.Equal(t, utils.ChainExternal, destination2.Chain) + assert.Equal(t, uint32(1), destination2.Num) + assert.NotEqual(t, testExternalAddress, destination2.Address) + assert.Equal(t, "test-value", destination2.Metadata["test-key"]) + }) + + ts.T().Run(testCase.name+" - error - missing xpub", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + metadata := map[string]interface{}{ + "test-key": "test-value", + } + opts := append(tc.client.DefaultModelOptions(), WithMetadatas(metadata)) + + destination, err := tc.client.NewDestination( + context.Background(), testXPub, utils.ChainExternal, + utils.ScriptTypePubKeyHash, opts..., + ) + require.Error(t, err) + require.Nil(t, destination) + assert.ErrorIs(t, err, ErrMissingXpub) + }) + } +} + +// TestClient_NewDestinationForLockingScript will test the method NewDestinationForLockingScript() +func (ts *EmbeddedDBTestSuite) TestClient_NewDestinationForLockingScript() { + for _, testCase := range dbTestCases { + + ts.T().Run(testCase.name+" - valid", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + _, err := tc.client.NewXpub(tc.ctx, testXPub, tc.client.DefaultModelOptions()...) + assert.NoError(t, err) + + lockingScript := "14c91e5cc393bb9d6da3040a7c72b4b569b237e450517901687f517f7c76767601ff9c636d75587f7c6701fe" + + "9c636d547f7c6701fd9c6375527f7c67686868817f7b6d517f7c7f77605b955f937f517f787f517f787f567f01147f527f7577" + + "7e777e7b7c7e7b7c7ea77b885279887601447f01207f75776baa517f7c818b7c7e263044022079be667ef9dcbbac55a06295ce" + + "870b07029bfcdb2dce28d959f2815b16f8179802207c7e01417e2102b405d7f0322a89d0f9f3a98e6f938fdc1c969a8d1382a2" + + "bf66a71ae74a1e83b0ad046d6574612102b8e6b4441609460d1605ce328d7a39e7216050e105738725b05b7b542dcf1f51205f" + + "a8b5671a8b577a44ea2d1e70ca9c291145d3da3a7c649fc4e9ea389a8053646c886d76a9146e12c6d84b06757bd4316c33cac4" + + "4e1e5965589088ac6a0b706172656e74206e6f6465" + + metadata := map[string]interface{}{"test_key": "test_value"} + opts := append(tc.client.DefaultModelOptions(), WithMetadatas(metadata)) + + var destination *Destination + destination, err = tc.client.NewDestinationForLockingScript( + tc.ctx, testXPubID, lockingScript, opts..., + ) + assert.NoError(t, err) + assert.Equal(t, "a64c7aca7110c7cde92245252a58bb18a4317381fc31fc293f6aafa3fcc7019f", destination.ID) + assert.Equal(t, testXPubID, destination.XpubID) + assert.Equal(t, utils.ScriptTypeNonStandard, destination.Type) + assert.Equal(t, utils.ChainExternal, destination.Chain) + assert.Equal(t, uint32(0), destination.Num) + assert.Equal(t, "test_value", destination.Metadata["test_key"]) + }) + + ts.T().Run(testCase.name+" - error - missing locking script", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + metadata := map[string]interface{}{"test_key": "test_value"} + opts := append(tc.client.DefaultModelOptions(), WithMetadatas(metadata)) + + destination, err := tc.client.NewDestinationForLockingScript( + tc.ctx, testXPubID, "", + opts..., + ) + require.Error(t, err) + require.Nil(t, destination) + assert.ErrorIs(t, err, ErrMissingLockingScript) + }) + } +} + +// TestClient_GetDestinations will test the method GetDestinationsByXpubID() +func (ts *EmbeddedDBTestSuite) TestClient_GetDestinations() { + for _, testCase := range dbTestCases { + ts.T().Run(testCase.name+" - valid", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + _, _, rawKey := CreateNewXPub(tc.ctx, t, tc.client) + xPubID := utils.Hash(rawKey) + + metadata := map[string]interface{}{ + ReferenceIDField: testReferenceID, + testMetadataKey: testMetadataValue, + } + opts := append(tc.client.DefaultModelOptions(), WithMetadatas(metadata)) + + // Create a new destination + destination, err := tc.client.NewDestination( + tc.ctx, rawKey, utils.ChainExternal, utils.ScriptTypePubKeyHash, + opts..., + ) + require.NoError(t, err) + require.NotNil(t, destination) + + var getDestinations []*Destination + getDestinations, err = tc.client.GetDestinationsByXpubID( + tc.ctx, xPubID, nil, nil, nil, + ) + require.NoError(t, err) + require.NotNil(t, getDestinations) + assert.Equal(t, 1, len(getDestinations)) + assert.Equal(t, destination.Address, getDestinations[0].Address) + assert.Equal(t, testReferenceID, getDestinations[0].Metadata[ReferenceIDField]) + assert.Equal(t, destination.XpubID, getDestinations[0].XpubID) + }) + + ts.T().Run(testCase.name+" - no destinations found", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + _, _, rawKey := CreateNewXPub(tc.ctx, t, tc.client) + + metadata := map[string]interface{}{testMetadataKey: testMetadataValue} + opts := append(tc.client.DefaultModelOptions(), WithMetadatas(metadata)) + + // Create a new destination + destination, err := tc.client.NewDestination( + tc.ctx, rawKey, utils.ChainExternal, utils.ScriptTypePubKeyHash, + opts..., + ) + require.NoError(t, err) + require.NotNil(t, destination) + + // use the wrong xpub + var getDestinations []*Destination + getDestinations, err = tc.client.GetDestinationsByXpubID( + tc.ctx, testXPubID, nil, nil, nil, + ) + require.NoError(t, err) + assert.Equal(t, 0, len(getDestinations)) + }) + } +} + +// TestClient_GetDestinationByAddress will test the method GetDestinationByAddress() +func (ts *EmbeddedDBTestSuite) TestClient_GetDestinationByAddress() { + for _, testCase := range dbTestCases { + ts.T().Run(testCase.name+" - valid", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + _, _, rawKey := CreateNewXPub(tc.ctx, t, tc.client) + xPubID := utils.Hash(rawKey) + + metadata := map[string]interface{}{ + ReferenceIDField: testReferenceID, + testMetadataKey: testMetadataValue, + } + opts := append(tc.client.DefaultModelOptions(), WithMetadatas(metadata)) + + // Create a new destination + destination, err := tc.client.NewDestination( + tc.ctx, rawKey, utils.ChainExternal, utils.ScriptTypePubKeyHash, + opts..., + ) + require.NoError(t, err) + require.NotNil(t, destination) + + var getDestination *Destination + getDestination, err = tc.client.GetDestinationByAddress( + tc.ctx, xPubID, destination.Address, + ) + require.NoError(t, err) + require.NotNil(t, getDestination) + assert.Equal(t, destination.Address, getDestination.Address) + assert.Equal(t, testReferenceID, getDestination.Metadata[ReferenceIDField]) + assert.Equal(t, destination.XpubID, getDestination.XpubID) + }) + + ts.T().Run(testCase.name+" - invalid xpub", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + _, _, rawKey := CreateNewXPub(tc.ctx, t, tc.client) + + metadata := map[string]interface{}{testMetadataKey: testMetadataValue} + opts := append(tc.client.DefaultModelOptions(), WithMetadatas(metadata)) + + // Create a new destination + destination, err := tc.client.NewDestination( + tc.ctx, rawKey, utils.ChainExternal, utils.ScriptTypePubKeyHash, + opts..., + ) + require.NoError(t, err) + require.NotNil(t, destination) + + // use the wrong xpub + var getDestination *Destination + getDestination, err = tc.client.GetDestinationByAddress( + tc.ctx, testXPubID, destination.Address, + ) + require.Error(t, err) + require.Nil(t, getDestination) + }) + } +} + +// TestClient_GetDestinationByLockingScript will test the method GetDestinationByLockingScript() +func (ts *EmbeddedDBTestSuite) TestClient_GetDestinationByLockingScript() { + for _, testCase := range dbTestCases { + ts.T().Run(testCase.name+" - valid", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + _, _, rawKey := CreateNewXPub(tc.ctx, t, tc.client) + xPubID := utils.Hash(rawKey) + + metadata := map[string]interface{}{ + ReferenceIDField: testReferenceID, + testMetadataKey: testMetadataValue, + } + opts := append(tc.client.DefaultModelOptions(), WithMetadatas(metadata)) + + // Create a new destination + destination, err := tc.client.NewDestination( + tc.ctx, rawKey, utils.ChainExternal, utils.ScriptTypePubKeyHash, + opts..., + ) + require.NoError(t, err) + require.NotNil(t, destination) + + var getDestination *Destination + getDestination, err = tc.client.GetDestinationByLockingScript( + tc.ctx, xPubID, destination.LockingScript, + ) + require.NoError(t, err) + require.NotNil(t, getDestination) + assert.Equal(t, destination.Address, getDestination.Address) + assert.Equal(t, destination.LockingScript, getDestination.LockingScript) + assert.Equal(t, testReferenceID, getDestination.Metadata[ReferenceIDField]) + assert.Equal(t, destination.XpubID, getDestination.XpubID) + }) + + ts.T().Run(testCase.name+" - invalid xpub", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + _, _, rawKey := CreateNewXPub(tc.ctx, t, tc.client) + + metadata := map[string]interface{}{testMetadataKey: testMetadataValue} + opts := append(tc.client.DefaultModelOptions(), WithMetadatas(metadata)) + + // Create a new destination + destination, err := tc.client.NewDestination( + tc.ctx, rawKey, utils.ChainExternal, utils.ScriptTypePubKeyHash, + opts..., + ) + require.NoError(t, err) + require.NotNil(t, destination) + + // use the wrong xpub + var getDestination *Destination + getDestination, err = tc.client.GetDestinationByLockingScript( + tc.ctx, testXPubID, destination.LockingScript, + ) + require.Error(t, err) + require.Nil(t, getDestination) + }) + } +} + +// TestClient_UpdateDestinationMetadata will test the method UpdateDestinationMetadata() +func (ts *EmbeddedDBTestSuite) TestClient_UpdateDestinationMetadata() { + for _, testCase := range dbTestCases { + ts.T().Run(testCase.name+" - valid", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + _, _, rawKey := CreateNewXPub(tc.ctx, t, tc.client) + + metadata := Metadata{ + "test-key-1": "test-value-1", + "test-key-2": "test-value-2", + "test-key-3": "test-value-3", + } + opts := tc.client.DefaultModelOptions() + opts = append(opts, WithMetadatas(metadata)) + destination, err := tc.client.NewDestination( + tc.ctx, rawKey, utils.ChainExternal, utils.ScriptTypePubKeyHash, + opts..., + ) + require.NoError(t, err) + require.NotNil(t, destination) + assert.Equal(t, metadata, destination.Metadata) + + destination, err = tc.client.UpdateDestinationMetadataByID(tc.ctx, destination.XpubID, destination.ID, Metadata{"test-key-new": "new-value"}) + require.NoError(t, err) + assert.Len(t, destination.Metadata, 4) + assert.Equal(t, "new-value", destination.Metadata["test-key-new"]) + + destination, err = tc.client.UpdateDestinationMetadataByAddress(tc.ctx, + destination.XpubID, destination.Address, Metadata{ + "test-key-new-2": "new-value-2", + "test-key-1": nil, + "test-key-2": nil, + "test-key-3": nil, + }, + ) + require.NoError(t, err) + assert.Len(t, destination.Metadata, 2) + assert.Equal(t, "new-value", destination.Metadata["test-key-new"]) + assert.Equal(t, "new-value-2", destination.Metadata["test-key-new-2"]) + + destination, err = tc.client.UpdateDestinationMetadataByLockingScript(tc.ctx, + destination.XpubID, destination.LockingScript, Metadata{ + "test-key-new-5": "new-value-5", + }, + ) + require.NoError(t, err) + assert.Len(t, destination.Metadata, 3) + assert.Equal(t, "new-value", destination.Metadata["test-key-new"]) + assert.Equal(t, "new-value-2", destination.Metadata["test-key-new-2"]) + assert.Equal(t, "new-value-5", destination.Metadata["test-key-new-5"]) + + err = destination.Save(tc.ctx) + require.NoError(t, err) + + // make sure it was saved + destination2, err2 := tc.client.GetDestinationByID(tc.ctx, destination.XpubID, destination.ID) + require.NoError(t, err2) + assert.Len(t, destination2.Metadata, 3) + assert.Equal(t, "new-value", destination2.Metadata["test-key-new"]) + assert.Equal(t, "new-value-2", destination2.Metadata["test-key-new-2"]) + assert.Equal(t, "new-value-5", destination2.Metadata["test-key-new-5"]) + }) + } +} diff --git a/engine/action_draft_transaction.go b/engine/action_draft_transaction.go new file mode 100644 index 000000000..dfdf79243 --- /dev/null +++ b/engine/action_draft_transaction.go @@ -0,0 +1,61 @@ +package engine + +import ( + "context" + + "github.com/mrz1836/go-datastore" +) + +// GetDraftTransactionByID will get a draft transaction from the Datastore +func (c *Client) GetDraftTransactionByID(ctx context.Context, id string, opts ...ModelOps) (*DraftTransaction, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_draft_transactions") + + // Get the draft transactions + draftTransaction, err := getDraftTransactionID( + ctx, "", id, c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return nil, err + } + + return draftTransaction, nil +} + +// GetDraftTransactions will get all the draft transactions from the Datastore +func (c *Client) GetDraftTransactions(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, queryParams *datastore.QueryParams, opts ...ModelOps, +) ([]*DraftTransaction, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_draft_transactions") + + // Get the draft transactions + draftTransactions, err := getDraftTransactions( + ctx, metadataConditions, conditions, queryParams, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return nil, err + } + + return draftTransactions, nil +} + +// GetDraftTransactionsCount will get a count of all the draft transactions from the Datastore +func (c *Client) GetDraftTransactionsCount(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, opts ...ModelOps, +) (int64, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_draft_transactions_count") + + // Get the draft transactions count + count, err := getDraftTransactionsCount( + ctx, metadataConditions, conditions, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return 0, err + } + + return count, nil +} diff --git a/engine/action_paymails.go b/engine/action_paymails.go new file mode 100644 index 000000000..5d20fc945 --- /dev/null +++ b/engine/action_paymails.go @@ -0,0 +1,220 @@ +package engine + +import ( + "context" + "errors" + "time" + + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/mrz1836/go-datastore" +) + +// GetPaymailAddress will get a paymail address model +func (c *Client) GetPaymailAddress(ctx context.Context, address string, opts ...ModelOps) (*PaymailAddress, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_paymail_address") + + // Get the paymail address + paymailAddress, err := getPaymailAddress(ctx, address, append(opts, c.DefaultModelOptions()...)...) + if err != nil { + return nil, err + } else if paymailAddress == nil { + return nil, ErrPaymailNotFound + } + + return paymailAddress, nil +} + +// GetPaymailAddresses will get all the paymail addresses from the Datastore +func (c *Client) GetPaymailAddresses(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, queryParams *datastore.QueryParams, opts ...ModelOps, +) ([]*PaymailAddress, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_paymail_addresses") + + // Get the paymail address + paymailAddresses, err := getPaymailAddresses( + ctx, metadataConditions, conditions, queryParams, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return nil, err + } + + return paymailAddresses, nil +} + +// GetPaymailAddressesCount will get a count of all the paymail addresses from the Datastore +func (c *Client) GetPaymailAddressesCount(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, opts ...ModelOps, +) (int64, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_paymail_addresses_count") + + // Get the paymail address + count, err := getPaymailAddressesCount( + ctx, metadataConditions, conditions, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return 0, err + } + + return count, nil +} + +// GetPaymailAddressesByXPubID will get all the paymail addresses for an xPubID from the Datastore +func (c *Client) GetPaymailAddressesByXPubID(ctx context.Context, xPubID string, metadataConditions *Metadata, + conditions *map[string]interface{}, queryParams *datastore.QueryParams, +) ([]*PaymailAddress, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_paymail_by_xpub") + + if conditions == nil { + *conditions = make(map[string]interface{}) + } + // add the xpub_id to the conditions + (*conditions)["xpub_id"] = xPubID + + // Get the paymail address + paymailAddresses, err := getPaymailAddresses( + ctx, metadataConditions, conditions, queryParams, + c.DefaultModelOptions()..., + ) + if err != nil { + return nil, err + } + + return paymailAddresses, nil +} + +// NewPaymailAddress will create a new paymail address +func (c *Client) NewPaymailAddress(ctx context.Context, xPubKey, address, publicName, avatar string, + opts ...ModelOps, +) (*PaymailAddress, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "new_paymail_address") + + // Get the xPub (make sure it exists) + _, err := getXpubWithCache(ctx, c, xPubKey, "", c.DefaultModelOptions()...) + if err != nil { + return nil, err + } + + // Check if the paymail address already exists + paymail, err := getPaymailAddress(ctx, address, opts...) + if paymail != nil { + return nil, errors.New("paymail address already exists") + } + if err != nil { + return nil, err + } + + // Start the new paymail address model + paymailAddress := newPaymail( + address, + append(opts, c.DefaultModelOptions( + New(), + WithXPub(xPubKey), + )...)..., + ) + + // Set the optional fields + paymailAddress.Avatar = avatar + paymailAddress.PublicName = publicName + + // Save the model + if err = paymailAddress.Save(ctx); err != nil { + return nil, err + } + return paymailAddress, nil +} + +// DeletePaymailAddress will delete a paymail address +func (c *Client) DeletePaymailAddress(ctx context.Context, address string, opts ...ModelOps) error { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "delete_paymail_address") + + // Get the paymail address + paymailAddress, err := getPaymailAddress(ctx, address, append(opts, c.DefaultModelOptions()...)...) + if err != nil { + return err + } else if paymailAddress == nil { + return ErrPaymailNotFound + } + + // todo: make a better approach for deleting paymail addresses? + var randomString string + if randomString, err = utils.RandomHex(16); err != nil { + return err + } + + // We will do a soft delete to make sure we still have the history for this address + // setting the Domain to a random string solved the problem of the unique index on Alias/Domain + // todo: figure out a different approach - history table? + paymailAddress.Alias = paymailAddress.Alias + "@" + paymailAddress.Domain + paymailAddress.Domain = randomString + paymailAddress.DeletedAt.Valid = true + paymailAddress.DeletedAt.Time = time.Now() + + return paymailAddress.Save(ctx) +} + +// UpdatePaymailAddressMetadata will update the metadata in an existing paymail address +func (c *Client) UpdatePaymailAddressMetadata(ctx context.Context, address string, + metadata Metadata, opts ...ModelOps, +) (*PaymailAddress, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "update_paymail_address_metadata") + + // Get the paymail address + paymailAddress, err := getPaymailAddress(ctx, address, append(opts, c.DefaultModelOptions()...)...) + if err != nil { + return nil, err + } else if paymailAddress == nil { + return nil, ErrPaymailNotFound + } + + // Update the metadata + paymailAddress.UpdateMetadata(metadata) + + // Save the model + if err = paymailAddress.Save(ctx); err != nil { + return nil, err + } + + return paymailAddress, nil +} + +// UpdatePaymailAddress will update optional fields of the paymail address +func (c *Client) UpdatePaymailAddress(ctx context.Context, address, publicName, avatar string, + opts ...ModelOps, +) (*PaymailAddress, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "update_paymail_address") + + // Get the paymail address + paymailAddress, err := getPaymailAddress(ctx, address, append(opts, c.DefaultModelOptions()...)...) + if err != nil { + return nil, err + } else if paymailAddress == nil { + return nil, ErrPaymailNotFound + } + + // Update the public name + if paymailAddress.PublicName != publicName { + paymailAddress.PublicName = publicName + } + + // Update the avatar + if paymailAddress.Avatar != avatar { + paymailAddress.Avatar = avatar + } + + // Save the model + if err = paymailAddress.Save(ctx); err != nil { + return nil, err + } + + return paymailAddress, nil +} diff --git a/engine/action_paymails_test.go b/engine/action_paymails_test.go new file mode 100644 index 000000000..93dcaf436 --- /dev/null +++ b/engine/action_paymails_test.go @@ -0,0 +1,213 @@ +package engine + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + externalXPubID = "xpub69PUyEkuD8cqyA9ekUkp3FwaeW1uyLxbwybEy3bmyD7mM6zShsJqfRCv12B43h6KiEiZgF3BFSMnYLsVZr526n37qsqVXkPKYWQ8En2xbi1" + testAvatar = "https://i.imgur.com/MYSVX44.png" + testAvatar2 = "https://i.imgur.com/cBJKPDh.png" + testPaymail = "paymail@tester.com" + testPublicName = "Public Name" +) + +// TestClient_NewPaymailAddress will test the method NewPaymailAddress() +func (ts *EmbeddedDBTestSuite) TestClient_NewPaymailAddress() { + for _, testCase := range dbTestCases { + ts.T().Run(testCase.name+" - empty address", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + // Create xPub (required to add a paymail address) + xPub, err := tc.client.NewXpub(tc.ctx, testXPub, tc.client.DefaultModelOptions()...) + require.NotNil(t, xPub) + require.NoError(t, err) + + var paymailAddress *PaymailAddress + paymailAddress, err = tc.client.NewPaymailAddress(tc.ctx, testXPub, "", testPublicName, testAvatar, tc.client.DefaultModelOptions()...) + require.ErrorIs(t, err, ErrMissingPaymailAddress) + require.Nil(t, paymailAddress) + }) + + ts.T().Run(testCase.name+" - new paymail address", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + opts := tc.client.DefaultModelOptions() + + // Create xPub (required to add a paymail address) + xPub, err := tc.client.NewXpub(tc.ctx, testXPub, opts...) + require.NotNil(t, xPub) + require.NoError(t, err) + + var paymailAddress *PaymailAddress + paymailAddress, err = tc.client.NewPaymailAddress(tc.ctx, xPub.RawXpub(), testPaymail, testPublicName, testAvatar, opts...) + require.NoError(t, err) + require.NotNil(t, paymailAddress) + + assert.Equal(t, "paymail", paymailAddress.Alias) + assert.Equal(t, "tester.com", paymailAddress.Domain) + assert.Equal(t, testAvatar, paymailAddress.Avatar) + assert.Equal(t, testPublicName, paymailAddress.PublicName) + assert.Equal(t, testXPubID, paymailAddress.XpubID) + assert.Equal(t, externalXPubID, paymailAddress.ExternalXpubKey) + + var p2 *PaymailAddress + p2, err = getPaymailAddress(tc.ctx, testPaymail, opts...) + require.NoError(t, err) + require.NotNil(t, p2) + + assert.Equal(t, "paymail", p2.Alias) + assert.Equal(t, "tester.com", p2.Domain) + assert.Equal(t, testAvatar, p2.Avatar) + assert.Equal(t, testPublicName, p2.PublicName) + assert.Equal(t, testXPubID, p2.XpubID) + assert.Equal(t, externalXPubID, p2.ExternalXpubKey) + }) + } +} + +// Test_DeletePaymailAddress will test the method DeletePaymailAddress() +func (ts *EmbeddedDBTestSuite) Test_DeletePaymailAddress() { + for _, testCase := range dbTestCases { + + ts.T().Run(testCase.name+" - empty", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + paymail := "" + err := tc.client.DeletePaymailAddress(tc.ctx, paymail, tc.client.DefaultModelOptions()...) + require.ErrorIs(t, err, ErrPaymailNotFound) + }) + + ts.T().Run(testCase.name+" - delete unknown paymail address", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + err := tc.client.DeletePaymailAddress(tc.ctx, testPaymail, tc.client.DefaultModelOptions()...) + require.ErrorIs(t, err, ErrPaymailNotFound) + }) + + ts.T().Run(testCase.name+" - delete paymail address", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + opts := tc.client.DefaultModelOptions() + + // Create xPub (required to add a paymail address) + xPub, err := tc.client.NewXpub(tc.ctx, testXPub, opts...) + require.NotNil(t, xPub) + require.NoError(t, err) + + var paymailAddress *PaymailAddress + paymailAddress, err = tc.client.NewPaymailAddress(tc.ctx, testXPub, testPaymail, testPublicName, testAvatar, opts...) + require.NoError(t, err) + require.NotNil(t, paymailAddress) + + err = tc.client.DeletePaymailAddress(tc.ctx, testPaymail, opts...) + require.NoError(t, err) + + var p2 *PaymailAddress + p2, err = getPaymailAddress(tc.ctx, testPaymail, opts...) + require.NoError(t, err) + require.Nil(t, p2) + + var p3 *PaymailAddress + p3, err = getPaymailAddressByID(tc.ctx, paymailAddress.ID, opts...) + require.NoError(t, err) + require.NotNil(t, p3) + require.Equal(t, testPaymail, p3.Alias) + require.True(t, p3.DeletedAt.Valid) + }) + } +} + +// TestClient_UpdatePaymailAddressMetadata will test the method UpdatePaymailAddressMetadata() +func (ts *EmbeddedDBTestSuite) TestClient_UpdatePaymailAddressMetadata() { + for _, testCase := range dbTestCases { + ts.T().Run(testCase.name+" - valid", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + metadata := Metadata{ + "test-key-1": "test-value-1", + "test-key-2": "test-value-2", + "test-key-3": "test-value-3", + } + opts := tc.client.DefaultModelOptions() + opts = append(opts, WithMetadatas(metadata)) + + // Create xPub (required to add a paymail address) + xPub, err := tc.client.NewXpub(tc.ctx, testXPub, opts...) + require.NotNil(t, xPub) + require.NoError(t, err) + + var paymailAddress *PaymailAddress + paymailAddress, err = tc.client.NewPaymailAddress(tc.ctx, testXPub, testPaymail, testPublicName, testAvatar, opts...) + require.NoError(t, err) + require.NotNil(t, paymailAddress) + + paymailAddress, err = tc.client.UpdatePaymailAddressMetadata(tc.ctx, testPaymail, Metadata{"test-key-new": "new-value"}, opts...) + require.NoError(t, err) + assert.Len(t, paymailAddress.Metadata, 4) + assert.Equal(t, "new-value", paymailAddress.Metadata["test-key-new"]) + + paymailAddress, err = tc.client.UpdatePaymailAddressMetadata(tc.ctx, testPaymail, Metadata{ + "test-key-new-2": "new-value-2", + "test-key-1": nil, + "test-key-2": nil, + "test-key-3": nil, + }, opts...) + require.NoError(t, err) + assert.Len(t, paymailAddress.Metadata, 2) + assert.Equal(t, "new-value", paymailAddress.Metadata["test-key-new"]) + assert.Equal(t, "new-value-2", paymailAddress.Metadata["test-key-new-2"]) + + var p2 *PaymailAddress + p2, err = getPaymailAddress(tc.ctx, testPaymail, opts...) + require.NoError(t, err) + require.NotNil(t, p2) + assert.Len(t, paymailAddress.Metadata, 2) + }) + } +} + +// TestClient_UpdatePaymailAddress will test the method UpdatePaymailAddress() +func (ts *EmbeddedDBTestSuite) TestClient_UpdatePaymailAddress() { + for _, testCase := range dbTestCases { + ts.T().Run(testCase.name+" - valid", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + opts := tc.client.DefaultModelOptions() + + // Create xPub (required to add a paymail address) + xPub, err := tc.client.NewXpub(tc.ctx, testXPub, opts...) + require.NotNil(t, xPub) + require.NoError(t, err) + + var paymailAddress *PaymailAddress + paymailAddress, err = tc.client.NewPaymailAddress(tc.ctx, testXPub, testPaymail, testPublicName, testAvatar, opts...) + require.NoError(t, err) + require.NotNil(t, paymailAddress) + assert.Equal(t, testPublicName, paymailAddress.PublicName) + assert.Equal(t, testAvatar, paymailAddress.Avatar) + + paymailAddress, err = tc.client.UpdatePaymailAddress(tc.ctx, testPaymail, testPublicName+"2", testAvatar2, opts...) + require.NoError(t, err) + + assert.Equal(t, testPublicName+"2", paymailAddress.PublicName) + assert.Equal(t, testAvatar2, paymailAddress.Avatar) + + var p2 *PaymailAddress + p2, err = getPaymailAddress(tc.ctx, testPaymail, tc.client.DefaultModelOptions()...) + require.NoError(t, err) + require.NotNil(t, p2) + assert.Equal(t, testPublicName+"2", p2.PublicName) + assert.Equal(t, testAvatar2, p2.Avatar) + }) + } +} diff --git a/engine/action_transaction.go b/engine/action_transaction.go new file mode 100644 index 000000000..d4db61fa5 --- /dev/null +++ b/engine/action_transaction.go @@ -0,0 +1,431 @@ +package engine + +import ( + "context" + "errors" + "fmt" + "math" + "time" + + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/libsv/go-bc" + "github.com/libsv/go-bt" + "github.com/mrz1836/go-datastore" +) + +// RecordTransaction will parse the transaction and save it into the Datastore +// +// Internal (known) transactions: there is a corresponding `draft_transaction` via `draft_id` +// External (known) transactions: there are output(s) related to the destination `reference_id`, tx is valid (mempool/on-chain) +// External (unknown) transactions: no reference id but some output(s) match known outputs, tx is valid (mempool/on-chain) +// Unknown transactions: no matching outputs, tx will be disregarded +// +// xPubKey is the raw public xPub +// txHex is the raw transaction hex +// draftID is the unique draft id from a previously started New() transaction (draft_transaction.ID) +// opts are model options and can include "metadata" +func (c *Client) RecordTransaction(ctx context.Context, xPubKey, txHex, draftID string, opts ...ModelOps) (*Transaction, error) { + ctx = c.GetOrStartTxn(ctx, "record_transaction") + + rts, err := getRecordTxStrategy(ctx, c, xPubKey, txHex, draftID) + if err != nil { + return nil, err + } + + return recordTransaction(ctx, c, rts, opts...) +} + +// RecordRawTransaction will parse the transaction and save it into the Datastore directly, without any checks or broadcast but spv wallet engine will ask network for information if transaction was mined +// The transaction is treat as external incoming transaction - transaction without a draft +// Only use this function when you know what you are doing! +// +// txHex is the raw transaction hex +// opts are model options and can include "metadata" +func (c *Client) RecordRawTransaction(ctx context.Context, txHex string, + opts ...ModelOps, +) (*Transaction, error) { + ctx = c.GetOrStartTxn(ctx, "record_raw_transaction") + + return saveRawTransaction(ctx, c, true, txHex, opts...) +} + +// NewTransaction will create a new draft transaction and return it +// +// ctx is the context +// rawXpubKey is the raw xPub key +// config is the TransactionConfig +// metadata is added to the model +// opts are additional model options to be applied +func (c *Client) NewTransaction(ctx context.Context, rawXpubKey string, config *TransactionConfig, + opts ...ModelOps, +) (*DraftTransaction, error) { + // Check for existing NewRelic draftTransaction + ctx = c.GetOrStartTxn(ctx, "new_transaction") + + // Create the lock and set the release for after the function completes + unlock, err := newWaitWriteLock( + ctx, fmt.Sprintf(lockKeyProcessXpub, utils.Hash(rawXpubKey)), c.Cachestore(), + ) + defer unlock() + if err != nil { + return nil, err + } + + // Create the draft tx model + draftTransaction := newDraftTransaction( + rawXpubKey, config, + c.DefaultModelOptions(append(opts, New())...)..., + ) + + // Save the model + if err = draftTransaction.Save(ctx); err != nil { + return nil, err + } + + // Return the created model + return draftTransaction, nil +} + +// 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") + + // Get the transaction by ID + transaction, err := getTransactionByID( + ctx, xPubID, txID, c.DefaultModelOptions()..., + ) + if err != nil { + return nil, err + } + if transaction == nil { + return nil, ErrMissingTransaction + } + + return transaction, nil +} + +// 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) + + // Get the transactions by it's IDs + transactions, err := getTransactions( + ctx, nil, conditions, nil, + c.DefaultModelOptions()..., + ) + if err != nil { + return nil, err + } + + return transactions, nil +} + +// GetTransactionByHex will get a transaction from the Datastore by its full hex string +// uses GetTransaction +func (c *Client) GetTransactionByHex(ctx context.Context, hex string) (*Transaction, error) { + tx, err := bt.NewTxFromString(hex) + if err != nil { + return nil, err + } + + return c.GetTransaction(ctx, "", tx.GetTxID()) +} + +// GetTransactions will get all the transactions from the Datastore +func (c *Client) GetTransactions(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, queryParams *datastore.QueryParams, opts ...ModelOps, +) ([]*Transaction, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_transactions") + + // Get the transactions + transactions, err := getTransactions( + ctx, metadataConditions, conditions, queryParams, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return nil, err + } + + return transactions, 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, +) (int64, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_transactions_count") + + // Get the transactions count + count, err := getTransactionsCount( + ctx, metadataConditions, conditions, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return 0, err + } + + return count, nil +} + +// GetTransactionsByXpubID will get all transactions for a given xpub from the Datastore +// +// ctx is the context +// rawXpubKey is the raw xPub key +// metadataConditions is added to the request for searching +// conditions is added the request for searching +func (c *Client) GetTransactionsByXpubID(ctx context.Context, xPubID string, metadataConditions *Metadata, + conditions *map[string]interface{}, queryParams *datastore.QueryParams, +) ([]*Transaction, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_transaction") + + // Get the transaction by ID + // todo: add queryParams for: page size and page (right now it is unlimited) + transactions, err := getTransactionsByXpubID( + ctx, xPubID, metadataConditions, conditions, queryParams, + c.DefaultModelOptions()..., + ) + if err != nil { + return nil, err + } + + return transactions, nil +} + +// GetTransactionsByXpubIDCount will get the count of all transactions matching the search criteria +func (c *Client) GetTransactionsByXpubIDCount(ctx context.Context, xPubID string, metadataConditions *Metadata, + conditions *map[string]interface{}, +) (int64, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "count_transactions") + + count, err := getTransactionsCountByXpubID( + ctx, xPubID, metadataConditions, conditions, + c.DefaultModelOptions()..., + ) + if err != nil { + return 0, err + } + + return count, nil +} + +// UpdateTransactionMetadata will update the metadata in an existing transaction +func (c *Client) UpdateTransactionMetadata(ctx context.Context, xPubID, id string, + metadata Metadata, +) (*Transaction, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "update_transaction_by_id") + + // Get the transaction + transaction, err := c.GetTransaction(ctx, xPubID, id) + if err != nil { + return nil, err + } + + // Update the metadata + if err = transaction.UpdateTransactionMetadata( + xPubID, metadata, + ); err != nil { + return nil, err + } + + // Save the model // update existing record + if err = transaction.Save(ctx); err != nil { + return nil, err + } + + return transaction, nil +} + +// RevertTransaction will revert a transaction created in the spv wallet engine database, but only if it has not +// yet been synced on-chain and the utxos have not been spent. +// All utxos that are reverted will be marked as deleted (and spent) +func (c *Client) RevertTransaction(ctx context.Context, id string) error { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "revert_transaction_by_id") + + // Get the transaction + transaction, err := c.GetTransaction(ctx, "", id) + if err != nil { + return err + } + + // make sure the transaction is coming from spv wallet engine + if transaction.DraftID == "" { + return errors.New("not a spv wallet engine originating transaction, cannot revert") + } + + var draftTransaction *DraftTransaction + if draftTransaction, err = c.GetDraftTransactionByID(ctx, transaction.DraftID, c.DefaultModelOptions()...); err != nil { + return err + } + if draftTransaction == nil { + return errors.New("could not find the draft transaction for this transaction, cannot revert") + } + + // check whether transaction is not already on chain + var info *chainstate.TransactionInfo + if info, err = c.Chainstate().QueryTransaction(ctx, transaction.ID, chainstate.RequiredInMempool, 30*time.Second); err != nil { + if !errors.Is(err, chainstate.ErrTransactionNotFound) { + return err + } + } + if info != nil { + return errors.New("transaction was found on-chain, cannot revert") + } + + // check that the utxos of this transaction have not been spent + // this transaction needs to be the tip of the chain + conditions := &map[string]interface{}{ + "transaction_id": transaction.ID, + } + var utxos []*Utxo + if utxos, err = c.GetUtxos(ctx, nil, conditions, nil, c.DefaultModelOptions()...); err != nil { + return err + } + for _, utxo := range utxos { + if utxo.SpendingTxID.Valid { + return errors.New("utxo of this transaction has been spent, cannot revert") + } + } + + // + // Revert transaction and all related elements + // + + // mark output utxos as deleted (no way to delete from spv wallet engine yet) + for _, utxo := range utxos { + utxo.enrich(ModelUtxo, c.DefaultModelOptions()...) + utxo.SpendingTxID.Valid = true + utxo.SpendingTxID.String = "deleted" + utxo.DeletedAt.Valid = true + utxo.DeletedAt.Time = time.Now() + if err = utxo.Save(ctx); err != nil { + return err + } + } + + // remove output values of transaction from all xpubs + var xpub *Xpub + for xpubID, outputValue := range transaction.XpubOutputValue { + if xpub, err = c.GetXpubByID(ctx, xpubID); err != nil { + return err + } + if outputValue > 0 { + xpub.CurrentBalance -= uint64(outputValue) + } else { + xpub.CurrentBalance += uint64(math.Abs(float64(outputValue))) + } + + if err = xpub.Save(ctx); err != nil { + return err + } + } + + // set any inputs (spent utxos) used in this transaction back to not spent + var utxo *Utxo + for _, input := range draftTransaction.Configuration.Inputs { + if utxo, err = c.GetUtxoByTransactionID(ctx, input.TransactionID, input.OutputIndex); err != nil { + return err + } + utxo.SpendingTxID.Valid = false + utxo.SpendingTxID.String = "" + if err = utxo.Save(ctx); err != nil { + return err + } + } + + // cancel draft tx + draftTransaction.Status = DraftStatusCanceled + if err = draftTransaction.Save(ctx); err != nil { + return err + } + + // cancel sync transaction + var syncTransaction *SyncTransaction + if syncTransaction, err = GetSyncTransactionByID(ctx, transaction.ID, c.DefaultModelOptions()...); err != nil { + return err + } + syncTransaction.BroadcastStatus = SyncStatusCanceled + syncTransaction.P2PStatus = SyncStatusCanceled + syncTransaction.SyncStatus = SyncStatusCanceled + if err = syncTransaction.Save(ctx); err != nil { + return err + } + + // revert transaction + // this takes the transaction out of any possible list view of the owners of the xpubs, + // but keeps a record of what went down + if transaction.Metadata == nil { + transaction.Metadata = Metadata{} + } + transaction.Metadata["XpubInIDs"] = transaction.XpubInIDs + transaction.Metadata["XpubOutIDs"] = transaction.XpubOutIDs + transaction.Metadata["XpubOutputValue"] = transaction.XpubOutputValue + transaction.XpubInIDs = IDs{"reverted"} + transaction.XpubOutIDs = IDs{"reverted"} + transaction.XpubOutputValue = XpubOutputValue{"reverted": 0} + transaction.DeletedAt.Valid = true + transaction.DeletedAt.Time = time.Now() + + err = transaction.Save(ctx) // update existing record + + return err +} + +// UpdateTransaction will update the broadcast callback transaction info, like: block height, block hash, status, bump. +func (c *Client) UpdateTransaction(ctx context.Context, callbackResp *broadcast.SubmittedTx) error { + bump, err := bc.NewBUMPFromStr(callbackResp.MerklePath) + if err != nil { + c.options.logger.Err(err).Msgf("failed to parse merkle path from broadcast callback - tx: %v", callbackResp) + return err + } + + txInfo := &chainstate.TransactionInfo{ + BlockHash: callbackResp.BlockHash, + BlockHeight: callbackResp.BlockHeight, + ID: callbackResp.TxID, + TxStatus: callbackResp.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, + } + + tx, err := c.GetTransaction(ctx, "", txInfo.ID) + if err != nil { + c.options.logger.Err(err).Msgf("failed to get transaction by id: %v", txInfo.ID) + return err + } + + syncTx, err := GetSyncTransactionByTxID(ctx, txInfo.ID, c.DefaultModelOptions()...) + if err != nil { + c.options.logger.Err(err).Msgf("failed to get sync transaction by tx id: %v", txInfo.ID) + return err + } + + return processSyncTxSave(ctx, txInfo, syncTx, tx) +} + +func generateTxIDFilterConditions(txIDs []string) *map[string]interface{} { + orConditions := make([]map[string]interface{}, len(txIDs)) + + for i, txID := range txIDs { + orConditions[i] = map[string]interface{}{"id": txID} + } + + conditions := &map[string]interface{}{ + "$or": orConditions, + } + + return conditions +} diff --git a/engine/action_transaction_test.go b/engine/action_transaction_test.go new file mode 100644 index 000000000..eb36386e5 --- /dev/null +++ b/engine/action_transaction_test.go @@ -0,0 +1,430 @@ +package engine + +import ( + "context" + "fmt" + "testing" + + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/libsv/go-bk/bip32" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_RevertTransaction(t *testing.T) { + t.Run("revert transaction", func(t *testing.T) { + ctx, client, transaction, _, deferMe := initRevertTransactionData(t) + defer deferMe() + + // + // Revert the transaction + // + err := client.RevertTransaction(ctx, transaction.ID) + require.NoError(t, err) + + // check transaction was reverted + var tx *Transaction + tx, err = client.GetTransaction(ctx, testXPubID, transaction.ID) + require.NoError(t, err) + assert.Equal(t, transaction.ID, tx.ID) + assert.Len(t, tx.XpubInIDs, 1) // XpubInIDs should have been set to reverted + assert.Equal(t, "reverted", tx.XpubInIDs[0]) + assert.Len(t, tx.XpubOutIDs, 1) // XpubInIDs should have been set to reverted + assert.Equal(t, "reverted", tx.XpubOutIDs[0]) + assert.Len(t, tx.XpubOutputValue, 1) // XpubInIDs should have been set to reverted + assert.Equal(t, int64(0), tx.XpubOutputValue["reverted"]) + + // check the balance of the xpub + var xpub *Xpub + xpub, err = client.GetXpubByID(ctx, testXPubID) + require.NoError(t, err) + assert.Equal(t, uint64(100000), xpub.CurrentBalance) // 100000 was initial value + + // check sync transaction was canceled + var syncTx *SyncTransaction + syncTx, err = GetSyncTransactionByID(ctx, transaction.ID, client.DefaultModelOptions()...) + require.NoError(t, err) + assert.Equal(t, SyncStatusCanceled, syncTx.BroadcastStatus) + + // check utxos where reverted + var utxos []*Utxo + conditions := &map[string]interface{}{ + xPubIDField: transaction.XPubID, + } + utxos, err = client.GetUtxos(ctx, nil, conditions, nil, client.DefaultModelOptions()...) + require.NoError(t, err) + assert.Len(t, utxos, 2) // only original + for _, utxo := range utxos { + if utxo.TransactionID == transaction.ID { + assert.True(t, utxo.SpendingTxID.Valid) + assert.Equal(t, "deleted", utxo.SpendingTxID.String) + } else { + assert.False(t, utxo.SpendingTxID.Valid) + assert.Equal(t, "", utxo.SpendingTxID.String) + } + } + }) + + t.Run("disallow revert spent transaction", func(t *testing.T) { + ctx, client, transaction, xPriv, deferMe := initRevertTransactionData(t) + defer deferMe() + + // we need a draft transaction, otherwise we cannot revert + draftTransaction := newDraftTransaction( + testXPub, &TransactionConfig{ + Outputs: []*TransactionOutput{{ + To: "1A1PjKqjWMNBzTVdcBru27EV1PHcXWc63W", // random address + Satoshis: 1000, + }}, + ChangeNumberOfDestinations: 1, + Sync: &SyncConfig{ + Broadcast: true, + BroadcastInstant: false, + PaymailP2P: false, + SyncOnChain: false, + }, + }, + append(client.DefaultModelOptions(), New())..., + ) + // this gets inputs etc. + err := draftTransaction.Save(ctx) + require.NoError(t, err) + + var hex string + hex, err = draftTransaction.SignInputs(xPriv) + require.NoError(t, err) + assert.NotEmpty(t, hex) + + var secondTransaction *Transaction + secondTransaction, err = client.RecordTransaction(ctx, testXPub, hex, draftTransaction.ID, client.DefaultModelOptions()...) + require.NoError(t, err) + assert.NotEmpty(t, secondTransaction) + + // + // Revert the transaction + // + err = client.RevertTransaction(ctx, transaction.ID) + require.Equal(t, "utxo of this transaction has been spent, cannot revert", err.Error()) + }) + + t.Run("revert spend to internal address", func(t *testing.T) { + ctx, client, _, xPriv, deferMe := initRevertTransactionData(t) + defer deferMe() + + testXPub2 := "xpub661MyMwAqRbcFGX8a3K99DKPZahQBj1z8DsMTE7gqKtYj9yaWv45nkjHYcWdwUcQkGdZMv62HVKNCF4MNqXK2oiRKcfSE7U7iu5hAcyMzUS" + xPub := newXpub(testXPub2, append(client.DefaultModelOptions(), New())...) + err := xPub.Save(ctx) + require.NoError(t, err) + + var destination *Destination + destination, err = xPub.getNewDestination(ctx, utils.ChainExternal, utils.ScriptTypePubKeyHash, client.DefaultModelOptions(New())...) + require.NoError(t, err) + require.NotNil(t, destination) + + err = destination.Save(ctx) + require.NoError(t, err) + + // we need a draft transaction, otherwise we cannot revert + draftTransaction := newDraftTransaction( + testXPub, &TransactionConfig{ + Outputs: []*TransactionOutput{{ + To: destination.Address, + Satoshis: 1000, + }}, + ChangeNumberOfDestinations: 1, + Sync: &SyncConfig{ + Broadcast: true, + BroadcastInstant: false, + PaymailP2P: false, + SyncOnChain: false, + }, + }, + append(client.DefaultModelOptions(), New())..., + ) + // this gets inputs etc. + err = draftTransaction.Save(ctx) + require.NoError(t, err) + + var hex string + hex, err = draftTransaction.SignInputs(xPriv) + require.NoError(t, err) + assert.NotEmpty(t, hex) + + var transaction *Transaction + transaction, err = client.RecordTransaction(ctx, testXPub, hex, draftTransaction.ID, client.DefaultModelOptions()...) + require.NoError(t, err) + assert.NotEmpty(t, transaction) + assert.Len(t, transaction.XpubOutIDs, 2) + assert.Equal(t, int64(1000), transaction.XpubOutputValue[xPub.ID]) + + xPub, err = client.GetXpub(ctx, testXPub2) + require.NoError(t, err) + assert.Equal(t, uint64(1000), xPub.CurrentBalance) + + var utxos []*Utxo + utxos, err = client.GetUtxosByXpubID(ctx, xPub.ID, nil, nil, nil) + require.NoError(t, err) + assert.Len(t, utxos, 1) + assert.Equal(t, uint64(1000), utxos[0].Satoshis) + assert.False(t, utxos[0].SpendingTxID.Valid) + + // + // Revert the transaction + // + err = client.RevertTransaction(ctx, transaction.ID) + require.NoError(t, err) + + // check the destination xpub / utxos etc + xPub, err = client.GetXpub(ctx, testXPub2) + require.NoError(t, err) + assert.Equal(t, uint64(0), xPub.CurrentBalance) + + utxos, err = client.GetUtxosByXpubID(ctx, xPub.ID, nil, nil, nil) + require.NoError(t, err) + assert.Len(t, utxos, 1) + assert.True(t, utxos[0].SpendingTxID.Valid) + assert.Equal(t, "deleted", utxos[0].SpendingTxID.String) + }) +} + +func Test_RecordTransaction(t *testing.T) { + ctx, client, _ := initSimpleTestCase(t) + // given + draftTransaction := newDraftTransaction( + testXPub, &TransactionConfig{ + Outputs: []*TransactionOutput{{ + To: "1A1PjKqjWMNBzTVdcBru27EV1PHcXWc63W", + Satoshis: 1000, + }}, + ChangeNumberOfDestinations: 1, + Sync: &SyncConfig{ + Broadcast: true, + BroadcastInstant: false, + PaymailP2P: false, + SyncOnChain: false, + }, + }, + append(client.DefaultModelOptions(), New())..., + ) + draftTransactionID := draftTransaction.ID + + t.Run("hex validation -> invalid hex", func(t *testing.T) { + invalidHex := "test" + // when + _, err := client.RecordTransaction(ctx, testXPub, invalidHex, draftTransactionID, client.DefaultModelOptions()...) + + // then + require.Error(t, err) + }) + + t.Run("hex validation -> empty hex", func(t *testing.T) { + emptyHex := "" + // when + _, err := client.RecordTransaction(ctx, testXPub, emptyHex, draftTransactionID, client.DefaultModelOptions()...) + + // then + require.Error(t, err) + }) + + t.Run("hex validation -> valid hex", func(t *testing.T) { + validHex := "020000000165bb8d2733298b2d3b441a871868d6323c5392facf0d3eced3a6c6a17dc84c10000000006a473044022057b101e9a017cdcc333ef66a4a1e78720ae15adf7d1be9c33abec0fe56bc849d022013daa203095522039fadaba99e567ec3cf8615861d3b7258d5399c9f1f4ace8f412103b9c72aebee5636664b519e5f7264c78614f1e57fa4097ae83a3012a967b1c4b9ffffffff03e0930400000000001976a91413473d21dc9e1fb392f05a028b447b165a052d4d88acf9020000000000001976a91455decebedd9a6c2c2d32cf0ee77e2640c3955d3488ac00000000000000000c006a09446f7457616c6c657400000000" + // when + _, err := client.RecordTransaction(ctx, testXPub, validHex, "", client.DefaultModelOptions()...) + + // then + require.NotContains(t, err.Error(), "invalid hex") + }) +} + +func initRevertTransactionData(t *testing.T) (context.Context, ClientInterface, *Transaction, *bip32.ExtendedKey, func()) { + // this creates an xpub, destination and utxo + ctx, client, deferMe := initSimpleTestCase(t) + + // we need a draft transaction, otherwise we cannot revert + draftTransaction := newDraftTransaction( + testXPub, &TransactionConfig{ + Outputs: []*TransactionOutput{{ + To: "1A1PjKqjWMNBzTVdcBru27EV1PHcXWc63W", // random address + Satoshis: 1000, + }}, + ChangeNumberOfDestinations: 1, + Sync: &SyncConfig{ + Broadcast: true, + BroadcastInstant: false, + PaymailP2P: false, + SyncOnChain: false, + }, + }, + append(client.DefaultModelOptions(), New())..., + ) + // this gets inputs etc. + err := draftTransaction.Save(ctx) + require.NoError(t, err) + + var xPriv *bip32.ExtendedKey + xPriv, err = bip32.NewKeyFromString(testXPriv) + require.NoError(t, err) + + var hex string + hex, err = draftTransaction.SignInputs(xPriv) + require.NoError(t, err) + assert.NotEmpty(t, hex) + + newOpts := client.DefaultModelOptions(WithXPub(testXPub), New()) + transaction, err := newTransactionWithDraftID( + hex, draftTransaction.ID, newOpts..., + ) + require.NoError(t, err) + + transaction.draftTransaction = draftTransaction + _hydrateOutgoingWithSync(transaction) + + err = transaction.processUtxos(ctx) + require.NoError(t, err) + + err = transaction.Save(ctx) + require.NoError(t, err) + assert.NotEmpty(t, transaction) + + // check transaction was recorded properly + var tx *Transaction + tx, err = client.GetTransaction(ctx, testXPubID, transaction.ID) + require.NoError(t, err) + assert.Equal(t, transaction.ID, tx.ID) + assert.Equal(t, testXPubID, tx.XpubInIDs[0]) + + // check sync transaction + var syncTx *SyncTransaction + syncTx, err = GetSyncTransactionByID(ctx, transaction.ID, client.DefaultModelOptions()...) + require.NoError(t, err) + assert.Equal(t, SyncStatusReady, syncTx.BroadcastStatus) + + var utxos []*Utxo + conditions := &map[string]interface{}{ + xPubIDField: transaction.XPubID, + } + utxos, err = client.GetUtxos(ctx, nil, conditions, nil, client.DefaultModelOptions()...) + require.NoError(t, err) + assert.Len(t, utxos, 2) // original + new change utxo + for _, utxo := range utxos { + if utxo.TransactionID == transaction.ID { + assert.False(t, utxo.SpendingTxID.Valid) + } else { + assert.Equal(t, transaction.ID, utxo.SpendingTxID.String) + } + } + + return ctx, client, transaction, xPriv, deferMe +} + +// BenchmarkAction_Transaction_recordTransaction will benchmark the method RecordTransaction() +func BenchmarkAction_Transaction_recordTransaction(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + ctx, client, xPub, config, err := initBenchmarkData(b) + if err != nil { + fmt.Printf("ERROR: %s\n", err.Error()) + b.Fail() + } + + var draftTransaction *DraftTransaction + if draftTransaction, err = client.NewTransaction(ctx, xPub.rawXpubKey, config, client.DefaultModelOptions()...); err != nil { + fmt.Printf("ERROR: %s\n", err.Error()) + b.Fail() + } + + var xPriv *bip32.ExtendedKey + if xPriv, err = bip32.NewKeyFromString(testXPriv); err != nil { + return + } + + var hexString string + if hexString, err = draftTransaction.SignInputs(xPriv); err != nil { + fmt.Printf("ERROR: %s\n", err.Error()) + b.Fail() + } + + b.StartTimer() + if _, err = client.RecordTransaction(ctx, xPub.rawXpubKey, hexString, draftTransaction.ID, client.DefaultModelOptions()...); err != nil { + fmt.Printf("ERROR: %s\n", err.Error()) + b.Fail() + } + } +} + +// BenchmarkTransaction_newTransaction will benchmark the method newTransaction() +func BenchmarkAction_Transaction_newTransaction(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.StopTimer() + ctx, client, xPub, config, err := initBenchmarkData(b) + if err != nil { + fmt.Printf("ERROR: %s\n", err.Error()) + b.Fail() + } + + b.StartTimer() + if _, err = client.NewTransaction(ctx, xPub.rawXpubKey, config, client.DefaultModelOptions()...); err != nil { + fmt.Printf("ERROR: %s\n", err.Error()) + b.Fail() + } + } +} + +func initBenchmarkData(b *testing.B) (context.Context, ClientInterface, *Xpub, *TransactionConfig, error) { + ctx, client, _ := CreateBenchmarkSQLiteClient(b, false, true, + withTaskManagerMockup(), + WithFreeCache(), + WithIUCDisabled(), + ) + + opts := append(client.DefaultModelOptions(), New()) + xPub, err := client.NewXpub(ctx, testXPub, opts...) + if err != nil { + b.Fail() + } + destination := newDestination(xPub.GetID(), testLockingScript, opts...) + if err = destination.Save(ctx); err != nil { + b.Fail() + } + + utxo := newUtxo(xPub.GetID(), testTxID, testLockingScript, 1, 122500, opts...) + if err = utxo.Save(ctx); err != nil { + b.Fail() + } + utxo = newUtxo(xPub.GetID(), testTxID, testLockingScript, 2, 122500, opts...) + if err = utxo.Save(ctx); err != nil { + b.Fail() + } + utxo = newUtxo(xPub.GetID(), testTxID, testLockingScript, 3, 122500, opts...) + if err = utxo.Save(ctx); err != nil { + b.Fail() + } + utxo = newUtxo(xPub.GetID(), testTxID, testLockingScript, 4, 122500, opts...) + if err = utxo.Save(ctx); err != nil { + b.Fail() + } + + config := &TransactionConfig{ + FeeUnit: &utils.FeeUnit{ + Satoshis: 5, + Bytes: 100, + }, + Outputs: []*TransactionOutput{{ + OpReturn: &OpReturn{ + Map: &MapProtocol{ + App: "example.com", + Type: "blast", + Keys: map[string]interface{}{ + "example": "blasting", + }, + }, + }, + }}, + ChangeDestinationsStrategy: ChangeStrategyRandom, + ChangeNumberOfDestinations: 2, + } + + return ctx, client, xPub, config, err +} diff --git a/engine/action_utxo.go b/engine/action_utxo.go new file mode 100644 index 000000000..e8ad517c1 --- /dev/null +++ b/engine/action_utxo.go @@ -0,0 +1,152 @@ +package engine + +import ( + "context" + + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/mrz1836/go-datastore" +) + +// GetUtxos will get all the utxos from the Datastore +func (c *Client) GetUtxos(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, queryParams *datastore.QueryParams, opts ...ModelOps, +) ([]*Utxo, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_utxos") + + // Get the utxos + utxos, err := getUtxos( + ctx, metadataConditions, conditions, queryParams, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return nil, err + } + + // add the transaction linked to the utxos + c.enrichUtxoTransactions(ctx, utxos) + + return utxos, nil +} + +// GetUtxosCount will get a count of all the utxos from the Datastore +func (c *Client) GetUtxosCount(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, opts ...ModelOps, +) (int64, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_utxos_count") + + // Get the utxos count + count, err := getUtxosCount( + ctx, metadataConditions, conditions, + c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return 0, err + } + + return count, nil +} + +// GetUtxosByXpubID will get utxos based on an xPub +func (c *Client) GetUtxosByXpubID(ctx context.Context, xPubID string, metadata *Metadata, conditions *map[string]interface{}, + queryParams *datastore.QueryParams, +) ([]*Utxo, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_utxos") + + // Get the utxos + utxos, err := getUtxosByXpubID( + ctx, + xPubID, + metadata, + conditions, + queryParams, + c.DefaultModelOptions()..., + ) + if err != nil { + return nil, err + } + + // add the transaction linked to the utxos + c.enrichUtxoTransactions(ctx, utxos) + + return utxos, nil +} + +// GetUtxo will get a single utxo based on an xPub, the tx ID and the outputIndex +func (c *Client) GetUtxo(ctx context.Context, xPubKey, txID string, outputIndex uint32) (*Utxo, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_utxo") + + // Get the utxos + utxo, err := getUtxo( + ctx, txID, outputIndex, c.DefaultModelOptions()..., + ) + if err != nil { + return nil, err + } else if utxo == nil { + return nil, ErrMissingUtxo + } + + // Check that the id matches + if utxo.XpubID != utils.Hash(xPubKey) { + return nil, ErrXpubIDMisMatch + } + + var tx *Transaction + tx, err = getTransactionByID(ctx, "", utxo.TransactionID, c.DefaultModelOptions()...) + if err != nil { + c.Logger().Error().Str("utxoID", utxo.ID).Msg("failed finding transaction related to utxo") + } else { + utxo.Transaction = tx + } + + return utxo, nil +} + +// GetUtxoByTransactionID will get a single utxo based on the tx ID and the outputIndex +func (c *Client) GetUtxoByTransactionID(ctx context.Context, txID string, outputIndex uint32) (*Utxo, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_utxo_by_transaction_id") + + // Get the utxo + utxo, err := getUtxo( + ctx, txID, outputIndex, c.DefaultModelOptions()..., + ) + if err != nil { + return nil, err + } else if utxo == nil { + return nil, ErrMissingUtxo + } + + var tx *Transaction + tx, err = getTransactionByID(ctx, "", utxo.TransactionID, c.DefaultModelOptions()...) + if err != nil { + c.Logger().Error().Str("utxoID", utxo.ID).Msg("failed finding transaction related to utxo") + } else { + utxo.Transaction = tx + } + + return utxo, nil +} + +// UnReserveUtxos remove the reservation on the utxos for the given draft ID +func (c *Client) UnReserveUtxos(ctx context.Context, xPubID, draftID string) error { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "unreserve_uxtos_by_draft_id") + + return unReserveUtxos(ctx, xPubID, draftID, c.DefaultModelOptions()...) +} + +// should this be optional in the results? +func (c *Client) enrichUtxoTransactions(ctx context.Context, utxos []*Utxo) { + for index, utxo := range utxos { + tx, err := getTransactionByID(ctx, "", utxo.TransactionID, c.DefaultModelOptions()...) + if err != nil { + c.Logger().Error().Str("utxoID", utxo.ID).Msg("failed finding transaction related to utxo") + } else { + utxos[index].Transaction = tx + } + } +} diff --git a/engine/action_xpub.go b/engine/action_xpub.go new file mode 100644 index 000000000..589938a40 --- /dev/null +++ b/engine/action_xpub.go @@ -0,0 +1,124 @@ +package engine + +import ( + "context" + + "github.com/mrz1836/go-datastore" +) + +// NewXpub will parse the xPub and save it into the Datastore +// +// xPubKey is the raw public xPub +// opts are options and can include "metadata" +func (c *Client) NewXpub(ctx context.Context, xPubKey string, opts ...ModelOps) (*Xpub, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "new_xpub") + + // Create the model & set the default options (gives options from client->model) + xPub := newXpub( + xPubKey, c.DefaultModelOptions(append(opts, New())...)..., + ) + + // Save the model + if err := xPub.Save(ctx); err != nil { + return nil, err + } + + // Return the created model + return xPub, nil +} + +// GetXpub will get an existing xPub from the Datastore +// +// xPubKey is the raw public xPub +func (c *Client) GetXpub(ctx context.Context, xPubKey string) (*Xpub, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_xpub") + + // Attempt to get from cache or datastore + xPub, err := getXpubWithCache(ctx, c, xPubKey, "", c.DefaultModelOptions()...) + if err != nil { + return nil, err + } + + // Return the model + return xPub, nil +} + +// GetXpubByID will get an existing xPub from the Datastore +// +// xPubID is the hash of the xPub +func (c *Client) GetXpubByID(ctx context.Context, xPubID string) (*Xpub, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_xpub_by_id") + + // Attempt to get from cache or datastore + xPub, err := getXpubWithCache(ctx, c, "", xPubID, c.DefaultModelOptions()...) + if err != nil { + return nil, err + } + + // Return the model + return xPub, nil +} + +// UpdateXpubMetadata will update the metadata in an existing xPub +// +// xPubID is the hash of the xP +func (c *Client) UpdateXpubMetadata(ctx context.Context, xPubID string, metadata Metadata) (*Xpub, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "update_xpub_by_id") + + // Get the xPub + xPub, err := c.GetXpubByID(ctx, xPubID) + if err != nil { + return nil, err + } + + // Update the metadata + xPub.UpdateMetadata(metadata) + + // Save the model + if err = xPub.Save(ctx); err != nil { + return nil, err + } + + // Return the model + return xPub, nil +} + +// GetXPubs gets all xpubs matching the conditions +func (c *Client) GetXPubs(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, queryParams *datastore.QueryParams, opts ...ModelOps, +) ([]*Xpub, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_destinations") + + // Get the count + xPubs, err := getXPubs( + ctx, metadataConditions, conditions, queryParams, c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return nil, err + } + + return xPubs, nil +} + +// GetXPubsCount gets a count of all xpubs matching the conditions +func (c *Client) GetXPubsCount(ctx context.Context, metadataConditions *Metadata, + conditions *map[string]interface{}, opts ...ModelOps, +) (int64, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "get_destinations") + + // Get the count + count, err := getXPubsCount( + ctx, metadataConditions, conditions, c.DefaultModelOptions(opts...)..., + ) + if err != nil { + return 0, err + } + + return count, nil +} diff --git a/engine/action_xpub_test.go b/engine/action_xpub_test.go new file mode 100644 index 000000000..15d37ce2d --- /dev/null +++ b/engine/action_xpub_test.go @@ -0,0 +1,217 @@ +package engine + +import ( + "testing" + + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestClient_NewXpub will test the method NewXpub() +func (ts *EmbeddedDBTestSuite) TestClient_NewXpub() { + for _, testCase := range dbTestCases { + ts.T().Run(testCase.name+" - valid", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + xPub, err := tc.client.NewXpub(tc.ctx, testXPub, tc.client.DefaultModelOptions()...) + require.NoError(t, err) + assert.Equal(t, testXPubID, xPub.ID) + + xPub2, err2 := tc.client.GetXpub(tc.ctx, testXPub) + require.NoError(t, err2) + assert.Equal(t, testXPubID, xPub2.ID) + }) + + ts.T().Run(testCase.name+" - valid with metadata (key->val)", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + opts := append(tc.client.DefaultModelOptions(), WithMetadata(testMetadataKey, testMetadataValue)) + + xPub, err := tc.client.NewXpub(tc.ctx, testXPub, opts...) + require.NoError(t, err) + assert.Equal(t, testXPubID, xPub.ID) + assert.Equal(t, Metadata{testMetadataKey: testMetadataValue}, xPub.Metadata) + + xPub2, err2 := tc.client.GetXpub(tc.ctx, testXPub) + require.NoError(t, err2) + assert.Equal(t, testXPubID, xPub2.ID) + assert.Equal(t, Metadata{testMetadataKey: testMetadataValue}, xPub2.Metadata) + }) + + ts.T().Run(testCase.name+" - valid with metadatas", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + opts := append( + tc.client.DefaultModelOptions(), + WithMetadatas(map[string]interface{}{ + testMetadataKey: testMetadataValue, + }), + ) + + xPub, err := tc.client.NewXpub(tc.ctx, testXPub, opts...) + require.NoError(t, err) + assert.Equal(t, testXPubID, xPub.ID) + assert.Equal(t, Metadata{testMetadataKey: testMetadataValue}, xPub.Metadata) + + xPub2, err2 := tc.client.GetXpub(tc.ctx, testXPub) + require.NoError(t, err2) + assert.Equal(t, testXPubID, xPub2.ID) + assert.Equal(t, Metadata{testMetadataKey: testMetadataValue}, xPub2.Metadata) + }) + + ts.T().Run(testCase.name+" - errors", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + _, err := tc.client.NewXpub(tc.ctx, "test", tc.client.DefaultModelOptions()...) + assert.ErrorIs(t, err, utils.ErrXpubInvalidLength) + + _, err = tc.client.NewXpub(tc.ctx, "", tc.client.DefaultModelOptions()...) + assert.ErrorIs(t, err, utils.ErrXpubInvalidLength) + }) + + ts.T().Run(testCase.name+" - duplicate xPub", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + xPub, err := tc.client.NewXpub(tc.ctx, testXPub, tc.client.DefaultModelOptions()...) + require.NoError(t, err) + assert.Equal(t, testXPubID, xPub.ID) + + _, err2 := tc.client.NewXpub(tc.ctx, testXPub, tc.client.DefaultModelOptions()...) + require.Error(t, err2) + }) + } +} + +// TestClient_GetXpub will test the method GetXpub() +func (ts *EmbeddedDBTestSuite) TestClient_GetXpub() { + for _, testCase := range dbTestCases { + ts.T().Run(testCase.name+" - valid", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + xPub, err := tc.client.NewXpub(tc.ctx, testXPub, tc.client.DefaultModelOptions()...) + require.NoError(t, err) + assert.Equal(t, testXPubID, xPub.ID) + + xPub2, err2 := tc.client.GetXpub(tc.ctx, testXPub) + require.NoError(t, err2) + assert.Equal(t, testXPubID, xPub2.ID) + + xPub3, err3 := tc.client.GetXpubByID(tc.ctx, xPub2.ID) + require.NoError(t, err3) + assert.Equal(t, testXPubID, xPub3.ID) + }) + + ts.T().Run(testCase.name+" - error - invalid xpub", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + xPub, err := tc.client.GetXpub(tc.ctx, "test") + require.Error(t, err) + require.Nil(t, xPub) + assert.ErrorIs(t, err, ErrMissingXpub) + }) + + ts.T().Run(testCase.name+" - error - missing xpub", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + xPub, err := tc.client.GetXpub(tc.ctx, testXPub) + require.Error(t, err) + require.Nil(t, xPub) + assert.ErrorIs(t, err, ErrMissingXpub) + }) + } +} + +// TestClient_GetXpubByID will test the method GetXpubByID() +func (ts *EmbeddedDBTestSuite) TestClient_GetXpubByID() { + for _, testCase := range dbTestCases { + ts.T().Run(testCase.name+" - valid", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + xPub, err := tc.client.NewXpub(tc.ctx, testXPub, tc.client.DefaultModelOptions()...) + require.NoError(t, err) + assert.Equal(t, testXPubID, xPub.ID) + + xPub2, err2 := tc.client.GetXpubByID(tc.ctx, xPub.ID) + require.NoError(t, err2) + assert.Equal(t, testXPubID, xPub2.ID) + }) + + ts.T().Run(testCase.name+" - error - invalid xpub", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + xPub, err := tc.client.GetXpubByID(tc.ctx, "test") + require.Error(t, err) + require.Nil(t, xPub) + }) + + ts.T().Run(testCase.name+" - error - missing xpub", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + xPub, err := tc.client.GetXpubByID(tc.ctx, testXPub) + require.Error(t, err) + require.Nil(t, xPub) + assert.ErrorIs(t, err, ErrMissingXpub) + }) + } +} + +// TestClient_UpdateXpubMetadata will test the method UpdateXpubMetadata() +func (ts *EmbeddedDBTestSuite) TestClient_UpdateXpubMetadata() { + for _, testCase := range dbTestCases { + ts.T().Run(testCase.name+" - valid", func(t *testing.T) { + tc := ts.genericDBClient(t, testCase.database, false) + defer tc.Close(tc.ctx) + + metadata := Metadata{ + "test-key-1": "test-value-1", + "test-key-2": "test-value-2", + "test-key-3": "test-value-3", + } + opts := tc.client.DefaultModelOptions() + opts = append(opts, WithMetadatas(metadata)) + + xPub, err := tc.client.NewXpub(tc.ctx, testXPub, opts...) + require.NoError(t, err) + assert.Equal(t, testXPubID, xPub.ID) + assert.Equal(t, metadata, xPub.Metadata) + + xPub, err = tc.client.UpdateXpubMetadata(tc.ctx, xPub.ID, Metadata{"test-key-new": "new-value"}) + require.NoError(t, err) + assert.Len(t, xPub.Metadata, 4) + assert.Equal(t, "new-value", xPub.Metadata["test-key-new"]) + + xPub, err = tc.client.UpdateXpubMetadata(tc.ctx, xPub.ID, Metadata{ + "test-key-new-2": "new-value-2", + "test-key-1": nil, + "test-key-2": nil, + "test-key-3": nil, + }) + require.NoError(t, err) + assert.Len(t, xPub.Metadata, 2) + assert.Equal(t, "new-value", xPub.Metadata["test-key-new"]) + assert.Equal(t, "new-value-2", xPub.Metadata["test-key-new-2"]) + + err = xPub.Save(tc.ctx) + require.NoError(t, err) + + // make sure it was saved + xPub2, err2 := tc.client.GetXpubByID(tc.ctx, xPub.ID) + require.NoError(t, err2) + assert.Len(t, xPub2.Metadata, 2) + assert.Equal(t, "new-value", xPub2.Metadata["test-key-new"]) + assert.Equal(t, "new-value-2", xPub2.Metadata["test-key-new-2"]) + }) + } +} diff --git a/engine/admin_actions_stats.go b/engine/admin_actions_stats.go new file mode 100644 index 000000000..44902cc14 --- /dev/null +++ b/engine/admin_actions_stats.go @@ -0,0 +1,101 @@ +package engine + +import ( + "context" +) + +// AdminStats are statistics about the SPV Wallet server +type AdminStats struct { + Balance int64 `json:"balance"` + Destinations int64 `json:"destinations"` + PaymailAddresses int64 `json:"paymail_addresses"` + Transactions int64 `json:"transactions"` + TransactionsPerDay map[string]interface{} `json:"transactions_per_day"` + Utxos int64 `json:"utxos"` + UtxosPerType map[string]interface{} `json:"utxos_per_type"` + XPubs int64 `json:"xpubs"` +} + +// GetStats will get stats for the SPV Wallet Console (admin) +func (c *Client) GetStats(ctx context.Context, opts ...ModelOps) (*AdminStats, error) { + // Check for existing NewRelic transaction + ctx = c.GetOrStartTxn(ctx, "admin_get_stats") + + // Set the default model options + defaultOpts := c.DefaultModelOptions(opts...) + + var ( + destinationsCount int64 + err error + paymailAddressCount int64 + transactionsCount int64 + transactionsPerDay map[string]interface{} + utxosCount int64 + utxosPerType map[string]interface{} + xpubsCount int64 + ) + + // Get the destination count + if destinationsCount, err = getDestinationsCount( + ctx, nil, nil, defaultOpts..., + ); err != nil { + return nil, err + } + + // Get the transaction count + if transactionsCount, err = getTransactionsCount( + ctx, nil, nil, defaultOpts..., + ); err != nil { + return nil, err + } + + // Get the paymail address count + conditions := map[string]interface{}{ + "deleted_at": nil, + } + if paymailAddressCount, err = getPaymailAddressesCount( + ctx, nil, &conditions, defaultOpts..., + ); err != nil { + return nil, err + } + + // Get the utxo count + if utxosCount, err = getUtxosCount( + ctx, nil, nil, defaultOpts..., + ); err != nil { + return nil, err + } + + // Get the xpub count + if xpubsCount, err = getXPubsCount( + ctx, nil, nil, defaultOpts..., + ); err != nil { + return nil, err + } + + // Get the transactions per day count + if transactionsPerDay, err = getTransactionsAggregate( + ctx, nil, nil, "created_at", defaultOpts..., + ); err != nil { + return nil, err + } + + // Get the utxos per day count + if utxosPerType, err = getUtxosAggregate( + ctx, nil, nil, "type", defaultOpts..., + ); err != nil { + return nil, err + } + + // Return the statistics + return &AdminStats{ + Balance: 0, + Destinations: destinationsCount, + PaymailAddresses: paymailAddressCount, + Transactions: transactionsCount, + TransactionsPerDay: transactionsPerDay, + Utxos: utxosCount, + UtxosPerType: utxosPerType, + XPubs: xpubsCount, + }, nil +} diff --git a/engine/authentication.go b/engine/authentication.go new file mode 100644 index 000000000..9eb840229 --- /dev/null +++ b/engine/authentication.go @@ -0,0 +1,282 @@ +package engine + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "time" + + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/bitcoinschema/go-bitcoin/v2" + "github.com/libsv/go-bk/bip32" + "github.com/libsv/go-bt/v2/bscript" +) + +// AuthenticateRequest will parse the incoming request for the associated authentication header, +// and it will check the Key/Signature +// +// 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) { + // Get the xPub/Access Key from the header + xPub := strings.TrimSpace(req.Header.Get(AuthHeader)) + authAccessKey := strings.TrimSpace(req.Header.Get(AuthAccessKey)) + if len(xPub) == 0 && len(authAccessKey) == 0 { // No value found + return req, ErrMissingAuthHeader + } + + // Check for admin key + if adminRequired { + if !utils.StringInSlice(xPub, adminXPubs) { + return req, ErrNotAdminKey + } + } + + xPubID := utils.Hash(xPub) + xPubOrAccessKey := xPub + if xPub != "" { + // Validate that the xPub is an HD key (length, validation) + if _, err := utils.ValidateXPub(xPubOrAccessKey); err != nil { + return req, err + } + } else if authAccessKey != "" { + xPubOrAccessKey = authAccessKey + + accessKey, err := getAccessKey(ctx, utils.Hash(authAccessKey), c.DefaultModelOptions()...) + if err != nil { + return req, err + } + if accessKey == nil || accessKey.RevokedAt.Valid { + return req, ErrAuthAccessKeyNotFound + } + + xPubID = accessKey.XpubID + } + + if req.Body == nil { + return req, ErrMissingBody + } + defer func() { + _ = req.Body.Close() + }() + b, err := io.ReadAll(req.Body) + if err != nil { + return req, err + } + + req.Body = io.NopCloser(bytes.NewReader(b)) + + authTime, _ := strconv.Atoi(req.Header.Get(AuthHeaderTime)) + authData := &AuthPayload{ + AuthHash: req.Header.Get(AuthHeaderHash), + AuthNonce: req.Header.Get(AuthHeaderNonce), + AuthTime: int64(authTime), + BodyContents: string(b), + Signature: req.Header.Get(AuthSignature), + } + + // adminRequired will always force checking of a signature + if (requireSigning || adminRequired) && !signingDisabled { + if err = c.checkSignature(ctx, xPubOrAccessKey, authData); err != nil { + return req, err + } + req = setOnRequest(req, ParamAuthSigned, true) + } else { + // check the signature and add to request, but do not fail if incorrect + err = c.checkSignature(ctx, xPubOrAccessKey, authData) + req = setOnRequest(req, ParamAuthSigned, err == nil) + + // NOTE: you can not use an access key if signing is invalid - ever + if xPubOrAccessKey == authAccessKey && err != nil { + return req, err + } + } + + req = setOnRequest(req, ParamAdminRequest, adminRequired) + + // Set the data back onto the request + return setOnRequest(setOnRequest(req, ParamXPubKey, xPub), ParamXPubHashKey, xPubID), nil +} + +// 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 + } + + // Check xPub vs Access Key + if strings.Contains(xPubOrAccessKey, "xpub") && len(xPubOrAccessKey) > 64 { + return verifyKeyXPub(xPubOrAccessKey, auth) + } + return verifyAccessKey(ctx, xPubOrAccessKey, auth, c.DefaultModelOptions()...) +} + +// 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 + } + + // Check the auth hash vs the body hash + bodyHash := createBodyHash(auth.BodyContents) + if auth.AuthHash != bodyHash { + return ErrAuhHashMismatch + } + + // Check the auth timestamp + if time.Now().UTC().After(time.UnixMilli(auth.AuthTime).Add(AuthSignatureTTL)) { + return ErrSignatureExpired + } + return nil +} + +// 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 + } + + // Cannot be nil + if auth == nil { + return ErrMissingSignature + } + + // Get the key from xPub + key, err := bitcoin.GetHDKeyFromExtendedPublicKey(xPub) + if err != nil { + return err + } + + // Derive the address for signing + if key, err = utils.DeriveChildKeyFromHex(key, auth.AuthNonce); err != nil { + return err + } + + var address *bscript.Address + if address, err = bitcoin.GetAddressFromHDKey(key); err != nil { + return err // Should never error + } + + // Return the error if verification fails + message := getSigningMessage(xPub, auth) + if err = bitcoin.VerifyMessage( + address.AddressString, + auth.Signature, + message, + ); err != nil { + return ErrSignatureInvalid + } + return nil +} + +// 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...) + if err != nil { + return err + } else if accessKey == nil { + return ErrUnknownAccessKey + } else if accessKey.RevokedAt.Valid { + return ErrAccessKeyRevoked + } + + var address *bscript.Address + if address, err = bitcoin.GetAddressFromPubKeyString( + key, true, + ); err != nil { + return err + } + + // Return the error if verification fails + if err = bitcoin.VerifyMessage( + address.AddressString, + auth.Signature, + getSigningMessage(key, auth), + ); err != nil { + return ErrSignatureInvalid + } + return nil +} + +// 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 { + return err + } + + // Set the auth header + header.Set(AuthHeader, authData.xPub) + + return setSignatureHeaders(header, authData) +} + +// 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 { + return err + } + + // Set the auth header + header.Set(AuthAccessKey, authData.accessKey) + + return setSignatureHeaders(header, authData) +} + +func setSignatureHeaders(header *http.Header, authData *AuthPayload) error { + // Create the auth header hash + header.Set(AuthHeaderHash, authData.AuthHash) + + // Set the nonce + header.Set(AuthHeaderNonce, authData.AuthNonce) + + // Set the time + header.Set(AuthHeaderTime, fmt.Sprintf("%d", authData.AuthTime)) + + // Set the signature + header.Set(AuthSignature, authData.Signature) + + return nil +} + +// CreateSignature will create a signature for the given key & body contents +func CreateSignature(xPriv *bip32.ExtendedKey, bodyString string) (string, error) { + authData, err := createSignature(xPriv, bodyString) + if err != nil { + return "", err + } + return authData.Signature, nil +} + +// getSigningMessage will build the signing message string +func getSigningMessage(xPub string, auth *AuthPayload) string { + return fmt.Sprintf("%s%s%s%d", xPub, auth.AuthHash, auth.AuthNonce, auth.AuthTime) +} + +// GetXpubFromRequest gets the stored xPub from the request if found +func GetXpubFromRequest(req *http.Request) (string, bool) { + return getFromRequest(req, ParamXPubKey) +} + +// GetXpubIDFromRequest gets the stored xPubID from the request if found +func GetXpubIDFromRequest(req *http.Request) (string, bool) { + return getFromRequest(req, ParamXPubHashKey) +} + +// IsAdminRequest gets the stored xPub from the request if found +func IsAdminRequest(req *http.Request) (bool, bool) { + return getBoolFromRequest(req, ParamAdminRequest) +} diff --git a/engine/authentication_internal.go b/engine/authentication_internal.go new file mode 100644 index 000000000..805af086b --- /dev/null +++ b/engine/authentication_internal.go @@ -0,0 +1,181 @@ +package engine + +import ( + "context" + "encoding/hex" + "net/http" + "strings" + "time" + + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/bitcoinschema/go-bitcoin/v2" + "github.com/libsv/go-bk/bec" + "github.com/libsv/go-bk/bip32" +) + +const ( + // AuthHeader is the header to use for authentication (raw xPub) + AuthHeader = "x-auth-xpub" + + // AuthAccessKey is the header to use for access key authentication (access public key) + AuthAccessKey = "x-auth-key" + + // AuthSignature is the given signature (body + timestamp) + AuthSignature = "x-auth-signature" + + // AuthHeaderHash hash of the body coming from the request + AuthHeaderHash = "x-auth-hash" + + // AuthHeaderNonce random nonce for the request + AuthHeaderNonce = "x-auth-nonce" + + // AuthHeaderTime the time of the request, only valid for 30 seconds + AuthHeaderTime = "x-auth-time" + + // AuthSignatureTTL is the max TTL for a signature to be valid + AuthSignatureTTL = 20 * time.Second +) + +// AuthPayload is the authentication payload for checking or creating a signature +type AuthPayload struct { + AuthHash string `json:"auth_hash"` + AuthNonce string `json:"auth_nonce"` + AuthTime int64 `json:"auth_time"` + BodyContents string `json:"body_contents"` + Signature string `json:"signature"` + xPub string + accessKey string +} + +// ParamRequestKey for context key +type ParamRequestKey string + +const ( + // ParamXPubKey the request parameter for the xpub string + ParamXPubKey ParamRequestKey = "xpub" + + // ParamXPubHashKey the request parameter for the xpub ID + ParamXPubHashKey ParamRequestKey = "xpub_hash" + + // ParamAdminRequest the request parameter whether this is an admin request + ParamAdminRequest ParamRequestKey = "auth_admin" + + // ParamAuthSigned the request parameter that says whether the request was signed + ParamAuthSigned ParamRequestKey = "auth_signed" +) + +// createBodyHash will create the hash of the body, removing any carriage returns +func createBodyHash(bodyContents string) string { + return utils.Hash(strings.TrimSuffix(bodyContents, "\n")) +} + +// createSignature will create a signature for the given key & body contents +func createSignature(xPriv *bip32.ExtendedKey, bodyString string) (payload *AuthPayload, err error) { + // No key? + if xPriv == nil { + err = ErrMissingXPriv + return + } + + // Get the xPub + payload = new(AuthPayload) + if payload.xPub, err = bitcoin.GetExtendedPublicKey( + xPriv, + ); err != nil { // Should never error if key is correct + return + } + + // auth_nonce is a random unique string to seed the signing message + // this can be checked server side to make sure the request is not being replayed + if payload.AuthNonce, err = utils.RandomHex(32); err != nil { // Should never error if key is correct + return + } + + // Derive the address for signing + var key *bip32.ExtendedKey + if key, err = utils.DeriveChildKeyFromHex( + xPriv, payload.AuthNonce, + ); err != nil { + return + } + + var privateKey *bec.PrivateKey + if privateKey, err = bitcoin.GetPrivateKeyFromHDKey(key); err != nil { + return // Should never error if key is correct + } + + return createSignatureCommon(payload, bodyString, privateKey) +} + +// createSignatureAccessKey will create a signature for the given access key & body contents +func createSignatureAccessKey(privateKeyHex, bodyString string) (payload *AuthPayload, err error) { + // No key? + if privateKeyHex == "" { + err = ErrMissingAccessKey + return + } + + var privateKey *bec.PrivateKey + if privateKey, err = bitcoin.PrivateKeyFromString( + privateKeyHex, + ); err != nil { + return + } + publicKey := privateKey.PubKey() + + // Get the xPub + payload = new(AuthPayload) + payload.accessKey = hex.EncodeToString(publicKey.SerialiseCompressed()) + + // auth_nonce is a random unique string to seed the signing message + // this can be checked server side to make sure the request is not being replayed + payload.AuthNonce, err = utils.RandomHex(32) + if err != nil { + return nil, err + } + + return createSignatureCommon(payload, bodyString, privateKey) +} + +// createSignatureCommon will create a signature +func createSignatureCommon(payload *AuthPayload, bodyString string, privateKey *bec.PrivateKey) (*AuthPayload, error) { + // Create the auth header hash + payload.AuthHash = utils.Hash(bodyString) + + // auth_time is the current time and makes sure a request can not be sent after 30 secs + payload.AuthTime = time.Now().UnixMilli() + + key := payload.xPub + if key == "" && payload.accessKey != "" { + key = payload.accessKey + } + + // Signature, using bitcoin signMessage + var err error + if payload.Signature, err = bitcoin.SignMessage( + hex.EncodeToString(privateKey.Serialise()), + getSigningMessage(key, payload), + true, + ); err != nil { + return nil, err + } + + return payload, nil +} + +// setOnRequest will set the value on the request with the given key +func setOnRequest(req *http.Request, keyName ParamRequestKey, value interface{}) *http.Request { + return req.WithContext(context.WithValue(req.Context(), keyName, value)) +} + +// getFromRequest gets the stored value from the request if found +func getFromRequest(req *http.Request, key ParamRequestKey) (v string, ok bool) { + v, ok = req.Context().Value(key).(string) + return +} + +// getBoolFromRequest gets the stored bool value from the request if found +func getBoolFromRequest(req *http.Request, key ParamRequestKey) (v bool, ok bool) { + v, ok = req.Context().Value(key).(bool) + return +} diff --git a/engine/authentication_test.go b/engine/authentication_test.go new file mode 100644 index 000000000..c1c0dbd30 --- /dev/null +++ b/engine/authentication_test.go @@ -0,0 +1,686 @@ +package engine + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strconv" + "testing" + "time" + + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/bitcoinschema/go-bitcoin/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + // testAccessKey = "9b2a4421edd88782a193ea8195cce1fe9b632df575c88d70f20a1fdf6835b764" + // testAccessKeyAddress = "1HuoHijPa7BqQNiV953pd3taqnmyhgDXFt" + // testAccessKeyID = "b7b91e8aca22b4ee33f3f0e48c00cd4631dc2dbba1f773829883eaae42fa2234" + // testAccessKeyPublic = "02719a5e3623bee13f8116f1db4ee54603c993e020087960f31d2e0b4cbd97d175" + // testSignatureAuthNonce = `dec0535f13b7ed61c2b188b7fe8fd5f578d6931aa90b6063c653ce0f8eefacf1` + // testSignatureAuthTime = "1643828414038" + // testSignatureXpub = `xpub661MyMwAqRbcFnj7dmEoX4ULYMJ2vxFBkH3oGrpuQMHTMpxUEGND1UXwskzgtUj6R7i9dRNGYj6NYuXWKVM5yAJYjSGuvBJfDTpqjsh8a3T` + testAccessKeyPKH = "b97e4834a13d188ab0588dc2aaff11a6658771cd" + testBodyContents = `{"test_field":"test_value"}` + testEncryption = "35dbe09a941a90a5f59e57020face68860d7b284b7b2973a58de8b4242ec5a925a40ac2933b7e45e78a0b3a13123520e46f9566815589ba2d345577dadee0d5e" + testSignature = `HxNguR72c6BV7tKNn5BQ3/mS2+RX3BGyQHFfVfQ3v4mVdAuh+w32QsFYxsB13KiXuRJ7ZnN7C8RhkAtLi/qvH88=` + testSignatureAuthHash = `5858adf09a0cc01f6d3a4d377f010408313031bb96b40d98e6edccf18c26464e` + testXpubAuth = "xpub661MyMwAqRbcH3WGvLjupmr43L1GVH3MP2WQWvdreDraBeFJy64Xxv4LLX9ZVWWz3ZjZkMuZtSsc9qH9JZR74bR4PWkmtEvP423r6DJR8kA" + testXpubAuthHash = "d8c2bed524071d72d859caf90da5f448b5861cd4d4fd47697f94166c13c5a987" +) + +// TestClient_AuthenticateRequest will test the method AuthenticateRequest() +func TestClient_AuthenticateRequest(t *testing.T) { + t.Parallel() + + t.Run("valid xpub", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) + require.NoError(t, err) + require.NotNil(t, req) + + req.Header.Set(AuthHeader, testXpubAuth) + + _, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + req, err = client.AuthenticateRequest( + context.Background(), req, []string{""}, false, false, false, + ) + require.NoError(t, err) + require.NotNil(t, req) + + // Test the request + x, ok := GetXpubFromRequest(req) + assert.Equal(t, testXpubAuth, x) + assert.Equal(t, true, ok) + + x, ok = GetXpubIDFromRequest(req) + assert.Equal(t, testXpubAuthHash, x) + assert.Equal(t, true, ok) + }) + + t.Run("xpub - valid signature", func(t *testing.T) { + key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) + require.NoError(t, err) + require.NotNil(t, key) + + var req *http.Request + req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) + require.NoError(t, err) + require.NotNil(t, req) + + err = SetSignature(&req.Header, key, `{}`) + require.NoError(t, err) + + _, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + req, err = client.AuthenticateRequest( + context.Background(), req, []string{}, false, true, false, + ) + require.NoError(t, err) + require.NotNil(t, req) + assert.Equal(t, true, req.Context().Value(ParamAuthSigned)) + }) + + t.Run("xpub - valid signature - not required", func(t *testing.T) { + key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) + require.NoError(t, err) + require.NotNil(t, key) + + var req *http.Request + req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) + require.NoError(t, err) + require.NotNil(t, req) + + var authData *AuthPayload + authData, err = createSignature(key, `{}`) + require.NoError(t, err) + require.NotNil(t, authData) + + err = SetSignature(&req.Header, key, `{}`) + require.NoError(t, err) + + _, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + req, err = client.AuthenticateRequest( + context.Background(), req, []string{authData.xPub}, false, false, false, + ) + require.NoError(t, err) + require.NotNil(t, req) + assert.Equal(t, true, req.Context().Value(ParamAuthSigned)) + }) + + t.Run("error - admin required", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) + require.NoError(t, err) + require.NotNil(t, req) + + req.Header.Set(AuthHeader, testXpubAuth) + + _, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + req, err = client.AuthenticateRequest( + context.Background(), req, []string{""}, true, false, false, + ) + require.Error(t, err) + require.NotNil(t, req) + assert.ErrorIs(t, err, ErrNotAdminKey) + }) + + t.Run("error - admin key - missing body", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) + require.NoError(t, err) + require.NotNil(t, req) + + req.Header.Set(AuthHeader, testXpubAuth) + + _, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + req, err = client.AuthenticateRequest( + context.Background(), req, []string{testXpubAuth}, true, false, false, + ) + require.Error(t, err) + require.NotNil(t, req) + assert.ErrorIs(t, err, ErrMissingBody) + }) + + t.Run("error - admin key - missing signature", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) + require.NoError(t, err) + require.NotNil(t, req) + + req.Header.Set(AuthHeader, testXpubAuth) + + _, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + req, err = client.AuthenticateRequest( + context.Background(), req, []string{testXpubAuth}, true, false, false, + ) + require.Error(t, err) + require.NotNil(t, req) + assert.ErrorIs(t, err, ErrMissingSignature) + }) + + t.Run("admin key - valid signature", func(t *testing.T) { + key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) + require.NoError(t, err) + require.NotNil(t, key) + + var req *http.Request + req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) + require.NoError(t, err) + require.NotNil(t, req) + + var authData *AuthPayload + authData, err = createSignature(key, `{}`) + require.NoError(t, err) + require.NotNil(t, authData) + + err = SetSignature(&req.Header, key, `{}`) + require.NoError(t, err) + + _, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + req, err = client.AuthenticateRequest( + context.Background(), req, []string{authData.xPub}, true, false, false, + ) + require.NoError(t, err) + require.NotNil(t, req) + }) + + t.Run("admin key - signing disabled", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) + require.NoError(t, err) + require.NotNil(t, req) + + req.Header.Set(AuthHeader, testXpubAuth) + + _, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + req, err = client.AuthenticateRequest( + context.Background(), req, []string{testXpubAuth}, true, false, true, + ) + require.NoError(t, err) + require.NotNil(t, req) + }) + + t.Run("no authentication header set", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) + require.NoError(t, err) + require.NotNil(t, req) + + _, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + req, err = client.AuthenticateRequest( + context.Background(), req, []string{""}, false, false, false, + ) + require.Error(t, err) + require.NotNil(t, req) + + // Test the request + x, ok := GetXpubFromRequest(req) + assert.Equal(t, "", x) + assert.Equal(t, false, ok) + + x, ok = GetXpubIDFromRequest(req) + assert.Equal(t, "", x) + assert.Equal(t, false, ok) + }) + + t.Run("invalid xpub length", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) + require.NoError(t, err) + require.NotNil(t, req) + + req.Header.Set(AuthHeader, "invalid-length") + + _, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + req, err = client.AuthenticateRequest( + context.Background(), req, []string{""}, false, false, false, + ) + require.Error(t, err) + require.NotNil(t, req) + + // Test the request + x, ok := GetXpubFromRequest(req) + assert.Equal(t, "", x) + assert.Equal(t, false, ok) + + x, ok = GetXpubIDFromRequest(req) + assert.Equal(t, "", x) + assert.Equal(t, false, ok) + }) + + t.Run("access key - not signed", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) + require.NoError(t, err) + require.NotNil(t, req) + + req.Header.Set(AuthAccessKey, "020202") + + _, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + _, err = client.AuthenticateRequest( + context.Background(), req, []string{""}, false, false, false, + ) + require.ErrorIs(t, err, ErrAuthAccessKeyNotFound) + }) + + t.Run("access key - key not found", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) + require.NoError(t, err) + require.NotNil(t, req) + + _, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + var authData *AuthPayload + // AuthAccessKey + authData, err = createSignatureAccessKey(testAccessKeyPKH, `{}`) + require.NoError(t, err) + require.NotNil(t, authData) + + err = SetSignatureFromAccessKey(&req.Header, testAccessKeyPKH, `{}`) + require.NoError(t, err) + + _, err = client.AuthenticateRequest( + context.Background(), req, []string{authData.xPub}, false, true, false, + ) + require.ErrorIs(t, err, ErrAuthAccessKeyNotFound) + }) + + t.Run("access key - valid signature", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) + require.NoError(t, err) + require.NotNil(t, req) + + ctx, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + accessKey := newAccessKey(testXPubID, append(client.DefaultModelOptions(), New())...) + err = accessKey.Save(ctx) + require.NoError(t, err) + + var authData *AuthPayload + // AuthAccessKey + authData, err = createSignatureAccessKey(accessKey.Key, `{}`) + require.NoError(t, err) + require.NotNil(t, authData) + + err = SetSignatureFromAccessKey(&req.Header, accessKey.Key, `{}`) + require.NoError(t, err) + + req, err = client.AuthenticateRequest( + context.Background(), req, []string{authData.xPub}, false, true, false, + ) + require.NoError(t, err) + require.NotNil(t, req) + assert.Equal(t, true, req.Context().Value(ParamAuthSigned)) + }) + + t.Run("access key - valid signature - not required", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", bytes.NewReader([]byte(`{}`))) + require.NoError(t, err) + require.NotNil(t, req) + + ctx, client, deferMe := CreateTestSQLiteClient(t, false, false) + defer deferMe() + + accessKey := newAccessKey(testXPubID, append(client.DefaultModelOptions(), New())...) + err = accessKey.Save(ctx) + require.NoError(t, err) + + var authData *AuthPayload + // AuthAccessKey + authData, err = createSignatureAccessKey(accessKey.Key, `{}`) + require.NoError(t, err) + require.NotNil(t, authData) + + err = SetSignatureFromAccessKey(&req.Header, accessKey.Key, `{}`) + require.NoError(t, err) + + req, err = client.AuthenticateRequest( + context.Background(), req, []string{authData.xPub}, false, false, false, + ) + require.NoError(t, err) + require.NotNil(t, req) + assert.Equal(t, true, req.Context().Value(ParamAuthSigned)) + }) +} + +// Test_verifyKeyXPub will test the method verifyKeyXPub() +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) + }) + + t.Run("error - missing auth signature", func(t *testing.T) { + err := checkSignatureRequirements(&AuthPayload{}) + require.Error(t, err) + assert.ErrorIs(t, err, ErrMissingSignature) + }) + + t.Run("error - auth hash mismatch", func(t *testing.T) { + err := checkSignatureRequirements(&AuthPayload{ + AuthHash: "bad-hash", + BodyContents: testBodyContents, + Signature: testSignature, + }) + require.Error(t, err) + assert.ErrorIs(t, err, ErrAuhHashMismatch) + }) + + t.Run("error - signature expired", func(t *testing.T) { + err := checkSignatureRequirements(&AuthPayload{ + AuthHash: testSignatureAuthHash, + BodyContents: testBodyContents, + Signature: testSignature, + AuthTime: 1643828414038, + }) + require.Error(t, err) + assert.ErrorIs(t, err, ErrSignatureExpired) + }) + + t.Run("error - bad xpub", func(t *testing.T) { + err := verifyKeyXPub("invalid-key", &AuthPayload{ + AuthHash: testSignatureAuthHash, + BodyContents: testBodyContents, + Signature: testSignature, + AuthTime: time.Now().UnixMilli(), + }) + require.Error(t, err) + }) + + t.Run("error - invalid signature - time is wrong", func(t *testing.T) { + err := checkSignatureRequirements(&AuthPayload{ + AuthHash: testSignatureAuthHash, + BodyContents: testBodyContents, + Signature: testSignature, + AuthTime: 0, + }) + require.Error(t, err) + assert.ErrorIs(t, err, ErrSignatureExpired) + }) + + t.Run("valid signature", func(t *testing.T) { + key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) + require.NoError(t, err) + require.NotNil(t, key) + + authData, err2 := createSignature(key, testBodyContents) + require.NoError(t, err2) + require.NotNil(t, authData) + + err = verifyKeyXPub(authData.xPub, &AuthPayload{ + AuthHash: authData.AuthHash, + AuthNonce: authData.AuthNonce, + AuthTime: authData.AuthTime, + BodyContents: testBodyContents, + Signature: authData.Signature, + }) + require.NoError(t, err) + }) +} + +// TestCreateSignature will test the method CreateSignature() +func TestCreateSignature(t *testing.T) { + t.Parallel() + + t.Run("valid signature", func(t *testing.T) { + key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) + require.NoError(t, err) + require.NotNil(t, key) + + var sig string + sig, err = CreateSignature(key, testBodyContents) + require.NoError(t, err) + require.NotNil(t, sig) + assert.Greater(t, len(sig), 40) + }) + + t.Run("missing key", func(t *testing.T) { + sig, err := CreateSignature(nil, testBodyContents) + require.Error(t, err) + assert.Equal(t, "", sig) + }) + + t.Run("missing body contents - still has signature", func(t *testing.T) { + key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) + require.NoError(t, err) + require.NotNil(t, key) + + var sig string + sig, err = CreateSignature(key, "") + require.NoError(t, err) + require.NotNil(t, sig) + assert.Greater(t, len(sig), 40) + }) +} + +// Test_createSignature will test the method createSignature() +func Test_createSignature(t *testing.T) { + t.Parallel() + + t.Run("valid signature", func(t *testing.T) { + key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) + require.NoError(t, err) + require.NotNil(t, key) + + var authData *AuthPayload + authData, err = createSignature(key, testBodyContents) + require.NoError(t, err) + require.NotNil(t, authData) + + assert.Equal(t, utils.XpubKeyLength, len(authData.xPub)) + assert.Equal(t, 64, len(authData.AuthHash)) + assert.Equal(t, 64, len(authData.AuthNonce)) + assert.Greater(t, authData.AuthTime, time.Now().Add(-1*time.Second).UnixMilli()) + + err = verifyKeyXPub(authData.xPub, &AuthPayload{ + AuthHash: authData.AuthHash, + AuthNonce: authData.AuthNonce, + AuthTime: authData.AuthTime, + BodyContents: testBodyContents, + Signature: authData.Signature, + }) + require.NoError(t, err) + }) + + t.Run("error - missing key", func(t *testing.T) { + authData, err := createSignature(nil, testBodyContents) + require.Error(t, err) + require.Nil(t, authData) + }) + + t.Run("error - empty body - valid signature", func(t *testing.T) { + key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) + require.NoError(t, err) + require.NotNil(t, key) + + var authData *AuthPayload + authData, err = createSignature(key, "") + require.NoError(t, err) + require.NotNil(t, authData) + + assert.Equal(t, utils.XpubKeyLength, len(authData.xPub)) + assert.Equal(t, 64, len(authData.AuthHash)) + assert.Equal(t, 64, len(authData.AuthNonce)) + assert.Greater(t, authData.AuthTime, time.Now().Add(-1*time.Second).UnixMilli()) + + err = verifyKeyXPub(authData.xPub, &AuthPayload{ + AuthHash: authData.AuthHash, + AuthNonce: authData.AuthNonce, + AuthTime: authData.AuthTime, + BodyContents: "", + Signature: authData.Signature, + }) + require.NoError(t, err) + }) +} + +// TestSetSignature will test the method SetSignature() +func TestSetSignature(t *testing.T) { + t.Parallel() + + t.Run("error - bad signature", func(t *testing.T) { + err := SetSignature(nil, nil, testBodyContents) + require.Error(t, err) + }) + + t.Run("valid set headers", func(t *testing.T) { + emptyHeaders := &http.Header{} + + key, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) + require.NoError(t, err) + require.NotNil(t, key) + + var xPub string + xPub, err = bitcoin.GetExtendedPublicKey(key) + require.NoError(t, err) + require.NotEmpty(t, xPub) + + err = SetSignature(emptyHeaders, key, testBodyContents) + require.NoError(t, err) + + assert.NotEmpty(t, emptyHeaders.Get(AuthHeader)) + assert.NotEmpty(t, emptyHeaders.Get(AuthHeaderHash)) + assert.NotEmpty(t, emptyHeaders.Get(AuthHeaderNonce)) + assert.NotEmpty(t, emptyHeaders.Get(AuthHeaderTime)) + assert.NotEmpty(t, emptyHeaders.Get(AuthSignature)) + + authTime, _ := strconv.Atoi(emptyHeaders.Get(AuthHeaderTime)) + err = verifyKeyXPub(xPub, &AuthPayload{ + AuthHash: emptyHeaders.Get(AuthHeaderHash), + AuthNonce: emptyHeaders.Get(AuthHeaderNonce), + AuthTime: int64(authTime), + BodyContents: testBodyContents, + Signature: emptyHeaders.Get(AuthSignature), + }) + require.NoError(t, err) + }) +} + +// Test_getSigningMessage will test the method Test_getSigningMessage() +func Test_getSigningMessage(t *testing.T) { + t.Parallel() + + t.Run("valid format", func(t *testing.T) { + message := getSigningMessage(testXpubAuth, &AuthPayload{ + AuthHash: testXpubAuthHash, + AuthNonce: "auth-nonce", + AuthTime: 12345678, + }) + assert.Equal(t, fmt.Sprintf("%s%s%s%d", testXpubAuth, testXpubAuthHash, "auth-nonce", 12345678), message) + }) +} + +// TestGetXpubFromRequest will test the method GetXpubFromRequest() +func TestGetXpubFromRequest(t *testing.T) { + t.Parallel() + + t.Run("valid value", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) + require.NoError(t, err) + require.NotNil(t, req) + + req = setOnRequest(req, ParamXPubKey, testXpubAuth) + + xPub, success := GetXpubFromRequest(req) + assert.Equal(t, testXpubAuth, xPub) + assert.Equal(t, true, success) + }) + + t.Run("no value", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) + require.NoError(t, err) + require.NotNil(t, req) + + xPub, success := GetXpubFromRequest(req) + assert.Equal(t, "", xPub) + assert.Equal(t, false, success) + }) +} + +// TestIsAdminRequest will test the method IsAdminRequest() +func TestIsAdminRequest(t *testing.T) { + t.Parallel() + + t.Run("no value", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) + require.NoError(t, err) + require.NotNil(t, req) + + isAdmin, ok := IsAdminRequest(req) + assert.Equal(t, false, ok) + assert.Equal(t, false, isAdmin) + }) + + t.Run("false value", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) + require.NoError(t, err) + require.NotNil(t, req) + + req = setOnRequest(req, ParamAdminRequest, false) + + isAdmin, ok := IsAdminRequest(req) + assert.Equal(t, true, ok) + assert.Equal(t, false, isAdmin) + }) + + t.Run("valid value", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) + require.NoError(t, err) + require.NotNil(t, req) + + req = setOnRequest(req, ParamAdminRequest, true) + + isAdmin, ok := IsAdminRequest(req) + assert.Equal(t, true, ok) + assert.Equal(t, true, isAdmin) + }) +} + +// TestGetXpubHashFromRequest will test the method GetXpubIDFromRequest() +func TestGetXpubIDFromRequest(t *testing.T) { + t.Parallel() + + t.Run("valid value", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) + require.NoError(t, err) + require.NotNil(t, req) + + req = setOnRequest(req, ParamXPubHashKey, testXpubAuthHash) + + xPubHash, success := GetXpubIDFromRequest(req) + assert.Equal(t, testXpubAuthHash, xPubHash) + assert.Equal(t, true, success) + }) + + t.Run("no value", func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "", nil) + require.NoError(t, err) + require.NotNil(t, req) + + xPubHash, success := GetXpubIDFromRequest(req) + assert.Equal(t, "", xPubHash) + assert.Equal(t, false, success) + }) +} diff --git a/engine/beef_bump.go b/engine/beef_bump.go new file mode 100644 index 000000000..4cf7fb921 --- /dev/null +++ b/engine/beef_bump.go @@ -0,0 +1,174 @@ +package engine + +import ( + "context" + "errors" + "fmt" + "sort" + + "github.com/libsv/go-bt/v2" +) + +func calculateMergedBUMP(txs []*Transaction) (BUMPs, error) { + bumps := make(map[uint64][]BUMP) + mergedBUMPs := make(BUMPs, 0) + + for _, tx := range txs { + if tx.BUMP.BlockHeight == 0 || len(tx.BUMP.Path) == 0 { + continue + } + + bumps[tx.BlockHeight] = append(bumps[tx.BlockHeight], tx.BUMP) + } + + // ensure that BUMPs are sorted by block height and will always be put in beef in the same order + mapKeys := make([]uint64, 0, len(bumps)) + for k := range bumps { + mapKeys = append(mapKeys, k) + } + sort.Slice(mapKeys, func(i, j int) bool { return mapKeys[i] < mapKeys[j] }) + + for _, k := range mapKeys { + bump, err := CalculateMergedBUMP(bumps[k]) + if err != nil { + return nil, fmt.Errorf("Error while calculating Merged BUMP: %s", err.Error()) + } + if bump == nil { + continue + } + mergedBUMPs = append(mergedBUMPs, bump) + } + + return mergedBUMPs, nil +} + +func validateBumps(bumps BUMPs) error { + if len(bumps) == 0 { + return errors.New("empty bump paths slice") + } + + for _, p := range bumps { + if len(p.Path) == 0 { + return errors.New("one of bump path is empty") + } + } + + return nil +} + +func prepareBEEFFactors(ctx context.Context, tx *Transaction, store TransactionGetter) ([]*bt.Tx, []*Transaction, error) { + btTxsNeededForBUMP, txsNeededForBUMP, err := initializeRequiredTxsCollection(tx) + if err != nil { + return nil, nil, err + } + + txIDs := make([]string, 0, len(tx.draftTransaction.Configuration.Inputs)) + for _, input := range tx.draftTransaction.Configuration.Inputs { + txIDs = append(txIDs, input.UtxoPointer.TransactionID) + } + + inputTxs, err := getRequiredTransactions(ctx, txIDs, store) + if err != nil { + return nil, nil, err + } + + for _, inputTx := range inputTxs { + inputBtTx, err := bt.NewTxFromString(inputTx.Hex) + if err != nil { + return nil, nil, fmt.Errorf("cannot convert to bt.Tx from hex (tx.ID: %s). Reason: %w", inputTx.ID, err) + } + + txsNeededForBUMP = append(txsNeededForBUMP, inputTx) + btTxsNeededForBUMP = append(btTxsNeededForBUMP, inputBtTx) + + if inputTx.BUMP.BlockHeight == 0 && len(inputTx.BUMP.Path) == 0 { + parentBtTransactions, parentTransactions, err := checkParentTransactions(ctx, store, inputBtTx) + if err != nil { + return nil, nil, err + } + + txsNeededForBUMP = append(txsNeededForBUMP, parentTransactions...) + btTxsNeededForBUMP = append(btTxsNeededForBUMP, parentBtTransactions...) + } + } + + return btTxsNeededForBUMP, txsNeededForBUMP, nil +} + +func checkParentTransactions(ctx context.Context, store TransactionGetter, btTx *bt.Tx) ([]*bt.Tx, []*Transaction, error) { + parentTxIDs := make([]string, 0, len(btTx.Inputs)) + for _, txIn := range btTx.Inputs { + parentTxIDs = append(parentTxIDs, txIn.PreviousTxIDStr()) + } + + parentTxs, err := getRequiredTransactions(ctx, parentTxIDs, store) + if err != nil { + return nil, nil, err + } + + 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 { + return nil, nil, fmt.Errorf("cannot convert to bt.Tx from hex (tx.ID: %s). Reason: %w", parentTx.ID, err) + } + validTxs = append(validTxs, parentTx) + validBtTxs = append(validBtTxs, parentBtTx) + + if parentTx.BUMP.BlockHeight == 0 && len(parentTx.BUMP.Path) == 0 { + parentValidBtTxs, parentValidTxs, err := checkParentTransactions(ctx, store, parentBtTx) + if err != nil { + return nil, nil, err + } + validTxs = append(validTxs, parentValidTxs...) + validBtTxs = append(validBtTxs, parentValidBtTxs...) + } + } + + return validBtTxs, validTxs, nil +} + +func getRequiredTransactions(ctx context.Context, txIds []string, store TransactionGetter) ([]*Transaction, error) { + txs, err := store.GetTransactionsByIDs(ctx, txIds) + if err != nil { + return nil, fmt.Errorf("cannot get transactions from database: %w", err) + } + + if len(txs) != len(txIds) { + missingTxIDs := getMissingTxs(txIds, txs) + return nil, fmt.Errorf("required transactions not found in database: %v", missingTxIDs) + } + + return txs, nil +} + +func getMissingTxs(txIDs []string, foundTxs []*Transaction) []string { + foundTxIDs := make(map[string]bool) + for _, tx := range foundTxs { + foundTxIDs[tx.ID] = true + } + + var missingTxIDs []string + for _, txID := range txIDs { + if !foundTxIDs[txID] { + missingTxIDs = append(missingTxIDs, txID) + } + } + return missingTxIDs +} + +func initializeRequiredTxsCollection(tx *Transaction) ([]*bt.Tx, []*Transaction, error) { + var btTxsNeededForBUMP []*bt.Tx + var txsNeededForBUMP []*Transaction + + processedBtTx, err := bt.NewTxFromString(tx.Hex) + if err != nil { + return nil, nil, fmt.Errorf("cannot convert processed tx to bt.Tx from hex (tx.ID: %s). Reason: %w", tx.ID, err) + } + + btTxsNeededForBUMP = append(btTxsNeededForBUMP, processedBtTx) + txsNeededForBUMP = append(txsNeededForBUMP, tx) + + return btTxsNeededForBUMP, txsNeededForBUMP, nil +} diff --git a/engine/beef_fixtures.go b/engine/beef_fixtures.go new file mode 100644 index 000000000..24d9e04f5 --- /dev/null +++ b/engine/beef_fixtures.go @@ -0,0 +1,9 @@ +package engine + +// Fixtures for beef_tx_test.go +var expectedBeefHex = map[int]string{ + 1: "0100beef02fea6790c000f02fda82c000703145efd05fec7c2edc1827ec685755a2d05208486e9bc2461268e0f5e533bfda92c02cb3553424ffc94b59a60fb358b6cb6dfb694aee894dcd1effc0ed0a9052464e301fd551600b7b53a09331453b5966589dc473b45c87823f109417593a82ff8ffe7ddc6f96e01fd2b0b0080085c6f18f35d5f0a231eda36c1be3f031734bb6cb6987978ef2aad007ea6da01fd940500d26d24b44097ed3f0c2927413dd2f1fb78bba948803abdb7f2fb51d9807a77bc01fdcb0200bad82dfd55455709713ea8390a7c76be5c076da9cd487b75f558728ef8572b7601fd64010022553e159788764f6b3c1e27324f99a13abee9c7069ce34b8a4fcabc45b7aabb01b300debfa516f54f4331ddf74067403d8a70915973e8300c298ff8dbe5bdcc94768101580079a933dbaeaee5dfb6af1ce9bd7a9ef40584e1844a938e398d19f94aba525bed012d005287b1c986e6495d00c103082618d9e0a30c0c620fd2d232eb9c31d59dec510e01170074639f74ebdaaac679e8fed504ae62a6f42c633a549a99e21940aff14e69ab08010a008eb3fde752d9e5c8e67c26daa95f8b9480ec11e608de7afae04eb19775f42342010400eea8e24927e93a0cf9ca50b315e48efca1f0b77643f7110c6efd94220dcedab6010300682c5f78d5f89e5e5d13619f543aacb00d1e4e85043f4d453bd6f6eb14755e790100006df75ca701801279bcb9b91579ac74b2131913ba35a0c3cb313c49062c8f453f0101009fe83bf3febfc2af4641b9bc8c1f76ff9b2ae7de9f4ab5374e672ac70cf8b9b9fec27f0c000b02fd8802000e40280d05fedfd66af3edb59a94f0512e804093f855c025b9d964ca23ca1b12fd890202624fbcb4e68d162361f456b8b4fef6b9e7943013088b32b6bca7f5ced41ff00401fd45010090f9751ef8c4daa8a15d0cc75d1a89e7fad8b8ba258f36da56fbb8faef725b1e01a3002863542b3fe0a1a8fdd6711f7b9af6d9fa2986bf6c67460e5e7fc544a3fa7ec90150003a0b1e497aae08b126ec790f9214517e32cff1a57cf3abe92e9580f634f369500129006d6f372a6b54acb13e4c0ce2b6ff53120685ac23bf8e1953f8358b87ebf90ca4011500c310dbdea5f87b557964e09c67926b6d756c9ea821948e36298544bf761bf9b5010b00a267e36fe2eef4ce3582e359d6b0b93df7d76afd00e19d4abff77d7cf2acf6db01040080c16fd797c5b4f8463e5bffc6ffacd7dae409c9908b4748ff28dabd8bbe8fe0010300b7e09ca039e8b8052f179d5f2c3d1d6e98bb983470784d62f03438621effe80a010000cf5b6da719ca8b752c50b5b9b4d7659cc5aaed7fe429d96d2270617414fb551f010100dc01860ec79aac9c4465b6afb0b0641bbf0b1c8a1c23bf7b920a1b662366ab40030100000001a114c7deb8deba851d87755aa10aa18c97bd77afee4e1bad01d1c50e07a644eb010000006a473044022041abd4f93bd1db1d0097f2d467ae183801d7842d23d0605fa9568040d245167402201be66c96bef4d6d051304f6df2aecbdfe23a8a05af0908ef2117ab5388d8903c412103c08545a40c819f6e50892e31e792d221b6df6da96ebdba9b6fe39305cc6cc768ffffffff0263040000000000001976a91454097d9d921f9a1f55084a943571d868552e924f88acb22a0000000000001976a914c36b3fca5159231033f3fbdca1cde942096d379f88ac0000000001010100000001cfc39e3adcd58ed58cf590079dc61c3eb6ec739abb7d22b592fb969d427f33ee000000006a4730440220253e674e64028459457d55b444f5f3dc15c658425e3184c628016739e4921fd502207c8fe20eb34e55e4115fbd82c23878b4e54f01f6c6ad0811282dd0b1df863b5e41210310a4366fd997127ad972b14c56ca2e18f39ca631ac9e3e4ad3d9827865d0cc70ffffffff0264000000000000001976a914668a92ff9cb5785eb8fc044771837a0818b028b588acdc4e0000000000001976a914b073264927a61cf84327dea77414df6c28b11e5988ac0000000001000100000002cb3553424ffc94b59a60fb358b6cb6dfb694aee894dcd1effc0ed0a9052464e3000000006a4730440220515c3bf93d38fa7cc164746fae4bec8b66c60a82509eb553751afa5971c3e41d0220321517fd5c997ab5f8ef0e59048ce9157de46f92b10d882bf898e62f3ee7343d4121038f1273fcb299405d8d140b4de9a2111ecb39291b2846660ebecd864d13bee575ffffffff624fbcb4e68d162361f456b8b4fef6b9e7943013088b32b6bca7f5ced41ff004010000006a47304402203fb24f6e00a6487cf88a3b39d8454786db63d649142ea76374c2f55990777e6302207fbb903d038cf43e13ffb496a64f36637ec7323e5ac48bb96bdb4a885100abca4121024b003d3cf49a8f48c1fe79b711b1d08e306c42a0ab8da004d97fccc4ced3343affffffff026f000000000000001976a914f232d38cd4c2f87c117af06542b04a7061b6640188aca62a0000000000001976a9146058e52d00e3b94211939f68cc2d9a3fc1e3db0f88ac0000000000", + 2: "0100beef01fe41800c000b06fd2c0402e01e74cab9a0571ab5a7d86794826f756a9c65dd0dea3bb3720c4051c488cf50fd2d0400d9060c543afb1c0faafb96667ed788324d4d1c338142a0841fe3ab9c30922cb4fd90040208c461a39a8877db46472f5cc59e5a108e417b1c9ea3091b71b65346d218f471fd910400ff8fa1e395088748feae2d7729ab9d5da0225f5ed80b2f295625f7c77da087f4fdcc05022256c94d07451664749e440f55cec8a37da1c46cf30a97579e2f9696b84ad484fdcd0500aecbe0a519d483bad8758c3a69cdc0dc12b19a363fded579ebc993edf510746503fd170200dd5d795e63f8777ef7a82453946150946435e52d4076089ce0cb15d8a1237c84fd4902000f3311a938e3f7977bb7a2db5ca912e4e0f26bd12744051333cd22cd3a2fad89fde702008695a1dfeec9393365a21690b089018c9d7dd94bbbf85b62f48701424e0e611c03fd0a010022373d021864aba56583c796bf9131c804a2ea40acede728b279af38b48dfdd2fd2501005e5f986a28e1cdf2b55b6e5bbcfa34742c45e016f7a920518f376c4b0cbfa868fd7201001287267e0f74f28a0dc5e3e0376fbf28c5ab06424a4dcfd02bb7a65b62d9849d038400a8109eb92b03a106ef15c9d120d7c34ff07ac280636632561e42280499f020659300a60aa07079a19a3600e7fc87cc6a72455f0c2f2735dd3d4039bdaf498469c4d3b800685a26978dfc493d1e03efd9a1c9cac0122d6d0bf027b523be85a4e2a2df3df90343006f78c5d6f4372c65cad0546446bd893db8c47f65e3eae2803ced606f4fd272924800b4a9bae40c785222f38e4127a169fbcbe45085b3e9c59d9631032d5a48dab2a35d00559d25e90b990524eb274251a22508ae04b36125dc894c4e1be21c43c19ab93c03200046bc3f2f79d2aa7da31093e690bb6c10a011a67f2a382937c5eaf423b903df5325006daf88be61cc906f104ac405b04d19f4771a63857a25915e376b53250abe112e2f0037461e9fa1f435caca254303b400b21cc452343572a68ad80d9d3287c2bd8f0f031100f14eebaa20670ebb3d9dab73d074550e3a93cfcb29d90c56dcc205aa8b6a51ab1300597b51a0440a0afe4c346f3b89c5f7aaa7478449f3eb6283e1e1f55b24e54b3b16001009f8ce41536d05ace952e35ce67fc94da2e97b6550f55fbc1d5aa5f3266829030800b668767e12637b80a04de0decb4b96b980b19bd0480557adebfc0c6a46cff1140900d24cec3667bedd9ff7e8bc26dff6ec5fcf8af5cc09f500cad08fdfa2ab2ccf870a00ebea0722a541a4f9e7c4659fdcad062e5806b8abba40cba82eba6882896a763d02040036de4d36e7fc3f273ddd83171a030a19c8668a1f5e03dd62ad53866f3afc127705005c9dc967c9a6dd0dd9c80660dd8e86faa3d7f070ed086f1b2d137147d0f52af00103008e9919d62be144a097dd23e1bf924b2e468022c12ccf50db6ceab3d043cdfd8d010000ae98483a460252d92b031d49f591d571e29f1c8b0ae9e2596e4cd24b1c549d3c0401000000019ed68f94dfa952554d777dbaa9e5c01acb3df767e40cabad7b6fb7547bfa871a010000006a4730440220287534d6ff51166e014ad91a2b677be4bd88cf08785624006cdb66553eafc8cf02204862f38e9d2982a5ee95a7850222f2208bff38637349ecfe41abe185498e4ead4121035ca1a2c6d2b46c61fd29e7697018f5ce2bae1ae735e23627046a2dd17ca8fb24ffffffff02de000000000000001976a914f5c9505bf02a4a2fb591e3568183f9c53cf157be88aca62b0000000000001976a91489b5e639bce3209e0888ea8b7eb4203de1c6148888ac000000000100010000000154aa46f1b3b7bde36c02e293b74d53e6c6eaed7411d286183b1dca766f42879a010000006b483045022100cd21d346073b4a0788018ff6938c44395d14cf5759fcc35a0899a8fe35a3c2a0022064eb9a005c3d0be03b61ab0e1c8757ed566dd935dacac37fcd1452adba4994b541210272d67492c31d0e6bead28c934fb1c9bb50ba9b46f886209fe95fb6a3e43bb27bffffffff0257040000000000001976a9140501308b6409cca5a7b5768c18ff2de8da4c1fa388ac39420000000000001976a91417e3d89f4aeacd5b4929fe04edc32c79b6182e1988ac0000000001000100000001e230ab1b300ac3ce334590fc308fee93ddbb252f6e4645e0a20f7e30dd541289010000006b483045022100a611fdf01eca42289d80e1265584e5bd487faa72e6142ebbc140a676f7c5037c0220409282aaadf580f458d97d61db43c94ac343e0b40674a80fd3ac47f43fd0c66c4121020a87e70cc26f7d5fe775f622d2705f27cfd6f5d2b574fea75401d6412a58b91affffffff02d2040000000000001976a9145d2117c4f66bdb335ce2707a74c46fa46d02cdb388acf23b0000000000001976a914effd80ee9df812990a8d7834fa8610491cbeb91688ac0000000001000100000003e01e74cab9a0571ab5a7d86794826f756a9c65dd0dea3bb3720c4051c488cf50000000006b483045022100bc7fc6ace1a5b1ab8601599d56b3adad4a11b7f11757f3225e96b46ca1ab7f7c0220324d6074aa987a7c63c404ac5b03c26e55d3c4209e298b4ca9df0e90aca43ef3412103ee05b34332b5662830c600b73f9c908bb8bff1813bc9b2690e9cad00fad23d3cffffffff08c461a39a8877db46472f5cc59e5a108e417b1c9ea3091b71b65346d218f471000000006b483045022100a936c496423ec03b1ad0f3bfe2348572d7b29ab14e4435c0c8e2ee093d930fde02203d9e86647ea18043c150289f74c6cf2ceb9ca3b228ae31c7b19c4eef813fb68d412103a19014bcc672ccdf18abb6972dd699367baed89c29b704385253ce2ae0eddad5ffffffff2256c94d07451664749e440f55cec8a37da1c46cf30a97579e2f9696b84ad484000000006b48304502210091b0bcf2e84d9ee65de437e8396b379941345e4cffac331af2ae29b8a16968a602205a00eed18a7ffe36f59ae6eb477d9002324cfc249c875260e6ade5bce852692d4121021446bd1df2b61952088a22a516550e43cd95e47ca2a778822d21268bd8b1cebeffffffff02c4090000000000001976a91497ebeffef6d9dd88ffbce922f1df97cbcd7f88d388ac42000000000000001976a91449457f2c101859d1c8ff90096385d3cc30e5488388ac0000000000", + 3: "0100beef02fe73780c000c02cc005d2fa529262bd8c5451e2e9e91ff0cffa1100b144bddbb537db6cef3868d68cccd02a114c7deb8deba851d87755aa10aa18c97bd77afee4e1bad01d1c50e07a644eb016700672bf5eb61032d3a9404ff6b2045a7ee3444a0612dd5f810fbf8ea363aae0b0b013200f62a9a8d99f570bfa8eb48ef0da91c84ab8110f826f67b72db053513b78baf430118000fec193c7d2ab70f1c74b7b2f4413db12a2da66cf40630ea0e57884d885dbf18010d0027f259788828eb8461da4e67e89c321c14be138576b039663da2dd84f84bcffc010700ebf4dd6d5eb43dcf25ef8025b17072ad3de6db031866c94570d86696bdb9533301020068d3cb01545e515692b45db66d8cf9f5020b0473fda254b2adc45308eee8db3801000072913db72c489f740085cb7aabe3c31271a2ea908b0c8498a75e89cab82a54c0010100491b7787ad57f9b034c950466026e844826a31d1227131dd54ffe965b3612bc20101001f83fda6372ec5bc5f4fb776506ebc01c14841d5173c4444cc3f37ab0539e00d010100cebf33ed7c7afce3c7c873d863ebccfc98a9450cefef843789c3683ab4dbf2c1010100dc365f1d99781e5053f678bb9e06d4a17bc6ed9a2258e2c69a2908800e4a7f2afea6790c000f02fda82c000703145efd05fec7c2edc1827ec685755a2d05208486e9bc2461268e0f5e533bfda92c02cb3553424ffc94b59a60fb358b6cb6dfb694aee894dcd1effc0ed0a9052464e301fd551600b7b53a09331453b5966589dc473b45c87823f109417593a82ff8ffe7ddc6f96e01fd2b0b0080085c6f18f35d5f0a231eda36c1be3f031734bb6cb6987978ef2aad007ea6da01fd940500d26d24b44097ed3f0c2927413dd2f1fb78bba948803abdb7f2fb51d9807a77bc01fdcb0200bad82dfd55455709713ea8390a7c76be5c076da9cd487b75f558728ef8572b7601fd64010022553e159788764f6b3c1e27324f99a13abee9c7069ce34b8a4fcabc45b7aabb01b300debfa516f54f4331ddf74067403d8a70915973e8300c298ff8dbe5bdcc94768101580079a933dbaeaee5dfb6af1ce9bd7a9ef40584e1844a938e398d19f94aba525bed012d005287b1c986e6495d00c103082618d9e0a30c0c620fd2d232eb9c31d59dec510e01170074639f74ebdaaac679e8fed504ae62a6f42c633a549a99e21940aff14e69ab08010a008eb3fde752d9e5c8e67c26daa95f8b9480ec11e608de7afae04eb19775f42342010400eea8e24927e93a0cf9ca50b315e48efca1f0b77643f7110c6efd94220dcedab6010300682c5f78d5f89e5e5d13619f543aacb00d1e4e85043f4d453bd6f6eb14755e790100006df75ca701801279bcb9b91579ac74b2131913ba35a0c3cb313c49062c8f453f0101009fe83bf3febfc2af4641b9bc8c1f76ff9b2ae7de9f4ab5374e672ac70cf8b9b904010000000150965003ea3d2c08bc79b116c9ffe7e730c9f9cf0a61e3df07868b24eac6f8d3000000006b4830450221009d3489f9e76ff3b043708972c52f85519e50a5fc35563d405e04b668780bf2ba0220024188508fc9c6870b2fc4f40b9484ae4163481199a5b4a7a338b86ec8952fee4121036a8b9d796ce2dee820d1f6d7a6ba07037dab4758f16028654fe4bc3a5c430b40ffffffff022a200000000000001976a91484c73348a8fbbc44cfa34f8f5441fc104f3bc78588ac162f0000000000001976a914590b1df63948c2c4e7a12a6e52012b36e25daa9888ac0000000001000100000001a114c7deb8deba851d87755aa10aa18c97bd77afee4e1bad01d1c50e07a644eb010000006a473044022041abd4f93bd1db1d0097f2d467ae183801d7842d23d0605fa9568040d245167402201be66c96bef4d6d051304f6df2aecbdfe23a8a05af0908ef2117ab5388d8903c412103c08545a40c819f6e50892e31e792d221b6df6da96ebdba9b6fe39305cc6cc768ffffffff0263040000000000001976a91454097d9d921f9a1f55084a943571d868552e924f88acb22a0000000000001976a914c36b3fca5159231033f3fbdca1cde942096d379f88ac00000000000100000001cfc39e3adcd58ed58cf590079dc61c3eb6ec739abb7d22b592fb969d427f33ee000000006a4730440220253e674e64028459457d55b444f5f3dc15c658425e3184c628016739e4921fd502207c8fe20eb34e55e4115fbd82c23878b4e54f01f6c6ad0811282dd0b1df863b5e41210310a4366fd997127ad972b14c56ca2e18f39ca631ac9e3e4ad3d9827865d0cc70ffffffff0264000000000000001976a914668a92ff9cb5785eb8fc044771837a0818b028b588acdc4e0000000000001976a914b073264927a61cf84327dea77414df6c28b11e5988ac0000000001010100000002cb3553424ffc94b59a60fb358b6cb6dfb694aee894dcd1effc0ed0a9052464e3000000006a4730440220515c3bf93d38fa7cc164746fae4bec8b66c60a82509eb553751afa5971c3e41d0220321517fd5c997ab5f8ef0e59048ce9157de46f92b10d882bf898e62f3ee7343d4121038f1273fcb299405d8d140b4de9a2111ecb39291b2846660ebecd864d13bee575ffffffff624fbcb4e68d162361f456b8b4fef6b9e7943013088b32b6bca7f5ced41ff004010000006a47304402203fb24f6e00a6487cf88a3b39d8454786db63d649142ea76374c2f55990777e6302207fbb903d038cf43e13ffb496a64f36637ec7323e5ac48bb96bdb4a885100abca4121024b003d3cf49a8f48c1fe79b711b1d08e306c42a0ab8da004d97fccc4ced3343affffffff026f000000000000001976a914f232d38cd4c2f87c117af06542b04a7061b6640188aca62a0000000000001976a9146058e52d00e3b94211939f68cc2d9a3fc1e3db0f88ac0000000000", + 4: "0100beef02fedd770c000b025a0250965003ea3d2c08bc79b116c9ffe7e730c9f9cf0a61e3df07868b24eac6f8d35b00d23a2585c30d0aa45dc2da97ea2da65d6e8a89600d719ada673861ab65ad525b012c000c4ffef5e63cde231502a66acf392337114b6daea97ca2f7ca5cf6a7be38c384011700dd80513a91875591df2265dae8cdc909a01113459ad6d8d9a5c051e092f26058010a0078ef0896fb0077e619d1a4942ba6272b23ff2f48d345826aaf6393688ab03f6301040052ebb99aa1adce1852250a49fd22e18a1798d08840eb13c3b8b1eb0c13bb804b010300cf10a22ebde10e1d98515d5288305e027e3cc0a28cf24c8b3c22a3dd35033dcf0100005bd338bdd23417971c877571c083b8f7fb2a7c0b179b77bed1ab30252c46c7990101007f673d90f535d5875ba5694d23d2a9653b4ad4ca3ef4e61b1b282654e3c7669a0101005b544e50fd2f878b1d7cc0b5c7b63377552e4ad7f01e5dada39984016141e3400101008713a257ca2ba89608cc62133eb6a3eeab15aec4f2e3d7b1a23e4d92d7b1e6dc0101007ccb140e81af58f6a301bd798024ee9515a53094c58b32dab99c18972b4efee1fea6790c000f02fda82c000703145efd05fec7c2edc1827ec685755a2d05208486e9bc2461268e0f5e533bfda92c02cb3553424ffc94b59a60fb358b6cb6dfb694aee894dcd1effc0ed0a9052464e301fd551600b7b53a09331453b5966589dc473b45c87823f109417593a82ff8ffe7ddc6f96e01fd2b0b0080085c6f18f35d5f0a231eda36c1be3f031734bb6cb6987978ef2aad007ea6da01fd940500d26d24b44097ed3f0c2927413dd2f1fb78bba948803abdb7f2fb51d9807a77bc01fdcb0200bad82dfd55455709713ea8390a7c76be5c076da9cd487b75f558728ef8572b7601fd64010022553e159788764f6b3c1e27324f99a13abee9c7069ce34b8a4fcabc45b7aabb01b300debfa516f54f4331ddf74067403d8a70915973e8300c298ff8dbe5bdcc94768101580079a933dbaeaee5dfb6af1ce9bd7a9ef40584e1844a938e398d19f94aba525bed012d005287b1c986e6495d00c103082618d9e0a30c0c620fd2d232eb9c31d59dec510e01170074639f74ebdaaac679e8fed504ae62a6f42c633a549a99e21940aff14e69ab08010a008eb3fde752d9e5c8e67c26daa95f8b9480ec11e608de7afae04eb19775f42342010400eea8e24927e93a0cf9ca50b315e48efca1f0b77643f7110c6efd94220dcedab6010300682c5f78d5f89e5e5d13619f543aacb00d1e4e85043f4d453bd6f6eb14755e790100006df75ca701801279bcb9b91579ac74b2131913ba35a0c3cb313c49062c8f453f0101009fe83bf3febfc2af4641b9bc8c1f76ff9b2ae7de9f4ab5374e672ac70cf8b9b9050100000002787a565270ec00b1bf6ed20100223176656705dc0cfe5ef9d1810ca6569f12d1020000006a47304402203cfe36be7ff5c2ac939bb6a625e4a1226be242f1f9950672b5f696ec58a3358902202a48d6c6e81e5950dc49d0dd1a35b46fa8f919b109b0e7c05deaef3db6051890412102fb130326dbd7c43841cde467196e5f289b9d8596e237725df84f768468426d8bffffffff008d9db2a5c8c310e6394c24c1f3c23b3adbdd6ab4a719e917a4a0ed78768773020000006a473044022049c80385f7f69e8ba6039ebe84fe5e6578f4c3c83eb622442a96219c59ac1a750220317fe2b47838dff11f88d909732d0846eba20acff57cb357a3ff39b5a7b61b3741210322b79b40a759c485eac318eabba60a73a49ec3307ded79ba8c47204405bb2f3fffffffff05414f0000000000001976a91400414bcf2602f309171901d837b4a155adbfb5ce88ac50c30000000000001976a91489ef778cc07c77cce1ad3ff6274615afe15f20c088ac204e0000000000001976a914971b76df1dc6acf01e8e7d2f8bfb3c86e69bc64c88acef250000000000001976a9144b4a836b444d5ed8d245ddb1aa878908e36cd6b588ac9d860100000000001976a9144405da67e318e9cfd9d6ce9dffce27af5f60522888ac000000000100010000000150965003ea3d2c08bc79b116c9ffe7e730c9f9cf0a61e3df07868b24eac6f8d3000000006b4830450221009d3489f9e76ff3b043708972c52f85519e50a5fc35563d405e04b668780bf2ba0220024188508fc9c6870b2fc4f40b9484ae4163481199a5b4a7a338b86ec8952fee4121036a8b9d796ce2dee820d1f6d7a6ba07037dab4758f16028654fe4bc3a5c430b40ffffffff022a200000000000001976a91484c73348a8fbbc44cfa34f8f5441fc104f3bc78588ac162f0000000000001976a914590b1df63948c2c4e7a12a6e52012b36e25daa9888ac00000000000100000001a114c7deb8deba851d87755aa10aa18c97bd77afee4e1bad01d1c50e07a644eb010000006a473044022041abd4f93bd1db1d0097f2d467ae183801d7842d23d0605fa9568040d245167402201be66c96bef4d6d051304f6df2aecbdfe23a8a05af0908ef2117ab5388d8903c412103c08545a40c819f6e50892e31e792d221b6df6da96ebdba9b6fe39305cc6cc768ffffffff0263040000000000001976a91454097d9d921f9a1f55084a943571d868552e924f88acb22a0000000000001976a914c36b3fca5159231033f3fbdca1cde942096d379f88ac00000000000100000001cfc39e3adcd58ed58cf590079dc61c3eb6ec739abb7d22b592fb969d427f33ee000000006a4730440220253e674e64028459457d55b444f5f3dc15c658425e3184c628016739e4921fd502207c8fe20eb34e55e4115fbd82c23878b4e54f01f6c6ad0811282dd0b1df863b5e41210310a4366fd997127ad972b14c56ca2e18f39ca631ac9e3e4ad3d9827865d0cc70ffffffff0264000000000000001976a914668a92ff9cb5785eb8fc044771837a0818b028b588acdc4e0000000000001976a914b073264927a61cf84327dea77414df6c28b11e5988ac0000000001010100000002cb3553424ffc94b59a60fb358b6cb6dfb694aee894dcd1effc0ed0a9052464e3000000006a4730440220515c3bf93d38fa7cc164746fae4bec8b66c60a82509eb553751afa5971c3e41d0220321517fd5c997ab5f8ef0e59048ce9157de46f92b10d882bf898e62f3ee7343d4121038f1273fcb299405d8d140b4de9a2111ecb39291b2846660ebecd864d13bee575ffffffff624fbcb4e68d162361f456b8b4fef6b9e7943013088b32b6bca7f5ced41ff004010000006a47304402203fb24f6e00a6487cf88a3b39d8454786db63d649142ea76374c2f55990777e6302207fbb903d038cf43e13ffb496a64f36637ec7323e5ac48bb96bdb4a885100abca4121024b003d3cf49a8f48c1fe79b711b1d08e306c42a0ab8da004d97fccc4ced3343affffffff026f000000000000001976a914f232d38cd4c2f87c117af06542b04a7061b6640188aca62a0000000000001976a9146058e52d00e3b94211939f68cc2d9a3fc1e3db0f88ac0000000000", +} diff --git a/engine/beef_tx.go b/engine/beef_tx.go new file mode 100644 index 000000000..b83915f2d --- /dev/null +++ b/engine/beef_tx.go @@ -0,0 +1,89 @@ +package engine + +import ( + "context" + "encoding/hex" + "fmt" + + "github.com/libsv/go-bt/v2" +) + +const maxBeefVer = uint32(0xFFFF) // value from BRC-62 + +type beefTx struct { + version uint32 + bumps BUMPs + transactions []*bt.Tx +} + +// ToBeef generates BEEF Hex for transaction +func ToBeef(ctx context.Context, tx *Transaction, store TransactionGetter) (string, error) { + if err := hydrateTransaction(ctx, tx); err != nil { + return "", err + } + + bumpBtFactors, bumpFactors, err := prepareBEEFFactors(ctx, tx, store) + if err != nil { + return "", fmt.Errorf("prepareBUMPFactors() error: %w", err) + } + + bumps, err := calculateMergedBUMP(bumpFactors) + if err != nil { + return "", err + } + sortedTxs := kahnTopologicalSortTransactions(bumpBtFactors) + beefHex, err := toBeefHex(bumps, sortedTxs) + if err != nil { + return "", fmt.Errorf("ToBeef() error: %w", err) + } + + return beefHex, nil +} + +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) + } + + beefBytes, err := beef.toBeefBytes() + if err != nil { + return "", fmt.Errorf("ToBeefHex() error: %w", err) + } + + return hex.EncodeToString(beefBytes), nil +} + +func newBeefTx(version uint32, bumps BUMPs, parentTxs []*bt.Tx) (*beefTx, error) { + if version > maxBeefVer { + return nil, fmt.Errorf("version above 0x%X", maxBeefVer) + } + + if err := validateBumps(bumps); err != nil { + return nil, err + } + + beef := &beefTx{ + version: version, + bumps: bumps, + transactions: parentTxs, + } + + return beef, nil +} + +func hydrateTransaction(ctx context.Context, tx *Transaction) error { + if tx.draftTransaction == nil { + dTx, err := getDraftTransactionID( + ctx, tx.XPubID, tx.DraftID, tx.GetOptions(false)..., + ) + + if err != nil || dTx == nil { + return fmt.Errorf("retrieve DraftTransaction failed: %w", err) + } + + tx.draftTransaction = dTx + } + + return nil +} diff --git a/engine/beef_tx_bytes.go b/engine/beef_tx_bytes.go new file mode 100644 index 000000000..5d5934889 --- /dev/null +++ b/engine/beef_tx_bytes.go @@ -0,0 +1,85 @@ +package engine + +import ( + "errors" + + "github.com/libsv/go-bt/v2" +) + +var ( + hasBUMP = byte(0x01) + hasNoBUMP = byte(0x00) +) + +func (beefTx *beefTx) toBeefBytes() ([]byte, error) { + if len(beefTx.bumps) == 0 || len(beefTx.transactions) < 2 { // valid BEEF contains at least two transactions (new transaction and one parent transaction) + return nil, errors.New("beef tx is incomplete") + } + + // get beef bytes + beefSize := 0 + + ver := bt.LittleEndianBytes(beefTx.version, 4) + ver[2] = 0xBE + ver[3] = 0xEF + beefSize += len(ver) + + nBUMPS := bt.VarInt(len(beefTx.bumps)).Bytes() + beefSize += len(nBUMPS) + + bumps := beefTx.bumps.Bytes() + beefSize += len(bumps) + + nTransactions := bt.VarInt(uint64(len(beefTx.transactions))).Bytes() + beefSize += len(nTransactions) + + transactions := make([][]byte, 0, len(beefTx.transactions)) + + for _, t := range beefTx.transactions { + txBytes := toBeefBytes(t, beefTx.bumps) + transactions = append(transactions, txBytes) + beefSize += len(txBytes) + } + + // compose beef + buffer := make([]byte, 0, beefSize) + buffer = append(buffer, ver...) + buffer = append(buffer, nBUMPS...) + buffer = append(buffer, bumps...) + + buffer = append(buffer, nTransactions...) + + for _, t := range transactions { + buffer = append(buffer, t...) + } + + return buffer, nil +} + +func toBeefBytes(tx *bt.Tx, bumps BUMPs) []byte { + txBeefBytes := tx.Bytes() + + bumpIdx := getBumpPathIndex(tx, bumps) + if bumpIdx > -1 { + txBeefBytes = append(txBeefBytes, hasBUMP) + txBeefBytes = append(txBeefBytes, bt.VarInt(bumpIdx).Bytes()...) + } else { + txBeefBytes = append(txBeefBytes, hasNoBUMP) + } + + return txBeefBytes +} + +func getBumpPathIndex(tx *bt.Tx, bumps BUMPs) int { + bumpIndex := -1 + + for i, bump := range bumps { + for _, path := range bump.Path[0] { + if path.Hash == tx.TxID() { + bumpIndex = i + } + } + } + + return bumpIndex +} diff --git a/engine/beef_tx_mock.go b/engine/beef_tx_mock.go new file mode 100644 index 000000000..c7adff57b --- /dev/null +++ b/engine/beef_tx_mock.go @@ -0,0 +1,29 @@ +package engine + +import ( + "context" +) + +type MockTransactionStore struct { + Transactions map[string]*Transaction +} + +func NewMockTransactionStore() *MockTransactionStore { + return &MockTransactionStore{ + Transactions: make(map[string]*Transaction), + } +} + +func (m *MockTransactionStore) AddToStore(tx *Transaction) { + m.Transactions[tx.ID] = tx +} + +func (m *MockTransactionStore) GetTransactionsByIDs(ctx context.Context, txIDs []string) ([]*Transaction, error) { + var txs []*Transaction + for _, txID := range txIDs { + if tx, exists := m.Transactions[txID]; exists { + txs = append(txs, tx) + } + } + return txs, nil +} diff --git a/engine/beef_tx_sorting.go b/engine/beef_tx_sorting.go new file mode 100644 index 000000000..4f4fa9822 --- /dev/null +++ b/engine/beef_tx_sorting.go @@ -0,0 +1,79 @@ +package engine + +import "github.com/libsv/go-bt/v2" + +func kahnTopologicalSortTransactions(transactions []*bt.Tx) []*bt.Tx { + txByID, incomingEdgesMap, zeroIncomingEdgeQueue := prepareSortStructures(transactions) + result := make([]*bt.Tx, 0, len(transactions)) + + for len(zeroIncomingEdgeQueue) > 0 { + txID := zeroIncomingEdgeQueue[0] + zeroIncomingEdgeQueue = zeroIncomingEdgeQueue[1:] + + tx := txByID[txID] + result = append(result, tx) + + zeroIncomingEdgeQueue = removeTxFromIncomingEdges(tx, incomingEdgesMap, zeroIncomingEdgeQueue) + } + + reverseInPlace(result) + return result +} + +func prepareSortStructures(dag []*bt.Tx) (txByID map[string]*bt.Tx, incomingEdgesMap map[string]int, zeroIncomingEdgeQueue []string) { + dagLen := len(dag) + txByID = make(map[string]*bt.Tx, dagLen) + 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 calculation each time + incomingEdgesMap[tx.TxID()] = 0 + } + + calculateIncomingEdges(incomingEdgesMap, txByID) + zeroIncomingEdgeQueue = getTxWithZeroIncomingEdges(incomingEdgesMap) + + return +} + +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 calculation each time + if _, ok := txByID[inputUtxoTxID]; ok { // transaction can contains inputs we are not interested in + inDegree[inputUtxoTxID]++ + } + } + } +} + +func getTxWithZeroIncomingEdges(incomingEdgesMap map[string]int) []string { + zeroIncomingEdgeQueue := make([]string, 0, len(incomingEdgesMap)) + + for txID, edgeNum := range incomingEdgesMap { + if edgeNum == 0 { + zeroIncomingEdgeQueue = append(zeroIncomingEdgeQueue, txID) + } + } + + return zeroIncomingEdgeQueue +} + +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 calculation each time + incomingEdgesMap[neighborID]-- + + if incomingEdgesMap[neighborID] == 0 { + zeroIncomingEdgeQueue = append(zeroIncomingEdgeQueue, neighborID) + } + } + + return zeroIncomingEdgeQueue +} + +func reverseInPlace(collection []*bt.Tx) { + for i, j := 0, len(collection)-1; i < j; i, j = i+1, j-1 { + collection[i], collection[j] = collection[j], collection[i] + } +} diff --git a/engine/beef_tx_sorting_test.go b/engine/beef_tx_sorting_test.go new file mode 100644 index 000000000..84a400036 --- /dev/null +++ b/engine/beef_tx_sorting_test.go @@ -0,0 +1,131 @@ +package engine + +import ( + "fmt" + "math/rand" + "testing" + + "github.com/libsv/go-bt/v2" + "github.com/stretchr/testify/assert" +) + +func Test_kahnTopologicalSortTransaction(t *testing.T) { + tCases := []struct { + name string + expectedSortedTransactions []*bt.Tx + }{ + { + name: "txs with necessary data only", + expectedSortedTransactions: getTxsFromOldestToNewestWithNecessaryDataOnly(), + }, + { + name: "txs with inputs from other txs", + expectedSortedTransactions: getTxsFromOldestToNewestWithUnnecessaryData(), + }, + } + + for _, tc := range tCases { + t.Run(fmt.Sprint("sort from oldest to newest ", tc.name), func(t *testing.T) { + // given + unsortedTxs := shuffleTransactions(tc.expectedSortedTransactions) + + // when + sortedGraph := kahnTopologicalSortTransactions(unsortedTxs) + + // then + for i, tx := range tc.expectedSortedTransactions { + assert.Equal(t, tx.TxID(), sortedGraph[i].TxID()) + } + }) + } +} + +func getTxsFromOldestToNewestWithNecessaryDataOnly() []*bt.Tx { + // create related transactions from oldest to newest + oldestTx := createTx() + secondTx := createTx(oldestTx) + thirdTx := createTx(secondTx) + fourthTx := createTx(thirdTx, secondTx) + fifthTx := createTx(fourthTx, secondTx) + sixthTx := createTx(fourthTx, thirdTx) + seventhTx := createTx(fifthTx, thirdTx, oldestTx) + eightTx := createTx(seventhTx, sixthTx, fourthTx, secondTx) + + newestTx := createTx(eightTx) + + txsFromOldestToNewest := []*bt.Tx{ + oldestTx, + secondTx, + thirdTx, + fourthTx, + fifthTx, + sixthTx, + seventhTx, + eightTx, + newestTx, + } + + return txsFromOldestToNewest +} + +func getTxsFromOldestToNewestWithUnnecessaryData() []*bt.Tx { + unnecessaryParentTx1 := createTx() + unnecessaryParentTx2 := createTx() + unnecessaryParentTx3 := createTx() + unnecessaryParentTx4 := createTx() + + // create related transactions from oldest to newest + oldestTx := createTx() + secondTx := createTx(oldestTx) + thirdTx := createTx(secondTx) + fourthTx := createTx(thirdTx, secondTx, unnecessaryParentTx1, unnecessaryParentTx4) + fifthTx := createTx(fourthTx, secondTx) + sixthTx := createTx(fourthTx, thirdTx, unnecessaryParentTx3, unnecessaryParentTx2, unnecessaryParentTx1) + seventhTx := createTx(fifthTx, thirdTx, oldestTx) + eightTx := createTx(seventhTx, sixthTx, fourthTx, secondTx, unnecessaryParentTx1) + + newestTx := createTx(eightTx) + + txsFromOldestToNewest := []*bt.Tx{ + oldestTx, + secondTx, + thirdTx, + fourthTx, + fifthTx, + sixthTx, + seventhTx, + eightTx, + newestTx, + } + + return txsFromOldestToNewest +} + +func createTx(inputsParents ...*bt.Tx) *bt.Tx { + inputs := make([]*bt.Input, 0) + + for _, parent := range inputsParents { + in := bt.Input{} + in.PreviousTxIDAdd(parent.TxIDBytes()) + + inputs = append(inputs, &in) + } + + transaction := bt.NewTx() + transaction.Inputs = append(transaction.Inputs, inputs...) + + return transaction +} + +func shuffleTransactions(txs []*bt.Tx) []*bt.Tx { + n := len(txs) + result := make([]*bt.Tx, n) + copy(result, txs) + + for i := n - 1; i > 0; i-- { + j := rand.Intn(i + 1) + result[i], result[j] = result[j], result[i] + } + + return result +} diff --git a/engine/beef_tx_test.go b/engine/beef_tx_test.go new file mode 100644 index 000000000..722b04618 --- /dev/null +++ b/engine/beef_tx_test.go @@ -0,0 +1,352 @@ +package engine + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type beefTestCase struct { + testID int + name string + hexForProcessedTx string + outputValue uint64 + receiverAddress string + ancestors []*beefTestCaseAncestor + expectedErrorMessage string +} + +type beefTestCaseAncestor struct { + // reverse condition to not set this value every time + doNotAddToStore bool + hex string + isMined bool + bumpJSON string + blockHeight int + parents []*beefTestCaseAncestor +} + +func Test_ToBeef_HappyPaths(t *testing.T) { + testCases := []beefTestCase{ + { + testID: 1, + hexForProcessedTx: "0100000002cb3553424ffc94b59a60fb358b6cb6dfb694aee894dcd1effc0ed0a9052464e3000000006a4730440220515c3bf93d38fa7cc164746fae4bec8b66c60a82509eb553751afa5971c3e41d0220321517fd5c997ab5f8ef0e59048ce9157de46f92b10d882bf898e62f3ee7343d4121038f1273fcb299405d8d140b4de9a2111ecb39291b2846660ebecd864d13bee575ffffffff624fbcb4e68d162361f456b8b4fef6b9e7943013088b32b6bca7f5ced41ff004010000006a47304402203fb24f6e00a6487cf88a3b39d8454786db63d649142ea76374c2f55990777e6302207fbb903d038cf43e13ffb496a64f36637ec7323e5ac48bb96bdb4a885100abca4121024b003d3cf49a8f48c1fe79b711b1d08e306c42a0ab8da004d97fccc4ced3343affffffff026f000000000000001976a914f232d38cd4c2f87c117af06542b04a7061b6640188aca62a0000000000001976a9146058e52d00e3b94211939f68cc2d9a3fc1e3db0f88ac00000000", + name: "all inputs are already mined - 2 inputs on the same level", + ancestors: []*beefTestCaseAncestor{ + { + hex: "0100000001cfc39e3adcd58ed58cf590079dc61c3eb6ec739abb7d22b592fb969d427f33ee000000006a4730440220253e674e64028459457d55b444f5f3dc15c658425e3184c628016739e4921fd502207c8fe20eb34e55e4115fbd82c23878b4e54f01f6c6ad0811282dd0b1df863b5e41210310a4366fd997127ad972b14c56ca2e18f39ca631ac9e3e4ad3d9827865d0cc70ffffffff0264000000000000001976a914668a92ff9cb5785eb8fc044771837a0818b028b588acdc4e0000000000001976a914b073264927a61cf84327dea77414df6c28b11e5988ac00000000", + isMined: true, + bumpJSON: `{"blockHeight":"817574","path":[[{"offset":"11432","hash":"3b535e0f8e266124bce9868420052d5a7585c67e82c1edc2c7fe05fd5e140307"},{"offset":"11433","hash":"e3642405a9d00efcefd1dc94e8ae94b6dfb66c8b35fb609ab594fc4f425335cb","txid":true}],[{"offset":"5717","hash":"6ef9c6dde7fff82fa893754109f12378c8453b47dc896596b5531433093ab5b7"}],[{"offset":"2859","hash":"daa67e00ad2aef787998b66cbb3417033fbec136da1e230a5f5df3186f5c0880"}],[{"offset":"1428","hash":"bc777a80d951fbf2b7bd3a8048a9bb78fbf1d23d4127290c3fed9740b4246dd2"}],[{"offset":"715","hash":"762b57f88e7258f5757b48cda96d075cbe767c0a39a83e7109574555fd2dd8ba"}],[{"offset":"356","hash":"bbaab745bcca4f8a4be39c06c7e9be3aa1994f32271e3c6b4f768897153e5522"}],[{"offset":"179","hash":"817694ccbde5dbf88f290c30e8735991708a3d406740f7dd31434ff516a5bfde"}],[{"offset":"88","hash":"ed5b52ba4af9198d398e934a84e18405f49e7abde91cafb6dfe5aeaedb33a979"}],[{"offset":"45","hash":"0e51ec9dd5319ceb32d2d20f620c0ca3e0d918260803c1005d49e686c9b18752"}],[{"offset":"23","hash":"08ab694ef1af4019e2999a543a632cf4a662ae04d5fee879c6aadaeb749f6374"}],[{"offset":"10","hash":"4223f47597b14ee0fa7ade08e611ec80948b5fa9da267ce6c8e5d952e7fdb38e"}],[{"offset":"4","hash":"b6dace0d2294fd6e0c11f74376b7f0a1fc8ee415b350caf90c3ae92749e2a8ee"}],[{"offset":"3","hash":"795e7514ebf6d63b454d3f04854e1e0db0ac3a549f61135d5e9ef8d5785f2c68"}],[{"offset":"0","hash":"3f458f2c06493c31cbc3a035ba131913b274ac7915b9b9bc79128001a75cf76d"}],[{"offset":"1","hash":"b9b9f80cc72a674e37b54a9fdee72a9bff761f8cbcb94146afc2bffef33be89f"}]]}`, + blockHeight: 817574, + }, + { + hex: "0100000001a114c7deb8deba851d87755aa10aa18c97bd77afee4e1bad01d1c50e07a644eb010000006a473044022041abd4f93bd1db1d0097f2d467ae183801d7842d23d0605fa9568040d245167402201be66c96bef4d6d051304f6df2aecbdfe23a8a05af0908ef2117ab5388d8903c412103c08545a40c819f6e50892e31e792d221b6df6da96ebdba9b6fe39305cc6cc768ffffffff0263040000000000001976a91454097d9d921f9a1f55084a943571d868552e924f88acb22a0000000000001976a914c36b3fca5159231033f3fbdca1cde942096d379f88ac00000000", + isMined: true, + bumpJSON: `{"blockHeight":"819138","path":[[{"offset":"648","hash":"121bca23ca64d9b925c055f89340802e51f0949ab5edf36ad6dffe050d28400e"},{"offset":"649","hash":"04f01fd4cef5a7bcb6328b08133094e7b9f6feb4b856f46123168de6b4bc4f62","txid":true}],[{"offset":"325","hash":"1e5b72effab8fb56da368f25bab8d8fae7891a5dc70c5da1a8dac4f81e75f990"}],[{"offset":"163","hash":"c97efaa344c57f5e0e46676cbf8629fad9f69a7b1f71d6fda8a1e03f2b546328"}],[{"offset":"80","hash":"5069f334f680952ee9abf37ca5f1cf327e5114920f79ec26b108ae7a491e0b3a"}],[{"offset":"41","hash":"a40cf9eb878b35f853198ebf23ac85061253ffb6e20c4c3eb1ac546b2a376f6d"}],[{"offset":"21","hash":"b5f91b76bf448529368e9421a89e6c756d6b92679ce06479557bf8a5dedb10c3"}],[{"offset":"11","hash":"dbf6acf27c7df7bf4a9de100fd6ad7f73db9b0d659e38235cef4eee26fe367a2"}],[{"offset":"4","hash":"e08fbe8bbdda28ff48478b90c909e4dad7acffc6ff5b3e46f8b4c597d76fc180"}],[{"offset":"3","hash":"0ae8ff1e623834f0624d78703498bb986e1d3d2c5f9d172f05b8e839a09ce0b7"}],[{"offset":"0","hash":"1f55fb14746170226dd929e47fedaac59c65d7b4b9b5502c758bca19a76d5bcf"}],[{"offset":"1","hash":"40ab6623661b0a927bbf231c8a1c0bbf1b64b0b0afb665449cac9ac70e8601dc"}]]}`, + blockHeight: 819138, + }, + }, + receiverAddress: "1A1PjKqjWMNBzTVdcBru27EV1PHcXWc63W", + outputValue: 3000, + expectedErrorMessage: "", + }, + { + testID: 2, + hexForProcessedTx: "0100000003e01e74cab9a0571ab5a7d86794826f756a9c65dd0dea3bb3720c4051c488cf50000000006b483045022100bc7fc6ace1a5b1ab8601599d56b3adad4a11b7f11757f3225e96b46ca1ab7f7c0220324d6074aa987a7c63c404ac5b03c26e55d3c4209e298b4ca9df0e90aca43ef3412103ee05b34332b5662830c600b73f9c908bb8bff1813bc9b2690e9cad00fad23d3cffffffff08c461a39a8877db46472f5cc59e5a108e417b1c9ea3091b71b65346d218f471000000006b483045022100a936c496423ec03b1ad0f3bfe2348572d7b29ab14e4435c0c8e2ee093d930fde02203d9e86647ea18043c150289f74c6cf2ceb9ca3b228ae31c7b19c4eef813fb68d412103a19014bcc672ccdf18abb6972dd699367baed89c29b704385253ce2ae0eddad5ffffffff2256c94d07451664749e440f55cec8a37da1c46cf30a97579e2f9696b84ad484000000006b48304502210091b0bcf2e84d9ee65de437e8396b379941345e4cffac331af2ae29b8a16968a602205a00eed18a7ffe36f59ae6eb477d9002324cfc249c875260e6ade5bce852692d4121021446bd1df2b61952088a22a516550e43cd95e47ca2a778822d21268bd8b1cebeffffffff02c4090000000000001976a91497ebeffef6d9dd88ffbce922f1df97cbcd7f88d388ac42000000000000001976a91449457f2c101859d1c8ff90096385d3cc30e5488388ac00000000", + name: "all inputs are already mined - 3 inputs on the same level", + ancestors: []*beefTestCaseAncestor{ + { + hex: "01000000019ed68f94dfa952554d777dbaa9e5c01acb3df767e40cabad7b6fb7547bfa871a010000006a4730440220287534d6ff51166e014ad91a2b677be4bd88cf08785624006cdb66553eafc8cf02204862f38e9d2982a5ee95a7850222f2208bff38637349ecfe41abe185498e4ead4121035ca1a2c6d2b46c61fd29e7697018f5ce2bae1ae735e23627046a2dd17ca8fb24ffffffff02de000000000000001976a914f5c9505bf02a4a2fb591e3568183f9c53cf157be88aca62b0000000000001976a91489b5e639bce3209e0888ea8b7eb4203de1c6148888ac00000000", + isMined: true, + bumpJSON: `{"blockHeight":"819265","path":[[{"offset":"1484","hash":"84d44ab896962f9e57970af36cc4a17da3c8ce550f449e74641645074dc95622","txid":true},{"offset":"1485","hash":"657410f5ed93c9eb79d5de3f369ab112dcc0cd693a8c75d8ba83d419a5e0cbae"}],[{"offset":"743","hash":"1c610e4e420187f4625bf8bb4bd97d9d8c0189b09016a2653339c9eedfa19586"}],[{"offset":"370","hash":"9d84d9625ba6b72bd0cf4d4a4206abc528bf6f37e0e3c50d8af2740f7e268712"}],[{"offset":"184","hash":"f93ddfa2e2a485be23b527f00b6d2d12c0cac9a1d9ef031e3d49fc8d97265a68"}],[{"offset":"93","hash":"3cb99ac1431ce21b4e4c89dc2561b304ae0825a2514227eb2405990be9259d55"}],[{"offset":"47","hash":"0f8fbdc287329d0dd88aa672353452c41cb200b4034325caca35f4a19f1e4637"}],[{"offset":"22","hash":"296826f3a55a1dbc5ff550657be9a24dc97fe65ce352e9ac056d5341cef80910"}],[{"offset":"10","hash":"3d766a898268ba2ea8cb40baabb806582e06addc9f65c4e7f9a441a52207eaeb"}],[{"offset":"4","hash":"7712fc3a6f8653ad62dd035e1f8a66c8190a031a1783dd3d273ffce7364dde36"}],[{"offset":"3","hash":"8dfdcd43d0b3ea6cdb50cf2cc12280462e4b92bfe123dd97a044e12bd619998e"}],[{"offset":"0","hash":"3c9d541c4bd24c6e59e2e90a8b1c9fe271d591f5491d032bd95202463a4898ae"}]]}`, + blockHeight: 819265, + }, + { + hex: "010000000154aa46f1b3b7bde36c02e293b74d53e6c6eaed7411d286183b1dca766f42879a010000006b483045022100cd21d346073b4a0788018ff6938c44395d14cf5759fcc35a0899a8fe35a3c2a0022064eb9a005c3d0be03b61ab0e1c8757ed566dd935dacac37fcd1452adba4994b541210272d67492c31d0e6bead28c934fb1c9bb50ba9b46f886209fe95fb6a3e43bb27bffffffff0257040000000000001976a9140501308b6409cca5a7b5768c18ff2de8da4c1fa388ac39420000000000001976a91417e3d89f4aeacd5b4929fe04edc32c79b6182e1988ac00000000", + isMined: true, + bumpJSON: `{"blockHeight":"819265","path":[[{"offset":"1168","hash":"71f418d24653b6711b09a39e1c7b418e105a9ec55c2f4746db77889aa361c408","txid":true},{"offset":"1169","hash":"f487a07dc7f72556292f0bd85e5f22a05d9dab29772daefe48870895e3a18fff"}],[{"offset":"585","hash":"89ad2f3acd22cd3313054427d16bf2e0e412a95cdba2b77b97f7e338a911330f"}],[{"offset":"293","hash":"68a8bf0c4b6c378f5120a9f716e0452c7434fabc5b6e5bb5f2cde1286a985f5e"}],[{"offset":"147","hash":"d3c4698449afbd39403ddd35272f0c5f45726acc87fce700369aa17970a00aa6"}],[{"offset":"72","hash":"a3b2da485a2d0331969dc5e9b38550e4cbfb69a127418ef32252780ce4baa9b4"}],[{"offset":"37","hash":"2e11be0a25536b375e91257a85631a77f4194db005c44a106f90cc61be88af6d"}],[{"offset":"19","hash":"3b4be5245bf5e1e18362ebf3498447a7aaf7c5893b6f344cfe0a0a44a0517b59"}],[{"offset":"8","hash":"14f1cf466a0cfcebad570548d09bb180b9964bcbdee04da0807b63127e7668b6"}],[{"offset":"5","hash":"f02af5d04771132d1b6f08ed70f0d7a3fa868edd6006c8d90ddda6c967c99d5c"}],[{"offset":"3","hash":"8dfdcd43d0b3ea6cdb50cf2cc12280462e4b92bfe123dd97a044e12bd619998e"}],[{"offset":"0","hash":"3c9d541c4bd24c6e59e2e90a8b1c9fe271d591f5491d032bd95202463a4898ae"}]]}`, + blockHeight: 819265, + }, + { + hex: "0100000001e230ab1b300ac3ce334590fc308fee93ddbb252f6e4645e0a20f7e30dd541289010000006b483045022100a611fdf01eca42289d80e1265584e5bd487faa72e6142ebbc140a676f7c5037c0220409282aaadf580f458d97d61db43c94ac343e0b40674a80fd3ac47f43fd0c66c4121020a87e70cc26f7d5fe775f622d2705f27cfd6f5d2b574fea75401d6412a58b91affffffff02d2040000000000001976a9145d2117c4f66bdb335ce2707a74c46fa46d02cdb388acf23b0000000000001976a914effd80ee9df812990a8d7834fa8610491cbeb91688ac00000000", + isMined: true, + bumpJSON: `{"blockHeight":"819265","path":[[{"offset":"1068","hash":"50cf88c451400c72b33bea0ddd659c6a756f829467d8a7b51a57a0b9ca741ee0","txid":true},{"offset":"1069","hash":"b42c92309cabe31f84a04281331c4d4d3288d77e6696fbaa0f1cfb3a540c06d9"}],[{"offset":"535","hash":"847c23a1d815cbe09c0876402de53564945061945324a8f77e77f8635e795ddd"}],[{"offset":"266","hash":"d2fd8db438af79b228e7edac40eaa204c83191bf96c78365a5ab6418023d3722"}],[{"offset":"132","hash":"6520f0990428421e5632666380c27af04fc3d720d1c915ef06a1032bb99e10a8"}],[{"offset":"67","hash":"9272d24f6f60ed3c80e2eae3657fc4b83d89bd466454d0ca652c37f4d6c5786f"}],[{"offset":"32","hash":"53df03b923f4eac53729382a7fa611a0106cbb90e69310a37daad2792f3fbc46"}],[{"offset":"17","hash":"ab516a8baa05c2dc560cd929cbcf933a0e5574d073ab9d3dbb0e6720aaeb4ef1"}],[{"offset":"9","hash":"87cf2caba2df8fd0ca00f509ccf58acf5fecf6df26bce8f79fddbe6736ec4cd2"}],[{"offset":"5","hash":"f02af5d04771132d1b6f08ed70f0d7a3fa868edd6006c8d90ddda6c967c99d5c"}],[{"offset":"3","hash":"8dfdcd43d0b3ea6cdb50cf2cc12280462e4b92bfe123dd97a044e12bd619998e"}],[{"offset":"0","hash":"3c9d541c4bd24c6e59e2e90a8b1c9fe271d591f5491d032bd95202463a4898ae"}]]}`, + blockHeight: 819265, + }, + }, + receiverAddress: "1A1PjKqjWMNBzTVdcBru27EV1PHcXWc63W", + outputValue: 4000, + expectedErrorMessage: "", + }, + { + testID: 3, + hexForProcessedTx: "0100000002cb3553424ffc94b59a60fb358b6cb6dfb694aee894dcd1effc0ed0a9052464e3000000006a4730440220515c3bf93d38fa7cc164746fae4bec8b66c60a82509eb553751afa5971c3e41d0220321517fd5c997ab5f8ef0e59048ce9157de46f92b10d882bf898e62f3ee7343d4121038f1273fcb299405d8d140b4de9a2111ecb39291b2846660ebecd864d13bee575ffffffff624fbcb4e68d162361f456b8b4fef6b9e7943013088b32b6bca7f5ced41ff004010000006a47304402203fb24f6e00a6487cf88a3b39d8454786db63d649142ea76374c2f55990777e6302207fbb903d038cf43e13ffb496a64f36637ec7323e5ac48bb96bdb4a885100abca4121024b003d3cf49a8f48c1fe79b711b1d08e306c42a0ab8da004d97fccc4ced3343affffffff026f000000000000001976a914f232d38cd4c2f87c117af06542b04a7061b6640188aca62a0000000000001976a9146058e52d00e3b94211939f68cc2d9a3fc1e3db0f88ac00000000", + name: "not all inputs are mined but all required ancestors are mined - one level below inputs", + ancestors: []*beefTestCaseAncestor{ + { + hex: "0100000001cfc39e3adcd58ed58cf590079dc61c3eb6ec739abb7d22b592fb969d427f33ee000000006a4730440220253e674e64028459457d55b444f5f3dc15c658425e3184c628016739e4921fd502207c8fe20eb34e55e4115fbd82c23878b4e54f01f6c6ad0811282dd0b1df863b5e41210310a4366fd997127ad972b14c56ca2e18f39ca631ac9e3e4ad3d9827865d0cc70ffffffff0264000000000000001976a914668a92ff9cb5785eb8fc044771837a0818b028b588acdc4e0000000000001976a914b073264927a61cf84327dea77414df6c28b11e5988ac00000000", + isMined: true, + bumpJSON: `{"blockHeight":"817574","path":[[{"offset":"11432","hash":"3b535e0f8e266124bce9868420052d5a7585c67e82c1edc2c7fe05fd5e140307"},{"offset":"11433","hash":"e3642405a9d00efcefd1dc94e8ae94b6dfb66c8b35fb609ab594fc4f425335cb","txid":true}],[{"offset":"5717","hash":"6ef9c6dde7fff82fa893754109f12378c8453b47dc896596b5531433093ab5b7"}],[{"offset":"2859","hash":"daa67e00ad2aef787998b66cbb3417033fbec136da1e230a5f5df3186f5c0880"}],[{"offset":"1428","hash":"bc777a80d951fbf2b7bd3a8048a9bb78fbf1d23d4127290c3fed9740b4246dd2"}],[{"offset":"715","hash":"762b57f88e7258f5757b48cda96d075cbe767c0a39a83e7109574555fd2dd8ba"}],[{"offset":"356","hash":"bbaab745bcca4f8a4be39c06c7e9be3aa1994f32271e3c6b4f768897153e5522"}],[{"offset":"179","hash":"817694ccbde5dbf88f290c30e8735991708a3d406740f7dd31434ff516a5bfde"}],[{"offset":"88","hash":"ed5b52ba4af9198d398e934a84e18405f49e7abde91cafb6dfe5aeaedb33a979"}],[{"offset":"45","hash":"0e51ec9dd5319ceb32d2d20f620c0ca3e0d918260803c1005d49e686c9b18752"}],[{"offset":"23","hash":"08ab694ef1af4019e2999a543a632cf4a662ae04d5fee879c6aadaeb749f6374"}],[{"offset":"10","hash":"4223f47597b14ee0fa7ade08e611ec80948b5fa9da267ce6c8e5d952e7fdb38e"}],[{"offset":"4","hash":"b6dace0d2294fd6e0c11f74376b7f0a1fc8ee415b350caf90c3ae92749e2a8ee"}],[{"offset":"3","hash":"795e7514ebf6d63b454d3f04854e1e0db0ac3a549f61135d5e9ef8d5785f2c68"}],[{"offset":"0","hash":"3f458f2c06493c31cbc3a035ba131913b274ac7915b9b9bc79128001a75cf76d"}],[{"offset":"1","hash":"b9b9f80cc72a674e37b54a9fdee72a9bff761f8cbcb94146afc2bffef33be89f"}]]}`, + blockHeight: 817574, + }, + { + hex: "0100000001a114c7deb8deba851d87755aa10aa18c97bd77afee4e1bad01d1c50e07a644eb010000006a473044022041abd4f93bd1db1d0097f2d467ae183801d7842d23d0605fa9568040d245167402201be66c96bef4d6d051304f6df2aecbdfe23a8a05af0908ef2117ab5388d8903c412103c08545a40c819f6e50892e31e792d221b6df6da96ebdba9b6fe39305cc6cc768ffffffff0263040000000000001976a91454097d9d921f9a1f55084a943571d868552e924f88acb22a0000000000001976a914c36b3fca5159231033f3fbdca1cde942096d379f88ac00000000", + isMined: false, + bumpJSON: ``, + blockHeight: -1, + parents: []*beefTestCaseAncestor{ + { + hex: "010000000150965003ea3d2c08bc79b116c9ffe7e730c9f9cf0a61e3df07868b24eac6f8d3000000006b4830450221009d3489f9e76ff3b043708972c52f85519e50a5fc35563d405e04b668780bf2ba0220024188508fc9c6870b2fc4f40b9484ae4163481199a5b4a7a338b86ec8952fee4121036a8b9d796ce2dee820d1f6d7a6ba07037dab4758f16028654fe4bc3a5c430b40ffffffff022a200000000000001976a91484c73348a8fbbc44cfa34f8f5441fc104f3bc78588ac162f0000000000001976a914590b1df63948c2c4e7a12a6e52012b36e25daa9888ac00000000", + isMined: true, + bumpJSON: `{"blockHeight":"817267","path":[[{"offset":"204","hash":"cc688d86f3ceb67d53bbdd4b140b10a1ff0cff919e2e1e45c5d82b2629a52f5d"},{"offset":"205","hash":"eb44a6070ec5d101ad1b4eeeaf77bd978ca10aa15a75871d85badeb8dec714a1","txid":true}],[{"offset":"103","hash":"0b0bae3a36eaf8fb10f8d52d61a04434eea745206bff04943a2d0361ebf52b67"}],[{"offset":"50","hash":"43af8bb7133505db727bf626f81081ab841ca90def48eba8bf70f5998d9a2af6"}],[{"offset":"24","hash":"18bf5d884d88570eea3006f46ca62d2ab13d41f4b2b7741c0fb72a7d3c19ec0f"}],[{"offset":"13","hash":"fccf4bf884dda23d6639b0768513be141c329ce8674eda6184eb28887859f227"}],[{"offset":"7","hash":"3353b9bd9666d87045c9661803dbe63dad7270b12580ef25cf3db45e6dddf4eb"}],[{"offset":"2","hash":"38dbe8ee0853c4adb254a2fd73040b02f5f98c6db65db49256515e5401cbd368"}],[{"offset":"0","hash":"c0542ab8ca895ea798840c8b90eaa27112c3e3ab7acb8500749f482cb73d9172"}],[{"offset":"1","hash":"c22b61b365e9ff54dd317122d1316a8244e826604650c934b0f957ad87771b49"}],[{"offset":"1","hash":"0de03905ab373fcc44443c17d54148c101bc6e5076b74f5fbcc52e37a6fd831f"}],[{"offset":"1","hash":"c1f2dbb43a68c3893784efef0c45a998fccceb63d873c8c7e3fc7a7ced33bfce"}],[{"offset":"1","hash":"2a7f4a0e8008299ac6e258229aedc67ba1d4069ebb78f653501e78991d5f36dc"}]]}`, + blockHeight: 817267, + }, + }, + }, + }, + receiverAddress: "1A1PjKqjWMNBzTVdcBru27EV1PHcXWc63W", + outputValue: 3500, + expectedErrorMessage: "", + }, + { + testID: 4, + hexForProcessedTx: "0100000002cb3553424ffc94b59a60fb358b6cb6dfb694aee894dcd1effc0ed0a9052464e3000000006a4730440220515c3bf93d38fa7cc164746fae4bec8b66c60a82509eb553751afa5971c3e41d0220321517fd5c997ab5f8ef0e59048ce9157de46f92b10d882bf898e62f3ee7343d4121038f1273fcb299405d8d140b4de9a2111ecb39291b2846660ebecd864d13bee575ffffffff624fbcb4e68d162361f456b8b4fef6b9e7943013088b32b6bca7f5ced41ff004010000006a47304402203fb24f6e00a6487cf88a3b39d8454786db63d649142ea76374c2f55990777e6302207fbb903d038cf43e13ffb496a64f36637ec7323e5ac48bb96bdb4a885100abca4121024b003d3cf49a8f48c1fe79b711b1d08e306c42a0ab8da004d97fccc4ced3343affffffff026f000000000000001976a914f232d38cd4c2f87c117af06542b04a7061b6640188aca62a0000000000001976a9146058e52d00e3b94211939f68cc2d9a3fc1e3db0f88ac00000000", + name: "not all inputs are mined but all required ancestors are mined - two levels below inputs", + ancestors: []*beefTestCaseAncestor{ + { + hex: "0100000001cfc39e3adcd58ed58cf590079dc61c3eb6ec739abb7d22b592fb969d427f33ee000000006a4730440220253e674e64028459457d55b444f5f3dc15c658425e3184c628016739e4921fd502207c8fe20eb34e55e4115fbd82c23878b4e54f01f6c6ad0811282dd0b1df863b5e41210310a4366fd997127ad972b14c56ca2e18f39ca631ac9e3e4ad3d9827865d0cc70ffffffff0264000000000000001976a914668a92ff9cb5785eb8fc044771837a0818b028b588acdc4e0000000000001976a914b073264927a61cf84327dea77414df6c28b11e5988ac00000000", + isMined: true, + bumpJSON: `{"blockHeight":"817574","path":[[{"offset":"11432","hash":"3b535e0f8e266124bce9868420052d5a7585c67e82c1edc2c7fe05fd5e140307"},{"offset":"11433","hash":"e3642405a9d00efcefd1dc94e8ae94b6dfb66c8b35fb609ab594fc4f425335cb","txid":true}],[{"offset":"5717","hash":"6ef9c6dde7fff82fa893754109f12378c8453b47dc896596b5531433093ab5b7"}],[{"offset":"2859","hash":"daa67e00ad2aef787998b66cbb3417033fbec136da1e230a5f5df3186f5c0880"}],[{"offset":"1428","hash":"bc777a80d951fbf2b7bd3a8048a9bb78fbf1d23d4127290c3fed9740b4246dd2"}],[{"offset":"715","hash":"762b57f88e7258f5757b48cda96d075cbe767c0a39a83e7109574555fd2dd8ba"}],[{"offset":"356","hash":"bbaab745bcca4f8a4be39c06c7e9be3aa1994f32271e3c6b4f768897153e5522"}],[{"offset":"179","hash":"817694ccbde5dbf88f290c30e8735991708a3d406740f7dd31434ff516a5bfde"}],[{"offset":"88","hash":"ed5b52ba4af9198d398e934a84e18405f49e7abde91cafb6dfe5aeaedb33a979"}],[{"offset":"45","hash":"0e51ec9dd5319ceb32d2d20f620c0ca3e0d918260803c1005d49e686c9b18752"}],[{"offset":"23","hash":"08ab694ef1af4019e2999a543a632cf4a662ae04d5fee879c6aadaeb749f6374"}],[{"offset":"10","hash":"4223f47597b14ee0fa7ade08e611ec80948b5fa9da267ce6c8e5d952e7fdb38e"}],[{"offset":"4","hash":"b6dace0d2294fd6e0c11f74376b7f0a1fc8ee415b350caf90c3ae92749e2a8ee"}],[{"offset":"3","hash":"795e7514ebf6d63b454d3f04854e1e0db0ac3a549f61135d5e9ef8d5785f2c68"}],[{"offset":"0","hash":"3f458f2c06493c31cbc3a035ba131913b274ac7915b9b9bc79128001a75cf76d"}],[{"offset":"1","hash":"b9b9f80cc72a674e37b54a9fdee72a9bff761f8cbcb94146afc2bffef33be89f"}]]}`, + blockHeight: 817574, + }, + { + hex: "0100000001a114c7deb8deba851d87755aa10aa18c97bd77afee4e1bad01d1c50e07a644eb010000006a473044022041abd4f93bd1db1d0097f2d467ae183801d7842d23d0605fa9568040d245167402201be66c96bef4d6d051304f6df2aecbdfe23a8a05af0908ef2117ab5388d8903c412103c08545a40c819f6e50892e31e792d221b6df6da96ebdba9b6fe39305cc6cc768ffffffff0263040000000000001976a91454097d9d921f9a1f55084a943571d868552e924f88acb22a0000000000001976a914c36b3fca5159231033f3fbdca1cde942096d379f88ac00000000", + isMined: false, + bumpJSON: ``, + blockHeight: -1, + parents: []*beefTestCaseAncestor{ + { + hex: "010000000150965003ea3d2c08bc79b116c9ffe7e730c9f9cf0a61e3df07868b24eac6f8d3000000006b4830450221009d3489f9e76ff3b043708972c52f85519e50a5fc35563d405e04b668780bf2ba0220024188508fc9c6870b2fc4f40b9484ae4163481199a5b4a7a338b86ec8952fee4121036a8b9d796ce2dee820d1f6d7a6ba07037dab4758f16028654fe4bc3a5c430b40ffffffff022a200000000000001976a91484c73348a8fbbc44cfa34f8f5441fc104f3bc78588ac162f0000000000001976a914590b1df63948c2c4e7a12a6e52012b36e25daa9888ac00000000", + isMined: false, + bumpJSON: ``, + blockHeight: -1, + parents: []*beefTestCaseAncestor{ + { + hex: "0100000002787a565270ec00b1bf6ed20100223176656705dc0cfe5ef9d1810ca6569f12d1020000006a47304402203cfe36be7ff5c2ac939bb6a625e4a1226be242f1f9950672b5f696ec58a3358902202a48d6c6e81e5950dc49d0dd1a35b46fa8f919b109b0e7c05deaef3db6051890412102fb130326dbd7c43841cde467196e5f289b9d8596e237725df84f768468426d8bffffffff008d9db2a5c8c310e6394c24c1f3c23b3adbdd6ab4a719e917a4a0ed78768773020000006a473044022049c80385f7f69e8ba6039ebe84fe5e6578f4c3c83eb622442a96219c59ac1a750220317fe2b47838dff11f88d909732d0846eba20acff57cb357a3ff39b5a7b61b3741210322b79b40a759c485eac318eabba60a73a49ec3307ded79ba8c47204405bb2f3fffffffff05414f0000000000001976a91400414bcf2602f309171901d837b4a155adbfb5ce88ac50c30000000000001976a91489ef778cc07c77cce1ad3ff6274615afe15f20c088ac204e0000000000001976a914971b76df1dc6acf01e8e7d2f8bfb3c86e69bc64c88acef250000000000001976a9144b4a836b444d5ed8d245ddb1aa878908e36cd6b588ac9d860100000000001976a9144405da67e318e9cfd9d6ce9dffce27af5f60522888ac00000000", + isMined: true, + bumpJSON: `{"blockHeight":"817117","path":[[{"offset":"90","hash":"d3f8c6ea248b8607dfe3610acff9c930e7e7ffc916b179bc082c3dea03509650","txid":true},{"offset":"91","hash":"5b52ad65ab613867da9a710d60898a6e5da62dea97dac25da40a0dc385253ad2"}],[{"offset":"44","hash":"84c338bea7f65ccaf7a27ca9ae6d4b11372339cf6aa6021523de3ce6f5fe4f0c"}],[{"offset":"23","hash":"5860f292e051c0a5d9d8d69a451311a009c9cde8da6522df915587913a5180dd"}],[{"offset":"10","hash":"633fb08a689363af6a8245d3482fff232b27a62b94a4d119e67700fb9608ef78"}],[{"offset":"4","hash":"4b80bb130cebb1b8c313eb4088d098178ae122fd490a255218ceada19ab9eb52"}],[{"offset":"3","hash":"cf3d0335dda3223c8b4cf28ca2c03c7e025e3088525d51981d0ee1bd2ea210cf"}],[{"offset":"0","hash":"99c7462c2530abd1be779b170b7c2afbf7b883c07175871c971734d2bd38d35b"}],[{"offset":"1","hash":"9a66c7e35426281b1be6f43ecad44a3b65a9d2234d69a55b87d535f5903d677f"}],[{"offset":"1","hash":"40e34161018499a3ad5d1ef0d74a2e557733b6c7b5c07c1d8b872ffd504e545b"}],[{"offset":"1","hash":"dce6b1d7924d3ea2b1d7e3f2c4ae15abeea3b63e1362cc0896a82bca57a21387"}],[{"offset":"1","hash":"e1fe4e2b97189cb9da328bc59430a51595ee248079bd01a3f658af810e14cb7c"}]]}`, + blockHeight: 817117, + }, + }, + }, + }, + }, + }, + receiverAddress: "1A1PjKqjWMNBzTVdcBru27EV1PHcXWc63W", + outputValue: 4500, + expectedErrorMessage: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // given + ctx, client, deferMe := initSimpleTestCase(t) + store := NewMockTransactionStore() + defer deferMe() + + var ancestors []*Transaction + for _, ancestor := range tc.ancestors { + ancestors = append(ancestors, addAncestor(ctx, ancestor, client, store, t)) + } + newTx := createProcessedTx(ctx, t, client, &tc, ancestors) + + // when + result, err := ToBeef(ctx, newTx, store) + + // then + assert.Equal(t, expectedBeefHex[tc.testID], result) + assert.NoError(t, nil, err) + }) + } +} + +func Test_ToBeef_ErrorPaths(t *testing.T) { + testCases := []beefTestCase{ + { + hexForProcessedTx: "0100000002cb3553424ffc94b59a60fb358b6cb6dfb694aee894dcd1effc0ed0a9052464e3000000006a4730440220515c3bf93d38fa7cc164746fae4bec8b66c60a82509eb553751afa5971c3e41d0220321517fd5c997ab5f8ef0e59048ce9157de46f92b10d882bf898e62f3ee7343d4121038f1273fcb299405d8d140b4de9a2111ecb39291b2846660ebecd864d13bee575ffffffff624fbcb4e68d162361f456b8b4fef6b9e7943013088b32b6bca7f5ced41ff004010000006a47304402203fb24f6e00a6487cf88a3b39d8454786db63d649142ea76374c2f55990777e6302207fbb903d038cf43e13ffb496a64f36637ec7323e5ac48bb96bdb4a885100abca4121024b003d3cf49a8f48c1fe79b711b1d08e306c42a0ab8da004d97fccc4ced3343affffffff026f000000000000001976a914f232d38cd4c2f87c117af06542b04a7061b6640188aca62a0000000000001976a9146058e52d00e3b94211939f68cc2d9a3fc1e3db0f88ac00000000", + name: "one input is mined and properly stored, the second one is missing in the store - should return error", + ancestors: []*beefTestCaseAncestor{ + { + hex: "0100000001cfc39e3adcd58ed58cf590079dc61c3eb6ec739abb7d22b592fb969d427f33ee000000006a4730440220253e674e64028459457d55b444f5f3dc15c658425e3184c628016739e4921fd502207c8fe20eb34e55e4115fbd82c23878b4e54f01f6c6ad0811282dd0b1df863b5e41210310a4366fd997127ad972b14c56ca2e18f39ca631ac9e3e4ad3d9827865d0cc70ffffffff0264000000000000001976a914668a92ff9cb5785eb8fc044771837a0818b028b588acdc4e0000000000001976a914b073264927a61cf84327dea77414df6c28b11e5988ac00000000", + isMined: false, + bumpJSON: ``, + blockHeight: -1, + }, + { + hex: "0100000001a114c7deb8deba851d87755aa10aa18c97bd77afee4e1bad01d1c50e07a644eb010000006a473044022041abd4f93bd1db1d0097f2d467ae183801d7842d23d0605fa9568040d245167402201be66c96bef4d6d051304f6df2aecbdfe23a8a05af0908ef2117ab5388d8903c412103c08545a40c819f6e50892e31e792d221b6df6da96ebdba9b6fe39305cc6cc768ffffffff0263040000000000001976a91454097d9d921f9a1f55084a943571d868552e924f88acb22a0000000000001976a914c36b3fca5159231033f3fbdca1cde942096d379f88ac00000000", + isMined: false, + bumpJSON: ``, + blockHeight: -1, + }, + }, + receiverAddress: "1A1PjKqjWMNBzTVdcBru27EV1PHcXWc63W", + outputValue: 3000, + expectedErrorMessage: "prepareBUMPFactors() error: required transactions not found in database: [ee337f429d96fb92b5227dbb9a73ecb63e1cc69d0790f58cd58ed5dc3a9ec3cf]", + }, + { + hexForProcessedTx: "0100000002cb3553424ffc94b59a60fb358b6cb6dfb694aee894dcd1effc0ed0a9052464e3000000006a4730440220515c3bf93d38fa7cc164746fae4bec8b66c60a82509eb553751afa5971c3e41d0220321517fd5c997ab5f8ef0e59048ce9157de46f92b10d882bf898e62f3ee7343d4121038f1273fcb299405d8d140b4de9a2111ecb39291b2846660ebecd864d13bee575ffffffff624fbcb4e68d162361f456b8b4fef6b9e7943013088b32b6bca7f5ced41ff004010000006a47304402203fb24f6e00a6487cf88a3b39d8454786db63d649142ea76374c2f55990777e6302207fbb903d038cf43e13ffb496a64f36637ec7323e5ac48bb96bdb4a885100abca4121024b003d3cf49a8f48c1fe79b711b1d08e306c42a0ab8da004d97fccc4ced3343affffffff026f000000000000001976a914f232d38cd4c2f87c117af06542b04a7061b6640188aca62a0000000000001976a9146058e52d00e3b94211939f68cc2d9a3fc1e3db0f88ac00000000", + name: "inputs not mined - no parents in store - should return error", + ancestors: []*beefTestCaseAncestor{ + { + hex: "0100000001cfc39e3adcd58ed58cf590079dc61c3eb6ec739abb7d22b592fb969d427f33ee000000006a4730440220253e674e64028459457d55b444f5f3dc15c658425e3184c628016739e4921fd502207c8fe20eb34e55e4115fbd82c23878b4e54f01f6c6ad0811282dd0b1df863b5e41210310a4366fd997127ad972b14c56ca2e18f39ca631ac9e3e4ad3d9827865d0cc70ffffffff0264000000000000001976a914668a92ff9cb5785eb8fc044771837a0818b028b588acdc4e0000000000001976a914b073264927a61cf84327dea77414df6c28b11e5988ac00000000", + isMined: true, + bumpJSON: `{"blockHeight":"817574","path":[[{"offset":"11432","hash":"3b535e0f8e266124bce9868420052d5a7585c67e82c1edc2c7fe05fd5e140307"},{"offset":"11433","hash":"e3642405a9d00efcefd1dc94e8ae94b6dfb66c8b35fb609ab594fc4f425335cb","txid":true}],[{"offset":"5717","hash":"6ef9c6dde7fff82fa893754109f12378c8453b47dc896596b5531433093ab5b7"}],[{"offset":"2859","hash":"daa67e00ad2aef787998b66cbb3417033fbec136da1e230a5f5df3186f5c0880"}],[{"offset":"1428","hash":"bc777a80d951fbf2b7bd3a8048a9bb78fbf1d23d4127290c3fed9740b4246dd2"}],[{"offset":"715","hash":"762b57f88e7258f5757b48cda96d075cbe767c0a39a83e7109574555fd2dd8ba"}],[{"offset":"356","hash":"bbaab745bcca4f8a4be39c06c7e9be3aa1994f32271e3c6b4f768897153e5522"}],[{"offset":"179","hash":"817694ccbde5dbf88f290c30e8735991708a3d406740f7dd31434ff516a5bfde"}],[{"offset":"88","hash":"ed5b52ba4af9198d398e934a84e18405f49e7abde91cafb6dfe5aeaedb33a979"}],[{"offset":"45","hash":"0e51ec9dd5319ceb32d2d20f620c0ca3e0d918260803c1005d49e686c9b18752"}],[{"offset":"23","hash":"08ab694ef1af4019e2999a543a632cf4a662ae04d5fee879c6aadaeb749f6374"}],[{"offset":"10","hash":"4223f47597b14ee0fa7ade08e611ec80948b5fa9da267ce6c8e5d952e7fdb38e"}],[{"offset":"4","hash":"b6dace0d2294fd6e0c11f74376b7f0a1fc8ee415b350caf90c3ae92749e2a8ee"}],[{"offset":"3","hash":"795e7514ebf6d63b454d3f04854e1e0db0ac3a549f61135d5e9ef8d5785f2c68"}],[{"offset":"0","hash":"3f458f2c06493c31cbc3a035ba131913b274ac7915b9b9bc79128001a75cf76d"}],[{"offset":"1","hash":"b9b9f80cc72a674e37b54a9fdee72a9bff761f8cbcb94146afc2bffef33be89f"}]]}`, + blockHeight: 817574, + }, + { + hex: "0100000001a114c7deb8deba851d87755aa10aa18c97bd77afee4e1bad01d1c50e07a644eb010000006a473044022041abd4f93bd1db1d0097f2d467ae183801d7842d23d0605fa9568040d245167402201be66c96bef4d6d051304f6df2aecbdfe23a8a05af0908ef2117ab5388d8903c412103c08545a40c819f6e50892e31e792d221b6df6da96ebdba9b6fe39305cc6cc768ffffffff0263040000000000001976a91454097d9d921f9a1f55084a943571d868552e924f88acb22a0000000000001976a914c36b3fca5159231033f3fbdca1cde942096d379f88ac00000000", + isMined: false, + bumpJSON: ``, + blockHeight: -1, + doNotAddToStore: true, + }, + }, + receiverAddress: "1A1PjKqjWMNBzTVdcBru27EV1PHcXWc63W", + outputValue: 3000, + expectedErrorMessage: "prepareBUMPFactors() error: required transactions not found in database: [04f01fd4cef5a7bcb6328b08133094e7b9f6feb4b856f46123168de6b4bc4f62]", + }, + { + hexForProcessedTx: "0100000002cb3553424ffc94b59a60fb358b6cb6dfb694aee894dcd1effc0ed0a9052464e3000000006a4730440220515c3bf93d38fa7cc164746fae4bec8b66c60a82509eb553751afa5971c3e41d0220321517fd5c997ab5f8ef0e59048ce9157de46f92b10d882bf898e62f3ee7343d4121038f1273fcb299405d8d140b4de9a2111ecb39291b2846660ebecd864d13bee575ffffffff624fbcb4e68d162361f456b8b4fef6b9e7943013088b32b6bca7f5ced41ff004010000006a47304402203fb24f6e00a6487cf88a3b39d8454786db63d649142ea76374c2f55990777e6302207fbb903d038cf43e13ffb496a64f36637ec7323e5ac48bb96bdb4a885100abca4121024b003d3cf49a8f48c1fe79b711b1d08e306c42a0ab8da004d97fccc4ced3343affffffff026f000000000000001976a914f232d38cd4c2f87c117af06542b04a7061b6640188aca62a0000000000001976a9146058e52d00e3b94211939f68cc2d9a3fc1e3db0f88ac00000000", + name: "last ancestor has corrupted hex in database - should return error", + ancestors: []*beefTestCaseAncestor{ + { + hex: "0100000001cfc39e3adcd58ed58cf590079dc61c3eb6ec739abb7d22b592fb969d427f33ee000000006a4730440220253e674e64028459457d55b444f5f3dc15c658425e3184c628016739e4921fd502207c8fe20eb34e55e4115fbd82c23878b4e54f01f6c6ad0811282dd0b1df863b5e41210310a4366fd997127ad972b14c56ca2e18f39ca631ac9e3e4ad3d9827865d0cc70ffffffff0264000000000000001976a914668a92ff9cb5785eb8fc044771837a0818b028b588acdc4e0000000000001976a914b073264927a61cf84327dea77414df6c28b11e5988ac00000000", + isMined: true, + bumpJSON: `{"blockHeight":"817574","path":[[{"offset":"11432","hash":"3b535e0f8e266124bce9868420052d5a7585c67e82c1edc2c7fe05fd5e140307"},{"offset":"11433","hash":"e3642405a9d00efcefd1dc94e8ae94b6dfb66c8b35fb609ab594fc4f425335cb","txid":true}],[{"offset":"5717","hash":"6ef9c6dde7fff82fa893754109f12378c8453b47dc896596b5531433093ab5b7"}],[{"offset":"2859","hash":"daa67e00ad2aef787998b66cbb3417033fbec136da1e230a5f5df3186f5c0880"}],[{"offset":"1428","hash":"bc777a80d951fbf2b7bd3a8048a9bb78fbf1d23d4127290c3fed9740b4246dd2"}],[{"offset":"715","hash":"762b57f88e7258f5757b48cda96d075cbe767c0a39a83e7109574555fd2dd8ba"}],[{"offset":"356","hash":"bbaab745bcca4f8a4be39c06c7e9be3aa1994f32271e3c6b4f768897153e5522"}],[{"offset":"179","hash":"817694ccbde5dbf88f290c30e8735991708a3d406740f7dd31434ff516a5bfde"}],[{"offset":"88","hash":"ed5b52ba4af9198d398e934a84e18405f49e7abde91cafb6dfe5aeaedb33a979"}],[{"offset":"45","hash":"0e51ec9dd5319ceb32d2d20f620c0ca3e0d918260803c1005d49e686c9b18752"}],[{"offset":"23","hash":"08ab694ef1af4019e2999a543a632cf4a662ae04d5fee879c6aadaeb749f6374"}],[{"offset":"10","hash":"4223f47597b14ee0fa7ade08e611ec80948b5fa9da267ce6c8e5d952e7fdb38e"}],[{"offset":"4","hash":"b6dace0d2294fd6e0c11f74376b7f0a1fc8ee415b350caf90c3ae92749e2a8ee"}],[{"offset":"3","hash":"795e7514ebf6d63b454d3f04854e1e0db0ac3a549f61135d5e9ef8d5785f2c68"}],[{"offset":"0","hash":"3f458f2c06493c31cbc3a035ba131913b274ac7915b9b9bc79128001a75cf76d"}],[{"offset":"1","hash":"b9b9f80cc72a674e37b54a9fdee72a9bff761f8cbcb94146afc2bffef33be89f"}]]}`, + blockHeight: 817574, + }, + { + hex: "0100000001a114c7deb8deba851d87755aa10aa18c97bd77afee4e1bad01d1c50e07a644eb010000006a473044022041abd4f93bd1db1d0097f2d467ae183801d7842d23d0605fa9568040d245167402201be66c96bef4d6d051304f6df2aecbdfe23a8a05af0908ef2117ab5388d8903c412103c08545a40c819f6e50892e31e792d221b6df6da96ebdba9b6fe39305cc6cc768ffffffff0263040000000000001976a91454097d9d921f9a1f55084a943571d868552e924f88acb22a0000000000001976a914c36b3fca5159231033f3fbdca1cde942096d379f88ac00000000", + isMined: false, + bumpJSON: ``, + blockHeight: -1, + parents: []*beefTestCaseAncestor{ + { + hex: "010000000150965003ea3d2c08bc79b116c9ffe7e730c9f9cf0a61e3df07868b24eac6f8d3000000006b4830450221009d3489f9e76ff3b043708972c52f85519e50a5fc35563d405e04b668780bf2ba0220024188508fc9c6870b2fc4f40b9484ae4163481199a5b4a7a338b86ec8952fee4121036a8b9d796ce2dee820d1f6d7a6ba07037dab4758f16028654fe4bc3a5c430b40ffffffff022a200000000000001976a91484c73348a8fbbc44cfa34f8f5441fc104f3bc78588ac162f0000000000001976a914590b1df63948c2c4e7a12a6e52012b36e25daa9888ac00000000", + isMined: false, + bumpJSON: ``, + blockHeight: -1, + parents: []*beefTestCaseAncestor{ + { + hex: "0100000002787a565270ec00b1bf6ed20100223176656705dc0cfe5ef9d1810ca6569f12d1020000006a47304402203cfe36be7ff5c2ac939bb6a625e4a1226be242f1f9950672b5f696ec58a3358902202a48d6c6e81e5950dc49d0dd1a35b46fa8f919b109b0e7c05deaef3db6051890412102fb130326dbd7c43841cde467196e5f289b9d8596e237725df84f768468426d8bffffffff008d9db2a5c8c310e6394c24c1f3c23b3adbdd6ab4a719e917a4a0ed78768773020000006a473044022049c80385f7f69e8ba6039ebe84fe5e6578f4c3c83eb622442a96219c59ac1a750220317fe2b47838dff11f88d909732d0846eba20acff57cb357a3ff39b5a7b61b3741210322b79b40a759c485eac318eabba60a73a49ec3307ded79ba8c47204405bb2f3fffffffff05414f0000000000001976a91400414bcf2602f309171901d837b4a155adbfb5ce88ac50c30000000000001976a91489ef778cc07c77cce1ad3ff6274615afe15f20c088ac204e0000000000001976a914971b76df1dc6acf01e8e7d2f8bfb3c86e69bc64c88acef250000000000001976a9144b4a836b444d5ed8d245ddb1aa87890", + isMined: true, + bumpJSON: `{"blockHeight":"817117","path":[[{"offset":"90","hash":"d3f8c6ea248b8607dfe3610acff9c930e7e7ffc916b179bc082c3dea03509650","txid":true},{"offset":"91","hash":"5b52ad65ab613867da9a710d60898a6e5da62dea97dac25da40a0dc385253ad2"}],[{"offset":"44","hash":"84c338bea7f65ccaf7a27ca9ae6d4b11372339cf6aa6021523de3ce6f5fe4f0c"}],[{"offset":"23","hash":"5860f292e051c0a5d9d8d69a451311a009c9cde8da6522df915587913a5180dd"}],[{"offset":"10","hash":"633fb08a689363af6a8245d3482fff232b27a62b94a4d119e67700fb9608ef78"}],[{"offset":"4","hash":"4b80bb130cebb1b8c313eb4088d098178ae122fd490a255218ceada19ab9eb52"}],[{"offset":"3","hash":"cf3d0335dda3223c8b4cf28ca2c03c7e025e3088525d51981d0ee1bd2ea210cf"}],[{"offset":"0","hash":"99c7462c2530abd1be779b170b7c2afbf7b883c07175871c971734d2bd38d35b"}],[{"offset":"1","hash":"9a66c7e35426281b1be6f43ecad44a3b65a9d2234d69a55b87d535f5903d677f"}],[{"offset":"1","hash":"40e34161018499a3ad5d1ef0d74a2e557733b6c7b5c07c1d8b872ffd504e545b"}],[{"offset":"1","hash":"dce6b1d7924d3ea2b1d7e3f2c4ae15abeea3b63e1362cc0896a82bca57a21387"}],[{"offset":"1","hash":"e1fe4e2b97189cb9da328bc59430a51595ee248079bd01a3f658af810e14cb7c"}]]}`, + blockHeight: 817117, + }, + }, + }, + }, + }, + }, + receiverAddress: "1A1PjKqjWMNBzTVdcBru27EV1PHcXWc63W", + outputValue: 4500, + expectedErrorMessage: "prepareBUMPFactors() error: required transactions not found in database: [d3f8c6ea248b8607dfe3610acff9c930e7e7ffc916b179bc082c3dea03509650]", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // given + ctx, client, deferMe := initSimpleTestCase(t) + store := NewMockTransactionStore() + defer deferMe() + + var ancestors []*Transaction + for _, ancestor := range tc.ancestors { + ancestors = append(ancestors, addAncestor(ctx, ancestor, client, store, t)) + } + newTx := createProcessedTx(ctx, t, client, &tc, ancestors) + + // when + result, err := ToBeef(ctx, newTx, store) + + // then + assert.Equal(t, "", result) + assert.NotNil(t, err) + assert.Equal(t, tc.expectedErrorMessage, err.Error()) + }) + } +} + +func createProcessedTx(ctx context.Context, t *testing.T, client ClientInterface, testCase *beefTestCase, ancestors []*Transaction) *Transaction { + draftTx := newDraftTransaction( + testXPub, &TransactionConfig{ + Inputs: createInputsUsingAncestors(ancestors, client), + Outputs: []*TransactionOutput{{ + To: testCase.receiverAddress, + Satoshis: testCase.outputValue, + }}, + ChangeNumberOfDestinations: 1, + Sync: &SyncConfig{ + Broadcast: true, + BroadcastInstant: false, + PaymailP2P: false, + SyncOnChain: false, + }, + }, + append(client.DefaultModelOptions(), New())..., + ) + + transaction, err := txFromHex(testCase.hexForProcessedTx, append(client.DefaultModelOptions(), New())...) + require.NoError(t, err) + + transaction.draftTransaction = draftTx + transaction.DraftID = draftTx.ID + + require.NotEmpty(t, transaction) + + return transaction +} + +func addAncestor(ctx context.Context, testCase *beefTestCaseAncestor, client ClientInterface, store *MockTransactionStore, t *testing.T) *Transaction { + ancestor, err := txFromHex(testCase.hex, append(client.DefaultModelOptions(), New())...) + if err != nil { + ancestor = emptyTx(append(client.DefaultModelOptions(), New())...) + ancestor.Hex = testCase.hex + } + + if testCase.isMined { + ancestor.BlockHeight = uint64(testCase.blockHeight) + + var bump BUMP + err := json.Unmarshal([]byte(testCase.bumpJSON), &bump) + require.NoError(t, err) + ancestor.BUMP = bump + } else { + // if we marked transaction as not mined, we need to add it's parents + for _, parent := range testCase.parents { + // no need a result from this func - we just want to add ancestors from level 1 and above to database if required + _ = addAncestor(ctx, parent, client, store, t) + } + } + + if !testCase.doNotAddToStore { + store.AddToStore(ancestor) + } + + return ancestor +} + +func createInputsUsingAncestors(ancestors []*Transaction, client ClientInterface) []*TransactionInput { + var inputs []*TransactionInput + + for _, input := range ancestors { + inputs = append(inputs, &TransactionInput{Utxo: *newUtxoFromTxID(input.GetID(), 0, append(client.DefaultModelOptions(), New())...)}) + } + + return inputs +} diff --git a/engine/bux.go b/engine/bux.go new file mode 100644 index 000000000..6ca31f128 --- /dev/null +++ b/engine/bux.go @@ -0,0 +1,7 @@ +// Package engine is the Bitcoin UTXO & xPub Management Engine +// +// If you have any suggestions or comments, please feel free to open an issue on +// this GitHub repository! +// +// By bitcoin-sv (https://github.com/bitcoin-sv) +package engine diff --git a/engine/bux_suite_database_test.go b/engine/bux_suite_database_test.go new file mode 100644 index 000000000..52c1b9be0 --- /dev/null +++ b/engine/bux_suite_database_test.go @@ -0,0 +1,15 @@ +//go:build database_tests +// +build database_tests + +package engine + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +// TestTestSuite kick-starts all suite tests +func TestTestSuite(t *testing.T) { + suite.Run(t, new(EmbeddedDBTestSuite)) +} diff --git a/engine/bux_suite_mocks_test.go b/engine/bux_suite_mocks_test.go new file mode 100644 index 000000000..8fa675e02 --- /dev/null +++ b/engine/bux_suite_mocks_test.go @@ -0,0 +1,60 @@ +package engine + +import ( + "context" + + "github.com/bitcoin-sv/spv-wallet/engine/taskmanager" + taskq "github.com/vmihailenco/taskq/v3" +) + +// taskManagerMock is a base for an empty task manager +type taskManagerMockBase struct{} + +func (tm *taskManagerMockBase) Info(context.Context, string, ...interface{}) {} + +func (tm *taskManagerMockBase) RegisterTask(string, interface{}) error { + return nil +} + +func (tm *taskManagerMockBase) ResetCron() {} + +func (tm *taskManagerMockBase) RunTask(context.Context, *taskmanager.TaskRunOptions) error { + return nil +} + +func (tm *taskManagerMockBase) Tasks() map[string]*taskq.Task { + return nil +} + +func (tm *taskManagerMockBase) Close(context.Context) error { + return nil +} + +func (tm *taskManagerMockBase) Debug(bool) {} + +func (tm *taskManagerMockBase) Factory() taskmanager.Factory { + return taskmanager.FactoryEmpty +} + +func (tm *taskManagerMockBase) GetTxnCtx(ctx context.Context) context.Context { + return ctx +} + +func (tm *taskManagerMockBase) IsDebug() bool { + return false +} + +func (tm *taskManagerMockBase) IsNewRelicEnabled() bool { + return false +} + +func (tm *taskManagerMockBase) CronJobsInit(taskmanager.CronJobs) error { + return nil +} + +// Sets custom task manager only for testing +func withTaskManagerMockup() ClientOps { + return func(c *clientOptions) { + c.taskManager.TaskEngine = &taskManagerMockBase{} + } +} diff --git a/engine/bux_suite_test.go b/engine/bux_suite_test.go new file mode 100644 index 000000000..16a5f9979 --- /dev/null +++ b/engine/bux_suite_test.go @@ -0,0 +1,364 @@ +package engine + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/rs/zerolog" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" + "github.com/bitcoin-sv/spv-wallet/engine/taskmanager" + "github.com/bitcoin-sv/spv-wallet/engine/tester" + "github.com/dolthub/go-mysql-server/server" + embeddedPostgres "github.com/fergusstrange/embedded-postgres" + "github.com/mrz1836/go-datastore" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "github.com/tryvium-travels/memongo" +) + +const ( + defaultDatabaseName = "spv-wallet-test" + defaultNewRelicTx = "testing-transaction" + defaultNewRelicApp = "testing-app" + mySQLHost = "localhost" + mySQLPassword = "" + mySQLTestPort = uint32(3307) + mySQLUsername = "root" + postgresqlTestHost = "localhost" + postgresqlTestName = "postgres" + postgresqlTestPort = uint32(61333) + postgresqlTestUser = "postgres" + postgresTestPassword = "postgres" + testIdleTimeout = 240 * time.Second + testMaxActiveConnections = 0 + testMaxConnLifetime = 60 * time.Second + testMaxIdleConnections = 10 + testQueueName = "test_queue" +) + +// dbTestCase is a database test case +type dbTestCase struct { + name string + database datastore.Engine +} + +// dbTestCases is the list of supported databases +var dbTestCases = []dbTestCase{ + {name: "[mongo] [in-memory]", database: datastore.MongoDB}, + {name: "[mysql] [in-memory]", database: datastore.MySQL}, + {name: "[postgresql] [in-memory]", database: datastore.PostgreSQL}, + {name: "[sqlite] [in-memory]", database: datastore.SQLite}, +} + +// EmbeddedDBTestSuite is for testing the entire package using real/mocked services +type EmbeddedDBTestSuite struct { + suite.Suite + MongoServer *memongo.Server // In-memory Mongo server + MySQLServer *server.Server // In-memory MySQL server + PostgresqlServer *embeddedPostgres.EmbeddedPostgres // In-memory Postgresql server + quit chan interface{} // Channel for exiting + wg sync.WaitGroup // Workgroup for managing goroutines +} + +// serveMySQL will serve the MySQL server and exit if quit +func (ts *EmbeddedDBTestSuite) serveMySQL() { + defer ts.wg.Done() + + logger := zerolog.Nop() + + for { + err := ts.MySQLServer.Start() + if err != nil { + select { + case <-ts.quit: + logger.Debug().Msg("MySQL channel has closed") + return + default: + logger.Error().Msgf("mysql server error: %s", err.Error()) + } + } + } +} + +// SetupSuite runs at the start of the suite +func (ts *EmbeddedDBTestSuite) SetupSuite() { + var err error + + // Create the MySQL server + if ts.MySQLServer, err = tester.CreateMySQL( + mySQLHost, defaultDatabaseName, mySQLUsername, mySQLPassword, mySQLTestPort, + ); err != nil { + require.NoError(ts.T(), err) + } + + // Don't block, serve the MySQL instance + ts.quit = make(chan interface{}) + ts.wg.Add(1) + go ts.serveMySQL() + + // Create the Mongo server + if ts.MongoServer, err = tester.CreateMongoServer(mongoTestVersion); err != nil { + require.NoError(ts.T(), err) + } + + // Create the Postgresql server + if ts.PostgresqlServer, err = tester.CreatePostgresServer(postgresqlTestPort); err != nil { + require.NoError(ts.T(), err) + } + + // Fail-safe! If a test completes or fails, this is triggered + // Embedded servers are still running on the ports given, and causes a conflict re-running tests + ts.T().Cleanup(func() { + ts.TearDownSuite() + }) +} + +// TearDownSuite runs after the suite finishes +func (ts *EmbeddedDBTestSuite) TearDownSuite() { + // Stop the Mongo server + if ts.MongoServer != nil { + ts.MongoServer.Stop() + } + + // Stop the postgresql server + if ts.PostgresqlServer != nil { + _ = ts.PostgresqlServer.Stop() + } + + // Stop the MySQL server + if ts.MySQLServer != nil { + /* + defer ts.wg.Done() + if ts.quit != nil { + close(ts.quit) + } + */ + _ = ts.MySQLServer.Close() + } +} + +// SetupTest runs before each test +func (ts *EmbeddedDBTestSuite) SetupTest() { + // Nothing needed here (yet) +} + +// TearDownTest runs after each test +func (ts *EmbeddedDBTestSuite) TearDownTest() { + // Nothing needed here (yet) +} + +// createTestClient will make a new test client +// +// NOTE: you need to close the client: ts.Close() +func (ts *EmbeddedDBTestSuite) createTestClient(ctx context.Context, database datastore.Engine, + tablePrefix string, mockDB, mockRedis bool, opts ...ClientOps, +) (*TestingClient, error) { + var err error + + // Start the suite + tc := &TestingClient{ + ctx: ctx, + database: database, + mocking: mockDB, + tablePrefix: tablePrefix, + } + + // Are we mocking SQL? + if mockDB { + + // Create new SQL mocked connection + if tc.SQLConn, tc.MockSQLDB, err = sqlmock.New( + sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual), + ); err != nil { + return nil, err + } + + // Switch on database types + if database == datastore.SQLite { + opts = append(opts, WithSQLite(&datastore.SQLiteConfig{ + CommonConfig: datastore.CommonConfig{ + MaxConnectionIdleTime: 0, + MaxConnectionTime: 0, + MaxIdleConnections: 1, + MaxOpenConnections: 1, + TablePrefix: tablePrefix, + }, + ExistingConnection: tc.SQLConn, + })) + } else if database == datastore.MySQL { + opts = append(opts, WithSQLConnection(datastore.MySQL, tc.SQLConn, tablePrefix)) + } else if database == datastore.PostgreSQL { + opts = append(opts, WithSQLConnection(datastore.PostgreSQL, tc.SQLConn, tablePrefix)) + } else { // todo: finish more Datastore support (missing: Mongo) + // "https://medium.com/@victor.neuret/mocking-the-official-mongo-golang-driver-5aad5b226a78" + return nil, ErrDatastoreNotSupported + } + + } else { + // Load the in-memory version of the database + if database == datastore.SQLite { + opts = append(opts, WithSQLite(&datastore.SQLiteConfig{ + CommonConfig: datastore.CommonConfig{ + MaxIdleConnections: 1, + MaxOpenConnections: 1, + TablePrefix: tablePrefix, + }, + Shared: true, // mrz: TestTransaction_Save requires this to be true for some reason + // I get the error: no such table: _17a1f3e22f2eec56_utxos + })) + } else if database == datastore.MongoDB { + + // Sanity check + if ts.MongoServer == nil { + return nil, ErrLoadServerFirst + } + + // Add the new Mongo connection + opts = append(opts, WithMongoDB(&datastore.MongoDBConfig{ + CommonConfig: datastore.CommonConfig{ + MaxIdleConnections: 1, + MaxOpenConnections: 1, + TablePrefix: tablePrefix, + }, + URI: ts.MongoServer.URIWithRandomDB(), + DatabaseName: memongo.RandomDatabase(), + })) + + } else if database == datastore.PostgreSQL { + + // Sanity check + if ts.PostgresqlServer == nil { + return nil, ErrLoadServerFirst + } + + // Add the new Postgresql connection + opts = append(opts, WithSQL(datastore.PostgreSQL, &datastore.SQLConfig{ + CommonConfig: datastore.CommonConfig{ + MaxIdleConnections: 1, + MaxOpenConnections: 1, + TablePrefix: tablePrefix, + }, + Host: postgresqlTestHost, + Name: postgresqlTestName, + User: postgresqlTestUser, + Password: postgresTestPassword, + Port: fmt.Sprintf("%d", postgresqlTestPort), + SkipInitializeWithVersion: true, + })) + + } else if database == datastore.MySQL { + + // Sanity check + if ts.MySQLServer == nil { + return nil, ErrLoadServerFirst + } + + // Add the new Postgresql connection + opts = append(opts, WithSQL(datastore.MySQL, &datastore.SQLConfig{ + CommonConfig: datastore.CommonConfig{ + MaxIdleConnections: 1, + MaxOpenConnections: 1, + TablePrefix: tablePrefix, + }, + Host: mySQLHost, + Name: defaultDatabaseName, + User: mySQLUsername, + Password: mySQLPassword, + Port: fmt.Sprintf("%d", mySQLTestPort), + SkipInitializeWithVersion: true, + })) + + } else { + return nil, ErrDatastoreNotSupported + } + } + + // Custom for SQLite and Mocking (cannot ignore the version check that GORM does) + if mockDB && database == datastore.SQLite { + tc.MockSQLDB.ExpectQuery( + "select sqlite_version()", + ).WillReturnRows(tc.MockSQLDB.NewRows([]string{"version"}).FromCSVString(sqliteTestVersion)) + } + + // Are we mocking redis? + if mockRedis { + tc.redisClient, tc.redisConn = tester.LoadMockRedis( + testIdleTimeout, + testMaxConnLifetime, + testMaxActiveConnections, + testMaxIdleConnections, + ) + opts = append(opts, WithRedisConnection(tc.redisClient)) + } + + // Add a custom user agent (future: make this passed into the function via opts) + opts = append(opts, WithUserAgent("spv wallet engine test suite")) + opts = append(opts, WithMinercraft(&chainstate.MinerCraftBase{})) + + // Create the client + if tc.client, err = NewClient(ctx, opts...); err != nil { + return nil, err + } + + // Return the suite + return tc, nil +} + +// genericDBClient is a helpful wrapper for getting the same type of client +// +// NOTE: you need to close the client: ts.Close() +// +//nolint:nolintlint,unparam,gci // opts is the way, but not yet being used +func (ts *EmbeddedDBTestSuite) genericDBClient(t *testing.T, database datastore.Engine, taskManagerEnabled bool, opts ...ClientOps) *TestingClient { + prefix := tester.RandomTablePrefix() + + if opts == nil { + opts = []ClientOps{} + } + opts = append(opts, + WithDebugging(), + WithChainstateOptions(false, false, false, false), + WithAutoMigrate(BaseModels...), + WithAutoMigrate(&PaymailAddress{}), + ) + if taskManagerEnabled { + opts = append(opts, WithTaskqConfig(taskmanager.DefaultTaskQConfig(prefix+"_queue"))) + } else { + opts = append(opts, withTaskManagerMockup()) + } + + tc, err := ts.createTestClient( + tester.GetNewRelicCtx( + t, defaultNewRelicApp, defaultNewRelicTx, + ), + database, prefix, + false, false, + opts..., + ) + require.NoError(t, err) + require.NotNil(t, tc) + return tc +} + +// genericMockedDBClient is a helpful wrapper for getting the same type of client +// +// NOTE: you need to close the client: ts.Close() +func (ts *EmbeddedDBTestSuite) genericMockedDBClient(t *testing.T, database datastore.Engine) *TestingClient { + prefix := tester.RandomTablePrefix() + tc, err := ts.createTestClient( + tester.GetNewRelicCtx( + t, defaultNewRelicApp, defaultNewRelicTx, + ), + database, prefix, + true, true, WithDebugging(), + withTaskManagerMockup(), + ) + require.NoError(t, err) + require.NotNil(t, tc) + return tc +} diff --git a/engine/bux_test.go b/engine/bux_test.go new file mode 100644 index 000000000..880a0a994 --- /dev/null +++ b/engine/bux_test.go @@ -0,0 +1,279 @@ +package engine + +import ( + "context" + "database/sql" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" + "github.com/bitcoin-sv/spv-wallet/engine/taskmanager" + "github.com/bitcoin-sv/spv-wallet/engine/tester" + "github.com/bitcoinschema/go-bitcoin/v2" + "github.com/libsv/go-bk/bec" + "github.com/libsv/go-bk/bip32" + "github.com/libsv/go-bt/v2" + "github.com/libsv/go-bt/v2/bscript" + "github.com/libsv/go-bt/v2/sighash" + "github.com/libsv/go-bt/v2/unlocker" + "github.com/mrz1836/go-cache" + "github.com/mrz1836/go-datastore" + "github.com/rafaeljusto/redigomock" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" +) + +// TestingClient is for testing the entire package using real/mocked services +type TestingClient struct { + client ClientInterface // Local spv wallet engine client for testing + ctx context.Context // Current CTX + database datastore.Engine // Current database + mocking bool // If mocking is enabled + MockSQLDB sqlmock.Sqlmock // Mock Database client for SQL + redisClient *cache.Client // Current redis client (used for Mocking) + redisConn *redigomock.Conn // Current redis connection (used for Mocking) + SQLConn *sql.DB // Read test client + tablePrefix string // Current table prefix +} + +// Close will close all test services and client +func (tc *TestingClient) Close(ctx context.Context) { + if tc.client != nil { + /*if !tc.mocking { + if err := dropAllTables(tc.client.Datastore(), tc.database); err != nil { + panic(err) + } + }*/ + _ = tc.client.Close(ctx) + } + if tc.SQLConn != nil { + _ = tc.SQLConn.Close() + } + if tc.redisClient != nil { + tc.redisClient.Close() + } + if tc.redisConn != nil { + _ = tc.redisConn.Close() + } +} + +// DefaultClientOpts will return a default set of client options required to load the new client +func DefaultClientOpts(debug, shared bool) []ClientOps { + tqc := taskmanager.DefaultTaskQConfig(tester.RandomTablePrefix()) + tqc.MaxNumWorker = 2 + tqc.MaxNumFetcher = 2 + + opts := make([]ClientOps, 0) + opts = append( + opts, + WithTaskqConfig(tqc), + WithSQLite(tester.SQLiteTestConfig(debug, shared)), + WithChainstateOptions(false, false, false, false), + WithMinercraft(&chainstate.MinerCraftBase{}), + ) + if debug { + opts = append(opts, WithDebugging()) + } + + return opts +} + +// CreateTestSQLiteClient will create a test client for SQLite +// +// NOTE: you need to close the client using the returned defer func +func CreateTestSQLiteClient(t *testing.T, debug, shared bool, clientOpts ...ClientOps) (context.Context, ClientInterface, func()) { + ctx := tester.GetNewRelicCtx(t, "app-test", "test-transaction") + + logger := zerolog.Nop() + + // Set the default options, add migrate models + opts := DefaultClientOpts(debug, shared) + opts = append(opts, WithAutoMigrate(BaseModels...)) + opts = append(opts, WithLogger(&logger)) + opts = append(opts, clientOpts...) + + // Create the client + client, err := NewClient(ctx, opts...) + require.NoError(t, err) + require.NotNil(t, client) + + // Create a defer function + f := func() { + _ = client.Close(context.Background()) + } + return ctx, client, f +} + +// CreateBenchmarkSQLiteClient will create a test client for SQLite +// +// NOTE: you need to close the client using the returned defer func +func CreateBenchmarkSQLiteClient(b *testing.B, debug, shared bool, clientOpts ...ClientOps) (context.Context, ClientInterface, func()) { + ctx := context.Background() + + logger := zerolog.Nop() + + // Set the default options, add migrate models + opts := DefaultClientOpts(debug, shared) + opts = append(opts, WithAutoMigrate(BaseModels...)) + opts = append(opts, WithLogger(&logger)) + opts = append(opts, clientOpts...) + + // Create the client + client, err := NewClient(ctx, opts...) + if err != nil { + b.Fail() + } + + // Create a defer function + f := func() { + _ = client.Close(context.Background()) + } + return ctx, client, f +} + +// CloseClient is function used in the "defer()" function +func CloseClient(ctx context.Context, t *testing.T, client ClientInterface) { + require.NoError(t, client.Close(ctx)) +} + +// we need to create an interface for the unlocker +type account struct { + PrivateKey *bec.PrivateKey +} + +// Unlocker get the correct un-locker for a given locking script. +func (a *account) Unlocker(context.Context, *bscript.Script) (bt.Unlocker, error) { + return &unlocker.Simple{ + PrivateKey: a.PrivateKey, + }, nil +} + +// CreateFakeFundingTransaction will create a valid (fake) transaction for funding +func CreateFakeFundingTransaction(t *testing.T, masterKey *bip32.ExtendedKey, + destinations []*Destination, satoshis uint64, +) string { + // Create new tx + rawTx := bt.NewTx() + txErr := rawTx.From(testTxScriptSigID, 0, testTxScriptSigOut, satoshis+354) + require.NoError(t, txErr) + + // Loop all destinations + for _, destination := range destinations { + s, err := bscript.NewFromHexString(destination.LockingScript) + require.NoError(t, err) + require.NotNil(t, s) + + rawTx.AddOutput(&bt.Output{ + Satoshis: satoshis, + LockingScript: s, + }) + } + + // Get private key + privateKey, err := bitcoin.GetPrivateKeyFromHDKey(masterKey) + require.NoError(t, err) + require.NotNil(t, privateKey) + + // Sign the tx + myAccount := &account{PrivateKey: privateKey} + err = rawTx.FillAllInputs(context.Background(), myAccount) + require.NoError(t, err) + + // Return the tx hex + return rawTx.String() +} + +// CreateNewXPub will create a new xPub and return all the information to use the xPub +func CreateNewXPub(ctx context.Context, t *testing.T, engineClient ClientInterface, + opts ...ModelOps, +) (*bip32.ExtendedKey, *Xpub, string) { + // Generate a key pair + masterKey, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) + require.NoError(t, err) + require.NotNil(t, masterKey) + + // Get the raw string of the xPub + var rawXPub string + rawXPub, err = bitcoin.GetExtendedPublicKey(masterKey) + require.NoError(t, err) + require.NotNil(t, masterKey) + + // Create the new xPub + var xPub *Xpub + xPub, err = engineClient.NewXpub(ctx, rawXPub, opts...) + require.NoError(t, err) + require.NotNil(t, xPub) + + return masterKey, xPub, rawXPub +} + +// GetUnlockingScript will get a locking script for valid fake transactions +func GetUnlockingScript(t *testing.T, tx *bt.Tx, inputIndex uint32, privateKey *bec.PrivateKey) *bscript.Script { + sh, err := tx.CalcInputSignatureHash(inputIndex, sighash.AllForkID) + require.NoError(t, err) + + var sig *bec.Signature + sig, err = privateKey.Sign(bt.ReverseBytes(sh)) + require.NoError(t, err) + require.NotNil(t, sig) + + var s *bscript.Script + s, err = bscript.NewP2PKHUnlockingScript( + privateKey.PubKey().SerialiseCompressed(), sig.Serialise(), sighash.AllForkID, + ) + require.NoError(t, err) + require.NotNil(t, s) + + return s +} + +/* + +// dbSchemaResult is the results +type dbSchemaResult struct { + TableName string `json:"table_name"` +} + +// dropAllTables will drop all tables in the current database +func dropAllTables(ds datastore.ClientInterface, database datastore.Engine) error { + + // Clearing DB is not implemented at this time + // todo: finish this clearing of db? + if database == datastore.MongoDB { + return nil + } + + // Set the select string + var selectQuery string + if database == datastore.MySQL || database == datastore.PostgreSQL { + selectQuery = "SELECT table_name FROM information_schema.tables WHERE table_schema = '" + ds.GetDatabaseName() + "';" + } else { + selectQuery = "SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%';" + } + + // Get all tables + rows, err := ds.Raw(selectQuery).Rows() + if err != nil { + return err + } + defer func() { + _ = rows.Close() + }() + + // Parse the records and build a list of table names + var result dbSchemaResult + for rows.Next() { + if err = rows.Scan(&result.TableName); err != nil { + return err + } + if len(result.TableName) > 0 { + db := ds.Execute("DROP TABLE IF EXISTS " + result.TableName + ";") + if db.Error != nil { + return db.Error + } + } + } + + return nil +} +*/ diff --git a/engine/chainstate/broadcast.go b/engine/chainstate/broadcast.go new file mode 100644 index 000000000..552c8f221 --- /dev/null +++ b/engine/chainstate/broadcast.go @@ -0,0 +1,144 @@ +package chainstate + +import ( + "context" + "fmt" + "strings" + "sync" + "time" +) + +var ( + // broadcastSuccessErrors are a list of errors that are still considered a success + broadcastSuccessErrors = []string{ + "already in the mempool", // {"error": "-27: Transaction already in the mempool"} + "txn-already-know", // { "error": "-26: 257: txn-already-known"} // txn-already-know + "txn-already-in-mempool", // txn-already-in-mempool + "txn_already_known", // TXN_ALREADY_KNOWN + "txn_already_in_mempool", // TXN_ALREADY_IN_MEMPOOL + } + + // broadcastQuestionableErrors are a list of errors that are not good broadcast responses, + // but need to be checked differently + broadcastQuestionableErrors = []string{ + "missing inputs", // Returned from mAPI for a valid tx that is on-chain + } + + /* + TXN_ALREADY_KNOWN (suppressed - returns as success: true) + TXN_ALREADY_IN_MEMPOOL (suppressed - returns as success: true) + TXN_MEMPOOL_CONFLICT + NON_FINAL_POOL_FULL + TOO_LONG_NON_FINAL_CHAIN + BAD_TXNS_INPUTS_TOO_LARGE + BAD_TXNS_INPUTS_SPENT + NON_BIP68_FINAL + TOO_LONG_VALIDATION_TIME + BAD_TXNS_NONSTANDARD_INPUTS + ABSURDLY_HIGH_FEE + DUST + TX_FEE_TOO_LOW + */ +) + +// broadcast will broadcast using a standard strategy +// +// NOTE: if successful (in-mempool), no error will be returned +// NOTE: function register the fastest successful broadcast into 'completeChannel' so client doesn't need to wait for other providers +func (c *Client) broadcast(ctx context.Context, id, hex string, timeout time.Duration, completeChannel, errorChannel chan string) { + // Create a context (to cancel or timeout) + ctxWithCancel, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + var wg sync.WaitGroup + + resultsChannel := make(chan broadcastResult) + status := newBroadcastStatus(completeChannel) + + for _, broadcastProvider := range createActiveProviders(c, id, hex) { + wg.Add(1) + go func(provider txBroadcastProvider) { + defer wg.Done() + broadcastToProvider(ctxWithCancel, ctx, provider, id, c, timeout, + resultsChannel, status) + }(broadcastProvider) + } + + go func() { + wg.Wait() + close(resultsChannel) + status.dispose() + }() + + var errorMessages []string + for result := range resultsChannel { + if result.isError { + debugLog(c, id, fmt.Sprintf("broadcast error: %s from provider %s", result.err, result.provider)) + errorMessages = append(errorMessages, result.provider+": "+result.err.Error()) + } else { + debugLog(c, id, fmt.Sprintf("successful broadcast to %s", result.provider)) + } + } + + if !status.success && len(errorMessages) > 0 { + errorChannel <- strings.Join(errorMessages, ", ") + } +} + +func createActiveProviders(c *Client, txID, txHex string) []txBroadcastProvider { + providers := make([]txBroadcastProvider, 0, 10) + + switch c.ActiveProvider() { + case ProviderMinercraft: + for _, miner := range c.options.config.minercraftConfig.broadcastMiners { + if miner == nil { + continue + } + + pvdr := mapiBroadcastProvider{miner: miner, txID: txID, txHex: txHex} + providers = append(providers, &pvdr) + } + 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 broadcastToProvider(ctx, fallbackCtx context.Context, provider txBroadcastProvider, txID string, + c *Client, fallbackTimeout time.Duration, + resultsChannel chan broadcastResult, status *broadcastStatus, +) { + bErr := provider.broadcast(ctx, c) + + if bErr != nil { + // check in Mempool as fallback - if transaction is there -> GREAT SUCCESS + // Check error response for "questionable errors"/(TX FAILURE) + if doesErrorContain(bErr.Error(), broadcastQuestionableErrors) { + bErr = checkInMempool(fallbackCtx, c, txID, bErr.Error(), fallbackTimeout) + } + + if bErr != nil { + resultsChannel <- newErrorResult(bErr, provider.getName()) + } + } + + // successful broadcast or found in mempool + if bErr == nil { + status.tryCompleteWithSuccess(provider.getName()) + resultsChannel <- newSuccessResult(provider.getName()) + } +} + +// checkInMempool is a quick check to see if the tx is in mempool (or on-chain) +func checkInMempool(ctx context.Context, client ClientInterface, id, initErrMsg string, timeout time.Duration) error { + if _, err := client.QueryTransaction( + ctx, id, requiredInMempool, timeout, + ); err != nil { + return fmt.Errorf("error query tx failed: %w, broadcast initial error: %s", err, initErrMsg) + } + return nil +} diff --git a/engine/chainstate/broadcast_client_init.go b/engine/chainstate/broadcast_client_init.go new file mode 100644 index 000000000..d1c0807e9 --- /dev/null +++ b/engine/chainstate/broadcast_client_init.go @@ -0,0 +1,45 @@ +package chainstate + +import ( + "context" + "errors" + + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "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/engine/chainstate/broadcast_providers.go b/engine/chainstate/broadcast_providers.go new file mode 100644 index 000000000..e66bc419f --- /dev/null +++ b/engine/chainstate/broadcast_providers.go @@ -0,0 +1,118 @@ +package chainstate + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + "github.com/tonicpow/go-minercraft/v2" +) + +// generic broadcast provider +type txBroadcastProvider interface { + getName() string + broadcast(ctx context.Context, c *Client) error +} + +// mAPI provider +type mapiBroadcastProvider struct { + miner *minercraft.Miner + txID, txHex string +} + +func (provider mapiBroadcastProvider) getName() string { + return provider.miner.Name +} + +func (provider mapiBroadcastProvider) broadcast(ctx context.Context, c *Client) error { + return broadcastMAPI(ctx, c, provider.miner, provider.txID, provider.txHex) +} + +// broadcastMAPI will broadcast a transaction to a miner using mAPI +func broadcastMAPI(ctx context.Context, client ClientInterface, miner *minercraft.Miner, id, hex string) error { + debugLog(client, id, "executing broadcast request in mapi using miner: "+miner.Name) + + resp, err := client.Minercraft().SubmitTransaction(ctx, miner, &minercraft.Transaction{ + CallBackEncryption: "", // todo: allow customizing the payload + CallBackToken: "", + CallBackURL: "", + DsCheck: false, + MerkleFormat: "", + MerkleProof: false, + RawTx: hex, + }) + if err != nil { + debugLog(client, id, "error executing request in mapi using miner: "+miner.Name+" failed: "+err.Error()) + return err + } + + // Something went wrong - got back an id that does not match + if resp == nil { + return emptyBroadcastResponseErr(id) + } + if !strings.EqualFold(resp.Results.TxID, id) { + return incorrectTxIDReturnedErr(resp.Results.TxID, id) + } + + // mAPI success of broadcast + if resp.Results.ReturnResult == mAPISuccess { + return nil + } + + // Check error message (for success error message) + if doesErrorContain(resp.Results.ResultDescription, broadcastSuccessErrors) { + return nil + } + + // We got a potential real error message? + return errors.New(resp.Results.ResultDescription) +} + +func incorrectTxIDReturnedErr(actualTxID, expectedTxID string) error { + return fmt.Errorf("returned tx id [%s] does not match given tx id [%s]", actualTxID, expectedTxID) +} + +func emptyBroadcastResponseErr(txID string) error { + return fmt.Errorf("an empty response was returned after broadcasting of tx id [%s]", txID) +} + +//// + +// BroadcastClient provider +type broadcastClientProvider struct { + txID, txHex string +} + +func (provider broadcastClientProvider) getName() string { + return ProviderBroadcastClient +} + +// Broadcast using BroadcastClient +func (provider broadcastClientProvider) broadcast(ctx context.Context, c *Client) error { + return broadcastWithBroadcastClient(ctx, c, provider.txID, provider.txHex) +} + +func broadcastWithBroadcastClient(ctx context.Context, client *Client, txID, hex string) error { + debugLog(client, txID, "executing broadcast request for "+ProviderBroadcastClient) + + tx := broadcast.Transaction{ + Hex: hex, + } + + result, err := client.BroadcastClient().SubmitTransaction( + ctx, + &tx, + broadcast.WithRawFormat(), + broadcast.WithCallback(client.options.config.callbackURL, client.options.config.callbackToken), + ) + if err != nil { + debugLog(client, txID, "error broadcast request for "+ProviderBroadcastClient+" failed: "+err.Error()) + return err + } + + debugLog(client, txID, "result broadcast request for "+ProviderBroadcastClient+" blockhash: "+result.BlockHash+" status: "+result.TxStatus.String()) + + return nil +} diff --git a/engine/chainstate/broadcast_test.go b/engine/chainstate/broadcast_test.go new file mode 100644 index 000000000..27942abc0 --- /dev/null +++ b/engine/chainstate/broadcast_test.go @@ -0,0 +1,144 @@ +package chainstate + +import ( + "context" + "strings" + "testing" + + broadcast_client_mock "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client-mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tonicpow/go-minercraft/v2" +) + +func Test_doesErrorContain(t *testing.T) { + t.Run("valid contains", func(t *testing.T) { + success := doesErrorContain("this is the test message", []string{"another", "test message"}) + assert.Equal(t, true, success) + }) + + t.Run("valid contains - equal case", func(t *testing.T) { + success := doesErrorContain("this is the TEST message", []string{"another", "test message"}) + assert.Equal(t, true, success) + }) + + t.Run("does not contain", func(t *testing.T) { + success := doesErrorContain("this is the test message", []string{"another", "nope"}) + assert.Equal(t, false, success) + }) +} + +// TestClient_Broadcast will test the method Broadcast() +func TestClient_Broadcast(t *testing.T) { + t.Parallel() + + t.Run("error - missing tx id", func(t *testing.T) { + // given + c := NewTestClient(context.Background(), t, + WithMinercraft(&MinerCraftBase{})) + + // when + provider, err := c.Broadcast( + context.Background(), "", onChainExample1TxHex, defaultBroadcastTimeOut, + ) + + // then + require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidTransactionID) + assert.Empty(t, provider) + }) + + t.Run("error - missing tx hex", func(t *testing.T) { + // given + c := NewTestClient(context.Background(), t, + WithMinercraft(&MinerCraftBase{})) + + // when + provider, err := c.Broadcast( + context.Background(), onChainExample1TxID, "", defaultBroadcastTimeOut, + ) + + // then + require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidTransactionHex) + assert.Empty(t, provider) + }) +} + +// TestClient_Broadcast_MAPI will test the method Broadcast() with MAPI +func TestClient_Broadcast_MAPI(t *testing.T) { + t.Parallel() + + t.Run("broadcast - success (mAPI)", func(t *testing.T) { + // given + c := NewTestClient( + context.Background(), t, + WithMinercraft(&minerCraftBroadcastSuccess{}), + ) + + // 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, + )) + }) +} + +// TestClient_Broadcast_BroadcastClient will test the method Broadcast() with BroadcastClient +func TestClient_Broadcast_BroadcastClient(t *testing.T) { + t.Parallel() + + t.Run("broadcast - success (broadcast-client)", func(t *testing.T) { + // given + bc := broadcast_client_mock.Builder(). + WithMockArc(broadcast_client_mock.MockSuccess). + Build() + c := NewTestClient( + context.Background(), t, + WithMinercraft(&MinerCraftBase{}), + 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, + )) + }) +} + +func containsAtLeastOneElement(coll1 []string, coll2 ...string) bool { + m := make(map[string]bool) + + for _, element := range coll1 { + m[element] = true + } + + // Check if any element from bool is present in the set + for _, element := range coll2 { + if m[element] { + return true + } + } + + return false +} diff --git a/engine/chainstate/broadcast_utils.go b/engine/chainstate/broadcast_utils.go new file mode 100644 index 000000000..73189b49a --- /dev/null +++ b/engine/chainstate/broadcast_utils.go @@ -0,0 +1,76 @@ +package chainstate + +import ( + "fmt" + "strings" + "sync" +) + +// struct handles communication with the client - returns first successful broadcast +type broadcastStatus struct { + mu *sync.Mutex + complete bool + success bool + syncChannel chan string +} + +func newBroadcastStatus(synchChannel chan string) *broadcastStatus { + return &broadcastStatus{complete: false, syncChannel: synchChannel, mu: &sync.Mutex{}} +} + +func (g *broadcastStatus) tryCompleteWithSuccess(fastestProvider string) { + g.mu.Lock() + defer g.mu.Unlock() + + if !g.complete { + g.complete = true + g.success = true + + g.syncChannel <- fastestProvider + close(g.syncChannel) + } + + // g.mu.Unlock() is done by defer +} + +func (g *broadcastStatus) dispose() { + g.mu.Lock() + defer g.mu.Unlock() + + if !g.complete { + g.complete = true + close(g.syncChannel) // have to close the channel here to not block client + } + + // g.mu.Unlock() is done by defer +} + +// result of single broadcast to provider +type broadcastResult struct { + isError bool + err error + provider string +} + +func newErrorResult(err error, provider string) broadcastResult { + return broadcastResult{isError: true, err: err, provider: provider} +} + +func newSuccessResult(provider string) broadcastResult { + return broadcastResult{isError: false, provider: provider} +} + +// doesErrorContain will look at a string for a list of strings +func doesErrorContain(err string, messages []string) bool { + lower := strings.ToLower(err) + for _, str := range messages { + if strings.Contains(lower, str) { + return true + } + } + return false +} + +func debugLog(c ClientInterface, txID, msg string) { + c.DebugLog(fmt.Sprintf("[txID: %s]: %s", txID, msg)) +} diff --git a/engine/chainstate/chainstate.go b/engine/chainstate/chainstate.go new file mode 100644 index 000000000..99452accf --- /dev/null +++ b/engine/chainstate/chainstate.go @@ -0,0 +1,90 @@ +/* +Package chainstate is the on-chain data service abstraction layer +*/ +package chainstate + +import ( + "context" + "fmt" + "time" +) + +// Broadcast will attempt to broadcast a transaction using the given providers +func (c *Client) Broadcast(ctx context.Context, id, txHex string, timeout time.Duration) (string, error) { + // Basic validation + if len(id) < 50 { + return "", ErrInvalidTransactionID + } else if len(txHex) <= 0 { // todo: validate the tx hex + return "", ErrInvalidTransactionHex + } + + // Debug the id and hex + c.DebugLog("tx_id: " + id) + c.DebugLog("tx_hex: " + txHex) + + // Broadcast or die + successCompleteCh := make(chan string) + errorCh := make(chan string) + + go c.broadcast(ctx, id, txHex, timeout, successCompleteCh, errorCh) + + // wait for first success + success := <-successCompleteCh + if success != "" { + return success, nil + } + + // successCompleteCh closed without any values + errorMessage := <-errorCh + return ProviderAll, fmt.Errorf("broadcast failed, errors: %s", errorMessage) +} + +// QueryTransaction will get the transaction info from all providers returning the "first" valid result +// +// Note: this is slow, but follows a specific order: mAPI -> WhatsOnChain +func (c *Client) QueryTransaction( + ctx context.Context, id string, requiredIn RequiredIn, timeout time.Duration, +) (transaction *TransactionInfo, err error) { + if c.options.metrics != nil { + end := c.options.metrics.TrackQueryTransaction() + defer func() { + success := err == nil + end(success) + }() + } + + // Basic validation + if len(id) < 50 { + return nil, ErrInvalidTransactionID + } else if !c.validRequirement(requiredIn) { + return nil, ErrInvalidRequirements + } + + // Try all providers and return the "first" valid response + info := c.query(ctx, id, requiredIn, timeout) + if info == nil { + return nil, ErrTransactionNotFound + } + return info, nil +} + +// QueryTransactionFastest will get the transaction info from ALL provider(s) returning the "fastest" valid result +// +// Note: this is fast but could abuse each provider based on how excessive this method is used +func (c *Client) QueryTransactionFastest( + ctx context.Context, id string, requiredIn RequiredIn, timeout time.Duration, +) (*TransactionInfo, error) { + // Basic validation + if len(id) < 50 { + return nil, ErrInvalidTransactionID + } else if !c.validRequirement(requiredIn) { + return nil, ErrInvalidRequirements + } + + // Try all providers and return the "fastest" valid response + info := c.fastestQuery(ctx, id, requiredIn, timeout) + if info == nil { + return nil, ErrTransactionNotFound + } + return info, nil +} diff --git a/engine/chainstate/chainstate_test.go b/engine/chainstate/chainstate_test.go new file mode 100644 index 000000000..6c771a530 --- /dev/null +++ b/engine/chainstate/chainstate_test.go @@ -0,0 +1,43 @@ +package chainstate + +import ( + "context" + "testing" + "time" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// NewTestClient returns a test client +func NewTestClient(ctx context.Context, t *testing.T, opts ...ClientOps) ClientInterface { + logger := zerolog.Nop() + c, err := NewClient( + ctx, append(opts, WithDebugging(), WithLogger(&logger))..., + ) + require.NoError(t, err) + require.NotNil(t, c) + return c +} + +// TestQueryTransactionFastest tests the querying for a transaction and returns the fastest response +func TestQueryTransactionFastest(t *testing.T) { + t.Run("no tx ID", func(t *testing.T) { + ctx := context.Background() + c := NewTestClient(ctx, t, WithMinercraft(&minerCraftTxOnChain{})) + + _, err := c.QueryTransactionFastest(ctx, "", RequiredInMempool, 5*time.Second) + require.Error(t, err) + }) + + t.Run("fastest query", func(t *testing.T) { + ctx := context.Background() + c := NewTestClient(ctx, t, WithMinercraft(&minerCraftTxOnChain{})) + + var txInfo *TransactionInfo + txInfo, err := c.QueryTransactionFastest(ctx, onChainExample1TxID, RequiredInMempool, 5*time.Second) + require.NoError(t, err) + assert.NotNil(t, txInfo) + }) +} diff --git a/engine/chainstate/client.go b/engine/chainstate/client.go new file mode 100644 index 000000000..3f6eb8a25 --- /dev/null +++ b/engine/chainstate/client.go @@ -0,0 +1,203 @@ +package chainstate + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + "github.com/bitcoin-sv/spv-wallet/engine/logging" + "github.com/bitcoin-sv/spv-wallet/engine/metrics" + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/rs/zerolog" + "github.com/tonicpow/go-minercraft/v2" +) + +type ( + + // Client is the client (configuration) + Client struct { + options *clientOptions + } + + // clientOptions holds all the configuration for the client + clientOptions struct { + config *syncConfig // Configuration for broadcasting and other chain-state actions + debug bool // For extra logs and additional debug information + logger *zerolog.Logger // Logger interface + metrics *metrics.Metrics // For collecting metrics (if enabled) + newRelicEnabled bool // If NewRelic is enabled (parent application) + userAgent string // Custom user agent for outgoing HTTP Requests + } + + // syncConfig holds all the configuration about the different sync processes + syncConfig struct { + callbackURL string // Broadcast callback URL + callbackToken string // Broadcast callback access token + excludedProviders []string // List of provider names + httpClient HTTPInterface // Custom HTTP client (Minercraft, WOC) + minercraftConfig *minercraftConfig // minercraftConfig configuration + minercraft minercraft.ClientInterface // Minercraft client + network Network // Current network (mainnet, testnet, stn) + queryTimeout time.Duration // Timeout for transaction query + broadcastClient broadcast.Client // Broadcast client + blockHedersServiceClient *blockHeadersServiceClientProvider // Block Headers Service 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 []*minercraft.Miner // List of loaded miners for broadcasting + queryMiners []*minercraft.Miner // List of loaded miners for querying transactions + + apiType minercraft.APIType // MinerCraft APIType(ARC/mAPI) + minerAPIs []*minercraft.MinerAPIs // List of miners APIs + } +) + +// NewClient creates a new client for all on-chain functionality +// +// If no options are given, it will use the defaultClientOptions() +// ctx may contain a NewRelic txn (or one will be created) +func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error) { + // Create a new client with defaults + client := &Client{options: defaultClientOptions()} + + // Overwrite defaults with any set by user + for _, opt := range opts { + opt(client.options) + } + + // Use NewRelic if it's enabled (use existing txn if found on ctx) + ctx = client.options.getTxnCtx(ctx) + + // Set logger if not set + if client.options.logger == nil { + client.options.logger = logging.GetDefaultLogger() + } + + if err := client.initActiveProvider(ctx); err != nil { + return nil, err + } + + if err := client.checkFeeUnit(); err != nil { + return nil, err + } + + // Return the client + return client, nil +} + +// Close will close the client and any open connections +func (c *Client) Close(ctx context.Context) { + if txn := newrelic.FromContext(ctx); txn != nil { + defer txn.StartSegment("close_chainstate").End() + } + if c != nil && c.options.config != nil { + // Close minercraft + if c.options.config.minercraft != nil { + c.options.config.minercraft = nil + } + } +} + +// Debug will set the debug flag +func (c *Client) Debug(on bool) { + c.options.debug = on +} + +// DebugLog will display verbose logs +func (c *Client) DebugLog(text string) { + c.options.logger.Debug().Msg(text) +} + +// IsDebug will return if debugging is enabled +func (c *Client) IsDebug() bool { + return c.options.debug +} + +// IsNewRelicEnabled will return if new relic is enabled +func (c *Client) IsNewRelicEnabled() bool { + return c.options.newRelicEnabled +} + +// HTTPClient will return the HTTP client +func (c *Client) HTTPClient() HTTPInterface { + return c.options.config.httpClient +} + +// Network will return the current network +func (c *Client) Network() Network { + return c.options.config.network +} + +// Minercraft will return the Minercraft client +func (c *Client) Minercraft() minercraft.ClientInterface { + return c.options.config.minercraft +} + +// BroadcastClient will return the BroadcastClient client +func (c *Client) BroadcastClient() broadcast.Client { + return c.options.config.broadcastClient +} + +// QueryTimeout will return the query timeout +func (c *Client) QueryTimeout() time.Duration { + return c.options.config.queryTimeout +} + +// FeeUnit will return feeUnit +func (c *Client) FeeUnit() *utils.FeeUnit { + return c.options.config.feeUnit +} + +// 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 + } + if !utils.StringInSlice(ProviderMinercraft, excluded) && (c.Network() == MainNet || c.Network() == TestNet) { + return ProviderMinercraft + } + return ProviderNone +} + +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") + } +} + +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) + } + return nil +} diff --git a/engine/chainstate/client_options.go b/engine/chainstate/client_options.go new file mode 100644 index 000000000..20e782c16 --- /dev/null +++ b/engine/chainstate/client_options.go @@ -0,0 +1,184 @@ +package chainstate + +import ( + "context" + "time" + + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + "github.com/bitcoin-sv/spv-wallet/engine/metrics" + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/rs/zerolog" + "github.com/tonicpow/go-minercraft/v2" +) + +// ClientOps allow functional options to be supplied +// that overwrite default client options. +type ClientOps func(c *clientOptions) + +// defaultClientOptions will return an clientOptions struct with the default settings +// +// Useful for starting with the default and then modifying as needed +func defaultClientOptions() *clientOptions { + // Set the default options + return &clientOptions{ + config: &syncConfig{ + 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, + metrics: nil, + } +} + +// getTxnCtx will check for an existing transaction +func (c *clientOptions) getTxnCtx(ctx context.Context) context.Context { + if c.newRelicEnabled { + txn := newrelic.FromContext(ctx) + if txn != nil { + ctx = newrelic.NewContext(ctx, txn) + } + } + return ctx +} + +// WithNewRelic will enable the NewRelic wrapper +func WithNewRelic() ClientOps { + return func(c *clientOptions) { + c.newRelicEnabled = true + } +} + +// WithDebugging will enable debugging mode +func WithDebugging() ClientOps { + return func(c *clientOptions) { + c.debug = true + } +} + +// WithHTTPClient will set a custom HTTP client +func WithHTTPClient(client HTTPInterface) ClientOps { + return func(c *clientOptions) { + if client != nil { + c.config.httpClient = client + } + } +} + +// WithMinercraft will set a custom Minercraft client +func WithMinercraft(client minercraft.ClientInterface) ClientOps { + return func(c *clientOptions) { + if client != nil { + c.config.minercraft = client + } + } +} + +// WithMAPI will specify mAPI as an API for minercraft client +func WithMAPI() ClientOps { + return func(c *clientOptions) { + c.config.minercraftConfig.apiType = minercraft.MAPI + } +} + +// WithQueryTimeout will set a different timeout for transaction querying +func WithQueryTimeout(timeout time.Duration) ClientOps { + return func(c *clientOptions) { + if timeout > 0 { + c.config.queryTimeout = timeout + } + } +} + +// WithUserAgent will set the custom user agent +func WithUserAgent(agent string) ClientOps { + return func(c *clientOptions) { + if len(agent) > 0 { + c.userAgent = agent + } + } +} + +// WithNetwork will set the network to use +func WithNetwork(network Network) ClientOps { + return func(c *clientOptions) { + if len(network) > 0 { + c.config.network = network + } + } +} + +// WithLogger will set a custom logger +func WithLogger(customLogger *zerolog.Logger) ClientOps { + return func(c *clientOptions) { + if customLogger != nil { + c.logger = customLogger + } + } +} + +// WithExcludedProviders will set a list of excluded providers +func WithExcludedProviders(providers []string) ClientOps { + return func(c *clientOptions) { + if len(providers) > 0 { + c.config.excludedProviders = providers + } + } +} + +// 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.feeUnit = feeUnit + } +} + +// WithMinercraftAPIs will set miners APIs +func WithMinercraftAPIs(apis []*minercraft.MinerAPIs) ClientOps { + return func(c *clientOptions) { + c.config.minercraftConfig.minerAPIs = apis + } +} + +// WithBroadcastClient will set broadcast client APIs +func WithBroadcastClient(client broadcast.Client) ClientOps { + return func(c *clientOptions) { + c.config.broadcastClient = client + } +} + +// WithConnectionToBlockHeaderService will set block headers service API settings. +func WithConnectionToBlockHeaderService(url, authToken string) ClientOps { + return func(c *clientOptions) { + c.config.blockHedersServiceClient = newBlockHeaderServiceClientProvider(url, authToken) + } +} + +// WithCallback will set broadcast callback settings +func WithCallback(callbackURL, callbackAuthToken string) ClientOps { + return func(c *clientOptions) { + c.config.callbackURL = callbackURL + c.config.callbackToken = callbackAuthToken + } +} + +// WithMetrics will set metrics +func WithMetrics(metrics *metrics.Metrics) ClientOps { + return func(c *clientOptions) { + c.metrics = metrics + } +} diff --git a/engine/chainstate/client_options_test.go b/engine/chainstate/client_options_test.go new file mode 100644 index 000000000..65c9d1134 --- /dev/null +++ b/engine/chainstate/client_options_test.go @@ -0,0 +1,282 @@ +package chainstate + +import ( + "context" + "net/http" + "testing" + "time" + + broadcast_client_mock "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client-mock" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// 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) + }) + + t.Run("apply opts", func(t *testing.T) { + opts := []ClientOps{ + WithNewRelic(), + WithMinercraft(&MinerCraftBase{}), + } + c, err := NewClient(context.Background(), opts...) + require.NotNil(t, c) + require.NoError(t, err) + + assert.Equal(t, true, c.IsNewRelicEnabled()) + }) +} + +// 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) + }) + + t.Run("apply opts", func(t *testing.T) { + opts := []ClientOps{ + WithDebugging(), + WithMinercraft(&MinerCraftBase{}), + } + c, err := NewClient(context.Background(), opts...) + require.NotNil(t, c) + require.NoError(t, err) + + assert.Equal(t, true, c.IsDebug()) + }) +} + +// TestWithHTTPClient will test the method WithHTTPClient() +func TestWithHTTPClient(t *testing.T) { + t.Parallel() + + t.Run("check type", func(t *testing.T) { + opt := WithHTTPClient(nil) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("test applying nil", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + opt := WithHTTPClient(nil) + opt(options) + assert.Nil(t, options.config.httpClient) + }) + + t.Run("test applying option", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + customClient := &http.Client{} + opt := WithHTTPClient(customClient) + opt(options) + assert.Equal(t, customClient, options.config.httpClient) + }) +} + +// TestWithMinercraft will test the method WithMinercraft() +func TestWithMinercraft(t *testing.T) { + t.Parallel() + + t.Run("check type", func(t *testing.T) { + opt := WithMinercraft(nil) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("test applying nil", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + opt := WithMinercraft(nil) + opt(options) + assert.Nil(t, options.config.minercraft) + }) + + t.Run("test applying option", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + customClient := &minerCraftTxOnChain{} + opt := WithMinercraft(customClient) + opt(options) + assert.Equal(t, customClient, options.config.minercraft) + }) +} + +// TestWithBroadcastClient will test the method WithBroadcastClient() +func TestWithBroadcastClient(t *testing.T) { + t.Parallel() + + t.Run("check type", func(t *testing.T) { + opt := WithBroadcastClient(nil) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("test applying nil", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + opt := WithBroadcastClient(nil) + opt(options) + assert.Nil(t, options.config.broadcastClient) + }) + + t.Run("test applying option", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + customClient := broadcast_client_mock.Builder().WithMockArc(broadcast_client_mock.MockSuccess).Build() + opt := WithBroadcastClient(customClient) + opt(options) + assert.Equal(t, customClient, options.config.broadcastClient) + }) +} + +// TestWithQueryTimeout will test the method WithQueryTimeout() +func TestWithQueryTimeout(t *testing.T) { + t.Parallel() + + t.Run("check type", func(t *testing.T) { + opt := WithQueryTimeout(0) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("test applying empty value", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + opt := WithQueryTimeout(0) + opt(options) + assert.Equal(t, time.Duration(0), options.config.queryTimeout) + }) + + t.Run("test applying option", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + opt := WithQueryTimeout(10 * time.Second) + opt(options) + assert.Equal(t, 10*time.Second, options.config.queryTimeout) + }) +} + +// TestWithNetwork will test the method WithNetwork() +func TestWithNetwork(t *testing.T) { + t.Parallel() + + t.Run("check type", func(t *testing.T) { + opt := WithNetwork("") + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("test applying empty string", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + opt := WithNetwork("") + opt(options) + assert.Equal(t, Network(""), options.config.network) + }) + + t.Run("test applying option", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + opt := WithNetwork(TestNet) + opt(options) + assert.Equal(t, TestNet, options.config.network) + }) +} + +// TestWithUserAgent will test the method WithUserAgent() +func TestWithUserAgent(t *testing.T) { + t.Parallel() + + t.Run("check type", func(t *testing.T) { + opt := WithUserAgent("") + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("test applying empty string", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + opt := WithUserAgent("") + opt(options) + assert.Equal(t, "", options.userAgent) + }) + + t.Run("test applying option", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + opt := WithUserAgent("test agent") + opt(options) + assert.Equal(t, "test agent", options.userAgent) + }) +} + +// TestWithLogger will test the method WithLogger() +func TestWithLogger(t *testing.T) { + t.Parallel() + + t.Run("check type", func(t *testing.T) { + opt := WithLogger(nil) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("test applying nil", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + opt := WithLogger(nil) + opt(options) + assert.Nil(t, options.logger) + }) + + t.Run("test applying option", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + customLogger := zerolog.Nop() + opt := WithLogger(&customLogger) + opt(options) + assert.Equal(t, &customLogger, options.logger) + }) +} + +// TestWithExcludedProviders will test the method WithExcludedProviders() +func TestWithExcludedProviders(t *testing.T) { + t.Parallel() + + t.Run("check type", func(t *testing.T) { + opt := WithExcludedProviders(nil) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("test applying empty string", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + opt := WithExcludedProviders([]string{""}) + opt(options) + assert.Equal(t, []string{""}, options.config.excludedProviders) + }) + + t.Run("test applying option", func(t *testing.T) { + options := &clientOptions{ + config: &syncConfig{}, + } + opt := WithExcludedProviders([]string{ProviderBroadcastClient}) + opt(options) + assert.Equal(t, 1, len(options.config.excludedProviders)) + assert.Equal(t, ProviderBroadcastClient, options.config.excludedProviders[0]) + }) +} diff --git a/engine/chainstate/client_test.go b/engine/chainstate/client_test.go new file mode 100644 index 000000000..e152bc3a7 --- /dev/null +++ b/engine/chainstate/client_test.go @@ -0,0 +1,124 @@ +package chainstate + +import ( + "context" + "net/http" + "testing" + "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" +) + +// TestNewClient will test the method NewClient() +func TestNewClient(t *testing.T) { + t.Parallel() + + t.Run("basic defaults", func(t *testing.T) { + c, err := NewClient( + context.Background(), + WithMinercraft(&MinerCraftBase{}), + ) + require.NoError(t, err) + require.NotNil(t, c) + assert.Equal(t, false, c.IsDebug()) + assert.Equal(t, MainNet, c.Network()) + assert.Nil(t, c.HTTPClient()) + assert.NotNil(t, c.Minercraft()) + }) + + t.Run("custom http client", func(t *testing.T) { + customClient := &http.Client{} + c, err := NewClient( + context.Background(), + WithHTTPClient(customClient), + WithMinercraft(&MinerCraftBase{}), + ) + require.NoError(t, err) + require.NotNil(t, c) + assert.NotNil(t, c.HTTPClient()) + assert.Equal(t, customClient, c.HTTPClient()) + }) + + t.Run("custom broadcast client", func(t *testing.T) { + arcConfig := broadcast_client.ArcClientConfig{ + Token: "", + APIUrl: "https://arc.gorillapool.io", + } + logger := zerolog.Nop() + customClient := broadcast_client.Builder().WithArc(arcConfig, &logger).Build() + require.NotNil(t, customClient) + c, err := NewClient( + context.Background(), + WithMinercraft(&MinerCraftBase{}), + WithBroadcastClient(customClient), + ) + require.NoError(t, err) + require.NotNil(t, c) + assert.NotNil(t, c.BroadcastClient()) + assert.Equal(t, customClient, c.BroadcastClient()) + }) + + t.Run("custom minercraft client", func(t *testing.T) { + customClient, err := minercraft.NewClient( + minercraft.DefaultClientOptions(), &http.Client{}, minercraft.MAPI, nil, nil, + ) + require.NoError(t, err) + require.NotNil(t, customClient) + + var c ClientInterface + c, err = NewClient( + context.Background(), + WithMinercraft(customClient), + ) + require.NoError(t, err) + require.NotNil(t, c) + assert.NotNil(t, c.Minercraft()) + assert.Equal(t, customClient, c.Minercraft()) + }) + + t.Run("custom query timeout", func(t *testing.T) { + timeout := 55 * time.Second + c, err := NewClient( + context.Background(), + WithQueryTimeout(timeout), + WithMinercraft(&MinerCraftBase{}), + ) + require.NoError(t, err) + require.NotNil(t, c) + assert.Equal(t, timeout, c.QueryTimeout()) + }) + + t.Run("custom network - test", func(t *testing.T) { + c, err := NewClient( + context.Background(), + WithNetwork(TestNet), + WithMinercraft(&MinerCraftBase{}), + ) + require.NoError(t, err) + require.NotNil(t, c) + assert.Equal(t, TestNet, c.Network()) + }) + + t.Run("no provider when using minercraft with customNet", func(t *testing.T) { + _, err := NewClient( + context.Background(), + WithNetwork(StressTestNet), + WithMinercraft(&MinerCraftBase{}), + WithFeeUnit(MockDefaultFee), + ) + require.Error(t, err) + }) + + t.Run("unreachable miners", func(t *testing.T) { + _, err := NewClient( + context.Background(), + WithMinercraft(&minerCraftUnreachable{}), + ) + require.Error(t, err) + assert.ErrorIs(t, err, ErrMissingBroadcastMiners) + }) +} diff --git a/engine/chainstate/definitions.go b/engine/chainstate/definitions.go new file mode 100644 index 000000000..1579b4ef9 --- /dev/null +++ b/engine/chainstate/definitions.go @@ -0,0 +1,147 @@ +package chainstate + +import ( + "time" +) + +// Chainstate configuration defaults +const ( + defaultBroadcastTimeOut = 15 * time.Second + defaultFalsePositiveRate = 0.01 + defaultFeeLastCheckIgnore = 2 * time.Minute + defaultMaxNumberOfDestinations = 100000 + defaultQueryTimeOut = 15 * time.Second + whatsOnChainRateLimitWithKey = 20 +) + +const ( + // FilterBloom is for bloom filters + FilterBloom = "bloom" + + // FilterRegex is for regex filters + FilterRegex = "regex" +) + +// Internal network names +const ( + mainNet = "mainnet" // Main Public Bitcoin network + mainNetAlt = "main" // Main Public Bitcoin network + stn = "stn" // BitcoinSV Public Stress Test Network (https://bitcoinscaling.io/) + testNet = "testnet" // Public test network + testNetAlt = "test" // Public test network +) + +// Requirements and providers +const ( + mAPIFailure = "failure" // Minercraft result was a failure / error + mAPISuccess = "success" // Minercraft result was success (still could be an error) + requiredInMempool = "mempool" // Requirement for tx query (has to be >= mempool) + requiredOnChain = "on-chain" // Requirement for tx query (has to be == on-chain) +) + +// List of providers +const ( + ProviderAll = "all" // All providers (used for errors etc) + ProviderMinercraft = "minercraft" // Query & broadcast provider for mAPI (using given miners) + ProviderBroadcastClient = "broadcastclient" // Query & broadcast provider for configured miners + ProviderNone = "none" // No providers (used to indicate no providers) +) + +// BlockInfo is the response info about a returned block +type BlockInfo struct { + Bits string `json:"bits"` + ChainWork string `json:"chainwork"` + CoinbaseTx CoinbaseTxInfo `json:"coinbaseTx"` + Confirmations int64 `json:"confirmations"` + Difficulty float64 `json:"difficulty"` + Hash string `json:"hash"` + Height int64 `json:"height"` + MedianTime int64 `json:"mediantime"` + MerkleRoot string `json:"merkleroot"` + Miner string `json:"Bmgpool"` + NextBlockHash string `json:"nextblockhash"` + Nonce int64 `json:"nonce"` + Pages Page `json:"pages"` + PreviousBlockHash string `json:"previousblockhash"` + Size int64 `json:"size"` + Time int64 `json:"time"` + TotalFees float64 `json:"totalFees"` + Tx []string `json:"tx"` + TxCount int64 `json:"txcount"` + Version int64 `json:"version"` + VersionHex string `json:"versionHex"` +} + +// CoinbaseTxInfo is the coinbase tx info inside the BlockInfo +type CoinbaseTxInfo struct { + BlockHash string `json:"blockhash"` + BlockTime int64 `json:"blocktime"` + Confirmations int64 `json:"confirmations"` + Hash string `json:"hash"` + Hex string `json:"hex"` + LockTime int64 `json:"locktime"` + Size int64 `json:"size"` + Time int64 `json:"time"` + TxID string `json:"txid"` + Version int64 `json:"version"` + Vin []VinInfo `json:"vin"` + Vout []VoutInfo `json:"vout"` +} + +// Page is used as a subtype for BlockInfo +type Page struct { + Size int64 `json:"size"` + URI []string `json:"uri"` +} + +// VinInfo is the vin info inside the CoinbaseTxInfo +type VinInfo struct { + Coinbase string `json:"coinbase"` + ScriptSig ScriptSigInfo `json:"scriptSig"` + Sequence int64 `json:"sequence"` + TxID string `json:"txid"` + Vout int64 `json:"vout"` +} + +// VoutInfo is the vout info inside the CoinbaseTxInfo +type VoutInfo struct { + N int64 `json:"n"` + ScriptPubKey ScriptPubKeyInfo `json:"scriptPubKey"` + Value float64 `json:"value"` +} + +// ScriptSigInfo is the scriptSig info inside the VinInfo +type ScriptSigInfo struct { + Asm string `json:"asm"` + Hex string `json:"hex"` +} + +// ScriptPubKeyInfo is the scriptPubKey info inside the VoutInfo +type ScriptPubKeyInfo struct { + Addresses []string `json:"addresses"` + Asm string `json:"asm"` + Hex string `json:"hex"` + IsTruncated bool `json:"isTruncated"` + OpReturn string `json:"-"` // todo: support this (can be an object of key/vals based on the op return data) + ReqSigs int64 `json:"reqSigs"` + Type string `json:"type"` +} + +// TxInfo is the response info about a returned tx +type TxInfo struct { + BlockHash string `json:"blockhash"` + BlockHeight int64 `json:"blockheight"` + BlockTime int64 `json:"blocktime"` + Confirmations int64 `json:"confirmations"` + Hash string `json:"hash"` + Hex string `json:"hex"` + LockTime int64 `json:"locktime"` + Size int64 `json:"size"` + Time int64 `json:"time"` + TxID string `json:"txid"` + Version int64 `json:"version"` + Vin []VinInfo `json:"vin"` + Vout []VoutInfo `json:"vout"` + + Error string `json:"error"` +} diff --git a/engine/chainstate/errors.go b/engine/chainstate/errors.go new file mode 100644 index 000000000..070d9e0ae --- /dev/null +++ b/engine/chainstate/errors.go @@ -0,0 +1,24 @@ +package chainstate + +import "errors" + +// ErrInvalidTransactionID is when the transaction id is missing or invalid +var ErrInvalidTransactionID = errors.New("invalid transaction id") + +// ErrInvalidTransactionHex is when the transaction hex is missing or invalid +var ErrInvalidTransactionHex = errors.New("invalid transaction hex") + +// ErrTransactionIDMismatch is when the returned tx does not match the expected given tx id +var ErrTransactionIDMismatch = errors.New("result tx id did not match provided tx id") + +// ErrTransactionNotFound is when a transaction was not found in any on-chain provider +var ErrTransactionNotFound = errors.New("transaction not found using all chain providers") + +// ErrInvalidRequirements is when an invalid requirement was given +var ErrInvalidRequirements = errors.New("requirements are invalid or missing") + +// ErrMissingBroadcastMiners is when broadcasting miners are missing +var ErrMissingBroadcastMiners = errors.New("missing: broadcasting miners") + +// ErrMissingQueryMiners is when query miners are missing +var ErrMissingQueryMiners = errors.New("missing: query miners") diff --git a/engine/chainstate/filters/filters.go b/engine/chainstate/filters/filters.go new file mode 100644 index 000000000..fac5579fd --- /dev/null +++ b/engine/chainstate/filters/filters.go @@ -0,0 +1,2 @@ +// Package filters is used for various known TX filters and format detection +package filters diff --git a/engine/chainstate/filters/metanet.go b/engine/chainstate/filters/metanet.go new file mode 100644 index 000000000..8422144b5 --- /dev/null +++ b/engine/chainstate/filters/metanet.go @@ -0,0 +1,23 @@ +package filters + +import ( + "strings" + + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" + "github.com/libsv/go-bt" +) + +// MetanetScriptTemplate script template for metanet transaction +const MetanetScriptTemplate = "14c91e5cc393bb9d6da3040a7c72b4b569b237e450" + +// Metanet filter processor +func Metanet(tx *chainstate.TxInfo) (*bt.Tx, error) { + // Loop through all the outputs and check for pubkeyhash output + for _, out := range tx.Vout { + // if any output contains a pubkeyhash output, include this tx in the filter + if strings.HasPrefix(out.ScriptPubKey.Hex, MetanetScriptTemplate) { + return bt.NewTxFromString(tx.Hex) + } + } + return nil, nil +} diff --git a/engine/chainstate/filters/metanet_test.go b/engine/chainstate/filters/metanet_test.go new file mode 100644 index 000000000..1219c7481 --- /dev/null +++ b/engine/chainstate/filters/metanet_test.go @@ -0,0 +1,42 @@ +package filters + +import ( + "testing" + + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" +) + +func TestMetanet(t *testing.T) { + type args struct { + tx *chainstate.TxInfo + } + tests := []struct { + name string + args args + passFilter bool + wantErr bool + }{ + { + name: "non-metanet transaction shouldn't pass filter", + args: args{ + tx: &chainstate.TxInfo{ + Hex: "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1c03d7c6082f7376706f6f6c2e636f6d2f3edff034600055b8467f0040ffffffff01247e814a000000001976a914492558fb8ca71a3591316d095afc0f20ef7d42f788ac00000000", + }, + }, + passFilter: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Metanet(tt.args.tx) + if (err != nil) != tt.wantErr { + t.Errorf("Metanet() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.passFilter && got == nil { + t.Errorf("Metanet() expected transaction to pass filter and it didn't") + } + }) + } +} diff --git a/engine/chainstate/filters/planaria-b.go b/engine/chainstate/filters/planaria-b.go new file mode 100644 index 000000000..f7bb6edb8 --- /dev/null +++ b/engine/chainstate/filters/planaria-b.go @@ -0,0 +1,26 @@ +package filters + +import ( + "strings" + + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" + "github.com/libsv/go-bt" +) + +// PlanariaBTemplate string template for a B transaction +const PlanariaBTemplate = "006a2231394878696756345179427633744870515663554551797131707a5a56646f417574" + +// PlanariaBTemplateAlternate alternate string template for a B transaction +const PlanariaBTemplateAlternate = "6a2231394878696756345179427633744870515663554551797131707a5a56646f417574" + +// PlanariaB processor +func PlanariaB(tx *chainstate.TxInfo) (*bt.Tx, error) { + // Loop through all the outputs and check for pubkeyhash output + for _, out := range tx.Vout { + // if any output contains a pubkeyhash output, include this tx in the filter + if strings.HasPrefix(out.ScriptPubKey.Hex, PlanariaBTemplate) || strings.HasPrefix(out.ScriptPubKey.Hex, PlanariaBTemplateAlternate) { + return bt.NewTxFromString(tx.Hex) + } + } + return nil, nil +} diff --git a/engine/chainstate/filters/planaria-b_test.go b/engine/chainstate/filters/planaria-b_test.go new file mode 100644 index 000000000..139d8104a --- /dev/null +++ b/engine/chainstate/filters/planaria-b_test.go @@ -0,0 +1,59 @@ +package filters + +import ( + "testing" + + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" +) + +func TestPlanariaB(t *testing.T) { + type args struct { + tx *chainstate.TxInfo + } + tests := []struct { + name string + args args + passFilter bool + wantErr bool + }{ + { + name: "non-b transaction shouldn't pass filter", + args: args{ + tx: &chainstate.TxInfo{ + Hex: "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1c03d7c6082f7376706f6f6c2e636f6d2f3edff034600055b8467f0040ffffffff01247e814a000000001976a914492558fb8ca71a3591316d095afc0f20ef7d42f788ac00000000", + }, + }, + passFilter: false, + wantErr: false, + }, + { + name: "b transaction should pass filter", + args: args{ + tx: &chainstate.TxInfo{ + Hex: "010000000106e7beeddab7dc51e04b46f69bbaa9a4b845d3e4ff6160c5870474b6a9c6900b000000006b483045022100cd8e7764f402b806a07a4c5ca25a541ec9f2fb9f4f4336f1b96773fb9acb886602203948cc5285b82057e292f805aac2314c3970a0235d27c002fcf1a6880f6d63cd4121039aceb6fa37a9d16a1c263e4e29eddc357ee7a7886a1c7ff7b0402c18db9742d3ffffffff020000000000000000fed93a01006a2231394878696756345179427633744870515663554551797131707a5a56646f4175744ea53a0100ffd8ffe000104a46494600010100006000600000ffe1008c4578696600004d4d002a000000080005011200030000000100010000011a0005000000010000004a011b0005000000010000005201280003000000010002000087690004000000010000005a00000000000000600000000100000060000000010003a00100030000000100010000a002000400000001000003b7a0030004000000010000038a00000000ffed003850686f746f73686f7020332e30003842494d04040000000000003842494d0425000000000010d41d8cd98f00b204e9800998ecf8427effc0001108038a03b703012200021101031101ffc4001f0000010501010101010100000000000000000102030405060708090a0bffc400b5100002010303020403050504040000017d01020300041105122131410613516107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728292a3435363738393a434445464748494a535455565758595a636465666768696a737475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000102030405060708090a0bffc400b51100020102040403040705040400010277000102031104052131061241510761711322328108144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a35363738393a434445464748494a535455565758595a636465666768696a737475767778797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f9faffdb004300020202020202030202030403030304050404040405070505050505070807070707070708080808080808080a0a0a0a0a0a0b0b0b0b0b0d0d0d0d0d0d0d0d0d0dffdb004301020202030303060303060d0907090d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0dffdd0004003cffda000c03010002110311003f00fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd0fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd1fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd2fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28af2bf167c5df0bf861ded55cdeddaf063848201f76e82bc5cf788b2cc9b0cf179a578d287793dfc92ddbf257675e0b0188c5d4f65868393f23d528af8f752fda07c493c99d36d60b7419e1c17247e9cd64afc77f1c2b6736c7d8c7c7f3afc6b13f495e0da551c212a925dd434fc5a7f81f594fc3fcd651bbe55eaff00c933ed8a2be49d37f685d66228ba9d84338e373212871ec2bd87c33f17fc25e227581a536570dc08e7e013ecdd2beb7873c66e11ceaa2a385c5a8cded19a706fd1bd1bf24cf331fc279a6122e7529dd775afe5a9eab4522b060194820f208e8696bf523e6c2b2f54d6b4ad161f3f54ba8ad93d6460bfceb52bc1bc4df05ee3c4924d7d79af5d4d76e498fcd0a624cf4014007007a1af92e30cd33bc16139b21c1fb7aaefa3928c55bbdda6dbe897cdaebe9e5786c1d6ab6c6d5e48fa36d9b37df1c3c0968488a79ae71d7ca8cfe9bb6e6a0b4f8ebe07b9204af716e09c6648bff892d5f237893c33aaf857527d33568f648bcab0fbaebeaa6b9fafe31c7fd2378db098c9d1c4d3a70945d9c1c1ab3ecef2bfe27eb14780f28a9494e9ca4d3d9dff00e058fd1bd1fc61e1ad780fecad4219988cec0c030faa9e6ba5afcc48679ada412dbc8d1ba9c8653823f2af63f07fc69f116832a41abb1d46cfa10e7f78a3d9bfc6bf4be0ff00a5060b1338e1f88687b26fedc2ee3f38bf792f4723e7f34f0eead34e7819f3793d1fc9edf91f6c51585e1ff12691e27b14d43489d668d80247f121f461d8d6ed7f53e0f19431742389c34d4e12574d3ba6bba67e715694e94dd3a8acd6e98514579a78ff00e25e95e0644b7910dcdecca5921538c0f563d8570e7b9f60326c14f30cceaaa74a3bb7f82d356df448db0782af8baaa861e3cd27d0f4ba2be446fda175e32ee5b08027f7724feb5d3e91fb4359c8c1359d39a2c9e5e16dc00fa1afcbb03f480e09c4d5f65f5a70f3942497df676f9d8fa3adc119bd38f37b3bfa3573e94a2b8bf0ff00c40f0b789405d36f53cd233e549f238f6c1ebf857695face5b9a60f30a0b1381ab1a907d62d35f7a3e671186ab427eceb45c5f66ac14514577980514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451401fffd3fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28ac9d5b5dd234387cfd5aee2b64edbdb04fd075358e2313470f4dd6af2518addb6925eade85d3a73a925082bb7d8d6a2bc3f53f8f1e12b3731d9a4d7647465185cfe35cc3fed136a1f09a5b95f5dff00fd6afcd31de34705e16a3a7571f16ffbaa525f7c5347d051e12cdaa479a345fcecbf368fa5e8af02b3fda03c37332a5ddb4f0e4f2c30c07f5af51d03c71e18f128034abe8de43ff2c98ed7fc8d7bb91f88bc359c545472ec6c2737b46f693f44ecdfc91c78cc8730c2ae6af49a5ded75f7a3aca28a2bed0f20e1fe20d9f89f51f0fbe9de15611dcdcbaa49216da52239dd83ea7a7d2bc13fe19eb5a7b732cba9c46e0f3b769233fef75fd2beb3ae6f56f17f86b4304ea7a8410b0c8da5c16c8ed81cd7e5bc6fe1cf0c67188799f11cdb518f2ae6a8e108aeeb54aefab6d9f4993e7d98e161f56c02576eeed1bb7ea7c09e25f0c6afe14d45b4dd5e131c98dc8c39475f553deb9eafa2be2cfc44f0878b74a5d3ac22926b98240f0dc6dda17b30e79c11dbe95f3ad7f9f5e2064794e559cd4c2e498955e86f19277b5fecb7b36bbad1ab1fb76498bc4e27091a98ba7c93eabf5f98528241c83823bd2515f147ac7b8fc3af8c1a8f87648f4bd759aef4e242863cc90fb83dc7b57d7f61aae9fa9d9c57f653a4b04ca191c11820d7e67d6841aaea76d01b6b7ba9a388e7e4572179f615fd0de1d7d20f34e1ec2bc06650788a497b9795a51f2e6b3bc7c9edd1db43e1f3de08c363aa7b6a0fd9cbae974fe575a9fa357baee8da77fc7f5ec101f479154fe44d47a7788b43d5dcc7a6df4170ebd551c13f96735f9bb24b2ca732bb39f5624ff3a9ec6fef74cb94bcd3e77b79e339492362ac0fd457dac3e95d8a78a8b96022a95f55cedcade4ec95fe4792fc35a5ecddab3e6f4d3f3fd4fb2be39e87677fe107d55c2adc58bab23f72ac7057f1af8aabb2f1178fbc51e28b64b3d5ef0c9021076280aacc3b903a9ae36bf17f17b8d32ce28cf7fb4f2ca2e11e48a7cd64e4d5f56937d2cb7e87d670be5388cbb05f56c4493776d5b64bb7ea1451457e587d11d6f83fc61aaf83b558f50d3dc98f204d093f2489dc11ebe86bef8f0f6bb65e24d22df57b06cc73a838eeaddc1f706bf36abd13c23f137c47e0eb46b0d38c6f6ecfe66c917383df073c66bf7ff057c61ff55ab4f039aca52c2495d24aee12ee9767d57a3ef7f8ae2de16fed282ad8649555f2baf3f4e87df95cf6abe14f0e6b7751deeada7c1753c6bb55e450c40f4af9a63fda1f5a54c49a6c0cdebb88a9acbf686d4d6e47dbf4e89a1279d8c4301ed5fd2788f1ef8031b18d0c554728b6b49536d27d1b4d3d8f81a7c179dd16e74959aed24991fc61f86167a2c5ff00091f87a1f26db38b8817eea13fc4a3b0f6af9d2bf40e7d6b42f19f82af2f20951eda6b67f3031198db6f2187620d7e7f3001881d0138afe69f1fb85b2acbb34c3e6992b8fb0c545cd28db96ead771b6c9dd3f5b9fa070566389af869e1f177e7a6edaeff003f343a296581c490bb23af2194e08af78f007c69d4f47923d33c4aed796270a263ccb17e3fc43f5af03a2bf2be14e32cdf87718b199556707d57d992ed25b35fd2b1f479965586c75274b131bafc57a33f4d6c2fed353b48afac6559a0994323a9c820d5baf8b3e0ff00c42b9f0f6ab16837f216d36f1c2aee3c43237423d8f7afb4c104647435fe8ff867e21e178bf2958ea2b96a47dda91fe5979793dd3f96e8fc1788722a995e27d8c9de2f58beebfcd750a28ac9d475ed1b49e352bd86dcf5c48e01c7d3ad7df62313470f0756bcd462bab692fbd9e253a739cb960aefc8d6a2b90b7f1f7836ea43141ab5b330edbf1fceba5b5bdb3bd4f36ce78e74e9ba360c3f435cb82cdf018cd3095e13ff000c94bf26cd6b616bd2fe2c1c7d5345aa28a2bd139c28a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00ffd4fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a0f4a00f1df89ff001420f06c5fd99a6ed9b5499738eab0a9e8cdee7b0af8d756d6b54d72edef754b992e2573925db3f90ec2beac87e0b9d675ed435bf17dd19fed12b3451c248c2e78dc4fa0c702b3bc47fb3fd84b6ed2f872e9a29c64ac737287db2391f5afe2ff00137833c44e2d954cc254b970f16f928f3252e55a2938ece4f7d5dfa247eb3c3f9b6459628d052bd476e69db4bf6bf647ca1455ed4f4dbdd1efe7d33508cc5736ee52443d88fe60f6aa35fc8f5a8d4a35254aaa6a517669ee9add33f4d8c94a2a51774c2a68279ada559addda375390ca7041fa8a868a98ca516a5176686d26accfa23c07f1c2eb4c4361e2d325d40abfbb9d066504763d322b535cfda1a46cc7e1fd3f60ed25c1c9ff00be47f8d7cc7457eb585f1cb8cb0f96c72ca58bd23f69a4e76edcceef4e9d7ccf9aa9c21954f10f112a5abe9d3ee477bad7c4bf196bbb96eb5091236ce6388ec5c7a715c3492cb331795d9d8f24b1c9a8e8afce335cf731ccea7b6cc2bcaa4bbca4dfe6cf7b0d83a1878f250828af2560a28a2bca3a028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00b515ede4113c10cce91c9f7d15880df515568a2ae552724a32774b6f2f4128a5aa0a28a2a062ab152194e0839047635f7efc2ff00121f13f83acaf6539b8841b79f3dde2e33f88c1fc6be01af6cf855f13b4ff03d9dee9faa4134b15c48268cc582436369182475c0e6bf71f00f8df0fc3bc412fed0abecf0f560d49bbd935ac5e97f35f33e438d3279e3b02bd846f38bbaf4d9ff009fc8fb52bc93c59f07f40f13dc4da879d3db5e4b925c3965ddeea73c7d2b26cbe3e783ae66114f1dd5ba9fe274047e8c6bd9ac6fecf52b64bbb0992785c655d0e41afed858ee0fe38c3cb08aa53c4c63ab8df55d2f6d24bd4fc8fd8e6b93d45579654dbebd1f9763f3bfc55e18d4fc21ac4ba4ea230cbf32483eec887a30aa1a6ebdac68f2acda6ddcb6eca72363103f2e95f57fc7ed0e1bcf0cc3adaa813d8ccaa5bb98e4e08ffbeb047e35f1d57f03789dc295383b89aa60b0351c61a4e9b4da6a32d95d754d357f2b9fb570f666b34cbe35ab24ded25d2ebfcf73e9ef047c777f323d3bc5ea36b10a2ed074f7703b7b8afa72dee20bb812e6d645962914323a1cab03d0822bf316bdc7e137c4f93c33729a16b2e5b4c9df08e79f21dbbffb87bfa75afd97c21f1ff154f130c9f89ea73539691aaf78be8a6fac5ff33d5756d6df29c51c154e54de2b2e8da4b78ad9fa79f9753ecfa29a8eb2209108656008239041a757f6b269aba3f230a28a298051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451401ffd5fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a2b2356d7b47d0e1f3b55bb8ad97b6f6009fa0eb5e6f7bf1bbc0f69218d27927c7431a12a7f1af9ace78cb22ca5f2e658ba74df694927f76ff0081e861329c66295f0f4a525e4b4fbcf5fa2bc3e3f8f5e0e66c4897083d7666babd2be2a782756d8b16a0913b8fb92fc87f5e2bcbcbfc4ce15c6cfd961b1f4dcbb7325f9d8e9afc3d99515cd5284ade97fc8f45a2a28a78678c4b0baba1e4329c8af30f197c59f0df8551ede2905edf0c810c47214ffb4dd0735ef679c499664f8478eccabc69d3eedefe8b76fc95ce2c1e5f88c555f63878394bfadfb1e9d717105ac4d3dcc8b146832cee42803ea6bc67c51f1c3c33a36fb7d241d4ae4647c9f2c40fbb77fc057cc1e2bf1ff893c5f70cfa95cb2c193b2de3256251f4ee7dcd7155fc85c6df49ec5d694b0dc354b923b7b49abc9f9a8ecbe7cde88fd4328f0f29412a9984b99ff2adbe6f77f2b1b9e23d7af3c4dad5ceb97e144d72c0b0418501405007d00ac3a28afe54c662eb62abcf15889734e6dca4deedb776fe6cfd2695285382a70564b44bc90514515cc585145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140057a3f80be24eafe08b92b1ffa4d8c87f796ee703eaa7b1af38a2bd6c8f3dc7e4f8c866196d574eac766bf27d1a7d53d19cd8bc1d1c552743111e68be87bafc48f8bd0f8c7464d174eb37b68ddd5e66918313b790a30077e735e154515ddc57c5b99f11e3de659b4f9aa3496892492d924b431cb72cc3e028fd5f0d1b477ee1451457cd9de7d83f03bc6efabe9cde1ad424dd7364b98598f2f0fa7fc07f957bfd7e72f83f5e9bc37e24b1d5e124086551201fc51b70c3f235fa296f347730477111ca48a1d48ee08c8aff433e8f3c73533cc81e03172bd6c35a37eae0fe17f2b38fc91f8771ce4f1c1e37dbd2568d4d7d1f5ff003398f1578d345f07470cbacb48ab39210a216191d727a0aa3a2fc4af066bd2086cb518d653d125fdd93f4ddd6babd4f49d3b59b46b2d4e04b885c72ae322be45f8a1f0a0f8555b5dd04bbd86e1e6464e5a127b83dd7f957bde23f1271770eb966f9751a75f07149ca3692a915d5def66badeda75565738f21c06578fb616bca50aaf67a38bf2b77f9ebdcfb24104020e41e41a5af87bc09f1775cf0ac8967a8335fe9d900c6e732463d518ff0023c57d85e1df12e91e28d3d751d22612a1c6e5fe243e8c3b1af4fc3cf16325e2da5cb84972574bdea72f8979afe65e6be691cd9ef0ce2f2c95ea2bc3a496df3eccdfa28a2bf4f3e7428a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00fffd6fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800af9dfe247c665d1e79744f0b9496e53e596e7ef2c6c3a85ec48fcaba3f8cfe339bc33e1f5d3f4f7d979a96e8c30ea9181f311ee73815f1498a729e7b239427ef90719fad7f2b78f3e30e2f29acf87723972d6b27526b78a7b463d9b5ab7d1356d76fd2782f85696261f5fc62bc7ecaefe6fcbc8b9a9eada96b172d79a9dc49712b9c967627f2f4acea28afe21af88ab5ea3ab5a4e527ab6dddbf56cfd76108c22a31564828a28ac8a3a2b0f16788f4cb3974fb1d42786de65dae8ae718f6f4fc2b9f666762ce4927924f24d368aebc463f135e10a55ea4a5186914db692f24f6f919c28c20dca3149bdfcfd428a28ae4340a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800afa3bc35f1e7fb2f4eb3d2eff004f3225b46b1b4aaff310bc6707dabe71a2beb78478df38e19c44b1393d5e4949252d134d277b34d3ff0033cdccf28c2e61054f151ba5b6ebf23f453c2de34d03c5f6de7e91701a45199216e244faafa7bd6ceb3a6c3ac69575a65c2868ee2268c83ee2bf39747d6351d0afe2d4b4c99a19e239054e323d0fa835f57e89f1ebc3d3e9064d6524b7bf89398d54b2c8c3fba474c9f5e95fd93e1ff8fd93e7b83a981e2671a35795a6de909ab6b6becedbc5bd7a765f95677c158ac1d58d6cbaf38dfe69ff009799f245fdab58decf66fd6091a3cfaed38cfe35d67813c6b7fe0bd663bdb762d6d210b730e7e574ff0011d8d72ba95e1d4350b8be231e7cacf8f40c720552afe21c166b5b2ccc963f2b9b8ca12bc1adf47a7deb75d763f5ead86862283a388574d59a3f4cf4dd42d356b18351b1904b05c207461dc1abb5f2a7c05f1a3c5752783ef9c98e60d2da127eebaf2c9f42391f435f55d7fa77e1d71ad0e29c8e966b4b493d271fe59add7a755e4d1fcf19f6513cb7192c34b6dd3ee9ec145782ea1f1966f0df8a2eb42f1369a62b78e52239e12493113f2b107af1d715ed1a56afa66b76897fa55c25cc0e321d0e7afa8ea0fb1e6bb787f8df26ce6bd5c2606b5ead26d4a0d38cd34ecfdd7676bf5574658ec9f1784846ad687bb2d535aa7f3469514515f587981451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007fffd7fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803cb3c6df0d21f1aebda6ea5797452d6cd4ac9005c990139e1b3c67a1e2bb7ff00846b40fb1fd83ec10790176ecf2c6318c7a7a56e579f78f3e21e93e07b2dd3fefef655260b653cb1f56f45f7af89cc32ee1cc896333ec74230f69675272d6f64a296b7eda456efa367b143118fc6ba582a2dbe5d229696ebfd367c81f133c2f0784fc5773a75a716ce04d08feeabf6fc0d79fd6ef88fc47aa78a7549356d5a4df349c0006151474551e82b0abfcc7e28c565f89cdf1388caa0e3425393827d22de8add3d3a1fd0997d3ad4f0d4e188779a4aefcc28a28af04ec0a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a28034748d4ae347d4ed754b562b2db4ab2291fec9e9f88afd1dd1f518757d2ed7538082973124831fed0cd7e68d7ab7833e2df88fc2bf64d3e475b9d2e03b5a0651b821393b5bae4678e715fbff813e29e13853155f0b9ab97b0abcbaa57e59276bb575a59eb6bbd1687c5718f0e55cca9c2a61adcf1befd576fbf63e8af8c1e0a87c49e1f9752b68c7dbac14c8840e5907de5fcabe4ef08f8d75af06dfadd69d213167f7b6ec4f96e3be4763ef5f7d691ac699e23d2e2d434f9567b6b94edcf51cab0ec47422bf3cbc4d69058f88b53b3b63ba282ea6443eaaac40afbbfa43e02196e6181e31c8eaf24eae8e507bb4938c95b7bad1f4692b9e370357788a15b2bc646ea3d1f4beebe4cfbf7c25e2bd33c61a447aae9add7896327e68dfba9fe87bd74f5f9f3e00f1b5ef8275b8ef6225ed25212ea1cf0f19ee3d197a835f7e58dedb6a3670df59b89219d03a30e855b915fb9783de27d2e2ecb1fb7b47154acaa2efda6bc9f55d1e9b58f8ee29e1d965788bc35a72d9fe8fd3f12d514515fb01f2c14514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007fffd0fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2802a5dea1636010dedc47079876a798c1771f419eb56810c0329c83c822be46fda1a7baff0084874d84b110ada9741db717393f5e056b7c1ff8a6eaf0f853c452ee5384b4b873c8f48d8fa7a1fc2bf10a5e3665f4f8c2bf0ae614fd9a8b518546f472b2d24ada5efeebbbf3b5cfb097085696550cca84b99b5771ecbcbbf99f52514515fb79f1e1451450014515e5bf12fe23d9f826c7ecf6e565d52e149862ebb074dedede9eb5e2f1071060324c054ccb32a8a14a0b57f924bab7d11d781c0d6c6568e1f0eaf27fd7dc1f123e2558f826ccdbdbed9f54957f750e72133d19fdbdbbd7c47ab6afa8ebb7f2ea7aaced717131cb3b7f203a003b01c545a8ea179aadecba86a12b4f713b1777739249aa55fe70f8a1e29e61c5f8d6e6dc30d17ee53be9fe297793fc365e7fbd70f70e50cae8d96b51ef2fd17641451457e567d1851451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451401b5a5788f5dd0d644d26fa6b559410eb1b90a73edd33efd6b1ddda47691c966624927a9269b4574d5c6e22a528d0a936e11bd936da57decb657f22234a11939c52bbddf70afaa3e03f8dd6585fc1da8c9f3c7992ccb1eabfc49f51d47e35f2bd5dd3b50bad2afa0d46c9cc73dbb891187622bec3c3be35c470b6794b34a3ac56938ff341eebd7aaf348f2f3cca69e6383961a7bee9f67d3faec7e99d15c67813c5f67e33d062d4e060265fdddc459e524039e3d0f506bb3aff0050f2acd30b9960e9e3f053e6a7512716baa7fd6aba33f9d71586a987ab2a1555a517661451457a060145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007ffd1fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803e77fda0f4092ef46b2f1040a58d8ca629703a472f427d830c7e35f24ab32307425594e411d4115fa5babe976bad69973a55e28686ea368d81f7eff00857e7878a3c3d79e17d6ee747bd521a163b1bb3a1fbac3ea2bf853e931c1757079bc388a847f77592527da71565ff81452b79a67ec9e1f66d1ab857819bf7a1aaf34ff00c9fe67d5bf08fe25c5e23b48f40d5df6ea56e8023b1ff5e83b8ff687715ee75f993657b75a75d457b6523433c2c191d4e0822becdf869f162cbc57147a4eb0cb6faaa0c0ec93e3bafa37a8fcabf47f03fc6ba59952a79067b52d885a426f69ae89bfe75ff937aefe0f187094a84a58dc1c6f07ab4ba79af2fcbd0f69a28a2bfa90fce4c3f11deea7a768b7579a3dafdb6f234262841c6e3fd71d71debf3c75cd4b53d5b56b9d4359676bc95c997cc0430238db83d001c01dabf4aabccbc6bf0b3c3de310d72ebf64bfc717310e49edbc746fe75f81f8e3e1966fc5786a5532bafad2bbf64f48c9f74ff9ada7bda5baad6ff6bc1fc4385cb6a4a38887c5f696ebcbd3d3f1e9f06515e85e2bf867e29f09c8c6eadcdc5a83f2dc400b211ee3aa9fad79e904706bf81338c8f30ca712f099951953a8ba495beeeebcd687ed785c5d0c4d35568494a3dd0514515e51d01451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007a57c2df193f843c4b1492b1fb15e620b85cf0013c3fd54fe99afbd5595d43a9c861907d41afcc0afbff00e176b6faf781b4cbc95b74b1c66de43df7424a67f1001fc6bfb27e8b9c61567f58e1baf2ba8af690f2d529afbda7f79f95788b95c57263e0b57eebfd3f26bee3d068a28afec33f2c0a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803fffd2fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800ae23e23c9a8c3e08d5e6d2a478ae63b72eae870c02b02d83fee835dbd41756f1dddb4b6b30dd1ca8c8c0f70c306bccceb032c6e5f5f070972ba9094535ba724d5fe573a3095951af0aad5d45a76f467c97f0f3e35dee9b22695e2c76b9b462025d1e658b3d9bfbcbefd47bf6faced6eadafade3bbb4916586550c8ea720835f9d3e2cd065f0d7886f746941ff47948427f8a33ca9fc4577bf0cbe285e783ee574ed459a7d2a56f990f26127f897dbd457f1c7859e36e3723c6be1ce2c9b74e32705396b2a6d3b5a4f771bf5de3e9b7eadc47c2147194bebf962b49abd96d25e5d9fe67dc34553b0bfb3d4ece2bfb095668265dc8e87208ab95fdad4aac2a4154a6d38bd535aa69f547e4728b8b7192b3415e53f14fe1ec5e33d2bed164a1754b404c2dd3cc5ee8df5ede86bd5a8af2788787f059de5f572ccc21cd4e6acfcbb35d9a7aa7dceac063ab60ebc71141da4bfab1f98f756b71657325a5dc6d14d0b14746182ac3a8351c52c90c8b342c51d08656538208ee0d7da7f14be16dbf8b2ddb57d21162d5a15edc0b851fc2dfed0ec7f03edf18ddd9dd585cc9677b1343344db5d1c61811ec6bfcd7f127c36cc783f31f635af2a3277a7516cd767da4baaf9ad0fdf720cfe8669439e1a497c51edff00fa77e1cfc6b8da38f46f17c9b5d4048aef1c374003e3bff00b5f9d7d2d14b14f1acd0b074700ab29c820d7e61d7ab7817e2beb9e0f65b49c9bdd3fbc3213b93dd0f6fa1e2bf66f0b3e9135706a195f14373a6b48d5de51ff1ff0032f35ef2eb73e5388f81635af89cb95a5d63d1fa76f4dbd0fbaa8ae43c2be38f0f78bedc49a55ca9980cbc0e76ca9ff01ee3dc575f5fd9d96e6784cc30f1c5e06a2a94e5b4a2ee9fdc7e4f88c3d5a151d2ad17192e8c6491c72a18e450ca78208c835e5be28f841e13f12069a387ec17241c4b00c0c9eecbd0d7aad15c79e70e6579ce1de1734a11ab0ed257b7a3dd3f34d336c1e3f11849fb4c34dc5f97f5a9f13f883e0778b74967934e09a840b920a1daf8ff0074ff004af26bfd2f51d2e530ea36d2db3838c48a579fa9e0d7e98d67dee95a6ea51986feda29d181043a86ebf5afe73e25fa2e653886ea64b889517fcb2f7e3f7e925f36cfbbcbfc45c4c2d1c5d3525dd68ff55f91f99f457dc7acfc13f036aa5a486ddec246e8d6cdb547fc00e57f4af31d53f676bc4dcda3ea8920ec93a6d3ff007d2939fc857e199dfd1df8cb00dba34635a2bac24bf2972bfb933ec707c7395565efc9c1f9afd55d1f35515ead7ff05fc7b644ecb25b951d0c32039fc0e2b91bbf04f8bac73f6ad22ed40ea444cc3f35cd7e6998704710e06ff5bc1558dbbc256fbed63dfa19be0ab7f0ab45fcd1cbd153cd6b736edb2789e36f4652a7f5a83a57cd4e9ca0f966accf4134d5d0514515030a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800afb37f67e98c9e0a9e3ff009e57f2afe688dfd6be32afaf7f67766ff846b525ec2fb3f898d6bf7bfa375570e34a715f6a9d45f85ff43e2f8f637ca64fb38fe67d07451457fa227e16145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007fffd3fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803e7df8e7e083aae9cbe28b08f37362bb670a39787ae7ea9fc8d7c835fa7b2469346d14aa191c15653c820f515f0dfc56f87f3f83f5837b6685b4bbd62d0b0e9139e4c6dfcc7a8fa57f167d243c329d2acf8af2e87b92b2aa9747b29fa3d14bcecfab3f5ae02e205287f66d77aaf87cd76f974f2f421f86ff132fbc1175f65b80d71a5ccd9961cf319eee9efea3bd7dafa26b7a678874f8b53d26759e0947054f20f70475047a1afcd5aeb3c27e32d6bc1d7c2ef4b94ec623cc85b9471ee3d7debe2bc24f1cb15c35cb95e6b7a983e9fcd4ffc3de3de3f7767ebf1370752cc2f88c37bb57f097af9f9fde7e8a515e79e07f88fa1f8d6dc2dbb882f90664b67386faaff007866bd0ebfbd326cef039b61218ecbaaaa94a5b34ff0f26baa7aa3f17c5e0eb61aaba35e2e325d18579b78f3e1a68de3687ce702db504188ee50727d9c771fcabd268a9cef22c0671839e0332a4aa5296e9fe6baa6ba35aa1e0f1b5f0b5557c3cb9648fcecf15f82b5ef07dd9b7d5a02232488e7504c4ff43d8fb1ae4abf4d6f6c2cf52b77b4be85278641864750c08fa1af9bfc65f012393ccbef08ca236e5becb29f94fb2b751f8d7f157885f46ecc72f72c670e375a96fc8fe38fa749fe0fc99fad647c7d42bda963d724bbfd97fe5f91f31da5e5d585c25d594cf04d19cac91b15607d88afa03c25f1ef51b211d9f8a61fb64638fb44600940ff0068746fc315e13aa68daae8972d67ab5acb6b329c1591719f707a11ee0d6657e23c39c63c41c298b93cbeaca9493f7a0d68fca507a7e1747d7e3f2ac0e65492af1525d1f5f9347e8bf87fc67e1cf13c424d22f6395b1968c9db22fd54f22ba9afcc48679ade412c0ed1b8e8ca7047e22bd5fc3bf19bc5da18586e6517f0ae3e59bef60760dd6bfa97847e94783aca34388b0ee9cbf9e1ac7e717aaf9391f9ce67e1d558de7819dd76968fefd9fe07dc945784685f1efc317db62d62296c243c17c7991fd78e47e55eb7a5f89bc3fad20934ad42dee4119c248370faa9e47e22bfa2320e3ae1fcee29e578b84df64ed2ff00c05da5f81f0b8ec971d8376c4526bceda7deb43728a28afac3cc0a28a2802bcd696970a527863914f50ca08fd6b9abbf01f83afb3f69d1ed093d4ac4aa7f35c1aeb68ae0c665582c5ae5c5518cd7f7a29fe699bd2c556a4ef4a6d7a368f26bcf82be02ba1f259bdb93de29187f3cd7257dfb3de852926c2fa787d0300d5f43515f1598784dc1f8dbfb6cbe9abff2c797ff0049b1ebd0e27cd68fc15e5f377fcee7c87a8fecf9afc3ce9d7b04fd787050ff005af3fd4fe15f8e74bcb4ba6bca833f34243f03bf15f7e515f9de6ff467e13c526f08ea517e52e65f74937f89eee17c41cca9e955464bd2cff0ff0023f322e6ceeecdf65dc1242de92295fe7d6ab57e98de695a6dfc4d0dedb453230c10ea0f15e6dad7c19f04eadbde2b53652b73badced1ff7cf4c7e15f91e7bf458cde8273cab170aabb49383fce4bf147d3e0fc46c2cf4c4d371f4d57e8cf85e8afa175efd9f75bb5579b40bc8ef14722297f76e7e87ee93f97d6bc5358f0e6bba04c60d62c66b56071975f94fd18654fe06bf09e25f0fb887207ff0ab849423fcd6bc7ff02578fe373ec7019de071abfd9aaa6fb6cfee7a98b451457c69ea851451400514514005145140051451400514a013c0e6a4104edd2373f45355184a5b213696e45455d5d37517fb96b337d2363fd291f4ed423fbf6d32fd5187f4adfea588b737b376f464fb486d729d15298665fbd1b0fa835156128496e8a4efb0514515230a28a2800a28a2800a28a2800a28a2800a28a2800a2a686dee2e5c476f1bcae780a8a589fc0574717823c613c7e6c7a35e95eb93038fc811cd77e0f2ac6e2d3785a329dbf962dfe4998d4c452a7fc4925eaec72d455dbcd3751d39fcbd42d66b66feecb1b21ffc780aa55c95a8d4a5374eac5a6ba35666919292bc5dd051451599414514500145145001451450015f707c0ed31b4ff015bcee30d7d3cb71ff0001cec5fd12be2bd3eca6d4afedf4fb71992e654897eac715fa47a469f0e93a5da69900c476b0a42a3d9140afea7fa2ce412ad9c62b3892f76943917f8a6eff00828bfbcfce7c46c6a86169e156f277f92ff82ff03468a2bc3f55f88cfa5fc53b5f0f7981ac2744b7987f7267fba47e3807eb5fd87c47c4f81c929d1ab8f95a352a469aff0014b45f25d7b23f2ccbf2ead8c94e3456b18b97c91ee1451457d11c01451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007ffd4fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800ac8d7743d3bc45a64da4ea71892099707d41ec41ec456bd1586270d4b1146587af15284934d3d534f74cba75254e6a70766b667e7af8ebc11a8f827576b2b9fde5b484b5bce070e9efe8c3b8ae26bf47bc4de18d27c57a649a66ad107461f238fbd1b76653d88af85bc6de07d5bc15a935a5ea992ddc9f22e00f9645fe8dea2bfcf4f18fc1bc4f0c622598e5d172c149e9d5d36feccbcbf965f27aeffb970af1553cc69aa15ddaaaff00c9bcd7ea8e46daeae2ce74b9b491a19632195d0956047a115f46f81be3b4d6c23d37c5e8658800ab7918f9c7fbebdc7b8e7d8f6f9ae8afcd783f8f33ae19c4fd6729ace37de2f58cbfc51dbe7a35d19efe699361330a7ecf130bf67d57a33f4bb4cd5f4dd66d96f34cb88ee627e8d1b035a55f9bba1789f5cf0ddc0b9d1eee480e72541ca37d57a1afa3bc2df1fad26096de28b730bf00cf0f287dcaf51fad7f687037d23b22cd631c3e73fecd5bbbd69bf497d9ff00b7b4f33f27ce380b19866e784fde47ff0026fbbafcbee3e94a2b0b47f13683afc4b2e937b0dc6e19daac370faaf5addafe83c2632862a92af869a9c1ece2d34fe6b43e1ead29d3972544d3ecf432f54d174ad6edcdaeab6b15cc67b48a0e3e87a8af14f107c01d02f8bcda1dcc96121e446c3cc8f3fcc67eb5eff457ce712f02e439fc39736c2c6a3ef6b497a495a4bef3d0cbf39c6e09df0d51af2e9f73d0f84f5df83be35d14965b65bc887f1dbb6efd0e0d79b5d585f58b98ef2de48187512295fe75fa6bd7ad65dfe89a46a8bb350b386e0641f9d01e95fcfbc43f458cb6b375325c5ca9bfe59ae65f7ab35f89f6f81f11ebc6cb17494bcd68feed57e47e69d4914d2c2e2485d918742a4823f2afbc750f847e04d401dda6a44cc725a2250fe8457177dfb3e7866724d95ddd5b7a0dc1c67df7026bf2acc7e8d3c5f857cf84953ab6fe59b8bffc992fccfa4a1c7f95d456a9cd1f557fcae7cefa57c49f1ae8e40b4d526651fc129f317ff1ecd7a169dfb4178a2db6aea1696b76a3ef101a373f88247e95af77fb3aea0a59acf578d973f2ac911ce3dd837f4ae6ae7e02f8d2104c4f6b30edb5d81fc8a8fe75186c93c5dc8972e1d57515d149545f75e4bf02aa63386319acdc2efbae57f7d91e8d61fb44e8f20ff899e957109ffa62eb28fd76576169f1bbc037380f752c0c7b49137f3008fd6be64baf84be3eb452cfa6338033fbb75727f006b99bbf09789ec41377a5dd4607ac4c7f966bd7878cde26e56ad98619cadff3f28b8fe31e5395f09f0f627f813b7f8669fe773eeab4f889e0bbd216df56b62c7f84b807f235d15beb3a4dd1c5b5e4321ebf2b83d7f1afcd69619adced9e378cfa3a953fad2c5737101cc32ba67fbac47f2af5b07f4aacce9be5c76020ff00c32947f06a472d5f0df0ef5a359af549ff0091fa6eb2230cab03f434fafce1b4f16f89ac405b4d4eea303b091b1fcebaab1f8bbe3cb160c3513301c0595430fe95f6997fd2a724a9658cc1d487a38cbff913c9afe1be2d7f0aac5fadd7f99f7a515f23e95fb426b70ed5d5ac61b851f79a32518fe1c8af48d27e3cf846f76adfa4d64c7a965dca3f11cfe95fa564be39705e6568c318a9c9f4a89c3f16b97f13e7f17c1d9b61f574b997f75a7f86ff0081ee14573ba578b7c37ad0074dd42098900ed0e0373ec79ae8bad7ea383c761f174d56c2d4538beb169afbd1f3b568d4a52e5a9169f9ab0555bbb2b3bf85adef618e78db82b228607f3ab5456f529c671709aba7d199c64e2ee9ea789f897e07785f580f3e95bb4db83923cbf9a327dd4ff435f35f8a7e1a78a7c28ecd796c67b604e2e20cba11cf5ee3f1afd00a6491c72a18e550eadc10c320d7e25c6be01f0d67b1955c343eaf5bf9a0bdd6ff00bd0d9fcacfccfafca38db30c1b51a8fda43b3dfe4f7fbee7e61515f6d78d3e0c787bc451bdce92a34dbee4878d7f76e7d1978ebea08c57c97e26f08eb9e12bd6b3d62029c9092af31c83d55b1fa75afe32e3ef0973ee149f3e321cf43a548eb1f9f58bf5f9367eaf92f1360b3256a52b4ff95eff002ee73345751e1ef06788fc51284d1ece4953383291b631f563c57d13e17f801a7db2adc78a2e4dd4bd7c8872b10f627ef37e83dab8f843c2ce24e249279761daa6fedcbdd87def7ffb75366b9a711e0300ad5e7ef765abfbba7cec7cab6f6b73772086d62799c9c05452c73f857a3e87f08bc6daded71682d22600efb83b783df1d6bed3d27c2fe1fd0d02697630418eea833f9f5adfe9d2bfa5386be8b182a56a99ee29cdff002d35cabff02776fee47c063fc47ab2f7707492f396afee5fe6cf99748fd9de0015f5bd4d98e394b750307fde6cff002af45d3fe0cf80ec3696b2372c3a999cb67f0e95eab457edb93f843c1f96a5f57c041b5d66b9dffe4cd9f238ae29cd711f1d66bd34fcac73369e0cf0ad8e3ecba55aa63a7ee94ff306b6a2d3ac21ff00536d1263fba8055ca2beeb0d95e0f0eb970f46315e514bf2478f53135aa3bce6dfab633cb8ff00babf90a3ca8ffb8bf90a7d15d9c91ec637651974cd367e27b5864cff007914ff003158577e06f085f67ed3a4dab67d230bfcb15d5d15c38aca303895cb89a3192f38a7f9a37a78aaf4dde9cdaf46cf22d4be09781afb7186de4b463d0c2e703f03915e79ab7ecedc33e89aa64ff0a5c27f3653fd2bea0a2be0b38f07383b324fdbe02116fac2f07ff92b5f8a3d9c2f15e6b87f82b36bcf5fccf8275bf84be36d103492597da625c9df01de303be3ad79e5c5b5c5ac862b989e2707055d4a9cfe35fa72403c1ae7f55f0af87b5b52ba9d8413e73cb20cf3ef5f8af127d15f0952f5323c5b83fe5a8b997fe04acd7dccfadc0788f517bb8ca49f9c74fc1ff99f9bf457d63e29f801a7dc86b9f0bdc35acbd7c897e789be87ef2fea3dabe72f107847c41e189cc3ac59bc2338593198dbe8c38afe6be31f0bb88f86a4de65877ecff9e3ef43ef5b7a4accfbfcab88b01982ff00679fbdd9e8feeff239ba28a2bf3e3db0a28a2800af7df871f06a5f10dbc7ad78899edecdf06285461e55f527b29eddcd733f08fc18be2cf11096f13758d8e25981e8cdfc2bf8d7dce8891a2c7180aaa0000700015fd4be02f83d86cea9bcff003b873504ed083da4d6f27de29e897577be8b5fceb8d38a6a60dfd4b06ed3eafb2ecbcff230f46f0c683e1f8160d26ca28028c6e551b8fd5ba9adec0a28afedec260e861692a386828416c92492f923f20ab567524e751b6df56676a5a4699abdb9b5d4eda2b989baac8a1bff00d55f2cfc4ef83a9a2dbcbaff00864335aa0dd35b1f98c63fbca7b8f51dabeb7a8e58a39a3686550e8e0ab2919041ea2be278f7c39ca38ab033c3e369a556deed44bde8be9af55dd3d1faea7af92e7d8acb6b29d297bbd63d1ffc1f33f30e8af4bf8a9e105f09789e58ad576d9dd0f3a0f4507aafe06bcd2bfcc9cff24c564f98d6cb318ad529c9c5fcbaaf26b55e47f4260b174f1542388a4fdd92ba0a28a2bc83a428a28a0028a28a00f59f82da40d53c756aee329648f707eaa30bfa9afb9ebe55fd9cec375e6b1a991f7238a01ff03258ff00e822beaaaff443e8dd94c709c1d0c45b5ad39cbe49f22ffd24fc338fb12ea66ae9f48a4bf5fd4ad7b751d95a4d792f090a339edc28cd7e765eeb536a3e297d7198ef92ec4c0fa00f91fa57db5f152f9ac3c05ab4ca769687cb0475064217f99af808120e457e53f4a4e21ab1ccb0395c1d9422eabf572b2fb945fde7d2f873818fd5eb621eedf2fc92bbfccfd3a82412c11caa721d5581fa8a96b82f869af27887c1da7dd86dd245188251dc3c7c73f860d77b5fd7d926694732cbe863e83bc6a46325f3573f2ec661a587af3a13de2dafb828a28af50e60a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd5fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800ac8d7342d2fc45a7c9a66af02cf0483183d54f62a7a823b115af456189c351c452950c4454a125669aba69f469974ea4a9c94e0ecd6ccf86be20fc29d57c1f2497d641eef4ccf1201978c1e81c0fe7d2bc92bf4f26862b889a19d03c6e0ab2b0c820f50457ce3e3af81705e34da9f84d8432b1dc6d1b88c9efb4ff000fd3a57f1778a5f475af879cb33e158f353ddd2fb51ff05fe25fdddd74bf4fd678738ee1512c3e64ed2e92e8fd7b7aede87ca3455fd4b4bd4747bc7b0d52de4b5b88fef4722ed38f51ea0f62383542bf942b51a946a3a5562e325a34d59a7d9a3f4a8ca324a51774c9edeeae6d6412db4af138e8c8c54fe62bd3b43f8cbe39d176c6f762fe15fe0ba5de71feff000ffad795515ece49c519be4f53dae57899d27fdd9349faad9fcce5c5e5f86c54797134d4979a3eb5d17f685d2270a9ae584b6cfdde13e627e4706bd334cf89fe07d554791aa431b1eab31f2c8faeec0fd6bf3fa8afdab24fa4bf15e0d2863142baef28f2bfbe2d2fc0f92c5f87f965577a5787a3bafc6ff99fa676fa8d85da092d6e22950f46470c0fe20d5c041e8735f98f05d5cdb3892de5789d7a3231523f2ae9ecbc7be32b0e6db57bae3a6f90be3fefacd7e9596fd2bb0b2b2c7e5f25e709a7f834bf33c0afe1ad45fc1ae9faab7e4d9fa25457c2d69f1a7c7b6a30d7693fbc91839fcb15d1db7ed01e2988013dadb4c475e0aff2afb4c1fd267842b2fdeaab0f5827ff00a4c99e4d5f0f7348fc2e2fe7fe691f63515f2841fb44ea23fe3e74b89bfdd723f98ad9b5fda26d19bfd2f4b741fec3ee3fc857d161bc7de07acedf5ce5f584d7feda7054e09ce23ff2eafe8d7f99f4b534aa9ea01af0bb5f8fde1399b13c17108f5201fe55d15a7c65f015d9c7db5a1f7910a8afa5c1f8a5c218ad296634be7251ff00d2ac7055e1bcd29fc5425f257fc8f44b8d274bbb52b736904a0f5df1a9fe95c9dffc32f026a24b5c68d6ea4f53103113f8a106afd978f3c1f7ff00f1ebab5ab7d640bfcf15d25bded9ddaefb59e3997d6370c3f435ec4b0bc3b9cc6ce346baf484ff00cce55531f847bce1f7a3c5effe017832e726ce4bab43d82c9bd47fdf609fd6b87d47f676bb4c9d2f55490761326dfd4135f54d15f219a7825c158ebb9e06317de0dc3f08b4bf03d3c3f17e6f476acdfad9fe7a9f0b6a7f067c73a76592d16e946798581e07b1c579d5fe8daae94e63d46d26b723fe7a215fd4d7e97553bbd3ec6fe230dec11ce87aac8a187eb5f96e7bf459ca2b272ca7153a72e8a494d7e1caff0033e8f07e23e262ed89a4a4bcb47faa3f33e39658983c4ec8c3a153835dde83f13fc6be1e2ab69a8bcb0aff00cb1b8fdea63d3e6e47e0457d43af7c14f076af992d626b094f3ba0381ff7c9e2bc5bc43f017c49a7069b45953508c7f07fab93f0cf07f3afc6f30f07fc40e14a8f1795b9492fb5464eff0038e92f959a3eae8714e49994553c45937d2697e7b7e2773e1cfda0ac2e0a41e25b336cc783341f327d769e47eb5eeda3788b45f105b8b9d1eee2b94c64ec6c95cfa8ea3f1afce6bfd3aff4ab96b3d4ade4b6993aa4aa54fd79ea3dfa5161a8dfe9772979a75c496d321cabc6c548fcabd9e17fa49f10e575161b3ea4abc568eeb92a2fc2cdf938dfcce4cc780703888fb4c1cb91bf9c7faf99fa67457c91e15f8f7a95988ed7c4b00bb8c70678fe5931c751d0d7d2fe1ef14689e28b4179a3dca4cbfc4a0fcea7fda5ea2bfac782fc50e1ee288db2dadfbceb097bb35f2ebeb16d1f9a66dc398ecb9debc3ddeeb55ff03e763a0aced4b49d3358b7fb2ea96d15d424e764aa18647d6b468afbdaf429d6a6e9568a945ee9aba7ea99e242728494a0ecd15edad6dace2582d22486341855450a001ec2ac51455c211845460ac909c9b77614514550828a28a0028a28a0028a28a0028a28a0028a28a0028a28a002a9dee9f65a95bbdadfc11dc42e30c92286047e35728a8ab4a1520e9d449a7ba7aa65464e2f9a2eccf9b3c65f016d27f32fbc2527d9dfa9b5909287fdd63c8fa1af99355d2352d12edec754b77b7990e0ab8c67dc1ee2bf4bab96f14783b42f17599b5d5edc391f7251c3a1f507fa57f35f88df474cb3358cb19c3f6a15f7e5ff009772f97d87e9a79753eff21e3bc461daa58ef7e1dfed2ff3fcfccfce8a2bd63c7bf09f5af0796beb606f74dc9fdf20f9a3ff007d7ae3dc71eb8af2735fc4bc41c3998e498c960334a4e9d45d1f55dd3d9af34ec7eb982c7d0c5d255b0d2528b3edff00821a22697e0986f19712ea2ed3b1efb73b53f415ec35cd783add6d7c2ba4dba0c04b4880ff00be4574b5fea4f04e554f2dc830781a4aca14e0be764dbf9bbb3f9d338c4cb118eab5a5d64ff3d02b805f893e187f148f092ccc6ec929bc0fddf99d766ef5febc575fab5d7d874bbbbcff009e30c8ff00f7ca935f9cf67a9cf1ebb0eaecc4ca2e9672d9e4b6fdc6bf32f18fc54c57096270187c2453f6b2bcefafb89a4d2ecddf7e963e8385786e9e670ad3aadfbaacaddddff23f4a28a86da513dbc538e9222b0fc466a6afdce13538a92d99f1cd59d99f3f7ed05a48b8f0f5a6aca3e6b59f631ff6641fe22be40afbefe2bd98bdf00eacb8c98a212afd5181fe59af812bf803e93795470dc570c5417f1a9c5bf54dc7f248fdb7c3ec4ba9963a6fecc9afbecff561451457f3a1f7414514500145145007d83fb3cdb84f0b5f5c63992f5949ff007117fc6bdfebc2bf67d607c1772bdc6a129fcd23af75aff503c1b8461c179728ff0027e6db7f89fcf1c5726f36aedf7fd0f26f8d9bbfe15fdf63a6f873ff007f16be18afbf3e2b5a7db3c03ab4639d90f998f5f2c861fa8af80ebf94fe947879438a68557b4a8c6df29ccfd27c3a9a796ce3da6ff247b37c1bf1d7fc22fad9d2afdf1a7ea2caac4f48e5e8adf43d0fe1e95f6c8208c8e41afcc0071c8afad7e0efc503aa2c5e15d7e4ff004a8d42dacec7fd6a8fe06ff680e9ebf5afa7fa3b78ab4f0c970b66b3b45bfdcc9ec9bde0fb5deb1f3baea8f3b8eb86e552f996196abe25fafcba9f45d14515fda27e4c14514500145145001451450014514500145145001451450014514500145145001451450014514500145145007fffd6fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28ae73c49e2bd13c296d15d6b53f949348b120032c4b1eb8f41d49ec2b931d8fc360a84b158b9a8538eae4dd925e6d9ad1a352b4d53a516e4f648e8e8aa761a8596a76c979a7cc93c2e01574391cd5cade95585582a94da717aa6b54d7932251716e325668e73c45e13d07c556bf64d66d526033b1f187427bab7515f33f8b3e026ad61beebc3130be8473e44842ca07b1e8dfa1afaee8afcff008d3c2ce1de278b966346d57a548fbb35f3ebe924d1ede53c498ecb9da84fddfe57aaff0081f2b1f99b7fa6ea1a5ced6da8dbc96d2a9c14914a91f9d51afd26d5fc3da2ebb098355b48ae14f775048fa1eb5e19e23fd9fb4ebadd3f876edad5c9e2297e68ff003ea2bf93f8bbe8cd9e6079ab64b516221db48cfee7eebf934fc8fd2f2cf107055ad1c5a707df75fe6beef99f26515e87affc2df19f878b3dcd8b5c423fe5adb7ef17f1180c3f2c579f3a3c6c52452ac3a861823f035fcf99b64798e5759d0cc684a9cbb4a2d7e7b9f7186c650c4479e84d49793b8da28a2bca3a028a28a0028a28a0028a28a00326adc37f7b6ec1a09e48c8e9b588feb5528aba756707cd06d3f2138a6acced34ff00889e33d348fb36ab7181c00edbc63e8d9af46d23e3f789ecf09a9dbc17a83a9c146fcc715e0b457dae4fe25714656d3c163aa24ba39392ff00c06575f81e4e2b21cbb12bf7d462fe567f7ab1f68687f1e7c23a8958f5449b4d90f1975f323ffbe979fd2bd774ed6749d5e113e99770dd46dd1a270dfcabf34aaf69fa9ea1a55c2dd69d7125bcaa721a362a6bf6be1afa51673866a9e754235a3fcd1f725fac5fdc8f92cc3c3bc254bcb093707d9eabfcff00167e99515f1cf863e3c6bfa615835e8975083805c7c928038fa37e38fad7d11e18f895e15f15285b1ba114fde19f08ff0096483f8135fd33c1de30f0c711f2d2c257e4aafec4fdd97cba4be4dfa1f9f669c2b98e02f2a90bc7bad57f9af9a3a2d6fc37a2788ad4da6b1691dcc67a6e1f329f556ea0fd0d7ce1e2ff008077102bdef84e7f39464fd966387fa2b743f43f9d7d57457a7c63e1a70f713536b33a0b9fa4e3eecd7fdbcb7f4775e473e55c438ecbdffb3cfddecf55f77f91f9977da7dee9972f67a840f6f321c3248a548fcea7d2b58d4f43bb4bed2ee1ede64390c8719fa8ee2bef3f1cf86bc29ace952cfe2458e148949fb49f95d3dc1ea7e95f056ad6f636ba8cf0699706eed91c88e62bb0b2fd2bf847c4df0cf17c0d8ea75686294a3277834f96a2b778deff00f6f2d3d363f65e1fe20a59c51929d369add5af17f3fd19f597c3bf8cf69aeb47a47894a5ade9c2c737dd8e53e87b2b1fc8d7be0218654e41ee2bf303e95f437c32f8c5268e23d0bc50ed2d9fdd86e7ef345ecfeabefd47d2bf67f097e90ae728651c553f28d67f82a9ff00c97fe05dcf92e26e07493c565abd63ff00c8ff0097ddd8faea8af10d7be3bf84f4c2f0e9a936a12ae70630163cff00bc4e7f206bcd2eff00686d7a4901b3d36de24eeb23339fcc6dfe55fb4675e39f0665b51d1a98b5392fe44e7f8af77f13e5309c1b9b578f32a565e6d2fc373ebaa2be6ff0efed0563753adb7886c8da2b6079f136f507fda5c0207d335f43d9de5aea16d1de594ab3432a8647539041aface13e3cc8b89693a993e214dade3aa92f58bb3f9ede679999e4b8ccbe5cb8a85afb3dd3f9a2cd14515f60794145145001451450014514500145145001451450014514500145145003248a3991a29543a302195864107d457caff13be0d9b559b5ff000a2168b979ad00fba3a931fb7b57d5748402082320f515f13c73c0395715605e0f3186abe19af8a0fba7dbba7a33d7c9b3bc4e5b5bdad07a755d1ff5dce73c1f234be16d29df826d22cffdf22ba4a8e28a386358a25088a30147000a92beaf2fc33c3e169e1e4eee314afdecad73cdaf5154ab29a5bb6cc6f1145e7e83a8c3fdfb6957f3535f9b5f71b8fe13fcabf4d2f806b29c1e4189f3f91afcceb95d93ca9d76bb0fc89afe3bfa57504b1597565bb5517dce2ff53f54f0d277a75e1e71fd7fc8fd20f0dcdf68f0fe9d3ff7ed623f9a8adbae73c2031e16d241ff009f387ff4115d1d7f5e649272cba849eee11ffd251f976315abcd2eeff3395f1ca07f06eb6a7fe7c2e0fe484d7e74d7e8c78d881e0fd6f3ff003e171ffa2cd7e73d7f1a7d2b52fed5c03ffa772ffd28fd5fc36ff76adfe25f90514515fca27e921451450014515ee3f05fc04be24d51b5dd4d0369f60c36ab0e259ba81f451c9f7c57d270970be3388735a394e057bf37bf48a5bc9f925afe1b9c199e634b03869626b6cbf1ecbe67ab7c02d3f56b0f0edeff00685b496f0cf70b2dbb38c7980a80c40eb8181cf7af79a6aaaa285401540c00380053abfd42e0ee1a870fe4d87c9e9d4735495b99e8dddb7b2d96ba79753f9df36cc1e3b173c54a36e67b7e064ebb682ff46bdb323779b048a07a9c1c7eb5f9b53c4d04f240dd637643f5538afd3a232083debf3afc71a7ff0065f8b354b3c6025c395fa139cfe35fccdf4adcaaf470199456ce707f3b497e4cfd07c35c4fbd5f0efc9fe8ff004394a9609e6b6992e2ddcc7246c19194e0823a1151515fc691938c94a2ecd1faab49ab33ef1f859e3a5f19e823ed440d42cf11dc2ff7bfbae3fdeefef5e9f5f9f7f0e7c58fe10f13dbdfbb116b29115ca8ef1b77ff00809e6bf406391268d658d832380ca472083d08aff483c0df10a5c4d90a862e57c4d0b467de4beccbe6959f9a7dcfc178c723597e379a92fddcf55e5dd7f5d18fa28a2bf6a3e4828a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00fffd7fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a28021b8b886d6092e6e1c47144a5dd98e02aaf249af81fe24f8d65f1a78824ba8c91650131daa1fee0fe223d5bad7b97c78f1a1b2b28fc27612625ba024b92a7958b3c29ff0078fe82be4bafe21fa49788f2c562ff00d56c0cbf774da751aeb3e91f48eeff00bdfe13f5fe01c8552a5fda3597bd2f87c977f9fe5ea75de17f1bf887c237227d26e084fe285fe68d81f51fd457d4de11f8dde1dd7425aeb1ff0012cbb381f39cc2c7d9bb7e35f15d15f9170278bdc43c2cd52c254e7a1ff3ee7ac7feddeb1f969dd33e9f39e17c0e63ef558da7fccb47f3eff33f4f229629e35961757461956539041f435257e7b785fe2178a3c2520fecdbb66b7ce5ade525e23f81e87dc62be8df0d7c7ad03510b0ebd1369f37771f3c47f10323f2afec1e0cfa42f0d672a34b1d2fab567d27f0bf49edff0081729f96e6dc0d98611b9515ed23e5bfddfe573df28aaf6b7505edbc7776ae248a550c8c3a107bd58afde2138ce2a70774f54cf8c945c5d9ee21018608c8ae4f5af037857c400ff69e9d0c8e41f9c285719ee1860e6bada2b8f30caf078fa4e8636946a41f492525f7335a188ab465cf464e2fba763e73d6ff00679d2672d2e83a84b68c7a4730f353e80f0c3f126bcab54f821e39d38b18218af507f140fc9fc1b06bee1a2bf1dcf7e8f5c1b98b73a5465464fad3934bff00017cd1fb923eab07c739ad0569494d7f797eaaccfcded43c2be23d2891a869d730edea5a338fcc71582415386041f7afd3c78e3954ac8a181ea08c8ae5754f02f84b590dfda1a5dbb96eaca9b1bfefa5c1afc9b39fa2949272ca71fe8aa47ff6e8bffdb4fa6c278931db1347e717fa3ff33f3b28afb1b56f801e18bbdcfa65c4f66c4e42e43a0f600f35e5dabfc05f14592b49a74b15e2819da0ed63edcd7e3d9e780dc67965e4f0bed62bad36a5f8692fc0fa9c1f1965588d154e57da5a7e3b7e2785515d56abe09f15e8b9fed0d32745071b9577af1eeb9ae56bf2ac765b8bc154f638ca5284bb4934fee763e92957a7563cd4a49af2770a28a2b88d428a28a0028a28a0029f1c9244e24898a32f20a9c11f8d328a69b4ee80f6df05fc6bd77c3e52cf5acea56438f98fef93e8ddf9ec69be26f8e1e2ad5a775d21869b6a0fc81399481dd98ff21c578a515fa1af1638b565ab298e3a6a9aecfdeb76e7f8ade573c3ff0056f2c788789745733fbbd6db5fcec753acf8d3c51e20b45b2d5f5096e6156de118f19e9cfad72d4515f118fccb178eabedf1b565527b5e4dc9d96caeee7af46853a31e4a5149764ac145145711a85145140057d35f007c572fda2e3c2d74e4a15335be4f43fc4a3f9d7ccb5dafc39d45b4bf1b691720e035cac4c7fd997e53fcebeffc2fe24ab91f13e131b4e568b9a8cbce327677fbefea91e2f1160238ccbaad26b5b36bd56a8fd0ba2901c807d696bfd4d3f9c428a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00cfd5674b5d32eee643858a191c93d80526bf34dbf7929c7f137f335f6b7c6cf1547a1f85a4d2a0702ef53fdd000f2b17f19fc471f8d7c65a65bb5dea36b6a9d659a341ff0002602bf86fe9399d52c767d83c9e83bca945f35ba4aa3565eb649fcd1fb1787984951c155c54f4527a7a47afdedfdc7e8ee811f95a258478c6db6887fe3a2b5aa0b58bc9b68a11ff002cd157f218a9ebfb6b0347d961a9d2fe58a5f723f21ad3e6a929776ce37e2149e578235a7e9fe8720ffbe863fad7e78d7deff176e85afc3fd579c1951221f5671fd057c115fc3ff4a9c42967f84a2b78d2bfdf397f91fb078714dac0d59f797e490514515fcba7e881451450028049c0ea6bf433e1fe871f87bc23a769eaa15fc95965f79241b8e7f3af84bc29a71d5fc4da5e9a0645c5dc487fdddc0b7e99afd1f50140503000c015fd83f454c8e32a98ece26b55cb4e2fd7de97e513f2ef12718d468e153def27f92fd45aab737b6965e5fdae648bce711c7bce373b7403dcd5aaf987e34f8945bf8bb41d3c3623d3e48aea5c1fe22e08cfd1467f1afe98e3de2fa5c35944b34a8af694629376bb9492fc15dfc8fcfb24caa59862961e2eda37f72ff3d0fa7abe21f8e3691db78e65913acf0c7237d718fe95f626b3e20d2bc3fa636adaaceb0dba81f375dc5ba01ea4d7c39f13bc4f65e2bf14cba969c58db8458d0b0c1217dabf17fa4de71977fabd0cbe5522ebb9c24a37f7b96d25cd6edd2e7d6f87b85aff005e957517c9cad37d2fa6879e514515fc1c7eca15f71fc17f129d7bc1f0dacedbae34c22d9f3d4a28fdd9ff00be78fc2be1caf5bf841e34b3f086bb37f6aca63b1bc8b6bb0048575395240fc457ec9e05f18c387f8a29cb13351a155384db764afac5bf4925af44d9f2bc63953c765d254d5e71d577f35f71f72d1583a4789f40d79776937d0dc7b230dc3bf4eb5bd5fe8fe131b87c55255f0b35383d9c5a69fcd687e0b568d4a52e4a9169f67a0514515d26614514500145145001451450014571de27f1e786bc24bb756ba0b315dcb0a7cd21fc07f5ae974fbfb7d4ec60d46d1b743711ac887fd961915e661f3ac057c5d4c050ad1955824e514d3714f6bae87454c2578528d69c1a8bd9db47e85ca28a2bd339c28a28a0028a28a0028a28a0028a28a00fffd0fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800ac7d7f59b5f0f68f77ac5e1c456b19723fbc7b28f7278ad8af973f681f1592f6be12b47e001737583dcfdc53fcff002af86f11f8be9f0cf0fe233597c695a0bbce5a457dfabf24cf6720cade618e861ba6efd16ffe47cf1aeeb37be20d5eeb59d41cbcf752176f61d1547b28c01ec2b268a2bfcb3c5626ae22b4f115e5cd3936db7bb6f56dfab3fa329d38d382841592d1051451581615f4dfc29f84425587c49e2987e5387b7b490751d43480fe8a7f1accf837f0d1b54953c53ad45fe8b1366da271feb187f111fdd1dbd4d7d6e000001c01d2bfaf3c09f0561898d3e24cfe1786f4a9bebda725dbf9575dde963f30e32e2d74dbc060a5afda92e9e4bf57f20555450aa3007000ed4b4515fda07e4c14521214649c01debc13e21fc68b2d09e4d23c37b6eef946249bac511f41fde61dfb0af98e2be30cab87304f1f9b55508f45bca4fb456edfe5d6c8f472ccab138fadec70d1bbfc179b67be515f19e97f1f3c5b6985d422b7bc5c8e4a946c7d477af45d2ff685d0a6c2eab61716c4e06e8c8957ea795207e75f9ee4ff00480e0bc7d94b12e949f49c5afc55e3f89ee62b82336a3aaa7ccbc9dff0d19f43d15c1e95f137c11ab85fb2eab0ab310024a4c4d9f4c3804d7690dddadcaefb7952407bab035faa6599f65b9943da65f888545fdd9297e4d9f3788c162283e5af0717e69a2c514515ea9cc35911c6d750c0f62335c6eb9f0f7c21e21dcda8e9b0b48dff002d506c93fefa5c1fd6bb4a2bceccb27c0e6349d0c7d18d48f6945497e28e8c3e2ab50973d09b8bf2763e67d7bf67ab665693c3b7ec8dda2b81b81ff810c1af0cf10fc3df177864b36a7a7c8615ff0096f10f322fc597a7e38afd0ba465570558020f5079afc3b8a3e8e1c2f99275300a586a9fddd63f38bfd1a3ec32ee3dcc70f68d7b548f9e8fef5faa67e60515f7bf88fe14f83fc47ba492d05adc37fcb5b7f90e7d481c1fcabe76f15fc10f11e881ee7472352b71ced4e2503fdd3d7f0afe64e31f0078a32352af461f58a4bed53bb6979c37fbaebccfd0b2ae35cbb18d4252e497696df7edf91e23454b3dbcf6d2b43731b45229c32382ac0fd0d455f894a328b7192b347d6a69aba0a28a2a461451450014514500145145001451450015734e91e2d42d658c95649e3604750430aa75b3e1db5fb76bfa6d9919135dc287e85c575e029ca78aa7086ee492f5b9956928d3937b599fa4a9f707d29d4d4cec5cf5c0a757faff000bf2ab9fcbaf70a28a2a84145145001451450014514500150cf716f6b134f7522431af2cf2305503dc9e2b90f1c78db4df046926fef3f793c84ac1003f348ffd00ee6be25f15f8e7c41e2fba336a970de4824a5ba1c4683e9dcfb9afc67c4cf1a72ae1297d5147db625abf2276514f67296b6bf44936fc96a7d670f709627335ed5be5a7dfbfa23ecfbff8a5e03d3dcc736af0bb0ed1664fd5411fad5687e2efc3e9b03fb59133fdf8dc7fecb5f04515fced53e94fc46ea73430b4547b3537f8f3afc8fba8f87180e5b3a93bfcbfc8fd13b6f1e7832ef020d6acb9e9ba654ffd088ae9a1bab6b94125bca92a1e8c8c187e62bf31aac45777501cc334919ff6588fe55eee5ff4afc5474c765f197f866e3f838cbf338eb786b49ff06bb5eaaff9347e9c64515f9b90f89fc476f8f2354bc8f1fdd9dc7f5ad0ff0084f3c65b767f6c5e63feba9cd7d252fa56e54d7ef701513f2945fe88e09786d88bfbb597dcff00ccfd11674452cec140e4927000af2df187c5bf0bf862278adee1350bdc1db0c0c1941edb98703f9d7c537baf6b7a88c5fdfdcdc0f4925661f9135935f2bc4bf4a6c657a12a3926115293fb7397335e91492bfab6bc8f4b2ff0e68c26a78babccbb256fc6f7fc8e8bc51e27d4fc59aac9aaea8fb9db8441f7517b2815d67c20d0df5cf1c590db98acb37729c700478dbf9b115e6d04135ccc96f6e8d24b2305545196627a002bed8f83fe03b8f086932deea6a16fb50dace9de345e8bf5e726bf34f08f86330e2ce2da78ec5f34e109fb4ab37addad526fbc9a4addafd11eff0013e63472dcb254a9d936b962bf0fc11ec7451457fa467e06785fc7ebe16fe1086cf3cdcdcaf1ec809feb5f19d7d03f1ff5d5bcd7adb4589b2b651ee703fbefcff2af9fabfcdbf1fb3b8663c678854dde349469fce2bdeffc99b47efbc158474329a7cdbcaf2fbf6fc2c1451457e2e7d5851451401eadf056d56ebe21e9ecc322de39e6fc446547fe855f7557c69fb3ec61bc697321ea961263f174afb2ebfd05fa32e1634b8425556f3ab37f728afd0fc47c42a8e59a28f68afcdbfd42be02f8a9a836a3e3cd5652722397c953ed18007e95f7ed7e6ff8ae43378975291bab5cc9fcebc0fa5563250c9307854f49546dff00dbb17ffc91dbe1bd24f1756a768a5f7bff0080749e2cf8837de29d0b49d16752834f882cad9ff5aea36ab1ff0080feb5e774515fc5d9ce778dcd713f5cc7cdcea5a2aefb4528afc17ea7eb185c252c353f6545596afef7761451457947485145140162d6eeeaca65b8b395e1950e55d18a91f88af7bf077c76d534f31d9f89d4dec1903cf5ff005aa3dfb37f3af9f28aface14e38ceb87311f58ca6bb8778ef197f8a2f47f9f6679b9965184c7d3f678a827e7d57a33f4a343d7f49f11d8a6a3a3dca5c42ddd4f2a7d187507d8d6c57e71786bc55acf84efd6ff00479da36e37a754907a30e86bed1f007c4dd1fc6d6e20ff008f5d4a35fdedbb1fbdeac87b8fd47eb5fdd5e1778e596f1472e031a951c5ff002dfdd9ff0081bebfdd7af66cfc7388f83abe5d7ad47dea5dfaaf5ff3fc8f4ca28a2bf763e3028a2aa5f5da58d94f7b27dd823690fd1466b3ab5634e0ea4dd92577e88a8c5c9a8add906a5ac695a343f68d56ee1b48c9c0699c264fa0c9e4fd2b8bf177c44d1743f0b4baee9b750de3c998ad846e1834a7a671d97a9af8bbc5fe2bd4fc5dac4da96a1212bb888a3cfcb1a0e800fe7ef5cc6f7dbb371da0e71db35fc61c4df4a2c4d49e270b94619460d38c26dfbc9eca76b5bcd2f4bbe87eb397f8754e2a9d4c554bbd1b4969e97dfe65cd4b51bdd5af66d475095a69e762eeec73927fa57df3f0d1655f0268a26ceefb2af5fc71fa57c13a4436571aa5a41a94df67b579904d2e09d9193f31c0f6afd20d2bec1fd9b6c34b647b4112885a320a9403030453fa2ce0ea56cc71f9a55a977caa2d37793727cce4d6f6d37ead8bc46ab1861e8e1e31d2f7db4d15adf8ec5fa28a2bfb48fc9428a28a0028a28a0028a28a0028a28a00ffd1fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2802b5e5d43636935e5c1db1c08d2313e8a326bf393c4bac4de20d7afb589c92d733338cf65ce147e0315f5f7c70f11ff637844e9f0b62e3537f2401d7cb1cb9fe43f1af896bf883e945c57f58cc70f905197bb4973cff00c52f853f48ebff006f1faff875967261e78d92d65a2f45bfe3f90514515fca47e9215e99f0c3c093f8d35b1e7295d3ad087b9931c1f441eedfa0ae2344d1ef75fd56df48b042f35c3851e807727d80e4d7e82f847c3163e12d0e0d1ec97ee0cc8fde490fde63f5afdd7c0cf0c5f13e69f5dc6c7fd928b4e5fdf96ea1e9d65e5a753e3b8c3887fb3b0deca8bfdecf6f25dffcbcfd0dfb6b682cede3b5b6411c5128545518000a9e8a2bfd15842308a8c55923f096db776155eeeeedac2da4bcbc916186252ceee70001eb4979796d616b2dede48b0c10a97777380aa3a926be25f899f13ef3c6376d61a7b341a4c2df22743311fc6ffd076afcdbc4cf1372fe0fc07b6afefd69df921d5beefb45757f25a9eff0f70f57cd2b7243482ddf6ff826d7c49f8c17bafcd2693e1d76b6d3972ad2838926ff00e257dbbf7af082493934515fe71f15f17669c458f96619a547293d9748aed15d12ff0087bb3f79cb72cc3e068aa1868d97e2fcd8514515f3477856a596b7ac69a41b0bc9e0c74092103f2ce2b2e8ada8626ad1973d1938bee9d9fe04ce119ae592ba3d4f48f8c5e36d2b6a9ba1728bc6d997771f5eb5eaba2fed0f6cecb1ebda734609e64b76dd81ebb4e3f9d7caf5a7a3e917daeea50695a746649ee1c2a8ec3d49f61debf4ee19f1738d32fad0a181c5cea5da4a12fde5dbd9252bbd7c9a3e7b30e19ca6bc5ceb524bcd7bbf97ea7df1e1ef885e14f13c8b6fa5de069d86442e0abfe5df1ed5dad79e7807e1e695e09b0511aacfa848bfbfb923927d17d147eb5e875fe8a70955ceaae594eaf104611c43d5c617b2f2776f5ef6d3b1f856671c2471128e05b705b37d7fe00514515f4879e14514500721e23f02f86bc530b47aa59a3391c4a836c8bf461cd7cade39f833adf86449a86924ea1a7a82cc40fdf443fda5ee3dc7e55f69cb343021926758d1464b3100003eb5e1de3ff8cba2e91693e9ba03adf5f480a6f5e618b23924f463ec2bf09f18f84382b1197d4c767ae342aa4f9671b29b7d172fdbbf66be6b73ecf85734cde15e347069ce3d53d97cfa1f1ad14f91da47691fef312c7ea6995fe754ad7d363f750a28a290051451400514514005145140057a3fc27d38ea5e3cd3176e56073393e9e58c83f9e2bce2bdbfe023c0be329164c076b57099f5c8c8fad7dbf86b82a78be2acbf0f55fbaeac2ff277fc6d63c8cfeb4a965b5e71df95fe47da3451457faaa7f370514514005145140051451400567ea9a9d9e8da7cfa9ea1208adedd0bbb1ec07f53dab42be6dfda07c46d0d8d9f876da4ff008f8632cc01fe15e141fc6be3b8ff008ae1c3990e233796b282f757793d22bef7af95cf5724cb1e3f1b4f0ab66f5f4ea7cffe34f16df78c75b9b53bb2562dc4411672238fb0fafa9ae468a2bfcb2ccb32c4e618aa98dc64dcaa4db726fab67f46e1e853a14e34692b4568905765e19f00f8a3c5a73a45a130e706794ec887fc0b1cfe00d759f08bc051f8c3587bbd4509d3ac76b483b4ae7a27d3b9afb6ad6d6daca04b5b48d618a301511061401e8057effe10f8132e26c32cdf36a8e9e19b6a2a3f14eda377774a37d366debb6e7c57147192cbea7d570d1e6a9d6fb2ff00367cb961fb3b5db44ada96aaa8e402cb0c790a7b8cb1e7eb815aedfb3a69db7e4d5a7ddee8b8fe55f4a515fd3787f00b81e94393ea7cde6e73bffe947e7b3e36ce24efed6de897f91f2bdc7ece7739cdaeb2807a490127f30e3f95654dfb3cf8857fd4ea36d27d5197fa9afaf68ae3c47d1db822a6b1c3ca3e9527fab66d0e3bcde3bcd3f58afd123e3c8ff67bf1431fdededaa7d3737f85749a67eceaa1c36afab175fee411ecfc32c5bf90afa7e8a781fa3c704e1e6a72c3ca76fe69cadf726855b8eb37a8aca697a25fadce1fc35f0efc2be15c3e9b66a6618fdf49f3c99fa9e9f857714515faee5593e072cc3ac2e5f4a34e9ae91492fc0f97c4e2eb6227ed2bc9ca5ddbb8562f8875ab6f0f68d75abddb623b78cb7d4f603dc9ad69658e089e79982a46a5998f400724d7c55f15fe26378baeff00b27492534bb66fbc7833b8fe223b28ec3f13e83e13c53f11709c27944f11395ebcd354e3d5cbbff863bbfbb767b3c399154ccf14a097b8b593f2edeafa1e55ac6a973ad6a773aade1dd2dcc8d237b64f03e83a566d1457f98d88af52bd5956acef2936db7bb6f56cfe85842308a8c55920a28a2b12828a28a00f73fd9fa40be35b84fefd849fa3a57d9b5f0d7c109fc9f8876684e3ce86e23fafc85bff0065afb96bfd06fa32e2154e0f70fe5ab35f845fea7e21e214397344fbc57e6d057e7478ded9acfc5bab5b30c14ba907eb5fa2f5f0cfc6bd30e9fe3cba900c25da24ebee587cdff8f66bc9fa52e5b2adc3d86c6457f0ead9fa4a2ff548eaf0e710a38ea949fda8fe4ffe09e4b451457f079fb28514514005145140051451400559b4bcbab0b94bbb295e19a33b91d0ed607ea2ab51574ea4a9c94e0ecd6cd6e8528a6acf63ec6f85ff001721f1084d0fc44eb16a2388a53c24ff00e0ff00a1af7aafcc14778dd648d8ab29c8238208afad7e127c57fed5f2bc33e23947db00db6d3b1c79a07f0b1fef7a7ad7f6b782be3afd75d3c8388e7fbdda151fdaed19ff007bb4bed6cf5dff0025e2de0df64a58dc02f777947b79af2eeba7a6df44d6078a91a4f0d6a91a7de6b4980fa9435bf54b524f334fb98faee8641f9a9afea5cd287b7c1d6a3fcd192fbd347e71869f2558cbb35f99f99a410483d452559bd8bc8bc9e0ff009e72bafe448aad5fe4254838c9c5f43fa853bab857d85fb3e6a17773e19beb49d8b4569720444f60eb92a3d81e7f1af912d6da7bdb98aced90c934eeb1a28e4b331c015f5fea108f847f0b1e08187f685c1085c77b898738ff007541c7b0afde7c01856c066989e25aadac2e1694dd47deebdd8f9b6d5d7a2f23e338d5c2b61a9e5f1d6a549251f2d756751e24f8bfe10f0d5eb69f2cb25d5c270eb6ea1950fa16240cfd3353e87f16bc13ae29db7a2d1d464a5cfeece3eb920fe75f06bbbcaed248c599892c4f2493d4d32bdb5f4a1e2458e956f634dd17b42cee974f7af7bf7d2de4727fc43bcbfd8a8734b9bbff00c03ef1b8f8c5e00b790c675032104825236238f7c74aea741f18f86fc4a08d1afa39dc72533b5ffef93835f9cb5d5786f45f15dd5c47a87872dae1a489b292c4080187bf4af6720fa4d711e271f1a75b030ab07bc69a9f3dbcb595fe6bee3971be1f602145ca159c5f79356f9e88fd16a2b90f056b3a9eb3a246fad5abda5fc3fbb9d1c632c3f887b1eb5d7d7f676599852c7e129e3285f9669357566afd1a7aa6b66ba33f26c4d0950ab2a53dd69a6c1451457718851451401fffd2fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28ac2f136b30f87f40bed667385b5859c7bb7451f8b102b9b198ba585c3cf135dda104e4df6495dbfb8d29529549aa70576dd97ccf8e7e34f885b5bf19cd6b1bee834d5fb3a01d377573f9f1f85791558bab996f6ea5bb9cee9267691c9eecc726abd7f937c559ed5ceb37c4e6959eb566e5e8afa2f92b2f91fd2f9760e384c2d3c34768a4bfcff0010a28af41f86de0f93c63e2586cdc1fb24189ae5bb6c53f77eac78ae5c9326c566d8fa596e0a3cd52a49452f5efe4b76fa235c5e2a9e1a8cabd5768c55d9efbf037c1034ad3dbc517f1e2eaf536c21baa42483f9b11f957d0551c31470449042a1238d42aa8e800e00a92bfd4de0be14c2f0e64f4729c22d20b57fcd27f149fabfb959743f9c737ccea63f153c554ebb2ecba20a8e69a2b789e79dc471c6a59d98e02a8e4926a4271c9af91fe327c4c3aa4f2f857429bfd0e16db752a1e2575eaa0f7507afa9af3fc42e3dc0709e552cc319ac9e908759cbb7a2eafa2f3b27be4792d6ccf12a852d1757d97f5b1cefc55f8993f8b2f1b49d2a429a4c0dc638f3dc7f11ff0067d07e35e335723d3ef25b592f5216fb3c58df2918404f419e993d875aa75fe69715f10e679e6612cd7356dcea6aaf74947a28ff00756cadf9dcfe80cb70387c1d0586c32b28fe7e7e61451457cd1de145145001451450015f51fecfde1a88c577e26b88f2fbbc88188e8072c47f2af972beeff837047078034e318c799e63b7d4b1afdf3e8e19251c7f17c6b5757546129a5fdeba8a7f2e6baf347c5f1e62e7432b7187db697cb56ff23d468a28aff440fc2c2b84d73e25783b4091a1bdd4236994e0c717cec3eb8af0bf8bff001175d9b53b8f0b68be6dbdb5b9d93c91a90f2b639191d147b75af9c645915bf7a1831e4eee0feb5fcabe23fd23bfb2b19532cc86829ca0dc6539df96eb46a2959bb3eada5e5d4fd2721e02fac518e231b3b27aa4b7b79bfd2c7d75a87ed07a04195b0b29ae083c1621011fad79feabfb407896e72ba65b416833c311bdbf5e2bc0a8afe7fce3c79e34cc138bc5fb34fa4128fe3f17e27db61783729a1afb2e67fde77fc36fc0ea75cf1a78a3c467fe26fa84d32768c1d918ff0080ae07e95cb51457e558fcc7178eacf118dab2a937bb93727f7b3e928d0a74a3c94a2a2bb2560a28a2b88d428a29eb14ae09446603a900914d26f6019454eb6d72df76190fd149ad7d3bc2fe22d59f669da75cce7383b626c0faf15d586c062b13354f0f4e5293d924dbfb919d4ad4e9ae69c925e6cc1a2bdc341f811e2cd4595f5668f4e88f5dc43c98fa29c7e66be85f0f7c26f056810c63ec11dedc2f59ee9448c4fa8072abed815fb27097801c559d3f695e9fd5e9f7a974dfa43e2fbecbccf95ccf8d72dc27bb0973cbb475fc76fccf82a8afd0dd73e1ff008475fb57b7bdd36056230b2c4812443ea18007f0e95f1278ebc1f75e0ad7a4d2676f32265f36093fbf19240fc411835c7e24f82d9bf08518e36ace3568376e68a6acfa2927b5fa34daf435c878b30b9a4dd282719ad6cfaaf2671b5b3e1fd6eefc39acdaeb5627f7b6b207c1e8cbfc4a7d88e2b1a8afc8f0b8aab86ad0c450972ce2d34d6e9ad533e9aa538d483a73574f467dfba37c4ff086aba38d5a4bf8ad76a8f36295b12237a63a9fc2b8bd63e3df862c8b269b0cb7ac3a1036a9fc4d7c6d457f4363be937c535b0b0a187853a734ace76bb6fba4fdd5e9667c3d1f0fb2d854739b9497457b25f76acfa7dbf68b3bfe4d270be864e7f956ce99fb4268b3c9b353b096d94e30ca43fe7d2be47a2be730bf483e37a353da4b14a4bb3842df824fee677d4e09c9e51e554ade69bff33f47b40f16681e2687ced1af239f032c80e1d7eaa79ae8abf32ec350bed2ee92f74e9e4b79e339592362ac08fa57d47e0af8ed60fa7b41e30631dcc2bf2cd1a16f3b1db0070dfa57f467877f48ccb3377f54cfd470f56df15ff772b79bd62fc9b69f7be87c1e7bc0788c32f6b816e71edf697f9ff5a1f48d67ea7ab69da3db35e6a7711dbc2bd5a460a3f5eb5f32f88ff683ba977c1e19b2112f459ee3e66c7a841c0fc49af04d6fc47adf88ae4dd6b3792dd3f6dedf2afb2af403e82af8c7e92d90e5f1951c962f1157beb1a6be6f57f2567dc595787f8caed4f16f923db77fe4bfad0f7ff1e7c737943e9be0fca03956bb65e7d3e407a7d4d7cdd7579757d3b5cdeccf3cae4967918b3127d49aad457f1cf1971f675c4f8aface6b55b4b68ad211f48febab7d59faa65592e132ea7ecf0d1b777d5fab0a28a2be30f54fb8fe08dada5bf80eda4b62acf3cb23cc54e4efce307d0818af5daf89fe0b78ca5d03c429a35cc9fe83a93042a4f0937f0b0f4cf435f6c57fa5be07713e0f38e14c3d3c2c7965412a728f6715bffdbcb5f5bae87e01c639755c2e6539547753f793f27d3e5b0514515faf9f2c14514500145145001451450032445951a37019581041e4106bf3cfc7be1f6f0cf8aeff004bdb8896432427d627f997f2ce0fb8afd0faf97ff686d0c7fc4bfc411af2336d21f6e597f239fcebf9dfe927c2eb31e19599417bf87927ff006ecbdd92fbf95fc8fbae00cc7d8661f5793f76a2b7cd6abf547cbf451457f9f67edc145145001524514b3ca90408d2492305445196663c0000ea4d475f59fc11f87d0da59c7e30d5620d713826d1587fab8cff001e3d5bb1f4afb9f0f381319c599bc72cc2be58ef39748c56efcdf44babf2b9e3e799cd2cb70af115757b25dd957e187c1ed4b49d46cfc51ae4ed6d3dbb1923b54c67e652b876e7b1e40fcebe97a28aff0049382b8232ce16cbff00b3b2b8b516ef26dddca5649b7d3a2d1248fc0f37ce31198d7f6f887aecadb25d82be69fda1b45f32d34ed7a35e6266b790ff00b2df32feb9fcebe96ae4bc73e1f5f13f85efb49c0323c65a227b489cafebc571f899c36f3de19c665b0579ca378ff8a3ef47ef6adf336e1ec7fd4f31a55dec9d9fa3d19f9d9452904120f5071495fe57b56d19fd1c145145200a28a2800a28a2800a28a2800a7a3bc4eb246c55d482ac0e0823a106994534da77407d83f09be2b2ebc91f877c43285d4146d8266e04e0763fedff003fad7bbddb05b5998f411b1fc81afccb8659609527858a491b0656538208e4106bec1f037c53b7f11f85efacb57709aa595ac8cd9e04f1aa9f9c7b8fe21f8d7f6d7829e367d7b0af23cfaa7efa117ece6feda4afcb27fcc92d1fda5e7bfe47c5bc23ec6a2c660a3ee37ef25d2fd5797e5e9b7c95abb06d56f187437129fcd8d67d3e573248ee4e4b3139fad32bf8a6bd4f695253eedb3f5a8ab248f68f813a341a9f8d7ed770bb974eb77b8407a79848453f806247b815dc7ed13aca11a56811b65817ba947a71b13f3cb5705f057c45a77877c437f71aa4ab0c0fa7c87731c65a3656da3dc8ce07735c178bfc473f8abc4177ad4f90267c46a7f8635e147e55fbc438af0196f858b28c335f58c4d5973aeaa31716dbf54a315eaedb1f1af2dad5f88feb5517b94e2adead3ff82fee399a28a2bf023ed0b36725bc37904b77179f0248ad2c592bbd011b97239191c66bf47f401a69d1ace5d221586d258239224550b84650471f4af8c3e1e7c2cd57c61711de5ea3da694a416948c34a07f0c79fe7d057dc16b6d0d9db45696ca122811638d47455518007d057f6d7d18386734c150c56658ba2a146aa8f236ad295af769efc9f837aa3f23f11330c3d59d3c3d295e71bdd27a2f5e9727c0a28a2bfac8fccc28a28a0028a28a00fffd3fdfca28a2800a28a2800a28a2800a28a2800a28a4660aa598e001927d850d80b45436f7105d429716ceb24520cab29c823d8d3a693ca85e5c676296c7d0542a9170f689e9bdfc87caefcbd4e6fc49e32f0f78521f3759bb4899865631cc8df451cd79fa7c77f033cbe596b95527018c471f8fa57c87e24d66fb5ed6aef52d42469249657c64f0ab93851e800e2b0ebf86788fe93b9ecb309aca29421422da5cc9ca524bac9dd5afd96ddd9fb1e03c3dc12a0beb526e6f7b3b25e87e9368be22d17c436ff0069d1eee3b94efb0fccbf51d456d57e6d681e21d57c35a8c7a9e933b452c672403f2b8ee187706bef6f0478b6d3c65a0c3ab5bfc927dc9e3cfdc90751f4ee3dabf78f093c67c3717a960b15054b15157e54fdd92eae37d74ea9dedbddeb6f8ce27e13a995dab527cd4df5ea9f67fe675f451457ee27c7057cdffb42788fc8d3acbc31037cd74ff689c03ff2ce3e101fab73ff0001afa3c90a0b1e83935f9f3f11bc407c49e2fbfbf56dd0a486187d3cb8f818faf5afc0be917c55fd95c2d2c15295aa625f27fdbab59fe168bff11f6dc0996fd673155a4bdda6aff3e9fe7f2386a28a2bfcf03f720009381d4d7de1f0a3c19ff088f86a3fb526dbfbe0b35c6472991f2c7ff011d7debe6cf837e12ff848fc511de5ca6eb3d34899f2386907dc5fcf9fc2bee3afec9fa31f01a51a9c538b8eaef0a5ff00b7cbff006d5ff6f1f9578859ceb1cba93f397e8bf5fb828a28afec33f2c3ccbe2e6bf73e1ff05ddcd665926b82b6eaebfc1bfa9cf6e2be79f871f0ca0d76c9bc59e269becfa44259f07832ac7f7989eca0823d4f35f61ea9a569dad59be9faa409736f263746e320e3915f2b7c62f1c5b285f027870ac56566025cf95c2965e918c765efef5fcd1e33e4997e17328f15f10cd54c3d2828d2a1afbf56edebfddeb2f2567a2b3fd0b84b195ea61de5b815cb393bca7da3e5e7dbfab79cf8f3c610788ae92c3468059e8d644adb40a36eeede6301dc8fc8579f51457f0fe759ce2b35c64f1d8c779cbb6892e892d924b4496c8fd7b0985a786a4a8d25a2feaefcdf50a28a2bca3a028a28a0028a29c88f2388e3059988000e49269a4dbb201bd7815f73fc178f5383c116d06a56ef06d7730efe0b46c720e3a8fc6b87f85df0752cd22f10f8ae30d70d8782d1b9118ecd27ab1f4eddf9e9f480014055180380057f707d1f7c28ccf26abfeb1667274e5520e31a7d795d9de7db64d477ef6d8fc838df8970f8a8fd430eb9927772f35d177f5168a28afeab3f3629b69d60f234cd6d1191bef314524fd4e2b3aefc31e1ebe565bad3addc375cc6a0fe62b768ae3ad97e16b45c2ad28c93e8d27f99b4311562ef1935f33c9757f82de08d4f73456ed66e7bc0d803f0e95e57acfecf37f1ab49a26a292ff7639d4afe1b867f91afab372fa8fce94107a57e739ef833c1d9b5dd6c1c6127d69fb8ff00f25b2fbd33dec1f1666b86f86ab6bb4b5fcf53f3a35ff05f89bc32e5758b09624ed281be23f461c7e0707dab97afd3b9a086e2368a78d6446182ac32083ec6bc6fc5bf04bc35af2bdce93ff12bbb2320c633131ff6938c67d457f3af18fd17f1b878cb11c3b5fdaa5f62768cbe52f85fcd44fbacabc44a351a863e1cafbad57ddbafc4f8a28aea7c53e0dd7bc2179f64d660d809fddca9f34720f556fe879ae5abf97331cb71580c44b098da6e1523a38c959af91fa2d0af4eb4155a524e2f6682bd4be17f8ced7c35ab7d8b588639f4cbe6559448a1bca6e81c641e9dc7a5796d15d9c3d9f62b26cc2966583769c1df5d535d535d535a332c760e9e2a84b0f5767fd5cfd2eb4b1d24c493595bc02370195a3450083d08c0ad05445ced5033d702be3df863f17e4f0f2c7a1f888b4ba7f48e6eaf0fb1f55fd457d73657d67a95b25dd84c93c320055d183020f3dabfd2ff000ebc42c9f8a700abe5ed46a25efd3d14a2fd3ac7b4968fc9dd2fe7fcfb23c5e5d59c2bddc5ed2e8ffe0f916e8a2a1b8b882d6269ee6458a341966721401ee4d7e8539c6317293b24784936ec89abe6dfda02e7c3b2585adacb2e75689b7448832446df7b71ec0e38a3c7bf1caded849a5f843134bcabde37dc5ff70773ee78af972f6faef52ba92f6fa569a7958b3bb9c924d7f2478e3e3464f5f2fadc3795255e53d253de11b3bfbafed4935a35a2eef63f4fe0fe13c553af0c7e26f04b65d5faf65f89528a28afe2c3f580a28a2800a28a2800a28a2800a28af68f0cfc0ff137882ce1d467b8b7b1b69d43a1626490a9e876af1f9b0afa1e1ce14cdf3ec43c365142556695ddba2eedbb25f3670e3b32c360a1ed3153515e678bd15f53c3fb395a803ed1adc8c7bec8028fd59aad37ece7a4edf9358b907de3423fa57e970fa3d71cca3ccf0a97fdc4a7ff00c91e03e38c9d3b7b5ffc965fe47c9d457d3d75fb39308d8d96b60be3e5596df827d090fc7e55e65af7c1ef1be828d335aade42bff2d2d5b7f1fee901bf4af9ecefc1ee31caa9bad8ac0c9c5758da76f5e4726be68eec27146578997252acafe775f9d8f328a59209526898aba306561d411c835d9c9f123c712b891b58b8caf4c1031fa5717247244e63954a329c1561820fd2995f0f83cdf30c0a953c2569d34f751938ddadaf66b63d8ab85a159a9558295b6ba4cfa23c01f1b355b7bf834cf15c82e2d666082e48c3c6cc782d8ea3d6beb8073c8afcc0048208ea39afd19f076bd6de24f0e596ab6c47ef2250ebfdd9146197f035fda3f46ef10730cda189c9f35aeea4a9a8ca0e4ef2e5d5495f7693e5b5f557edb7e4dc7d91d0c33a78ac343954aea56dafd3d2fa9d3514515fd507e6e145145001451450015e69f1734a5d53c0da80c65edd44ebf54393fa66bd2eb3f56b45bfd32eacd86e1342e983ea41c7eb5e1713e531ccf28c565d257f690947e6d34bf13b72ec4bc3e2a9d75f65a7f89f99f4559bdb7369793dab7586478ce7fd924556aff24a70709384b747f4d269aba0a28a2a4675be06f0e378abc4f65a39cf9523ef988ed1272df98e3f1afd0f8618ade14b78542471a84451c00aa3000fa0af967f676d2124bbd535b71930aa5ba1f42f966fd00afaaabfd02fa3570cd3c070cbcd24bf79889377feec5b8c57dfccfe67e25e20660eb661f564fdd82fc5eaff0040acb3ad694baaae886e63fb73c6651067e7283a9c5684b2ac313cce70a8a589f60335f21f8075d7d7be3336a73b67cffb4ac79ecaa8768fc857e93c6bc731c8f1b9765f08a94f155630d7a46e94a5ebaa4bd4f0728c99e328d7aedd9538b7eafa2fc0fb02b8af889ae7fc23fe0fd4b5056db2184c517aef93e518f719cd747ab6ada7e87612ea7aa4c20b78465dcf38fc0724d7cc7f1b3c75a4ebda469b61a15da5cc33334f2943d36f0a181e41ce7835cde28f19e1323c83192f6d155fd9be58dd735e5ee2928ef64def6e85f0e65357198da5ee3e4e6d5db4d356ae7cdbd793451457f97a7f440514514005145140051451400514514005145140053d2478cee8d8a9208c838e0f514ca29a6d3ba00a28a29005145140057b27c23f0458f88af67d6f5c655d334d2a5c39015e43c8049ec00c9af1badeff008486fd3414f0edbb98ad7cd79e60a706591b006ef6550001f5afaae0dcc72ccbf328e3f34a5ed614d3928749cfeca7fdd4df33ee95b5b9e766943115f0ee8e1a5cae5a5fb2eb6f3e8bd4facf5bf8dfe0fd05858e951bdf08c6dff4701635c74009c0fcab8d7fda30863b346caf6ccd83ff00a09af9828afd2333fa44719e26ab9e1eb46943a46308d92ed792937f79e061f81b29a71b4e0e4fbb6ff4b23edbf077c67f0ff89eee3d32ea36d3eee4c0412106376f40c3a1f4cf5af65afcc247789d648d8ab29055870411dc57de1f09fc5ade2cf0a4335d386bdb4636f71cf24af2adff0002520fd73e95fd01e06f8cf8ce23af3c9b3b69d74b9a12492e64b74d2d3996eac95d5fb6bf13c63c274b01058bc1fc17b35bdbb3f43d368a28afe9a3f3d0a28a2803ffd4fdfca28a2800a28a2800a28a2800a28a2800aa1aa971a65d98feff009126dfaed359365e2cd1751d7ee3c3b65309aead23df36d395539c6dcf723bfa5746caaea51b90c083f435e7d2c5e1f1f427f55a8a4bde8b69decd68d7aa7bf99bca94e84e3ed636d9ebd8f897e1efc59d47c1ceda6ea4ad79a696384cfcf0b13c9527a8f55ff27da354f8e7e107d1ee5ac9a67b968d96389908f9c8e327d01af9a3e207866ebc2de27bcb19a32b0c9234b6ed8c2bc6c7231f4e95c557f9e783f1738c78529d6e1c94d354dca094e37943a7baee9dbaabdd76d0fdceb70c655994a18f4be2b3d1d93f5fd473b991d9dbab124fe34da28afc41b6ddd9f5c15efbf00bc406c7c4371a1cadfbabf8f7a02781247fe23f957815779f0c673078ef4771fc57013fefa0457dc78699bd5cb38a7018ba2ecd548a7e927cb25f34d9e467f858e232ead4a7fcadfcd6abf147e82514504e3935fea99fcdc79b7c55f140f0bf842ea589f6dd5e29b683d419061987fbab93f5c57c104e7935ebbf187c669e29f111b5b27df63a7ee8a320f0ef9f9987b7615e455fe6ef8efc6f1e21e249c30d2bd0a0b9216d9bfb525eaf4bf5491fbe706e4ef0397a7515a73d5fe8be4bf16c29f1c6f348b1460b3b90aa07524f4a657b1fc16f0a7fc241e2817f709bad74d0256cf4321fb83fafe15f9af0af0f6233dcda865385f8aa492f45d5fa2577f23dfcc71d4f07869e26a6d157ff0025f367d33f0c7c20be10f0c416b281f6bb8fdf5cb7fb6dd07fc04715e894515feabe459361729cbe8e5b838da9d38a8af9757e6f77e6cfe6dc6e2ea62abcf1155fbd277614514c9244891a590854405989e800eb5ea3692bb39923cebe2878c13c23e199a589f6df5d830da8efb8f56ff808e7eb5f04bbbcaed248c599892c4f2493d49af44f89fe3193c63e269ae2363f62b5cc16abdb6a9e5beac79fa6076af39aff00363c6ee3f7c4d9fc961e57c3d1bc21d9ff0034bfede7b7f7523f7ee11c93fb3f04b9d7ef27abfd17cbf3b8514515f8d9f54145145001451450015f4b7c11f879f689078bb598818938b38dc756cf3211edd07e75e4df0efc1973e34d7e3b30a45a42449752765407a7d5ba0afbeed2d2dec6d62b3b5411c50a84451d001c0afea2fa3bf85eb33c57fac99942f4693fdda7b4a6baf9a8fe32f468fcef8eb88beaf4bea1877efcb7f25fe6ff0022c514515fdd47e361589ae788b46f0e5a1bdd62e52de31d371f99be83a9fc2b85f88bf13f4ef05406d2dc2dcea922e521cfca99e8cfededdebe2fd7bc45abf896f9f50d62e1a79589c03c2a8f455e8057e05e2978eb97f0cca597e5e956c5f557f761fe26b77fdd5f368fb6e1ce0daf98255ebbe4a5f8bf4f2f33de7c57f1feee6df6be14b610ae71f699c6e623fd94e83ea49fa57935efc4df1ddfe7cfd62719ff009e588fff004002b84a2bf8c3883c52e29ce6b3ad8bc6cd27f662dc22bc946365f7ddf767eaf81e1dcbb09051a5497ab577f7b3564d735a966f3e4beb96909cee32b673f9d6ee9ff10bc69a64824b4d5ee46de8aefe62ff00df2d91fa571b457ca6173dccb0d53dae1f1138cbba9493fbd33d3a983a1523cb3826bcd23e88f0f7ed05abdaed87c45669768300cb01f2e4c0ee54e558fd36d7bef863e23785bc5788f4eba0b3e326197e47fc8f5fc2bf3eaa48a5960916581da375395653820fb115fb47087d22789f299469e3e5f59a4b753f8ede535adffc5cc7c9e67c0d9762539515ece5e5b7ddfe563f46fc4fe1ad37c57a4cda4ea49b92407638fbd1bf6653ea2be06f16785b52f086b12e93a8af2bf34720fbb221e8c3fa8ed5ed7f0f7e375cdabc5a4f8bdfcd832152ef1f3a7fbf8ea3dfad7a7fc55f09db78d7c2dfda9a5149eead10cf6f2464379a98c950475c8e9ef5fadf1e60722f13320967bc3eff00db6846ee0f49b8ad5c64baf57092babe9d74f99c96b63387f1ab058efe0cde8fa5fbafd51f0fd14515fc4e7eb415d1e83e2df11786a5f3745be96dfd501dd19faa3654fe55ce515d782c7e2707596230951c26b6716d35e8d6a67568d3ab170ab14d3e8f547b3ffc2f8f1d793e566d33ff003d3ca3bbff0043c7e95c06bde34f137895b3ac5fcb32768c1d918ff80ae0572f457d066bc77c459951fabe3f1b5270ece6edf357b3f99c386c9f03879fb4a34629f7495c28a28af943d20a28a2800a28a2800a28a2800a28a2800af54f057c5af11784234b16db7d60bd2094e1933fdc7ea3e8723e95e57457b590711e659262d6372aace9d45d5755d9ad9af2774726370387c5d2747130528f9ff5a1f6c687f1cbc21aa058ef4c96129e3128caff00df4323ad7afda5e5adfc0b75672a4d138caba10c0fe22bf322bd4fe1afc48b8f045ebc779e65c69d329dd0a9e55fb32e7a7bd7f52787df497c5cf194f05c4f18fb3969ed62ace3e724b46bbd92b6f667e779df87f4952955cb9be65f65eb7f47fe67ddb457868f8fde0e29b8c3741bfbbb07f3cd62ea1fb43e8f1a11a6e9b3caff00f4d5822fe9935fd0389f19782e853f693cc20d795e4fee49b3e229f09e6f3972aa0fe765f9b34fe31fc3bb1d574997c45a642b15fda2979360c79d18ea08f51d41af8e2bd63c4ff18bc59e23824b2574b2b5906192118665f42c727eb8af27afe1af19388f8773ccf3fb4387a0e29af7db5caa52fe64b7d56f74aef5b6e7ec3c2b80c760f07ec31d24da7a6b7b2ecd857b77c1af1fc5e18d4df47d5e5d9a75e91b5dbeec537627d03743f85788d5896d2ea08e39a689d1261ba36652038f507bd7c5f08f11e3f20cce9e71977c54dddef669e8d4bc9edff04f5b33c051c6e1e585afb4befbf75e87e9ac7247322cb1307461956539041ee0d3ebf3fbc37f133c5fe17885b69f785edc6310cc048a3e99e47e15e9963fb446b31e05fe9904def1b327f3dd5fdbf917d25b85317462f30e7a153aa71728dfc9c6edaf548fc8319e1fe654a4fd85a71e9ad9fdcffccfad68ae4fc19e2693c59a2a6af2594b63bd885497f880fe25f635d657ef597661431d85a78cc2caf4e6938bb3574f676767f7a3e2f1142742a4a9545692d18514515da62145145007e7a7c44b1fecef1a6ad6ca36afda19947b3735c557aefc6eb7f23c7b74e0604b1c4dff008e8cd79157f941c7d81582e24c7e162b48d59a5e9ccedf81fd2d9356f6b80a351f58c7f20a28a2be48f48fb33f67fb4587c1935d639b8bd9493ea115547f235ee95e59f05e0107c3bd338c7986790ffc0a56af53aff53bc2cc22c3708e5d497fcfa83f9c9733fc59fce5c4957da6695e5fde6bee7638ef881a81d2fc1bab5e29c15b7651ff0003f97f91af87fc09ad2683e2ed37559db6c71ce048de88ff002b7e86beb9f8d571e4780aec7fcf59234fcf35f0bd7f2dfd24b3faf84e2fc0d4a0f5a108cd7f8b9dbffdb51fa3700e0a1532bad19ed36d3f4b25fab3e9cf8ebe35d36fec2cfc3fa4dc2ce5dfed13b46d950a06141c77279fa0f7af98e8249eb457e0bc77c658ae28ce2a66f8b8a8b95928ada292b24bf37e6cfb3c9f2aa797616385a4ee975eed8514515f1e7a81451450014514500145145006de85e1dd5bc4973259e8f0f9f3471994a6403b475c67af5a8752d0f58d1e431ea96735b1071fbc4207e7d2bbaf83da8ff67f8f74f24e16e37c07fe06a7fad7dcf7363677a863bb823994f6750dfcebfa23c33f05b05c63c3f3c751c4ba58884dc5a694a2f44d69a35bf77b6c7c371071655cab1aa8ce9a941a4f7b3eabcd1f993457dd7acfc19f02eb04bad99b2908c6eb56f2f1efb79527ea2bcab58fd9deee305f43d4d641da3b85da7fefa5e3f415e6e7df475e30cbdb961e9c6bc5758495ff00f01972bfbae7460b8eb2aaf6539383f35faaba3e69a2b575bd1eef40d4e7d26f8a19eddb6bf96db973f5acaafc431386ab87ad2c3d78b8ce2da69ee9ad1a7e87d753a919c54e0ee9ec145145605851451400514576de1af879e2bf15812e9764df67271e7cbf247ef827afe19af472bc9f1d996216172fa32a951f48a6dfe1d3ccc3118aa38787b4af2518f76ec713457d083f678f11791b8ea36a25c7ddc363f3c5703aefc29f1b6820c93583dcc43fe5a5b7ef47e43e6fd2bebb34f0b38b72ea3f58c5e02a28774b9adebcb7b7ccf330dc45965797252ad16fd6df99e735b9a0788f58f0cdf26a1a3dc3412291b803f2381d987420d7a2780fe12eb9e23d4629b59b59ac74c43ba579018ddc0fe15079e7d71c0af60f12fc02f0fdddbb49e1a91ec6e147ca8eed244c7df712c3f035f47c2be0ff0019e3705fdbb965274dc1fba9b70a92b6b785eda79b6afd2e70e63c539551adf53c44af7df4bc57933d2fc05e30b7f1ae8116a91a88e753e5dc440e76483ae3d8f515dad7cc3f06acb5af09f8b352f0b6b10bc2d2422500f28db0e37a9e84107ad7d3d5fdd3e1871163739e1ea389cce2e3888b70a89ab3e783b36d746d59b5dd9f8df1160296131d2a78777a6ece3d7461451457e827867fffd5fdfca28a2800a28a64b2c70c6d34cc11101666638000ea49a4da4aec695f443890064f0050086008e41e95f2af8dbe2a5cf89f5b83c27e1894c5653dc25bcb70bf7a6dec1485f45fe75f52dbc4b0c11c2a30114281ec2be238578f72fe22c6e2f0f95fbd4e838c5cfa4a4ef751ee95be2eb7d34d4f6332c96be028d29e27494eeedd5256dfd6fb13578b7c61f1fbf85b4b1a4e9926cd46f54e194fcd147d0b7b13dabd8ee278eda092e262152352cc4f400726bf3a7c5dafdc789bc4379abdc313e6c84460ff000c6bc28fcabf3df1ff00c41abc3b9247078195b1188bc535bc62be292f3d525eadf43dce09c8e38ec5bab595e10b3b777d17eacd8f877e2bff00844fc576faadc3130484c573dcec7ea7df079afbfadee21ba812e6ddc49148a19194e4107a106bf316bdbbe1a7c5cb9f0a88f46d6435c6985b0ae397801f41dd7dabf03f017c5cc3f0fce5926712b61ea4b9a33fe493d1dffbb2d2efa3d766cfb4e34e189e3a2b178557a91566bbaf2f35f89f59788fc2fa278aacbec1ad5bacc83946e8e87d55ba8af0ed43f678d3ddd9b4dd4a48c1c90b2a86c7a0c8c57bfe97ac697addaa5ee95731dcc2e321a36cfe63a83ec6b4abfaeb3fe00e17e27e5c5e61878556d6934ecdae9ef45abaedabf23f30c167798e5d7a5466e3e4ff00c9ec7cb31feceb3ffcb4d5931ed19cff003ad04fd9dad367cfaabeef6418afa5a8af9aa5e02f03c36c15fd6737ff00b71e84b8d7387ff2f7f05fe47cad7bfb3bdd2a16b1d511980e15d319fc735cef873e1a78abc31e39d224d42d4bdb2dcab79f17cd18c64fcd8fbb9c77afb2eb3753d634ad1eddaef55ba8ada24ead2301f97727e95e4661e0170851ad4f31c2a78774a519df9af1f75a7ef29b7a69d1ab1d5438d7349c6542a5a7cc9adb5d55b4b7f91a43818af9fbe327c4b5d1ada4f0be8b2837d709b6e244393046c3eef1d1d87e439ae63c7ff1c4dca49a5783cb2a1cabde30c123fd80791f535f3549249348d2cac5ddc9666639249ea49f5afcebc63f1f30ff0057a99270ccf9a52d27556c975507d5bdb9b64b6bbd57bbc2bc15539e38ccc1592d545fe6ff00cbefec328a28afe303f570fa57dedf0a7c2c3c2fe10b68a55db7779fe937191c8671f2affc05703eb9af92fe17f873fe126f18d9da48bbadedcfda67f4d91e300fd5b03f1afbf400a001c01d2bfb13e8b7c1e9bc4712578edfbb87e0e6d7e0afea8fcb3c45cd6ca197c1ff007a5fa2fcdfdc2d14515fd907e5415e2ff1b3c5dfd81e1afecab57db77aa663183cac43efb7e3d3f1af6591d22469243b5541249ec057e7efc47f13b78afc5577a82b136f19f26dc7611a700fe279afc3bc7ce3879070e4b0f8795abe22f08f751fb72f92d179b4cfb1e0ac9febb8f5526bdca7abf5e8bf5f91c2514515fe721fbb8514514005145140051451401f71fc1683c3f1783e27d15c493c873784f0e26f423d00e9ed5ebb5f9d9e0df17ea5e0dd5d352b16263242cf0e7e5913d0fb8ec6bef6f0e78874ef13e930eada6481e294723ba377561d88aff44bc08f1172dcef26a794422a957a1149c16d28afb71f5fb4b74df99f86719e4588c262a58a6dca1377bf67d9fe9e46ed453894c320808590a9d8586406c7048f4cd4b457ef128f345c4f8b4ecee7e7578d74bf10697e22bb8bc4a59ef2472ed29e44a0f4653e9e83b74ae52bf44bc61e0cd1fc67a69b1d4d30ea098665fbf1b7a83e9ea3bd7c3be31f04eb5e0bd45acf528cb4249f26e141f2e45ed83d8fa8aff39fc5ef08331e1ac5cf30a2dd5c2cdb7cfbca2dbda7e7da5b3f27a1fbc70bf1461f30a6a84ad1a896dd1f9aff002e871f451457e1a7d78514514005145140057aff00c33f89f77e0f9d74dd44b4fa4cadf32753093d597dbd45790515ef70d712e6190e3e1996595392a47ee6baa6baa7d51c78fc050c651787c446f17fd5d799d978fac2c2c7c537874a7592c6e585cdbb272bb25f9b03e8735c6d29627009271d292b8b37c6d3c6636ae2e943914e4e5cab65777b2f25d3c8d70d49d2a51a7295da495fbd828af44f879f0fef7c77a8bc2ae6decedc033cf8ce33d1547727f4aeefc61f02753d1ed5f50f0f4e7508a35dd242c31371d4ae386fa75afabcafc32e25cc728967982c2b9d057d55aeedbb51f89a5dd2fc99e6e23883014314b075aa2537fd6af64780514acac8c55810c0e083c10692be11a69d99ec85145148028a28a0028a28a0028a28a0028a28a0028a5009381d4d6b6a7a0eb3a32c2faa59cb6c970a1e2671f2b83e84706ba29e12bd4a72ad4e0dc636bb49b4afb5decafd2e44aa4232516f57b799914515e81e15f87da9f8af45d5357b1241d3d331c7b72666032541f503a7bf15d993e4b8dcd713f54cbe9b9d4b49d96f68a6dfe0be7b2d4cb158aa587a7ed6b4ad1d15fd5d91e7f45290549561820e083da90024e057996e8740515f5afc28f84f656d6117887c49009eea701e0824195893b120f563fa55af8c1f0ce2d56c8f88742802de5b2fef628d71e6c6bdc01dd47e95fb9c3c01e217c34f881db9edcca959f3b86f7f5b6bcb6bdbcf43e3df1ae07fb43ea3d36e6e97ede9e67c815f67fc33f0d69de22f8676761e21b5171133cad17983e6552c7050f51f857c62ca549561823820f6afd16f045ddbdf784b49b9b5411c6d69100a3800aa807f515f47f464ca30d8dcef19f5ab492a56706aea4a5257ba7a595969e679fe2162aa51c252f67a3e6bdd74b2679a4df007c20f26e866b98d7fbbb837eb8aea343f847e08d0e459e3b21732a1cabdc1f3307e878fd2bd328afebbc0f863c2983aff0059c3e5f494fbf2a76f4bdd2f91f98d6e22cceac3d9d4af2b7a88aa14055180380052d1457dd1e285145140051451401f18fc7e8c278c6161fc76a87f2245786d7bbfed0241f175b7b59a8ffc78d78457f979e3024b8cf314bfe7e3fd0fe8ae1777caa87f850515d9f837c0dadf8daffecba647b208f067b971fbb887d7bb1eca39fc2bebaf0c7c21f08f87a25696dc5fdc81f34d703773c745e839e95e8787fe0de7dc571face1e2a9d0fe79decffc2b797e5e6619df1560b2d7ecea3e69ff002afd7b17fe13aedf879a28f5858fe6ed5e894c8a28e18d6289422200155460003b0a7d7fa3b90658f2ecb30f97b95fd94230bed7e58a57b79d8fc1b1b88f6f88a95ed6e66dfdeee78dfc76cffc203363fe7e60ff00d0abe23afbafe33db7da3c057ad8cf92c927e591fd6be14afe15fa4f519438b69cded2a31fc25347ec7e1e493cb1aed27f920a28a2bf9ccfbb0a28a2800a28a2800a28a2800a28a28037fc2b766c7c49a65d838f2eea224fb6e19fd2bf48148650c3b8cd7e6242e639a391782aea47e06bf4c34f944f616d30fe3891bf3515fd9bf450c6b74731c23e8e9cbef524ff00247e53e2552f7a855ff12fc8b95e73f127c756de0ad11e5460fa85c8296b177dddd8fb28e7eb81debb5d5f54b3d134db8d575090476f6d1991d8fa0ec3d49e83debf3d3c59e25bdf166b771ac5eb1fde3111213911c60fcaa3e83afbd7e97e38f89ef85b2c584c0bff006aac9a8ff723b39faf48f9ebd2c7cff07f0eff0068e23dad65fbb86fe6fb7f9ffc130eeeeee2faea5bcba7324d3397763d4b1eb55e8a2bfcea9ce53939cdddbdd9fbaa492b20a2ba69bc337dff0008ddaf89eda3692d659248662a33e54919e33ecc30735ccd7563b2ec4612518e222d73454979c64ae9af97e3a19d2ad0a89b83bd9b4fd56e15d4787bc1de20f144c23d26d5a44cfcd2921517ea4d72f5345713dbb6e82578cfaa3153fa5565b53070c44658f84a74faa8c945bf9b8cbf2157555c1aa2d2979abafbaebf33ebdf05fc0dd234a297de2365d42e07222ff962bd3a8fe2e73d78af798a28a08d61851638d00555518000e00007615f01683f137c61e1f2ab6d7cf2c4b81e54c77ae076e79afa27c1bf1cb46d6a48b4fd7a3fecfba9085126730b31f53d573efc57f71784be27f00d1a31cb32fa5f54a92b7c76f7df9d4eafb7372f923f20e27e1dcea73788ad2f6b15dba7fdbbfe573de28a404300ca41079047434b5fd367e781451450040d6b6ef70976d1a99a30555f1f300dd467d0e2a7a28a98c2316dc55afabf31b6dee145145508ffd6fdfca28a2800af98fe3a78ede303c1da64bb4b00f78ca79c1e91e7dfa9af7bf156bd0f86b40bcd626ff96119283d5cfdd1f9d7e766a37f75aa5f4fa8deb9927b87691d89ce4b1cd7f337d23fc429e5596c720c14ad56babc9add53dadff6fbd3d13ee7e85c0591ac4d778eacbdd86de72ff81f9d8e8fc011acbe35d151ba0bc89bf153b87ea2bf4440c0c57e7578166fb3f8cb4594f417d083f4660a7f9d7e8a0c9009af37e8a6e3fd8f8e4b7f691ffd274fd4dfc49bfd6a8ff85fe6709f13aee4b2f02eaf3c670de415ff00be8853fa1afcfbafd09f88f62fa8782756b641926dd9b1fee7cdfd2bf3dabf3ffa5446aff6fe11cbe1f65a7af3caff00a1edf870e3f51aa96fcdfa20a28a2bf978fd10d5d2b5cd5f439c5ce93772dac83bc6c403f51d0d7af691f1f3c5d62ab1ea10dbdfa8fe260637fcd78ffc76bc328afabe1fe39cff0024d32ac5ce9aec9fbbff0080bbc7f03cec6e5182c5ff00bcd252f96bf7ee7d3f0fed1c48227d0f69c7056e7393f4318aa973fb46dfba9169a2451376325c1900fa808b9fcebe6ba2bece7e3c71d4a1c8f1efe50a69fdfc973c95c1993277f63f8cbfccf5bd5fe3578e755568e3b88ec91ba8b64da7fefa625bf5af31bdd4b50d4a533ea1732dc487f8a572c7f5354a8af82ceb8b33acde5cd99e2a757ca526d7c96cbee3dac265b85c2ab61e9a8fa20a28a2be78ed0a28ab9a7d9cba85f5bd8c232f7122c6a3dd8e2ae9539549aa70576dd97ab14a4a29b7b1f5b7c02f0e0d3fc3f3ebd32626d464da84f510c7d3f36c9fcabdfab2341d2e1d1746b3d2e0184b68523fc40e7f5ad7aff56f80b86e19070fe172a8ab38457379c9eb27ff0081367f366758f78dc754c4beaf4f4d97e01451457d71e59e49f19bc507c3be1096081b6dd6a4df668f1d42904bb7e0bc7d48af86abd7be34f894ebbe3096ce26cdbe9a3ece801e37f573f9f1f85790d7f9b1e3af187f6f7155654a57a543f771edeefc4fe72bfc923f7ee0ecafea596c3997bd3f79fcf6fc028a28afc68faa0a28a2800a28a2800a28a2800af42f87be3ebef03eaa265dd358ce42dcc19ea3fbcbd830edebd2bcf68af5724ceb1b94e369e6397cdc2ac1dd35faf74f66b668e7c5e1296268ca857578bdcfd2cd1f59d3b5ed3e2d534b9967b79865597f5047623b8ad4af83fe15f89fc47a3788edb4ed1b33c37b2aa4b6cc7e4607ab7b103bd7dde338e7ad7fa4fe14f88f0e30ca5e31d270ab06a335f679ad7bc5f54fb6eb67d1bfc0789721795627d9295e32d577b79ff5a8b59faa695a7eb567269fa9c097104830c8e323ea3d0fbd68515fa5d7a14eb53952ad1528b5669aba6bb347cfc2728494a2ecd1f1f78dfe076ada5ccf7de1706f6cc927c827f7d1fb0fef0fd6bc2aeecaf2c25305ec3241229c15914a9fd6bf457c45e28d17c2d646f759b85857f857abb9f455ea6be5ef187c65d3f5adf0586876d227204b78a1db9ee147f8d7f1278c3e18f04e515a55f0f8efabd596bec6cea2f925ac176be9d8fd7f85788337c54142a51e78afb57e5fcf47f23c068a96797cf99e62891ef39db18daa3e83b0a8abf95669293517747e8cb6d428a28a9185145140051451401f757c19d320d3fc0d672c4a049745a59187724e07e82bd5abc33e04f88a1d47c2e745771f68d3dc8dbdcc6fc83f9e6bdcebfd4ff0bf1584c4709e5f3c15b91538ad3a34ad25ebcc9dcfe72e23a756199d755b7e67f73dbf03e6ef8cff000dace4b19fc5fa3a2c334037ddc6a30b22ff00780fef0efeb5f2857e88f8f8c63c19ac197eefd8e6ebfee1af8b3c27f0dfc4be30b5b8bdd362090c0a4abcb95595c7f0a1c727dfa57f2778fbc05cdc57469e438772a95e0e728c1754dde56e975bf77e6cfd2f82b3ab65b29636a5a30764df67b2381a2b4b55d1f53d12edacb55b692da653f75c633ee0f423e959b5fccf88c3d5a152546bc5c64b469ab34fcd33f4084e338a941dd30a28a2b12828a28a00294024800649e8292bdafe0b78323f116b8daadf461ecf4f20e18655e53d07e1d6be8785786f159fe6b4729c1af7ea3b5fa25bb6fc92bb38b31c7d3c161a789abb457f4be655baf841ac58f821bc5374e52e54095ad71cac3ea4ff007bbe3d2bc7abf437e20dec3a7782f559e6c05fb332283dcb70057e7bc104d75325bdba3492c8c1511464927a002bf53f1c38072ae18cc30781ca6edca9ae64f56e49db9bd65d969a687ce70867589cc6855ad89e92d3d2db7c8dff000868571e24f11d8e916ea58cd282e7b2c6bcb13f402bea7f8ed60abe0580431e45a5cc401c7dd4c15ff0ad6f84ff000f23f08699fda1a8460eab76a3cc27931275083d3dfd4d7a66b1a3e9faf69d3695aa4426b69d76ba9c8fa1047208afde3c39f07b1585e08c6e0f1568e2b191d9fd8497b89f9df57daf6dd1f179ef1553a99c51ab4f5a749f4ebddafd0f813c0fe0ebcf1aeb69a5dbb795128df3cd8c8441eddc9ec2bef0f0e78734cf0be93168fa5c7b6188724f2cec7ab31ee4d54f0bf83740f07db496da25bf95e6b6e776259dbeac79c7a0aea6bedbc1ef09a8f09609d6c628cb193f8a4aed28f48c6f6d3ab76577e491e4714f134f33abc94aea92d9777ddfe9d8f90fe31fc359f4dbe93c4da242d25a5cb16b88d173e539ead81fc2dfa1af3bf867e1897c51e2cb4b53196b6b76135c363e5089ce0ffbc78afbf5e34950c722865618208c822a869fa3e97a5071a6dac56de61cbf94817711eb8af99ce3e8ed9762b8a29e7787a8a141c94e74adbb4ef68be9193dd3db5b797a384e3baf4b2e7849c6f3b594afd3cfcd7434155514228c05180076029d4515fd1e925a23e04f15f1bfc17d1bc4f3b6a3a5c834ebc6c97dab98e427b951d0fb8fcabd1fc25a07fc231e1eb3d13cdf3cdac7b4c98c64939381e9cd747457cae59c0f9265d9a55ce7038750af515a4d5d27addfbb7e5bb6b5692b9e9e2338c657c347095a77845dd5ff00cf70a28a2beacf3028a28a0028a28a0028a28a00f8abe3c4de678db67fcf3b78d7f319feb5e7be0ef0adf78c35d8347b3042b1dd349da3887de63fd3d4d753f196e05c78ff0050dbd2311c7f8aa807f957d1df06fc1e9e1cf0cc7a85c478bdd48095c91f32c67ee2fe5cd7f00e03821f19f8978fa353fdde15672a8ffbb1959453ef26ade977d0fdb6be6ffd95c3f466be3714a3ead5eff23d2341d0b4df0e69b1695a542218621dbab1eecc7b93eb5b145233050598e00e4935fded85c2d1c2d18e1e8454611564968925d11f8ad4a93a93739bbb62d158ba3788748d7c5cb69370b702d6630ca57a071fcc7bd6d5185c551c4d255b0f35283d9a69a7d346b4dc2a529d393854567d99cb78dec3fb53c25aad9019325b3903d4afcc3f957e74b29562adc10706bf4cf51b986cec2e6eae31e54313bbe7fbaa0935f9afa8cd15c5fdc4f0a848e495d95474009e2bf8d7e95d83a2b1597e294bf78e338b5d6c9a69fded9fabf86b566e957a56f7534fe6ff00e18a7451457f229fa7051451400514514005145140051451400671cfa57e91785a5f3fc33a54dfdfb2b76fce306bf374f4afd14f02397f0668ac7fe7c601f92015fd5ff452aad6698fa7de9c5fdd2ff827e6be24c7fd9a8cbfbcff002384f8ef712c3e0468e3c8135d428ffeefccdfcc0af89abf43bc7fe1a3e2cf0adee8f190276512404f412c7cafe07a13d81afcf8bab5b8b2b892d2ee368a6898a3a38c32b0ea08af2be9459562e9f1150c7cd3f653a6a317d2f172baf5d53f99d3e1de2694b013a09fbca4dbf46959fe0414515de7813c09aa78d3548e286264b1471f68b8230a147500f7623b76afe78c9f27c66698ca780c05373a9376497f5b2eafa1f738ac552c3d2956ad2b451f52fc1ad2513e1c5ac57d18912f9e79991c6414772a38f42141fc6b0fc4df01740d4bccb9d0666d3e739223237c24fa63aa8cfa671e95ee565676fa7da43636a823860458d147002a8c01566bfd34878679262721c2e499bd08d55469c60a4f469a4937192b3577ae8cfe7d7c438ba78dab8cc2cdc79e4ddba6fd56ccfcf3f147803c4de129586a76a4c2090b3c7f346c3d73dbf1ae2ebf4e6e6dadef216b7ba8d6589c6191c6411f435f347c4df833025bcbaff8463d8d1e5a7b25e432f768fd08eebd0f6c743fcb7e24fd1cb139551a99970f4dd5a51d5c1fc6975b35a492ed64fd4fd1b20e3ca78992c3e3972c9ecd6cfd7b7e47cbb4529041208c11c10692bf970fd10fa73e0afc4898dc278475c98b07005948e72770ff009664fb8fbbf957d495f9870cd2dbcd1dc40c5248983a329c156539041f506bf473c2baab6b7e1cd3b55720bdcdbc6ee474df8f9bff001ecd7f76fd1b38ff00139ae06ae458f9394e824e127ab706ed67fe176b7934ba1f8df1fe494f0d5a38da2aca7a35e7dfe7f99d0514515fd3e7e7614514500145145007ffd7fdfca28a2803e77fda0f5836fa2d968f1b60dd4be6381fdd8fa7eb5f23d7bffed0972d2789ac6d89e21b6240f4de47f8578057f9a5e3be6b3c771ae3399e94da82f2518afd6ecfe80e0dc32a39452b7dabbfbdff00958b76174f637b6f7919c3412a480fa1420ff4afd27d32ee3bfd3edaf623949e24914fb3006bf336bed9f81fe251ad784869b338373a5388587731b64c6df88047d54d7e8df45ce23861b38c4e4f55dbdb454a3fe285eebe716dfc8f0bc45c03a985a78a8af81d9fa3ff0082bf13d8a7863b98648251b92452ac0f706bf3a3c5be1fbaf0cf882f348ba523ca9098c9fe28c9ca91f857e8e57927c54f8771f8cb4efb6d8a85d4ed54f947a798bfdc27f957ee3e3cf8735b89f278e2700af88a17715fcd176e68af3d135e96ea7c7f05e7d0cbf14e9d776a73b26fb35b3f4ee7c35454f736d71677125addc6d0cd131478dc6d6561c1041a82bfceb9c2509384d59add1fba269aba0a28a2a4614514500145145001570585cfd84ea25088048220c7a33e3381eb81d6bd07e1dfc36d4bc6f7627915adf4b89bf7b70463711d523cf53ea7a0ef4bf13f56d2e4d562f0df8795534bd154c11ede449313fbc7cf7e78cf7c66beda9f0756a190cb88331f729c9f2d24f7a92ead2fe48abb72eaec96f75e4bcd213c62c150d64b59768aff0037dbe6798d7b1fc11f0fff006c78c52f655cc3a6a19ce7a6f3f2aff3cd78e57da1f01f41fecdf0a3ea922e25d4642d9ffa669c2feb9afaaf02f861673c5f878d457a747f792ffb77e1ff00c9b97e479bc61987d532ba8e2fde97babe7bfe173dc28a28aff4b0fe7f0ae73c5daec5e1bf0dea1ad48466da16280ff1487841f8b115d1d7cf5fb42eaad6fa0d8e908d8fb5ce6471eab10e3f53fa57c67887c42f23e1bc66690f8a107cbfe27eec7ff2668f5b22c0ac663e961decdebe8b57f81f244d349712bcf331692462eec7a9663927f3a8ebd03c39e079b53d0b50f14ea4e6df4cb089995ba34d28c0555f6c91cd79fd7f97998e518cc2d1a38bc5c6cab2728df76af6e6b766ef67d6ccfe89a389a55272a74ddf9747e4fb0514515e49d2145145001451450014514500145145007d2dfb3df87127b9bef134eb910116d013fdf61b9c8fa0207e35f5557927c11b68e0f879632a0c35cc9712bfbb095933f920af5baff4f3c19c929659c1d81a74d6b520aa37ddcfdefc134be47f3d716632588cd6b4a5f65f2af96815cd78bfc4b6be13d02eb5bbac1f25711a671be46e1547d4fe95d2d7cadfb41eaf7173a9699e1ab6cb055370d1af5691ced418efc671f5af47c4fe2d9f0e70de2333a3ad4b28c3fc72765f76f6eb639f8772c58fc7d3c3cfe1ddfa2d5ff91e09e21f116abe27d4a5d53569da59643c0fe145ecaa3a002b0ebd9b4ff819e33bdb31752f916cccbb96291b2dcf638e01fceb80f12f83bc45e129c43adda342ac7092afcd13fd1c719f6383ed5fe73f10708f1450a6f37ce30b55466eee728bd5beb27d2fe763f77c1667974e4b0b85a91bad124d74edff0000e628a28af8a3d60a28a2800a28a2800a28a2803a6f0978a2ff00c23ad43abd892761c491e78910f5535f7ef873c43a6789f4a8756d2e512c520f980fbc8e3aab0ec457e6e575fe0ff1aeb5e0bbf377a5c998a423ce81bee4807a8f51d8d7ee9e0d78bf5384f10f058ebcf0751dda5bc1ff003457fe94baeeb5dfe3b8af85e399c155a3a558fe2bb3fd19fa07a969b65abd94ba76a3189ade71b648c92030ce707047153db5adb59c096d6912430c636a246a15540ec00e2b8df0578fb45f1b59f9962fe5dca28335bbfdf43edea3debb9aff0040328c765b99d2866d97ca338cd594d5aed76befa3be8f677d2e7e278aa388c3c9e16ba69a7b3efdff00e0993abe85a3ebd6c6d358b38aea261d245048f707a83ee2bc8b51f803e0eba73258cb75679fe1571220fc186efd6bd9353d4ad348b09f53be6d905ba19246032428ebc573fa6f8fbc1faac4b2da6ab6df363e57708c09ed86c57cdf13e43c2199626386cf69d19566aeb9f954dadb47a4ad7eccf432ec6e6987a6ea60e52505bdaed5ff0023c757f673d377e5b5898afa08541fcf756d59fc02f07da2996fa7bbbbda09da5c46a71ebb467f515eec082010720f208a8e71ba091477461fa579387f06382b0efda52cbe0df9f3497dd26d1d33e2ccdea7bb2aefe565f9247e6a6ad0c36faa5dc16e36c51cf22a0ce70aac40193cf4acfad3d6d4a6b37c87aadc4a0ffdf46b32bfcd0c7c52c4d4495bde7f99fd0149fb8bd02bef0f83fa32691e07b26db892ec1b873dc963c67e82be101d6bf483c2b1087c37a6463a0b58bf5506bfa67e8af9753ab9de2f1935ef53a692f2e696bf846df33f3ef11f1128e0e9525b4a5f92ff008279f7c57f0f78a3c5b6d69a16871a8b667f32795db6af1d07a9f5ab1e00f851a478376dfdc1179a911feb987cb1e7a841dbebd6bd628afeb07e1e64f533e9711e2a0ea6234517277504969c91d977bbbbbbd1a3f35feddc54704b014df2c3adb777eec28a28afb83c62bdd5e5ad8c467bc95218c75772140fc4d3a0b882e6312dbc8b221e8ca720d51d6748b2d774d9f4bd4104904ea5587a67b8f715f0e2ea9e25f857e2abbb1b09d9441290d13e4c5327f092bee3a1afca3c44f126af086270f5b1b86e7c1d47cae717ef425beb1b59a6b55aa7a33e9b22c8219a53a90a552d563aa4f66bd7a7ddd8fbde8ae33c0fe34d3bc6ba3aea168424e985b8833f346ffe07b1aecc90064d7e8f9566b85ccb094f1d829a9d39abc5aeabfadd747a1e06270d570f56546b2b4968d1e15f13357f11780b52b6f1468d70f2d85d3f97776529dd16f1c865ce76161c1231c8f7af42f05f8e746f1b69e6eb4e7d9347813dbbfdf8c9fe60f635e5ff001db5fd264f0d2e976f750cd72f70b98d1c33a85e49201e3a57cc3e1af126a7e15d5a2d5b4b90a4919c32ff000c887aab0ee0d7f2d71378b33e0ee3aab848d4f6b81a9cb29c2fcdece52f89c3b7f338edabd13d4fd1f2fe1959ae4d1aae3cb595d276b7325b5ff24cfd21a2b9ff000bf88acfc53a25b6b3647e49d7e65ee8e3ef29fa1ae82bfaaf058da18cc3c3158692942694935b34d5d33f35ad4674aa3a5515a49d98514515d46614514500145145001451593af5ea69ba2dedf39c086076cfa715862b110c3d19d7a8fdd8a6dfa25765d2a6ea4d423bb763e32b6d3078e3e2d4d6ee37412df3cb30ff00a6511cb0fc48c7e35f70aaaa2845180a3000ec057cadfb3f597db75ad635d9865d54203e8d2b166fe55f55d7e17f47ccad2c8eb679517ef31756736fc949a4befe67f33ecb8e712de321838fc34a297cedfe560af08f8e1e359342d263f0fe9f26cbcd4413215fbc900e09f6dc781f435eeccc114b31c00324fb0afcf3f881af49e23f16ea1a939ca79862887a471fcaa3f4cd5fd2138d2a647c37f55c2cad5710f9135ba8daf36be568ff00dbc2e07ca638cc7fb5a8af1a7afcfa7f9fc8fa73e005b471f82a5b951f3cf7b2973dced0a057b8f4eb5f2e7c0ff1d689a468d77a16b37496ac9399e1690e1595c0dc33ea08fd6b0fe247c63d4351bc7d33c2574d6f631fcad711e56494f7da7aa8f42306bc7e15f15b87b8738070388ad514a6a0a3ece2d73b92bdeeba2beadbefd6e8eaccb86b1d8fceeb4231b26efccef6b74f5f43d7fe357881747f06cd69138136a0c20500f3b7ab7e82be1fabd79a9ea5a8edfed0bb9eeb6676f9d23498cf5c6e2719aa35fc99e2978853e30ce1663ecdd38462a318b77b2576f5b2ddb7d3b1fa5f0e646b2bc2fb0e6e66db6dedfd681451457e6c7be1451450014514500145145001451450007a57e87fc3eff912745ffaf28bff004115f9e07a57e89780976f82f451ff004e50feaa2bfaa7e8a8bfe1671aff00e9d2ff00d2d1f9c7890ffd8e92fef7e875d5f207c7af0b4b63ad47e24813fd1ef408e52070b2a8e33fef0fe55f5fd63788342d3fc49a54fa3ea49be09d707d411c820f620f22bfa7fc51e068f15e41572d4d2aabde837b29adafe4d5d3f5bf43f3be1cce1e5b8d8e21fc3b3f47fe5b9f9d1a4a412ea9691dc8cc2f3c6ae0f752c33fa57e9258595969f691dae9f0c70408a02246a1540fa0af877c6df0b7c43e10b96b8b7864bcd3c36527886e283afce072318ebd2bea0f857e3483c5be1d8924702fec5562b84ee703e571ecc07e75f807d1da94f87f3ac6f0f67347d962a6938732b3928df9945f55aa92b68d26fa1f6fc74d637094b1d849735357bdba5ed66fb763d3a8a28afec33f2b0a4201183d0d2d1401f1d7c6cf012687a88f11e971edb3bd63e722f48e63d48f40de9d8d78257e946bda1d8f88b4ab8d23515dd0dc29538ea0f623dc57c65e21f835e32d26f648f4fb43a85b6e3e5cb132e76f6dca4820fd335fc21e39783d8dc1e6b2cdf22c3ca742aeb28c137c93ebeead795ee9dac9dd69a1fb3707f14d1ab86585c6544a71d136ed75d357d51e495ef5f057c7f368daac7e18d4a42d617adb612c7fd4cc7a63fd96e87df9ae39fe12f8e61d2ee354b8b0f296dc6e3117532151c9202923007bd70166d2c7790b459122caa57d4303c7eb5f916418acfb82f39c2e67528ce949eb6945c79e17b4959a574f6f2767d8fa8c6d3c166d85a9878cd49774d3b3e8cfd36a2aa5834cf636ef71c4ad1217ff0078819fd6add7fa9509f34549753f9ca4acec14514550828a28a00fffd0fdfca28a2803e32f8fc8cbe3285cf47b44c7e04e6bc32be95fda274f65bdd2b5403e5647849f53c11fa57cd55fe6178cf83961b8d73084bacf9be524a5fa9fd0fc295554ca6835d15beed02bb6f0078bee3c17e218b544cb5bb8f2ae631fc711233f883c8ae268af81ca335c56598da59860a5cb529b528bf35fa775d51ed6270d4f114a542aabc64accfd35b0bfb5d4ece1bfb1904b04e81d1d4e4106add7c49f0bbe28cfe109c695aa96974a95bb72d031eebfec9ee3f1afb46caf6d350b68eeeca559a1900657439041aff4c7c34f12f2fe2ecb957a0d46bc52f694fac5f75de2fa3f93d4fe7ee20e1faf95d7e49eb07f0befff0004f3bf1efc2fd17c6c86e73f63d454616e1067763a071fc43f5af8fbc4de03f13784ee1a2d52d1cc409db71182f1301dc30e9f43835fa1f514d0c3711b453a2c88c305580208f706be73c44f02f23e279cb194bf71897bce2aea5fe28e977e69a7ddb3bf22e31c665e95297bf4fb3dd7a3fd363f3128afbb35df83be0bd6d8cab6c6ce56392f01db9fc3a57986a1fb3b49c9d2f551c9e04c9d3fef9eb5fcb59e7d1cb8c30327f55a71af1ef0924fee959fdd73f46c1f1de55597ef24e0fcd7eaae7cc5457d0a3f677f10eec1d4ed31ebb1ff009574da3fecf1669b64d6f51790e3e68e01819ff78f35e1603c09e36c55554fea4e1e7294525f8dfee4cecadc6394538f37b64fd137fa1f2cc104f7322c36f1b4b231c05404927e82be81f017c0fd435178b53f16036b6a0865b5ff0096b20ff6bfba3f5fa57d17e1cf037867c2e83fb2acd1640399586e73f89e95c8fc47f8a7a67846d64b0d3dd6e75691488e35e562ff006a43db1d87535fb564be06643c298479f71b6214d435e45f037d16b694dbe91492ef747c9e2f8c7199954fa96514da6fabdeddfb2f531be2978e2c3c13a22f85fc3db22bd9a3d8a91702de1c633c7427a0fcebe3624b1249c93c9356efefeef54bc96fefe569ae2762eeec7249354ebf9efc48f10311c5799fd6651e4a105cb4e0b68c7f2bbebf25b247dbe4392d3cb70fecd3bcdeb27ddff9762d58da4da85ec1636ca5e5b89522451dd9c8007e66bf48b45d322d1b48b3d2a0fb96b0a463dca8e4fe279af8e7e06f87ff00b5bc62ba8cabba1d2e3337238f35b2a9f9727ea057db55fd41f45be19fabe5589cf2aad6acb923fe186efe7276ff00b74fcefc46cc39f114f0717a455dfabff81f98514515fd527e6e15f2dfed016b7777ac6896f0a332caad1a639cbb3018afa92ab4f676974f1c9730a4ad0b6f8cba8255bd467a1af8af107847fd66c92ae4fed3939dc5ded7f8649bd3cd23d7c8f34fecfc64715cbcd6be9eaac7ccdf17e78bc33e06d17c1967fbb69b6bcca3ba44bce7eae41fc2be61af54f8c7aeff006d78dee911b7436205b27a65796ffc789af2baff003d3c5dce69661c51888e1bf8346d4a0ba28d35cba7cd37f33f71e18c2ca865d0753e29fbcfd65afe560a28a2bf333e8028a28a0028a28a0028a28a0028a28a00fb5be03ea6979e065b207e7b0b996223d9cf980fd0ef23f0af6aaf8a7e0878a4687e273a5dc36db6d4c08cfa0957ee1fe63f1afb5abfd29f023896966fc2186827efd05ece4bb72fc3f7c6df89f81719e5f2c2e6951f49fbcbe7bfe370ac1b9f0ce8579ac45af5d59c72df40bb2395c64a819c7078c8cf07a8adea2bf5ac560b0f898a86260a4934d2693b35b3d7aae8cf98a75674db74db57d34ec159daae93a7eb5652e9da9c0b716f28c3238cfe23d08ec6b468ad2bd0a75a9ca8d68a945ab34d5d35d9a26139424a707668f84be267c38b9f04ea1e7da06974bb863e4c879287fb8c7f91ee2bcb2bf417e245f68363e12be7f104626b77428b1746790fdd0be873ce7b57e7d9c64e381dabfce3f1d38172ee1acf553cb26b92aae7f67d69ebb7f85fd9ebbaecdfef3c1d9cd7cc305cd885ac5daffcdff07b89451457e287d685145140051451400514514017f4dd52ff0047bb4bfd3677b79e339574383ffd715f53f813e39d8df08b4cf1762d6e3855bb03f74e7fdbfee93ebd3d715f24d15f7bc0de24677c2988f6b9654f71fc5096b097aae8fcd599e2e7190e1332a7cb888ebd1add7f5d99f527c6ef885692d88f0a68d32cde7ed7ba9632194275540470727935f2eab323075ea0e47e14da2b9b8f38e31dc539bcb36c62e5764a315b462b64bf16df56cd326ca28e5b86586a3af56fbbee7e907852f86a7e1bd36fc1cf9d6d131faed19fd6ba0ebc5790fc11d4fedfe04b680b65ace492023d00391fcebd7abfd31e0acd16659060b1c9fc74e0dfaf2abfe373f9fb37c37d5f1b568f693fccfcecf1dd89d3fc5faadb118c5cbb7fdf473fd6b92af6ff008efa41b1f172dfa8c25ec21b3fed2f06bc42bfccbf107279657c498dc0c95b96a4ade8ddd7e0d1fd0792e296270146b2eb15f7f5fc4057e8ef84675b9f0be973a9c86b58bf4503fa57e7157ddff077535d4bc0962b9cb5aee818771b49c7e95fb97d15f30853cf7178396f3a69affb764bf491f1fe23d072c153aabeccbf35ff0000f51a88cf00985b19144aca58267e62a38ce3d2a5af0cf8d73ea7a2dae95e2bd1e4314f617063661d0ac83a11dc123906bfb0f8bf887fb0b29ab9b3a6e71a76724b7e5ba526bcd26dfc8fcb32ac0fd73131c37359caf67e76d3ef7a1ee745793f833e2df86fc4b691c77b32d8df850258a53b54b772873c8fd6bd026f106876e9e64d7f6e8bd72645ff001ad327e2dc9f34c1c71d81c4c254dabdf992b7934f54fba64e2b2bc5e1aaba35a9b525e5f977362be2cf8f4b10f1a831e371b68f7fd79c7e95f456aff167c0fa3e565bf13b8fe080190ff87eb5f19f8dbc487c59e24bcd682b2473362256eab1af0a0fbe2bf9d7e91dc6d92e2787e394e0f110a959d48b6a3252e5514eeddaf6e8be67ddf016518ba78d789ab0718f2bdd5af7b1a5f0ebc653782fc450dfb126ce6c457483bc67be3d57a8aed3e207c65d53c42f269da033d969fca971c4b28f73d81f4af0ea2bf95b2ff1133ec0e493e1fc1d770a129393b692d56b14f7517bb4b77eaeff00a457c8f055b16b1b56179a56f2f5b771ccccec59c9627a93c9a6d1457c4b77d59eb1daf85fc7fe26f08298b47b9db0336f685d43213eb8afaabe1dfc5ad3bc64e34cbf45b2d4c0caa67f77363aec27b8f43f857c41535b5c4f69711dd5b398e58983a3a9c156539041afd63c3ff1873ee18af4e9c6aba9865a3a727756ebcb7f85f6b697dd33e6b3be16c166309371e5a8f692defe7dcfd3aa2b88f87be27ff84b3c2f6baa4873381e5cff00f5d13827f1eb5dbd7fa4594e6987ccb054b1f8577a7522a49f93573f03c561a787ad2a1517bd1767f20a28a2bd1300a28a2800aa3a969d69ab58cba75f26f8275db22e48dca7a8e2af515956a34eb53952ab14e324d34f669ee9f932a139424a717668c8d1b40d1bc3f6e6d746b48ad23620b08d70588e993d4fe26b5e8a2a70b85a386a51a187828c16c92492f44b41d4a93a9273a8eedf5661789eecd8f87752bc1c18ad656fc94d7e6f3b167663ce493f9d7e83fc479c5bf81f5890f7b665ff00beb8feb5f9ed5fc53f4acc53966981c35f45093fbe56ff00db4fd73c36a4961ab54ef24bee5ff0428a28afe503f490a28a2800a28a2800a28a2800a28a2800a28a2800a28a28003d2bf46bc18863f08e8c87b58dbffe8b15f9cc06580f538afd28d021fb3e87a7c1ff003ced615fc900afeb4fa28d16f30cc2af68417df26ff43f33f12a5fb8a11f37f9235e8a28afed83f2310a86186008f435970687a45b5f1d4adad228ae9815696340acc0ff00788ebf8d6ad15856c2d1ab28ceac1371775749d9f75d9970ab38a6a2ed70a28a2b72028a28a0028a28a004201183c835f3e689f07a37f1b5e788f52458b4f8ae9a5b4b61d646ce4330eca0f41d4fd2be84a2be5f88b83f2bcf2b61aae654f9fd84b9e2ba5ed6d7baeb6d9b4af7d8f4b019ae23070a91c3cadceacfd3cbfcc28a28afa83cd0a28a2800a28a2803ffd1fdfca28a2803c7fe36e8adaaf82e5b889374b6522cc31d768e1bf4af87ebf4e2eada1bcb696d2e177473294607b835f9efe39f0adc7843c4371a5caa7c9dc5eddcf4689ba7e5d0d7f147d28783aad3c650e24a11bc269427e5257e56fd569f2f347eb9e1de6b19519e026fde4eebd1eff73fcce3e8a28afe4a3f4c0aeebc1ff10bc43e0c9c1d3e5f36d49cbdb4a498dbe9dc1f715c2d15e9e519ce3b2bc5471b975574ea47671767ff0005774f47d4e7c4e168e229ba55e2a517d19f73f85be31f84fc44a90dccbfd9b76d8062b8385cff00b2fd0fe38af558e58a641242eae8c32194e411ec457e61d741a4f8afc49a1e3fb2b52b9b751cec59094cff00b872a7f2afea1e17fa52e328c151cfb0aaa5bedc1f2bf9c5fbadfa38af23f3ccc7c39a336e582a9cbe4f55f7eff99fa41457c356df1b7e205ba0592ee2b823bc90ae4ffdf1b454f2fc73f1e4abb44b6d1fba45cfea4d7e971fa4ff000938733a7593edc91ffe4ec7cf3f0ef33bdb9a1f7bff0023edee9d6b96d7fc69e19f0d46cfabdfc5132ffcb2077487e8a326be1dd4fe2378df575d97bac5ced3c158984208f42230b9fc6b8c777918b48c598f52c726be2f883e9574f91c324c13bf49546b4ffb7637bffe048f5f03e1bbba78bada768afd5ff91f4178cfe3bea5a9abd8785e33636ed90d70f83330ff0064745fd4fd2be7f9a696e2569a77692473966639249f526a3a2bf97f8b38db39e24c4fd6b37ace6d6cb68c7fc315a2fcdf56cfd0f2dca30980a7ecf0b0b777d5fab0a28ad2d234d9f58d52d74bb704c9752ac631db71e4fe039af99a142a56a91a3495e526925ddbd123d09c9462e52d91f617c0bd03fb2bc25fda52ae25d4a432f3d7cb5f957f3c135ed954b4eb1834db0b7d3ed942456f12448a3a0540001f90abb5feb0f06f0f53c8b24c2e534ffe5dc127e72de4fe72bb3f9a735c73c6632a6265f69fe1d3f00a28a2be98f3c2b2f5bd4a2d1f47bdd567e12d209263ce33b149c7e3d2b52bc6be3a6ac74ff03c9668d87d4268e1f7d8a77b7f2c7e35f35c639dac9f23c5e66f7a709497adbdd5f37647a19560feb78ca587fe6697cbafe07c5977732dedd4d7939dd2cf2348e7d59ce4ff003aaf4515fe4dd4a929c9ce6eedeacfe9749256414514540c28a28a0028a28a0028a28a0028a28a009ade796d678ee6062b244c1d5875041c8afd0df04f8920f14f86acb574237ba049973f7655e187e7cfd2bf3b2af5aea9a959218eceee78149c958e46419fa022bf5cf097c54abc198bad39527569554938a76f793d249d9ec9b5b6b7f23e6789787219b528479b96517a3b5f47bae9e47e98ee5f51f9d2d7e6b8f106bd91ff00131bbffbfeff00e35fa07e0e90cde16d2e5662e5ada32598e49247526bfb27c2ef1868f19e26be1e9615d2f67152bb9295eeedd91f95711f0acb29a70a92a9cdccedb5bf5674b451457ece7c91f1cfc7af12497fe234f0fc4c7c8d3d159c0e8659006fd148af05af50f13683ad78afe25eb5a7699119ae4ddca48270151380493d001815e7ba9e9b7ba3df4da6ea31186e206dae8dd8d7f96de24d6ccb32cf3199d6269cbd94aace11934f97dd76514f6ba8a5a1fd199042861f074b094dae6514daebaf5b79b28d14515f9e1ee05145140051451400514514005145140051451401f4efecedaa857d534676fbdb2741f4f94d7d475f04fc26d6868be36b29246db1dc1303fa7cfd3f5afbd81cf22bfd0ffa3867cb1dc231c249fbd42528fc9fbcbf36be47e1bc7b82747337556d349fcd68ff0023c17e3ee886f7c3306af12e5ec251b8ff00d339383f91af8e6bf4a75ed2a1d7346bcd26e0652ea168fe848e0fe079afce3d4ac27d2efee34eb952b2dbc8d1b03eaa715f87fd27f861e173ba39d535ee578d9ff8e1a7e31b7dccfaff000f331557072c249eb0775e8ffe0dca55f4e7ecf3ad857d434091bef62e231fa37f4af98ebb5f87baf1f0ef8b6c3502db623208a53d3e47e0d7e41e16f12ff61714613309bb414b965fe197bafeebdfe47d4f11603eb99755a0b7b5d7aad51fa175c8f8eb435f11785351d2caee7921668ffeba27ccbff8f015d6ab0650cbc823228232083debfd3dcd32fa39860aae0ababc2a45c5fa4958fe78c3d7950ab1ad0de2d3fb8fcc26568dcab65594907d88a719a561867623dc9aeffe28e82da078cafa055db0dc3f9f17a157e4e3f1cd79e57f9319e6575f2bcc2be5d5b4953938bf93b7e27f4c613110c45085786d249fde1451457927485145140051451401b1a3f87f58d7e53068f6cf7522f548f0580f5c67a577faafc1bf1a697a5c7a99b75b8caee96184ee922fa8eff8570de18d6aebc3faf596ad68c55e0990b0071b909c329f6238afd21521941ec457f47f82de17f0f71865f8bfae4aa46bd3b2d1ae55cd76a495b5d9a69b6befd3e0f8b788b1d9557a5ec945c257def7d375f89f39fecf12ceba6ead672865115c2361b8c315c118fc2be8daa169a5e9f6135c5c59c090c974c1e62831bd80c64e3be2afd7f6770170d55e1fc8a864f56a73ba575cdb5d3936b4e9a347e519de611c763678a846ca56d3e4828a28afaf3ca0a28a2800a28a2800a28a2803cd3e2fb32fc3dd54a9c7cb18ff00c88b5f0557e817c50b5379e04d5e1032443bff00ef860dfd2bf3f6bf843e94f4a6b88f0b51ece8a4be539dff00347ecde1c497d42a47af37e8828a28afe623f420a28a2800a28a2800a28a2800a28a2800a28a2800a28a2802d58c26e2f6de01d649517f3602bf4c2d9765bc483f85147e42bf3afc1b6a6f7c57a4db019df77171ec1813fcabf46870315fda7f450c1db099862adbca11fb949ffedc7e4de2555bd4a14fb293fbedfe41451457f5c9f9805145140051451400514514005145140051451400514514005145140051451401ffd2fdfca28a2800af32f89de028fc6da3116fb5351b505ed9cf46f5427d1bf435e9b457919f64783ce72fab9663e1cd4aa2b35faaecd3d53e8ceac1636ae12bc71141da513f31eeed2e6c2e64b3bc89a19e1629246e30cac3a8355ebee2f895f0bac7c636efa8586db7d5635f95f1f2cb8e8aff00d0f6af8bb54d2750d16f24b0d4e078278c9055c63a771ea2bfcd7f12bc2fccf8471ae9d74e7424fdca8968d767da4baaf9ad0fdfb20e21c3e69479a1a4d6f1ede9dd19d451457e647d005145140051451400514514005145140057be7c05f0d9d435f9b5d99731582ed42471e63ff80af055567608a3258e001dc9afbffe1a785c7853c27696520c5ccca27b8f50f273b7fe02302bf77fa3df073ce789e18cab1bd2c37befb737d85f7fbdff006e9f1bc6f9a2c265ce945fbd5345e9d7f0d3e677f451457fa2a7e121451450015f2a7ed13a897bdd2b4a0788e379987bb1c0fe55f55d7c3bf1befbed7e3db98b3916b14517d0e371fe75f837d23f33785e0ca9453d6ace10fc799ffe927da70161fda66aa6feca6ff4fd4f21a28a2bfcec3f740a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800afbdfe136a91ea7e07d3ca905add4c2c3b8287fc2be08af76f823e378f42d59b40d45f65a6a04796c7a24ddb3ecdd3eb8afdc7e8ffc5d4323e288c3172e5a75e3ecdbe89b69c5bf2bab795cf90e36cae78ccb9ba4af283e6f975fc0fb2a8a4041191cd2d7fa367e0c625af87347b2d62eb5eb5b654bebd5559a519cb05181c741d39c75af9bbf682f0d986eecfc4d027c930fb3ce40fe31ca93f51915f565739e2cf0f5af8a740bcd12e80c4e87637f7241ca30fa1c57e6fe23702e1f3ce1ac4e5585a6a33779c6c92fde5f9afeb2774df9b3dfc87399e0f30a789ab26d2d1ff876fc3a7a1f9c545686a9a5dee8da84fa66a1198a7b7728ea7dbb8f63dab3ebfcc0af42a51a92a35a2d4a2da69e8d35ba67f43c2719c54a2ee9851451591414514e44791c471a966638000c924d349b764036bd17c1bf0c7c4be32fdfdac7f65b2ff9f9981087fdc1d5bf0e3debd43e1cfc157b911eb5e2d4d919c34767fc4de85fd07b57d4d0c30db4290408b1c71a8555518000e800afea4f0b7e8ef5f3384733e25bd3a2f58d35a4e4bbc9fd95e5f13f2ebf9d711f1cc30cde1f2fb4a7d65d17a777f87a9f285cfecedac29cda6a9030e38746073f81ab163fb3adf17ff898ead1aa7fd318c93ff8f1c57d59457ef30fa3cf0446afb4fab49f97b49dbf3bfe27c5be3ace1c797da2ff00c057f91f9b5e23d19fc3fadde68f236f36b2140c463701d0fe35895dd7c4abc8afbc6faacf01053ce2808e876f15c2d7f9fbc4d84c3e1737c561b09fc38549a8fa2934bf03f6ec0549d4c353a953e2714dfad89ade792da78ee223878983a9f42a722bf463c25ad47e20f0ed86ad19cf9f0a96f66030c3f035f01f85bc3b79e29d6edf48b3524cac37b0e8883ef31fa0afd0cd1f4bb6d174cb6d2ecd76c56d1ac6a3e83ad7f537d157019946a637196b61a4a31d7acd3be9e89bbfaa3f38f122bd071a34bfe5e2bbf45ff05ede86957c73f1e7c3074df1045afc09883515c4840e04c83bff00bc3f957d8d5c2fc46f0da789fc277b61b419910cd01ee244e47e7c8fc6bf7af18383bfd63e18af85a6af560b9e1fe28f4ffb795d7ccf8be16cd7ea19842abf85e8fd1ff93d4fcf9a29f223c4ed1c80ab212a41ea08eb4caff311a69d99fd0c7dff00f0c7c4bff093f842ceee46dd710afd9e7c9e77c600c9ff007860d7a0d7c75f017c4c34dd7e5d02e1f10ea2b98f3d3ce4e9f891c57d8b5fe9cf839c5dfeb0f0b61f13395eac1724ff00c51d2ff3567f33f9eb8ab2bfa8e633a715eebf797a3ff27a1f3ffc7af0b9d434587c436c999ac1b6cb8ea627ff00035f1fd7e996a76106a9a7dc69d72a1a3b88da3607d1862bf3db55f0a6ad67e25b9f0ddb5b493dcc72b2a22292597b1fa63bd7f367d25b81e74338a39e60e0dac45a32495ff78b6dbaca3f8a67dff87f9c29e16583aaf586abfc2ffc9fe672d457d17e1ffd9fb53ba852e35fbc5b52dcf9310dec07b9e99aee5bf67df0b98762dddd093fbf95fe58af82cafe8fdc698da0b11f56504d5d29c945fddab5f3b1ed6278db29a33e4f697f44dafbff00c8f8eeac5a4904575149731f9d12b82f1e48dcbdc64722bd17e20fc33d4fc0d24739905d58cc76a4ca3055bd1876f6af32afcc339c9330c8f30960731a5c9560d5d3b3f34faa69fcd33e87098ca18ca2ab5095e2faafeb43edcd1fe19fc38f1068769a85ae9c162b9895d583b06191dce7391ef5e6be33f80d359c0f7fe1395ee020c9b59305f1fec3773ec6bd7fe0e994fc3fd37cdeb8931f4dc71fa57a7d7fa0785f0c385b8a38770b88c4e0a14aa55a70973538a84939453d2cadbf469a3f12abc459965d8fa94e9d6728c64d5a4ee9a4fcff0043f3a7c2de1bbed6bc5367a11864490cea2752a43468872e483d30077ef5fa2a0600154e3d374f8af24d422b6892e65015e60803b01d32d8c9abb5ea784fe16d3e0bc362297b6f6b3ab24ef6b7bb1bf2ab5debab6fd4e6e26e2379b54a72e5e5515b5efabdff00241451457eb47cc0514514005145140057944df13ad6e7c7769e0dd1a317037baddcff00c2855492abeb82393d2a5f8b1e33ff00844bc38c96cd8bebecc5073ca8c7ccff0080e9ef5f3a7c0fc4de3e8a494ee7f2666c9ea588eb5f83f883e2656c2713e5fc2995ced52a5483ab2ded1725ee2f392dfb2b5b73ed323e1e8d5cbebe6589578c632e55dddb7f974f33edda28a2bf783e2ccfd5acd751d32eec5c656785e33ff020457e6c5edbb5a5dcd6aff7a29190fd54e2bee2f8b7e335f0a786de2b6702feff0030c201e5548f99ff0001fa9af855999d8b31c927249ee4d7f0ff00d29b3ac062333c265d475ad4a32737d94ad68fae97f46bb9fb0787384ad4f0f52bcfe09356f95eeff4128a28afe533f480a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803d3fe0ed97db7c7fa70c6443be63ff00535f78d7c83fb3cd819fc497fa811f2db5a8507d1a56e3f406bebeaff0041fe8cf96bc3f087d61afe2d49cbe4ad1fce2cfc43c41c473e69ecd7d98a5f7ebfa8514515fd0a7c305145140051451400514514005145140051451400514514005145140051451401ffd3fdfca28a2800a28a2802a5f5fda69b6e6eaf6410c2a402edc01b8e064f6e6b1b5ef0af87fc536de4ead6b1dc2b0f9641f7c67b861cd667c47b09352f04eaf6d1025fecceea07aa8c8af97fc07f19758f0b84d37580da869c301771fdf4207f758f51fec9fc08afc778efc47ca325ce29e47c47453c2d785d4dae68a97334d4a3ae96b6a93b5f556d57d5e4b90e2b17859633013b5483daf676b26acfbee74fe28fd9fefa067b9f0add0b84ebf66b8f95c7fbafd0fa0040fad78b6a7e0bf1568ec5750d2ee6300e37042cbf815c835f76787bc71e19f1342b26977b1b39eb131db2038cf2a79e2babf91c7661f9d7c2669f47ae10cfe3f5fc8310e9c65afb8d5487c93775e9cdf247b187e38cd304fd86369f335dd72cbfaf91f990f6b7317fad8644ff007948fe74a9697527fab8647ff7509fe42bf4b64b0b197fd6dbc4ff00ef203fd2abca9a3e9f134d32dbdbc6832cc42a8007ad7c9cfe8a4a0dce799a505d7d9fff006f63d35e24df48e1f5ff0017fc03f3ef4ef04f8b355c7d874bb9756fe228517f36c0ae6e7864b799ede51878d8a30041c1070791c57d43f127e33da8b59b41f08b6f9240526bc1c2a2f4223f527fbdd076cf6f968924927927ad7f3ff88390f0ee4d898e5f9262a5889c6fed27a285fa4636bdedaddddad92ea7db6498cc762a9baf8ba6a9a7b2d6feaff4d04a28a2bf3c3dc0a28ad6d0f46bcf106ab6fa4582ee9ae1c28f403b93ec056f86c355c4558d0a11729c9a492ddb7a244549c611739bb247a9fc16f05b788bc41fdb1791e6c34d218e470f37555f7c753f87ad7daf5cef853c3763e14d0edb45b15c2c2b976eef21e598fb935d157fa71e13700c385321860a7675e7ef547fde7d3d22b45f37d4fe7be26ce9e678d7557c0b48fa77f9ee1451457e9a7cf05145140057e78fc42bbfb6f8db5a9f391f6c9501f643b47e82bf434f009afcd0d5ae0dd6a979727acb3c8fff007d3135fc9df4adc672e5b80c2dfe29ce5ff80c52ff00db8fd33c35a57af5ea76497dedff00919f451457f121fae051451400514514005145140051451400514514005145140052825486538239045251401edde04f8cdabf879d2c75d2f7f61c2ee2732c63d413f787b1fcebebad0f5ed2bc47609a96913acf03f71d54fa30ec6bf35ebe8efd9fbc4496ba85e787a76c0ba0268813fc6bc11f957f55f811e306691cce870e66d579e8d4f761297c5195bdd57ea9bd2cf6bab3b687e71c65c2d867879e3f0d1b4e3ab4b66babb77ea7d65451457f709f8f1e67f107e1a695e37b7f3b22d75188623b8519c8feeb8e323f515f1f788fc01e2af0bcacba95939881e278817888f5c81c7e38afd0da6ba248a52450ca78208c835f8a7887e06e49c5155e3a2dd1c43de5149a97f8a3d5f9a69f76cfaec8b8c7199745516b9e9f67d3d1ff00c39f983457e89df780fc1da8b992f347b4773d5bcb00fe6314cb1f0078334d712d9e916a8e3a318c311f89cd7e12fe8a79c7b6e558ea7c9df9657fbb6ffc98fb2ff889384e5bfb195fd57e7ff00f87fc33e05f12f8aee920d32d1c46c46e9e50562407b96c7f2c9afaebc0df09f41f08225d4e05f6a181ba7917853e88bc803dfad7a9471470a0489151470028c01f95495fb8787de03e45c35358caff00ed1885b4a4aca3fe18ea93f36dbed63e3f3ce33c6660bd943dc8765bbf57fa0514515fb89f1e15e67f137c756be0fd0e5589c36a372a638230791bb82e7d00a9bc7de22f1668f6be4f85b496bc9a4524dc1e522ff8003966fd3ebd2be45bbf0d7c41f14ea525e5f595ddcdd4adf33c8a40fc338007b0afc13c5cf13b1b96519e4d9161aa4f15356e750972c6fd62edef4bb5b45d5f43edb85f876962251c5e32a45535adaeaefd7b2f5d4e0e595e691a694967725989ea49eb5b3e1ef0e6afe28d463d334780cb2b9196e888bdd98f602bda7c2ff00b55bd74b8f12dc0b38382628be69587a67a2feb5f4df87bc31a2785ac858e8b6cb027f1375773eacc7926bf9f3807e8f19de6f5a38acf93a143769ff125e8becdfbcb5ec99f6f9df1ce130b074f06f9e7ff0092af9f5f45f79cf7807e1fe99e07d3c450e27bd94033dc11cb1f45f451d857a051457f7564b92e0b29c153cbf2fa6a14a0ac92feb56fab7ab67e378bc5d6c555957af2bc9f50a080460d1457a8731f0ffc65f091f0ef89dafadd36da6a399530380ffc43fad78fd7e80fc49f0a278b7c317166aa0dcc20cd6e7b875edf88e2be0296292195e1954aba315607a823835fe7178f3c09feaf7114b11878da8622f38f64fed47e4f55e4d1fbd706e73f5ec0a84dfbf0d1fe8c9ac6f2e34ebc86fad58a4d6f22c88c3b329c8afd14f0a6bd0f89740b3d62123f7f182e07f0b8fbc3f3afce1afa67f67ff146c9ae7c2f72fc3fefe0c9ee3ef01fcebddfa37719ff0065e7ef28af2fdd62745d94d7c3f7eb1fb8e3e3dca7eb381facc17bd4f5f975fbb73ea7aa9f60b1fb5fdbbc88fed0576f9bb46fc7a67ad5ba2bfbf674a13b73a4edaaf27dcfc4d49ad98514515649cef8b344b7f10f87ef74bb840e2585b6fa87032a47a1cd7e77a69f7126a234c894b4cd3792001d5b38afd323cf06bcfb4ef865e15d37c472f89e081daea462eaaed98e363d59171c13ee4e3b57e05e3178435b8bb1b82c561251838371a8defc9bab69ab5ad9799f6dc2bc510cae8d6a755377d62bcffe0e9f71d1f863495d0b40b1d297fe5de1443fef639fd6b7a8a2bf74c161296170f0c2d1568c128af44ac8f8dad56556a4aa4f76eff78514515d266145145001451450014d665452ec7014649f614eaf3ef89fe20ff8477c1d7b7687134cbe445fef49c57959e66f472bcbabe6588f82945c9fc95ff1d8e9c1e16789af0c3c37934bef3e46f8a3e2793c4fe2cb9995c9b6b63e44033c055ea7f1349f0a3525d33c79a5c9236c49a430b1ff00ae8081fae2bced98b31663924e49f734e8a49219126898a3a306561c1047208aff002d571762a7c491e23afef54f6aaa3f94af6f4b68bc8fe8d796535807808691e5e5fc2c7e9b5cdcc1696f25ddcb88e2894bbbb1c05551924d7ce3aa7ed0d6d06a4d0e97a77da2d118af98edb59c0ee0761f5af3ef12fc67d57c47e145f0f3db086795552eae15b89157d171c6eefcfd2bc56bfa2fc50fa44e22a55a343842b72c2ca539b8abf33fb16927b7576d5e89d96bf09c3bc0b4e319cf3485ddec95f4b77d3bf43aff001af8bef7c67ad49aadd031c78db0c59c88d076fafad7214515fcab99e6789cc717531d8d9b9549b6e4df56cfd1f0f87a7429c68d2568ad120a28a2b84d828a28a0028a28a0028a28a0028a28a0028a28a0028a28a00fb03f67ad2fecfe19bed55861af2eb629f5485401ff008f3357d015c3fc37d27fb17c13a4d930dafe4095c7fb72fce7f535dc57faabe1ae4dfd95c2f81c0b5671a716ff00c52f7a5f8b67f37f1062feb39956acb67276f45a2fc10514515f6e78e14514500145145001451450014514500145145001451450014514500145145007ffd4fdfca28a2800a28a2801924692c6d148372b82ac0f707ad7c1bf143c133f83fc41279687ec176c64b77ec33c94cfa8fe55f7ad73fe26f0d699e2bd265d23558f746fcab0fbd1b8e8ca7b115f94f8b9e1b52e2fca3d8d36a388a7774e4f6bf58bf297e0ecfc9fd2f0bf104b2bc4f34b5a72d24bf55e68fce14778d8346c558742a706babb2f1e78c74f50b6babdd003a0790c98f61bf3c56e78dbe187887c1d3b4ad135dd812765cc4a4803d1c7f09fd2bcdabfceec76173ce1bc6cf095f9e8555ba4dc7e69a7aaecd5d33f74a357098fa4aac2d38bf467a04bf14fc7f326c93589b1ecb1a9fcc2835ca6a3aeeb5ab36ed4efae2e719c096466033e809c0fc2b2a8ae4c7711e6d8d8f26331552a2ed29ca4bf16cd68e070d49de9538c7d125f90514515e29d414528058e14649ec2bd1fc21f0bfc4de2d9919206b3b3cfcd733a955c679da3ab1fd2bd7c9721cc737c547079651954a8fa455fe6fa25e6f447362f19430b4dd5c449462bb9c3699a65f6b17d169da6c2d3dc4cdb5110649f7f603b9afb6be1a7c35b3f05d9fdb2ec2cdaa4eb8924ea231fdd5fea7bd743e0cf01685e09b430e9b1efb890626b971991fdb3d97d8576d5fdd7e10781943871c735ce2d5317d16f1a7e9de5fdee9b2eeff1be28e319e3d3c3617dda5d7bcbfc9797dfd828a28afe8a3e1028a28a0028a28a00ab7ce62b2b89071b2276fc81afccc90e6473eac4feb5fa61a921974fb98d7abc4ea3ea4115f99f20c48c3d188fd6bf8d7e962e5cf96ae96abffb61fabf8696e5c47fdbbfa8ca2ad4365757104f730c65a2b650d2b0e8a18e067ea6aad7f20ce94e118ca4ac9eabcd5eda7cd347ea0a49b69740a28a2b318514514005145140051451400514514005145140051451400568e93aa5de8ba95bea962fb27b670ea7e9d8fb1acea2b5a15ea51a91ad4a56945a69add35b344ce119c5c64ae99fa2be0cf14d978bf42b7d5ad186e601668f3f34720fbc0fe3d3d457575f9ede07f1cea9e07d4bed767fbdb797027818fcaea3d3d08ec6bed7f09f8efc3de31b5136973813000c96ee71221fa771ee38aff467c23f18301c4f82a784c65450c6c55a517a73dbed47bdf7696a9df4b6a7e11c4fc2d5b2fab2ab4a37a2f67dbc9ff99d9514515fb71f20145145001451d2bcf7c4df12fc35e1aba8b4e9271737b33aa791090c5371032e7a2fae0f26bcbcdf3bc065743eb598d68d386d793b6bd9777e4b53a70b83ad899fb3a11727e47a15148ac19432f208c8fc696bd44ce60a4000e94b450014514500145145001451450015f1d7c71f04ae8dab8f1269f1edb5d41bf7c07449fb9fa3f5fae6bec5ae73c59e1fb6f13e8177a35c818990ec63fc2e3956fc0d7e6fe2af03d3e28e1fab814bf7d1f7a9bed25d3d24b47ebe47bfc379c4b2ec746b5fdd7a4bd3fe06e7e7156c681ac5ce81acda6af68db64b6955feabfc40fb1191546face7d3ef27b1ba52935bc8d1ba9ecca706aad7f98b46ad7c1625548371a90775d1a69fe699fd0b28c2ac1c5eb16bef4cfd2fd23528357d32db53b63ba3b98d6453f515a35e03f013c4bfda3a0cfa04ed9974f6053279313e71f91e3f2af7eaff0055781789a9f1064386cda0f59c57379496925f7a67f3767397cb058da9867d1e9e9d3f00a28a2beb4f3028a28a0028a28a0028a28a0028a28a0028a28a002be59fda1f5b66974df0fc6df2aeeba917b13f753f2f9bf3afa9abe0ff008c3a81bff1e5f8ce56df6420761b473fad7e01f492cea582e1178683b3ad38c3e4bde7ff00a4a5f33edb80708ab668aa3fb09bf9edfa9e5f451457f9e47ee4145145001451450014514500145145001451450014514500145145001451450015b9e19d31f59f1069fa5a0cfda6e23423db3cfe95875ee7f013413a8f8b25d5e45cc3a6425813ff003da6f957f25dc6beb7813219675c4383cb12ba9ce37ff0a7797dd14cf3339c6ac2606ae21f44edebb2fc4fb2628d2189228c61514281e800c0a928a2bfd5e8c52565b1fcd6ddf561451453105145140051451400514514005145140051451400514514005145140051451401ffd5fdfca28a2800a28a2800a28a280239628a74314c8ae8dc1561907f035e65af7c20f066b8cd2fd97ec92b124bdb9dbc9393c74af51a2bc5ceb8732bce297b0cd30f1ab1fef24ede8f75f23af078fc4e165cf869b8bf267cd177fb3ada3396b3d59e35ecaf186fd73596ff00b3a5f6ef935788afbc6735f55d15f9a623c01e06ab2e6fa9dbd2735ffb71f430e37ce22aded6fea97f91f2ec5fb39bff00cb6d631feec5fe26ba1d3bf67df0f41b5b50bc9ee08ea140453f857d03457560bc0be08c34d4e1814dff007a5392fb9c9a32abc659bd45675ade892fd0e2746f875e0fd0f0d65a7445c747906f6fd6bb50028c28c0f6a5a2bf4acb728c0e5d4bd86028c69c7b462a2bf0478188c556af2e7af3727e6ee1451457a27385145140051451400514514008464115f9bde28d38e91e23d4b4d231f67ba9507ba86383f88afd22af8afe3b68674df187f6922e22d4625933dbcc4f95bf402bf993e94591cb15c3f87cca0aee8cecfca33567ff0093289fa178758c54f1b3c3bfb6bf15ff0001b3b3f87de1282fbe13eb276069f5149181c73fb91941f8115f30904120f51c1afb4be05de477be09366704c133c6c3d9b9af94fc69a4b689e29d4b4d23022b87dbfeeb1c8fd0d7e1be2870f5087086439be123eefb2e4935dfe3ff00d29ccfb2e1ec74de698dc2d57af35d7a6df958e628a28afc00fb40a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800ab16b7775633adcd9caf04a9cabc6c5587e22abd1570a928494e0ecd75426935667b3689f1c7c61a5a2c37862bf45e3328c3e3fde1debbe83f68b8768fb4e90f9efb2507f9815f2dd15fa7e53e34f19e5f4d51a18e938afe6519fe324dfe27cf62784f29af2e69d157f2bafc9a3eb03fb45e958e349b8cff00beb8ac3bff00da26f5d5974dd292339e1a590b7e800fe75f35d15e9627c7de38ad0e478cb7a4209fdfcb730a7c1793c1dfd8dfd5bff33d275cf8b1e35d711a192f4db44dd52dc797c7fbc3e6fd6bced2575996724960c1b2793907351515f9a66dc4399e695962331c44eacd6ce526ede97dbe47bf86c0e1f0f0e4a10515e4ac7e91f866fc6a7e1fd3ef97fe5b5bc6df8e056ed78f7c10d5c6a5e098ad99b32594ad091df1d47f3af61aff00537833388e6b91613318bbfb4a716fd6dafdcee8fe72cdf0af0d8dab41f493fbba7e014515f2c7c4cf885e2bf0b78f047637056d2148d84047eee407ef67dcfaf6ae1e39e38c0f0ae02198e61193a6e6a1eea4dabdf5776b456f536c9b27ad99567428349a4debe47d4f45733e13f1469fe2ed1e2d5f4f380e312464fcd1bf7535d357d4e031f87c6e1a18bc2cd4a9cd2716b669ec79b5e84e8d474aaab4968d0514515d66414514500145145007c83f1e7c2834fd5e2f11daa622bdf926c0e922f43f88af9f6bf41fe2378797c49e12beb10b9992332c3fefa723f3afcf9652ac5586083823dc57f9ddf487e0e8e4dc4af194236a5895cebb297db5f7d9fccfdd781f3578bcbd529bf7a9e9f2e9fe5f23d13e15f881bc3be34b2b866db05c9fb34de9b64e013f46c1afbefaf22bf30a391a29165438642181f715fa2fe0dd5c6b9e18d3b530726681777fbca307f515fab7d15f89253c3e2f22a8fe16aa47d1fbb2fc547ef3e6bc47c0253a58c8adfdd7f9afd4e9a8a28afebb3f2f0a28a2800a28a2800a28e9c9af13f1ff00c64d2bc3224d3745db7fa90e0f3fba84ff00b44753fec8fc4d7cf713715657c3f8378fcdab2a705df76fb456edf923bb2fcb7138eaaa8e1a377f97abe87b05eea361a745e75fdc476e83f8a460a3f5a874dd634bd62379b4bba8ee9236dacd136e01bd0d7e79eb3e22d7fc577e26d4ee64b89a560a899c28dc780aa3815f737c3cf0b41e12f0c5ae9c8a04f2289ae5bbb4ae067f2e83d857e5de1d78bb89e31ce6b61f0184e4c252577393f79b7f0ab25657d5daef45b9f479f70bd3cab0909d6a97ab27a24b4f3d773b8a28a2bf723e384270093e95f9c9e31bafb678a755b8fef5d4bfa363fa57e8a5db6cb499b38c46c7f206bf342f66371793cedd6491dcff00c08935fc87f4afc65b0f97617bb9cbee515fa9fa8786b4bdfaf53ca2bf3ff22b514515fc5e7eb0145145001451450014514500145145001451450014514500145145001451450015f6a7c09d0db4bf079bf957126a5319bdf628dabfc89fc6be3bd274f9b56d4ed74cb705a4ba9922503d58e2bf4834ad3e1d274cb5d36dc623b689235ff808c57f52fd177865e2338c4677517bb463cb1ff14f7fba29fde7e75e22e61ecf090c24779bbbf45ff07f23428a28afee73f1c0a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd6fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800af38f89de081e36f0fb5b5b6d5bfb63e6db33700b77427b061fae2bd1e8af273dc97099be5f572dc7479a9d44d35faaecd3d53e8ce9c1e2eae16bc71145da517747cd5f01f4bf1068d7fac58ea96b35b42047c48a54190641c1e878ee335cc7ed05a0b5a6bb67af44bfbbbd8bca908ff009e917afd548fcabebb000e40c66bccbe2ef87cf883c1576912ee9ecc8b98b1d729f780faa935f8af1678574f0de1d56e1ec3d47565479aa41b5add49ced65e4dc7e67d7659c48ea67b1c7548a8a9da2ede96bfdf667c1b451457f9e67ee214514500145145001451450014514500145145001451450014514500145145001451450014514500145145007d03f0035e165aedd6892b612f630e83fdb8ff00c41fd2bebfafcddf0c6ad2687afd8ea919c7913296ff0077383fa57e8e5bcab3c11cebc891430fa119afef4fa31712fd7387eae5351fbd87969fe19ddaff00c9b98fc63c43cbfd963638a8ed35f8aff81626af993f683f0f3bc565e23857223260988ec1b9527f1e2be9bac1f13e876fe23d0af347b8195b88caa9f46fe13f81afd7fc49e145c47c3b89cad7c725787f8e3ac7ef7a3f267cbf0fe67f50c7d3c43d93b3f47a33e36f845e357f0af8852d6e9f1617e447283d15cfdd7fe86bee5565750ea7208c835f99fa9585ce93a84fa7dd2949ada428c3a72a6bec5f831e3b5f1168ff00d897f20fed0b05c0c9e6484602b7b91d0d7f387d1c7c409e1ab4f8433476776e9dfa497c50fcda5defdcfbde3dc915482cd30eafb735bb747fa7dc7b6d14515fd907e52145145001451450021008c1e41afcf3f883a3ff0061f8c352b0036a0999d07fb2ff0030fe75fa1b5f1afc7eb2583c596f76a31f68b619faa922bf9bbe93d944711c314f1d6f7a9545f74934ff001b7dc7df78798a70cc2547a4a3f8ad7fccf09afb33e016abf6cf094da7b1cb595c301feeb8c8af8cebe8efd9def8a6adaa69e4f12c29201eea4e6bf9c3e8f99b3c171ae1e17d2aa941fce2dafc628fbde37c32ad94d47d6367f8ff00933eb2a28a2bfd203f040a28a2800a6492c70c6d2cac111012ccc70001dc9a712141663803924d7c8bf17be28b6ad2c9e1ad02522ce33b6e26438f35875507fba3f5af85f1078fb2fe13cb258fc6bbc9e908759cbb7a2eafa2f3b27ece479257ccf10a8d2d1757d12feb645ef89df195eec49a0f84a52909cacf76bc330eeb19ec3d4fe55f3692589663927924d2515fe6d71971b66bc4f8f78fcd2a5dfd98af862bb4574fcdeecfdf32aca30d97d15430d1b777d5beeceb7c07025cf8cb468241b95eee2041fad7e8981818afcdbf0cea2ba478834fd4dbeedb5c4721fa03cd7e8f5bcd1dcc11dc4477248a194fa82322bfab7e8a588a2f2ec7d04fdf538b7e8e365f8a67e6de24c27ede8cfecd9af9dc9a8ae3bc6fe31b1f05e8afa9dd0f32563b208b383239e83e83bd7c8d77f1a3c7d71786ea2be581724ac491aec507b60839c7be4d7eb5c7be32641c2789860b1dcd3aad5dc6093715dddda4afd15eff0023e6725e14c6e674dd5a36515d5f5f4b267d75e32f15e87e17d2669756b808d2a32c71af323923b0afcf173b9d987724d6aeb5aeeade21be7d4758b87b99dcf56e83d940e00f615915fc4fe2e78a33e32c7d3a94e97251a57504fe277b5dcba5dd968b6eecfd7386387565545c5caf395afdb4ec1451457e467d305145140051451400514514005145140051451400514514005145140051451401ef7f013c35fda3e229b5f9d730e9a84479e86690607e4b9fc715f62d79d7c2df0d7fc231e10b4b69536dcdc0fb44f9ebb9f900fd06057a2d7fa77e0e708ffabdc2d87c2d48daacd7b49f7e69747e8ad1f91fcf5c5799fd7b329d48bf763eeaf45fe6eec28a28afd44f9c0a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803fffd7fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a6491a4b1b4520dcae0ab03dc1eb4fa29349ab304cfce7f1ae931e85e2bd53498798edee5c27fba7e61fa1ae5ebd53e345afd9be226a4c0604eb04bf9c4a0fea2bcaebfc9be35cbe381e20c760e9ab46156a24bb25276fc0fe98ca6bbad81a3564eedc62ff00041451457cc1e805145140051451400514514005145140051451400514514005145140051451400514514005145140057dc5f067c5a3c47e164b2b87cde699882404f2c9fc0df9707dc7bd7c3b5dbfc3ff00174de0df1143a98c9b77fdd5ca0fe28d8f3f88ea2bf57f06f8effd57e22a788acff7153dca9e49bd25ff006ebd7d2eba9f35c55937f68e0654e0bdf8eb1f5edf35fa1fa134557b4bab7beb58af2d5c490cc81d1d790558641ab15fe99c2719c54e0ee9eccfe7d945a767b9f2d7c78f04323af8c74e8f28d88ef028fba7a2b9f63d0fe15f3e683addff0087755b7d5f4d72935bb6e1e8c3ba9f504706bf47afacadb51b39ac6ed0490ce8d1ba9e855860d7e7ef8f3c2373e0cf104da5cb9681bf7b6f27678989c7e23a1afe1cfa41f00d6c9735871665578c2a49395b4e4a8b552f2e6b5ffc57ee8fd8781f3b862f0cf2dc4eae2b4bf58f6f97e47dc5e0ff001669de31d161d5ac186e236cd11fbd1483aa91fc8f715d557e7a781bc6ba87827575beb525ede4216e21cf0ebfe23b1afbc3c3fe20d2fc4da645aae953096194723f891bbab0ec457efde0ff008af86e2cc02a1896a38ca6bdf8ff0032fe78f93eaba3f2b5fe278a786aa6595b9e9abd296cfb793fd3b9b7451457ece7c985145140057c89fb4348875ed3a207e65b76247d5abebbe9c9af81be2aebe9e20f19decf0b6e86dc8b78cf62138247e35fcf3f497cd6961b84bea927ef559c525fe1f79bf9597de7dd787d86954ccbdaada29fe3a1e715edbf016529e35651d1ed6407f315e255ed7f01a367f1b161d12da427f315fc79e13737fae39772efed627ea5c4b6fecbaf7fe567dab451457fa927f398514571be3bf175af837c3f36a93106661e5dbc7dde43d3f01d4d7066999e1b2ec1d4c7e325cb4e9a7293ec97f5a7737c361ea622ac68d257949d91e67f1abe212e8b647c2fa5483edd74999d94f30c4ddbd99bf973dc57c7b57751d42eb55be9f51bd7324f70e64763dc9aa55fe60f893c7b8ae2cce679856d29ad29c7f963d3e6f76fbf958fe86c8325a7966115086b2de4fbbff2ec1451457c01ed857d5df0a3e2ce94ba4c5e1ef13dc2da4d68bb60b895b6c72463a0663c065f7ea3debe51a2bedb80f8f732e13cc7fb472eb3bab4a32f864bb3b6ba3d535b7de9f919ce4b87cce87b0afea9add33d53e2cf8dd3c61e20c58b96d3ec818e03d9cff13fe27a7b015e574515e2711e7f8bcef32ad9ae39dea54777d9764bc92d17923b30382a584c3c30d457bb1560a28a2bc43ac28a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a002bd3be12f853fe129f16db8b84dd676045ccf91c36d3f221ff79bafb035e635f757c20f09ff00c233e13826b84db79a885b99b3d5430ca2fe0bd7dc9afd87c0fe09ff0058b89a92ad1bd1a3fbc9f6767eec7fede97e099f2dc5f9bfd432f9383f7e7eeaf9eefe4bf1b1ead451457fa567e00145145001451450014514500145145001451450014514500145145001451450014514500145145007ffd0fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803e3cfda16c443e2bb1be55c0b9b20a4fab44edfd18578157d63fb44e9e64d274ad4c7fcb09de23f49141fe6b5f2757f9a3e3b658f05c6d8d56d26e335ff006f4537f8dcfe81e0dc47b5ca28bed75f737fa5828a28afc84fa70a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803eaaf80de3333412784afe4cb45992d771e4a9fbca3e9d457d2d5f99ba66a57ba3dfc1a969f218ae2ddc3a30f51fcc1ef5fa07e08f15daf8c340b7d560204bb424f1e79490751f4ee3dabfbc3e8e7e244733cbbfd5cc6cbf7f457b8dfdaa7dbd61b7f86dd99f8d71e642f0f5febf457b93dfca5ff07f33afae03e21781ecfc6da335b3e12f21cbdb4b8e437a1f63debbfa2bfa2739c9b079ae0aa65f8f873529ab34ff00ad1add3e8cf85c262eae1ab46bd17694763f33f54d2ef746bf9b4dd462314f03156523f51ec6ba6f04f8eb57f046a22eec7f7d6ee409ed98e1645f63fc2de87f9d7d65f13fe1bdb78cb4f37764ab16ab6ea4c4f8c79807f037d7b1ec6be1fbab5b8b1b992d2ee368a6858a3a30c32b0ea08aff0039b8f382738f0f73d862305524a17bd2aab4ff00b75f4e65b35b35adacec7eef9366f85cf306e1562afb4a3faaf2ecfa1fa11e11f1be85e32b25bad2e602503f7b6ef812c67d08cf4f71c57615f99763a85f69970b77a7cf25bcc9d1e362ac3f115ed3a07c7af13e9b12dbead0c5a8a291f3b7c9263ea3827debf7fe08fa4e65d5e8c70fc4b4dd3a8b79c55e0fcdc57bd17e8a4bd0f89cdfc3daf0939e5f2e68f67a35f3d9fe07d97457cd63f68bb1f28b1d224dfd97cd18fcf6d707e24f8ebe27d5e16b6d2d134c8db3978c932e3d98f4fa8afbfcdbe909c1783a0ead1c43ab2e91842577f3928a5f3678986e06cdaacf9670515ddb5fa36cf63f8b5f126dbc3ba6c9a2695307d4ee54a9da73e4a1eac7dc8e82be2d24b12cc7249c927b9a7cd34b712b4d3bb49239cb331c924fa9351d7f127893e22e3b8bf33faee2572d38e908277515fab7d5fe88fd7720c8a8e5787f634f593d5beeff00cbb057d2bfb3b698cd79aa6accbf2a2242a7fda3927f4af9ac024e0724d7debf0a3c367c37e0fb58a65db7175fe91367ae5fa0fc057dcfd1cb876a661c5b0c6dbdcc3c5c9bf369c62bef77f91e3f1de3e3432c952eb3697eaffaf33d2a8a28aff440fc2c6bba448d24842aa82493d0015f087c55f193f8b7c472085c9b2b2262807638fbcdf535f40fc6df193e83a12e8d6326cbbd472a483f32443ef1fc7a57c5f5fc5df497f10dd4ad1e15c14bdd8da555aeaf78c7e5f13f3b763f59f0ff0023e583ccaaad5e91f4eaff0040a28a2bf910fd3c28a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a29caacec11012cc40007524d349b76407a57c2af089f15f8a6149d3759d9913cfc70429f957f13fa57de60050154600e00af35f857e0f4f097862149d00bebc027b96ee0b7dd4fa28fd735e975fe94f823c05feacf0f43eb11b622b5a73eeaebdd8ff00dbab7fef367e05c5f9d7f68639f23f721a2fd5fcff002b0514515fb11f28145145001451450014514500145145001451450014514500145145001451450014514500145145007fffd1fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803cbfe30e9bfda3e02d43032d6db271ff00619fd2be0fafd31d56c23d534dbad3a5e52e61788ff00c08115f9b17d6b2d8dedc594c0ac96f2bc6c0f5050906bf87fe95192ba59ae0f358ad2a41c1bf383bfe52fc0fd83c38c5a961aae19ef177f935ff00ab456d78774a6d735db1d257fe5ea748ce3aed27e6c7be335e93f15fe1b1f075dc7a96968cda5dc90a3bf95263ee93e87b1afe79c0708e678cc9f119e61a1cd468ca319beab9badbb2d2efa5d7cbee2b66787a58a860ea3b4e69b5f2feb4f43c728a28af993d00a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800aeffe1ef8eaf3c11acadd29692ca7212ea1cf0cbfde1fed2f51f95701457a992e738ccab1b4f31c04dc2ac1dd35fd6a9ecd755a1cf8bc2d2c4d1950acaf1968cfd31d2f53b2d66c21d4b4e944d6f3a86461e87b1f423b8abf5f0c7c32f89777e0cbc165784cba54effbc4ea6327f897fa8afb72c6fad352b48afac6559a0994323a9c820d7fa53e17f89d81e2fcb955a6d471104bda43b3eebbc5f47d367e7f80f1170f56caebf2bd60fe17fa3f32dd791fc47f859a7f8ce237f65b6d75541812e3e5940e8af8fd0f5af5ca2becf88b8732ecf7033cbb33a6a74e5d3b3e8d3e8d7468f27018faf83acabe1e5692feacfc8fcd4d6744d4fc3f7d269dab40f6f3c67a30e08f507a11ee2b2abf45bc51e0dd03c5f6bf66d66dc3b2fdc957e5913e8dd7f0af977c59f02fc43a4f9975a0b0d4ad97276642cc075e8701b18ec73ec6bf83fc42fa3f67992559e27298bc461b75caaf38aed28ad5dbbc6fe691fb3649c6f83c6454312f92a79ecfd1ff0099e174558b9b4bab299adef21920950e1924528c0fb8201aaf5f814e12849c26acd773ed534d5d0515a1a7693a9ead38b6d2ed26ba94ff000c285c8f7381c0f73c57d17e06f8132318f51f18fc8010cb668c09ff0081b0247e02becb83bc3ecf389b12a865745b5d66f4847d65b7c95dbe88f2b34cef0797d3e7c4cecfa2eafd11cafc24f86975e21bf875fd56331e976ec1d030c19dd7a01fec83d4fe15f67001405518038005456f6d05a4096d6b1ac5146a1511461540e80015357fa2be1bf87781e10cafea5867cd525ace7d652fd22ba2fd59f84e7f9ed6cd311edaa6915a25d97f9f70aab7d7b6da6d9cd7f79208a0b74692476e815464d5aaf967e39f8f3cc73e0ed324ca2e1af194f523909f8753ef5d9e2171a61b85f24ab9a57d64b4847f9a6f65fabec9332c8f28a998e2e38686dbb7d975ff0081e678978dbc513f8bbc4575abc9911336c810ff000c4bf747d4f53ef5c951457f96f9966388c7e2ea637152e6a936e527ddb7767f45e1e8428d38d1a6ad14ac828a28ae1360a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800af71f829e076f106b3fdbf7f1e6c34e71b770e249fa81ee17a9fc2bca3c3da1de788f59b5d1ac54b4b72e178fe151cb31f6039afd0bf0ee8363e1ad1edb46d3d36c56e9b73dd98f2cc7dc9e6bfa1be8fbe1c7f6ee6dfdaf8d8ff00b361da7aed29ee97a47e27f25d4f87e37cfbea586faad27fbc9fe0babf9ecbe66dd14515fe831f8785145140051451400514514005145140051451400514514005145140051451400514514005145140051451401fffd2fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800af88be377871b46f1849a84498b7d517cf53dbcce920fcf9fc6beddaf37f8a1e0dff0084c3c352c16ca0df5ae66b6f761d53fe043f5afc97c6ae0a9f1270cd5a1878deb52fde43bb693bc7fede8b6979d8fa7e12cd9603308cea3b425eebf9f5f933e42f8673c56fe3ad1e49880be7edc9e80b2903f535f796b1a4d8ebba6cfa56a31896dee10a329fd08f707915f9b90cb71a75ea4cb98e7b69430cf055e339fcc115fa27e13f105b78a340b4d66d7813a0debfdd71c32fe06bf18fa2f66d85ad83c7f0f6292726f9f95fda8b5c9256f2b2baf33eb3c44c354855a18ea6f45a5fb34eebf5fb8f85fc75e0bbff00056b4fa7dca96b7932f6d363e5913ebfde1d08ae2abf45bc61e12d37c65a349a56a0307ef43281f34527661fd477af827c4de1bd4bc29abcda36a89892239571f72443d194f707f43c57e4be34784d578571df5cc126f0751fbaff0091bfb0ff00f6d7d579a67d3709f134732a3ecaae9563bf9aeebf539fa28a2bf0e3ebc28a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a002bd53e1dfc50d4bc1530b39c35d696ed97873f3267ab267a7d3a1af2ba2bdae1fe21cc324c74331cb2a38558ecd7e29ad9a7d533931b81a18ba2e8622378b3f4a743d7b4af11e9f1ea7a3dc25c4120eaa7953e8c3a823d0d6bd7e74f853c63adf83efbed9a44b80dfeb216e63907b8f5f7eb5f63782fe2b7877c5e8b06efb0df63e6b7988e4f7dadc061f91f6afefcf0c7c73cab89611c1e3daa38bdb95bf766fbc1bff00d25bbf6b9f8af10f076270127568273a5dfaaf5ff3fc8f50a28a2bf763e30cfbed274bd493cbd46d21b95f49515c7ea0d610f01f8343f99fd8f679ff00ae2b8fcb18aeb68af33159265d899fb4c4d084e5ddc62dfded1d34b1988a6b969cda5e4d956d6c6cac6310d9c11c11af458d0228fa0000ab54515e853a50a7150a6924ba2d0e7949c9de4eec28a6bba46a5e460aa064927000af09f1f7c6ad2f448e5d37c3645eea1ca99460c317b93fc47d871ea7d7e738ab8c329e1dc1bc6e6b59423d17da93ed15bb7fd3b23d0cb72ac563eafb2c346eff0005eace9fe267c44b3f0669ad6f6eeb26a97084431752a0f1bd87a0af85ae2e26bb9e4b9b872f2cac5dd8f2493c9a9b50d42f755bc96ff5099e7b899b73bb9c926a9d7f9d3e28f89b8ce31cc5569ae4a10ba842fb5f76fbc9f5edb2f3fddb87787e965543923acdfc4ff45e4828a28afcbcfa10a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a500b1000c93c01495efff00067e1c36b5789e28d662ff0040b66cdbc6dff2da51fc447755fd4fd2bea383b84b1dc499ad2cab011f7a5bbe918f593f25f8bb2dd9e766b99d1c061a589aef45f8be891eadf06fc023c33a4ff6d6a31e352bf4070c398a23c85fa9ea6bdae8e9c0a2bfd43e15e19c164195d2cab011b420be6df593f36f567f3be659855c7626589acf57f82e897a0514515f427005145140051451400514514005145140051451400514514005145140051451400514514005145140051451401fffd3fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803e72f8a7f0826d62ea4f11785a35fb549cdcdb70a246fefa76dc7b8efd6ba8f829a16bfa07876e6d75d81ed8b5cb3431bfde0b800f1ee41af65a2bf34c0f85593607899f1460b9a1564a5cd14d72372dddad74fad93b5f5b1f415b893175b2ffecead67156b37bab740af3ff885e03b0f1c6926070b15f420b5b4f8e55bfba7d54f7fcebd028afb7ce726c1e6d82a997e3e0a74a6acd3feb46b74f74f53c8c262eae1ab46bd07692d8fcd1d5f48d4342d426d2f5389a1b881b6b2b7e847a83d8d66d7d09fb435998fc41a75e81c4d6c509f746ffebd7cf75fe5bf1f70d4387f883159453939469cac9bdda69357f933fa2f25c7bc6e0a9e29ab392d7d7661451457c79ea0514514005145140051451400514514005145140051451400514514005145140051451400514514005391de360e84ab0e4107045368a69b4ee80f56f0bfc61f1778736c324aba85b0eb15ce4903d9c7cc3f515ef1a27c7af09ea0aa9aac7369d29ebb87991e7fde5e7f315f18d15fadf0a78dfc59914151a388f6b4d7d9a9ef25e8eea4bd13b791f3399708e598d6e73872cbbc74ff0081f81fa2d63e38f086a2a1ad357b46cf4065543f93106b58eb7a305de6fedb6faf9a98fe75f9a74ededfde3f9d7eb187fa5763d42d5f2f839794da5f734ff33e6a7e1ad06fdcaed2f44ff547e876a3e3ff0006e94a5af357b6047647121fc9735e6bacfc7ff0bd9864d26da7bf9074247949f99c9fd2be39a2be7739fa4f71362a2e180a54e8aef67397df2d3ff253bb09e1e65d4ddeb4a53fc17e1afe27a3f8b3e2978a7c58cd14f3fd96d0f4b780955c7fb47ab579c51457e099d67f98e6f8978ccceb4aa547d64eff0025d12f25647da61307430b4d52c3c1463d90514515e41d2145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014515d1f85bc31a978b7578b49d3532ce73239fbb1a7763f4aebc0603118dc4430984839d49b4925ab6d99d6ad0a50756a3b456ad9d0fc38f01dcf8df5a48240e9a74043dd4abc7cbfdc53fde6e9edd6bef2b2b3b5d3ad22b1b28d628204091a2f4551d05637857c31a7784b468747d357e58c65e423e691cf566f73fa0ae8ebfd25f087c31a1c2395daada58aa967525dbb413fe58fe2f5ed6fc0b8a388679a627ddd29c7e15fabf37f820a28a2bf5c3e6028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00fffd4fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803e77fda1b4df3b43d3f5351cdbdc18d8fb4838fd457c8f5fa01f147483acf81755b745dd2430fda1077cc3f31fd01afcffaff003f3e933933c2715c71897bb5a9c5fce378bfc12fbcfdbbc3fc5aab963a5d6126be4f5ff30a28a2bf9d8fba0a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28abfa66997dacdf43a6e9b134d713b054451ebfc80ee6b5a142a56a91a3462e5293b24b56dbd9244ce71845ca4ec913e89a26a3e21d4e1d274b88cb713b6001d00eecc7b01dcd7de1e02f02e9de07d245a5b812ddca035cdc11f33b7a0f451d8566fc39f87361e08b0124989b539d479f3ff77fd84f451fad7a657fa05e09783b0e1ba0b36cd629e326b45bfb38be8bfbcfed3f92eb7fc4b8bb8a9e3e7f56c33fdd2ffc99f7f4edf78514515fd0a7c3051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451401fffd5fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a280192224b1b47200cae0ab03d083c115f9cfe30d064f0cf896ff46604241337944f7898e50ffdf35fa375f31fed03e17774b5f15db2644605bdc903a027e463ed9e2bf9dfe923c24f34e1c599d18dea619f37fdb92b297dda3f44cfbae01ccd61f1ef0f37eed456f9adbf547cb7451457f9f67edc1453e38a499c2448cec7b28c9fd29d2c134076cd1b467fda047f3aae4972f35b415d5ec454514548c28a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a2ba8f0af84759f17ea2b61a54448c8f3256e238d7d58ff004eb5db97e5d89c7e26184c1d373a9376514aedb32af5e9d1a6ead5768addb33b43d0b54f11ea51695a440d3dc4a7000e8a3bb31e8147735f71fc3ff873a5f81ec4150b71a8ca079f72477feea6790a3f33deb43c11e05d27c11a68b4b21e6dcc9ccf72c00791bd3d947615db57f7f783de09e1f86611cd3344a78c6bd553bf48f797797c969abfc538ab8baa6612786c369497df2f5f2ecbeff228a28afe813e1c28a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00ffd6fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800aced5b4bb3d6b4db9d2afd0496f751b46ea7d0f71ee0f20f635a345655e853af4a546b4538c934d3d9a7a34fd4a84e50929c1d9a3f393c5be19bdf09eb971a3de29fddb13139e92467eeb0fc3f5ae6abf417c77e03d2fc71a6fd9ee8795770826dee147cc84f63eaa7b8af8afc53e04f127846729aa5ab1832425c460b44df88e87d8d7f9cbe2c783b98f0c632a62b094dcf052778c92bf25feccfb5b64f67eba1fbcf0d714d0cc692a751a5556ebbf9aff2e87bc7ecf163a54ba7ea978d123df24e8859802cb115cae33d32d9ce3d2bdf354f0de83ad42d06a76305c23000ee419c0f71cd7c23e03f1b5ef81f58fed0b74f3a191764f09380ebfd08ed5f67f83be21f873c6a8cba5cac97318cc96f28db228f51d430f707eb8afe85f01f8cf8731fc3b4786b13c8abc79938492fde5db7757d24da7aad5e8f4b1f0fc679563e8e3a79853bb83b6abecf4f97e478e78cbe0245e5c97be0f90ab28cfd9256c83ec8e791f8d7ccf7f617ba65dc963a842f6f7109daf1c836b035fa6b5c6f8bbc09e1ff0019db795aac004e8088ae538953f1ee3d8f159788df471cbf318cb1bc376a35bf93fe5dcbd3f91fa7bbe4b71e43c7b5e8354b30f7e3dfed2ff3fcfd4fcf1a2bd43c6df0abc45e0f66b95437ba7e4e2e22192a3fdb5eabf5e95e5f5fc579f70f66592e2e581cd28ba751746b7f34f66bcd5d1fad60f1d43174956c3c94a2fb0514515e31d41451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145490c32dc4ab0c08d248e4054504b127b003ad7d2bf0ff00e073cbe56ade32528870c9640fcc7d3cc23a7fba3f1afb1e0ce03ce38a319f54caa95edf149e918aef27fa6efa23cbcd739c2e5d4bdae2656ecbabf4479c7c3ff85dabf8d264bb9835a6960fcd70c39931d4460f5f4cf41fa57da3e1ef0d68de16d3d34dd1add60897a9eaeeddd998f249ff00f556c410436b0a5b5b22c7144a1111461555780001c00054b5fe83f86de13653c2387e6a2bda625af7aa35afa47f963e5bbead9f87e7fc4d8acd27697bb4d6d15f9beeff00a41451457ea87cd8514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451401ffd7fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a867b782ea2682e6359636182ae0303f81a9a8a99454938c95d31a6d3ba3cc752f841e05d4a433358881cffcf162a3f2e957fc2bf0dbc35e10ba7bed323733b8dbbe46ce14f615dfd15f2b478138768e3239851c1538d68bba92824d3efa2dfccf4e79d63e749d0956938be9761451457d61e58d744910c722865618208c822bc43c67f04743d799ef74461a6ddb649551989cfbaf6cfb57b8d15f37c4dc2394710617ea99bd05523d2fbaf38c96a9fa33bf2fcd31581a9ed70b3717f83f55b33f397c4be10d7fc2778d69acdab4782424abf34520f557e87e87047702b99afd32d434cd3f55b66b3d4ade3b985f864914303f9d7ce9e31f80514bbef7c1f3089ba9b49cfca7fdc7edf43c7b8afe36e3dfa35e6997b962f8765ede96fc8ecaa25e5b29fcacfc99fab64bc7f87af6a58e5c92eff65ff97e5e67cb1456c6b3e1fd67c3f726d758b492da41d378e0fd0f4358f5fcd18bc257c2d5950c4c1c271d1a69a6bd53d4fd029d58548a9d3774faa0a28a2b9cb0a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28ad1d2f49d4b5aba5b2d2ede4b999ce02c6b9fcfd2b6a187ab5ea468d18b949e892576df6491339c611729bb2467576de10f00f887c677223d361296c0e24ba941112639ebfc47d00fc715ee5e09f80b1c25350f19482460432d9447283feba3f7fa0e3debe8eb3b2b4d3edd2d2c6148218c00a8802a803d857f4ff875f46ec7e3dc31bc4add1a5bfb35f1cbfc5d20bef97923f3ccf78f68d0bd1cbfdf97f3745e9dff002f53cffc13f0c3c3fe0d459e341777d8f9ae6551b867fba39dbf87e75e954515fda19264397e4f848e072ca4a9d38ec92fc5f56fbb776cfc9b198daf8aaaeb6224e527d58514515eb9ca145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007fffd0fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a28033753d1f4bd66ddad754b68ee626182b2283f97a57cfde2bf80369397bbf0adc1b763cfd9a6f993fe02dd47e39afa4e8af8de2df0ff0021e24a5ecf36c3a93e925a4d7a496bf2775e47ad966798dcbe5cd869b4bb6e9fc8fce8f10f82fc49e1772bac593c51e70255f9a33ff02191f9d72d5fa753dbdbdd44d0dcc6b2c6c08657018107d8d78df89fe0778535a67b9d2c1d2ae1b271081e493ff5cfa0ff0080e2bf9578cbe8bf8ea0e55f872b2ab1fe49da32f94be17f3e53f48cabc44a33b431f0e57dd6abeedd7e27c53457ac788be0d78cf42dd2c16e351817f8ed72cd8f74fbdf966bcae58a581cc532346ea705581041fa1afe6dcf786336c96b7b0cd70f2a52fef2693f47b3f9367df60f30c362e3cf869a92f2647451457827605145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145749a0f843c47e2593cbd1ac65b819c1931b635fab9e05766032fc5636b2c3e0e9ca737b28a6dbf9232ad5a9d283a9564925d5e87375a1a6e95a8eb1742cf4bb692e666fe08d4b11ee7d07b9afa43c31fb3ef2b71e2bbbe3a9b7b638fc0b91fc87e35f42e89e1bd0fc396c2d346b38ad631fdc5f98fbb31c92781c924d7f42f05fd1b33dcca51af9d4961e976dea3f92d23f3775d8f87cdb8fb0587bc308bda4bee8fdfd7e5f79f32f84fe01ea376c973e299c5ac3c1f22121a43ec5ba2feb5f4be85e19d0bc356c2d745b48edd40c16032edeecc7935bd457f5d705f861c3dc2f0ff0084da3fbceb397bd37f3e8bca3647e619b71163b317fed13f77f9568bfe0fcee1451457e827861451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007fffd1fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2801199514b310001924f402bc3bc5df1c740d0a67b1d1d3fb4ee10956646c42a7fdefe223db8f7af52f12680be24d39b4b9ae66b7824ff5be49dacebfdd27ae0f7af37ff8511e06f2f66cb8cff7bcd39cd7e63c7ef8dabc7eabc291a7056d6a4e5afa4636697acbe4ba9f45922ca20fdae64e4fb452d3e6f4fb91e487f684f1419370b1b40bfddf9bf9d773e1cfda034bbc952dfc436ad645b03ce8cef8c1f718dc07e7595e21fd9ed442f3f86af899141221b91c37b071c8fc41fc2be70d5b48d4b43bd7d3b55b77b6b88cf28e31f88ec47b8afe57cef8bfc54e0cc546be715252837bc94674e5e575b3f2bc59fa3e132be1bcda9b86162935daea4be4f7fc51fa4563a858ea76cb77a74f1dcc2fcac9130753f88c8ab45d41c1201fad7e787863c73e22f08f9aba3dc948e5520c6df32027f880ec4552b9f16f896f2e1aeae352b86918939de4633e9e95fa2d3fa5565cb054e75305275dfc494928af34dddbbf6b69dcf0a5e1bd6f6b251acb93a3b6bf35ff0004fd1ea2be17f0b7c60f15787e745bb9db50b5c8df1cc72d8efb5bb1afb3bc3faf69fe25d2a0d5f4c903c332e71dd5bbab7a115fb17877e2ce4bc5f19430578568abb84ad7b774d6925e6b6ea9687cae7bc338bcada955d60f66b6f47d8daa28a2bf4f3e742b9bd73c23e1bf11a11ac58433b118f30ae241ff00021835d25739e2dd722f0e7876fb5894e3c8898a7bb9e147e26bcacee381fa8d59e6508ca8c62e525249ab257774f4d8e9c1badeda2b0eda9b7656d1dd9f0c7c42d2f43d17c5577a5f87d9dadadf08db9b7624fe200fa0e95c4d4f75712dddccb7539dd24cecec4f72c726a0aff2733bc6d1c5e615b1587a6a9c2526e315a28a6f44bd11fd2f84a52a54614e72e66924dbeafb8514515e59d01451450014514500145145001451450014514500145145001451450014514a1598e14127da9a4dec02515b363e1dd7b53711e9fa7dccec7a6c898ff4af44d27e08f8ef5321ae2de2b08ce3e6b890671feea6e3f9e2be9328e0ccfb346965f83a953cd41dbefb597de7062b35c1e1d5ebd58c7d5afc8f22a2beabd2bf676b28f0dacea924a4104ac08114fb12726bd4348f855e08d18ab43a724cebfc73fef4ff00e3dc57ebb91fd1b38b71b6963142847fbd2bbfba37fc5a3e5f19c7d9651d295e6fc9597deec7c3ba678775dd65c2697617173938cc7192a0fbb6303f135eb9e1ff0080fe26d4764dabc91d844704afdf9307d8702bec586de0b6411dbc6b1a8e005000e2a6afdbf86fe8bf90e11aa99b579d79765ee47f0bc9ff00e048f91c7f8898ca978e160a0bbeeffcbf03c8fc3ff05fc1ba2ed96e603a84cbfc539cae7fdd1c7e75eaf04105ac4b05b46b144830a8802a81ec0702a5a2bf7ec8b86329c9a97b0cab0f1a51feea49bf57bbf9b67c4e3731c562e5cf89a8e4fcd8514515ee9c414514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007ffd2fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800af867e347885b5cf1a4f6ca00834d02da3c7527ab927fde38fc2bee6af81be2ae817da1f8cf506b946f26f256b882423e5757e4e0fa83c115fcdbf49fad8d8f0c52861d3f66ea2e76bb24dc6fe5cdf8a47dff8771a4f309b9fc4a3a7dfafe079c514515fc0a7ed215ed7f057c632e85e215d1ae64ff41d44ecda4f092ff0b0faf435e2952c13496d325c42c55e360ca47623915f47c25c4989c8337a19b615da54e49bf35f6a2fc9aba38733c053c6e1a786a9b497e3d1fc8fd3ba2b92f03f8823f137862c75546cbbc61651dc48bc30fcebadaff56f2ccc6863f094b1b86778548a927e4d5d1fcd789a13a156546a2d62da7f20af98ff00684f11958ecbc31037df3f699c0f41c203f8e4fe55f4cc8eb146d239c2a8249f615f9e1e3ad79fc49e29bfd4cb168da52917b469c2d7e0ff00491e2bfecce1afecea52b54c4be5ff00b716b2fbf45f367da700e5bf58cc3eb125a5357f9bdbf56723451457f9f27edc14514500145145001451450014514500145145001451450014f8e29269162894bbb1c2aa8c924fa0a657d0ff00b3e6956979aaea57b776e92b5b471f92eeb9d8c49c919e8715f57c0fc2d3e23cef0f93539f23a8dfbd6bd924db76f4479b9be631c0e1278b92bf2f4f9d88fc15f02ef758b61a87896592c23700c70a006420f76cfddfa75af52b5f80be0880e6637571ecf2003ff1d515ed7457fa0f907823c219661a1465848d59ade5517336fbd9e8bd123f10c6f1866988a8e6aab8a7d23a25fa9e7169f09bc0367ca6951c9ff5d599ff0099aea6cfc2de1bb0c7d8f4cb58b1d3112ff515bd457de60785b26c17fba6129c3fc308afc91e2d6ccb175bf8b564fd5b1a8891a858d42a8e800c0a751457ba925a238828a28a6014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007fffd3fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28af03f8b5f1525f0d39f0fe80ca6fdd3334bf7bc956e807fb67afb57ccf17716e5dc3796cf34cce568474496f26f68a5d5bff82f43d0caf2caf8fc42c3e1d6afee4bbb3d7f56f13f87f43c0d5f50b7b466c95596455638eb804e4d62dafc48f03de3f970eb16a189c00f204c9f6dd8cd7e7f5d5ddd5f5c3ddde4af3cd21dcf248c5998fb9355ebf91b19f4abcd5e25bc2e0a9aa57d149c9cade6d595fe47e9f4bc37c32a76ab565cde56b7ddafe67deff113e205af833445bbb6d97177744adb2672a48192c71d86457c75a8fc42f1a6a972d7573ac5dab31c8586568917e8a84015c93cf3491a43248ed1c59d8858954cf2768e8327ae2a2afcc3c44f19339e27c529d39ca8518a56a7193b5edab6d5b99df6bad17ceff459170b6132ea4e2d29cdfda6bee4b7b1ea1a0fc60f1c688ea1ef9afe10798eeff7848ff7cfce3f3afa5fc13f177c3fe2c68ec673f61d41f004321f96463fdc6e87e9d6be18a72b32307425594e411c10455f05f8dfc4d90558a95675e8f58546e5a7f764ef28f96b6f262cdb8472fc745b50509f78ab7deb67fd6a7e9f563eb7e1fd1fc47666c35ab58eea13c80e3953eaac3907dc57cebf0cbe32c8d2c3a0f8b24c86c243767d7a0127d7fbdf9d7d42acaca194820f208e8457f78f08f18e49c67953c4616d383d274e493717da517a34fa3d99f8c6699563329c4a854d1eea4baf9a67ccfe26fd9f6dd95ee3c2d76c87a8b7b83b87d038fea2be6bd6346d4b41bf934cd560682e223f32b771ea0f707d6bf4b6be75fda0f40866d1ed3c471a8135b4a2090faa499c7e4dfcebf03f1a3c0fc9b0f9456cf723a7eca74bde9417c0e3d6cbecb5be9a596c7daf09f1862aa62a182c64b994b44faa7d3d6ff79f24d14515fc527eb47bff00c09f17a697ab4be1cbd936c17e774393c09876ff00810fd457d835f9870cd2dbca93c0e6392360c8ca7055872083ea0d7debf0bfc62de32f0cc7757247db6d8f937207761d1b1fed0fd735fdb1f469f10d6230cf85718fdf85e54df78ef28fac5bbaf2bf63f24f1032370a8b32a4b47a4bd7a3f9edebea37e2cf880f87fc137d344db67ba02da2f5ccbc123e8b935f0457d13fb416bff69d5ecfc3f130296886690039f9e4e067e83f9d7ced5f8f7d22389bfb538b2786a6ef0c3a54d7f8b793fbddbe47d4f0365ff56cb23524b59be6f96cbf0d7e61451457e107d9051451400514514005145140051451400514b82393de92800a28a2800afb47e04682fa6784df529976c9a8ca6419ff009e6bc0fcebe5ff0002f852ebc61e21b7d2e153e483e65c48070912f524fbf41ef5fa0b63676fa7d9c3636aa12181163451d828c0afeb1fa30f0555ad8fabc4b5e36a704e10f393f89af45a7abf23f35f10f368c28472f83f7a5abf45b7deff0022d514515fdb67e4214514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007ffd4fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a28039df15eb4fa068575a8c3199ee15765bc4a326499f845007bf27dabe65d2fe08f8b3c4f349acf89ef12c24ba732b861e6cc4b7392a0855fa6723d2beb89218a52ad228628772e46707d45495f9d716f86f97f13e36956cea529d1a4bdda49b8c799ef2935ab76b2493564bccf7b2bcfebe5d4650c224a72de5bbb744ba1f35b7ece761b30bacca1fd4c2a47e5bbfad78cfc45f001f00de5a5a9be17bf6b4791488cc65429039e58724f635f7d578cfc64f025f78c34bb5bbd2143dee9ece446783246e06403ea080457e57e27f81b90c3877115f877036c4c1271e594db6935cc927269be5bf4bf6d4fa4e1ee31c6cb1d0863eb7eeddef74974d35b773e25a2acddd9ddd85c35adec2f04c870c9229561f81aad5fc1f529ce9c9c2a2b35ba7b9fb2c649abad828a28a81857d4ff0005be24c97053c27ae4bb9c0ff4395cf240ff009664ff002af962a7b5b99ecee23bbb6731cb1307461c10c3906bedb8038e31dc2b9b53ccb08fddda71e938f54ff34fa33c9ceb28a398e16587aab5e8fb3ee7e9cd79dfc57b0fed1f006b1105dcd143e7a8f78583ff00206a6f877e33b4f196810ddab01790a88ee62eeae3bfd1ba835d2f88adbed9a0ea3698cf9d6b34633eac840aff004971d8ac1f11f0cd6ab8392952af4a567fe28bdfcd6cd747a1f8151a7570198463555a5092fc19f9b145182383d4515fe52b47f49857d09fb3f6aeb69ac6a3a7ccfb6396dfcee7a0319e4fe46be7bad5d2757bad1e59a6b43b5a7825809e985954a9afb0e00e25fec0cff0d9b3da9b77f34d34d7e27979d65ff5dc154c2ff322ef8b75a7f10f89350d618e45c4ec53be23070a3f202b9da28af99c7e36ae331353175dde7393937e6ddd9e851a51a54e34e1b2565f20a28a2b90d028a28a0028a28a0028a28a00b3676b2df5dc365060c93c8b1264e06e7200c9fa9afabfc27f00f4cb1f2eefc4f3fdb66183e44595881f427ab7e95f3bf8034e9355f18e936918ce6e51cfb043b89fd2bf442bfabfe8e3e1de519cd0c466d9b50551d39a8c14be1bdaeef1d9bd56f75e47e6dc799ee2b093a786c2cf979936edbf96bd3a9c86a5e03f09ea9a77f665c69b02c4abb50c681593e8c066be2bf889e0a97c11af369e1ccb6b32f9b6f21ea509c60fba9e3f5afd03af94bf68bba81b50d22cd71e6a452c8dfeeb1007ea0d7e93f48ae0fc9df0ccb358d28c2b52715194524da6d2e576dd59dd76b69d4f9fe05cd717fda0b0ae4dc249dd3d6d657b9f3656ae8da2ea5afea11699a542d3cf29c0007007724f603b9ae83c1fe03d7fc697422d322d96ea7f79732644683ebdcfb0afb4bc13e02d1fc136220b25f36e5c0f3ae187cce7be3d07b57f38785be0ce67c555e389c4274b06b79b5acbca17ddf9ecbcde87def11715e1f2d83a707cd57a2ede6ffcb71bf0ff00c1167e08d196ce3c4977361ee661fc6fe83fd91dabbba28aff0044727c9f0995e0a9e5f808285282b24bfaddeedf567e178ac555c4d6957acef296ac28a28af48e70a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd5fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a28039cd77c25e1cf12c461d6ac62b91d988dae3b70cb861f9d786f887f67ab3937cfe1abf785b9220b9f9d7e81c0040fa83f5afa568af86e27f0db86f881379a612329bfb4bdd9ff00e051b37f3ba3d9cbb88330c0e987a8d2edbafb99f9ede21f875e2ef0d166d46c1cc2bff2da2fde478f5c8e9f8d710410706bf4fd95581560083d41af27f187c21f0cf8995ee6da21617a412258405563fed2f43cf7eb5fcd3c67f45ea94a12c470dd7e7b7fcbba964fe535a37e4d2f53f40cabc458c9aa78f85bfbd1dbe6bfc9bf43e18a2ba8f15f8435af07dffd875784a86c98a55e52451dd4ff0031d4572f5fc9f98e5d8ac0626783c6d370a9176716acd33f4aa15e9d6a6aad295e2f668e83c37e27d5fc29a8a6a7a3cc639178653ca48bdd587715f5e785be30f867c4d6bf64d4645d3af5d0ab4731c46c48fe17e9f9d7c47457e83c01e2c677c29cd4708d4e84b7a72f87d5758bf4d1f54cf0f3be1ac1e676955569ada4b7f9f72f6a707d9751bab6c86f2e691411c82031c11f515468a2bf36af38cea4a71564db76ec7bf04d452614514564505145140051451400515b9a1786f5bf12dd7d9345b492e5c637151f2a67bb37415f497843e02da5b88ef3c572f9f270df668ce107b31ea6beff0082fc32e20e27a8965b41fb3eb396905f3ebe8aefc8f1736e20c165d1be227af65abfbbfccf9d3c3de13d7fc51702df46b479b9c33e311afd58f02be96f0a7c03d22d20f3fc552b5e5c3ae04313148a3cfb8f9988fa81ec6bdeac34eb1d2ed92d34f823b78506152350a07e5576bfb2781fe8eb9064ea388cd7fda6b7f797b8bd23d7d657f447e559c71de37157861bf771f2f8befe9f2fbcf817e23f80ae3c0dab08558cb637396b790f5c0eaadee3f5af39afacff006889e05d274cb76c79ad33b2faed039af21f871f0cb50f19dda5d5dabdbe951b664988c1931d553dfdfb57f307883e1c3871c55e1ee1ca6e5cdcad47a439926d37d231beef647e899267ca593c71d8f95ad7bbef676fbd9e8df00bc25289e7f165dc7840a61b6cf727ef30fe55f52d54b1b1b5d36ce1b0b28d618204091a28c00a2add7f76f87bc1b4785f23a394d27771d652fe693ddfa745e491f8ce799b4f31c64b132d13d12ec96c213819eb5e2377f0aa4f16f8a6e3c4be309bf719096f6511e91270a1dbdfa903d7ad7b7d15ea710f0ae5d9e46952cd21cf4e12e6517f0b92d1392eb6bbb27a774ce7c06655f06e52c33b49ab5faa5e5d8a9656167a6db259d84296f0c630a91a85503d80ab74515ef52a50a7054e9a4a2b4496892f24714a4e4f9a4eec28a28ad090a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd6fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a2a19e786da369ae1d6345192cc7000af1bf14fc6ff0b687badf4bcea974bc6d88e2253ef2118ffbe7757ce711716e4f90d0fac66d888d38f4bbd5fa456afe499df80cb3158d9fb3c2c1c9fe1f37b23daaa379a1886647541fed103f9d7c41adfc6bf1b6ac596da74b088f01605f9bfefa6c9cd79adfeb3abea8e5f51bc9ee49ebe648cc3f22715fcfd9dfd29b25a1270cb30b3abe726a0bff006e7f823ee307e1c62a6af88a8a3e9abfd11fa312ebba2427136a16a87fda9907f3352c3ab69771fea2f20933fdd914ff00235f99f4a090720e08ef5f271fa586279bdecb636ffafaff00f903d27e1a53b698877ff0ff00c13f44bc65e16d3fc65a14da65d052c417824ea52403820fe87dabf3eb53d3ee74ad42e34dbc5d935b48d1b83eaa6b6f48f1b78ab43757d3b53b840a7ee339743edb5b22a878835dbcf126a726afa82c62e260a2431aed0c5463711ea457e67e2c788791718d2a38fc3e1e5471907cb2bd9c650ff12b3bc5ed74b46cfa1e1ac8f1995ca5427514e93d57469fa79fa98b451457e1e7d7051451400514514005145140051451401dcf853e21f893c1e0c5a5caa606259a29141524f7f5afa13c35f1f745be296fe2281ac646c033265e2fa903e61fad7c85457e9dc21e2ff1470e28d2c16239a947ec4fde8dbb2eb1ff00b75a3e7f34e17cbb1f795685a4faad1ffc1f99fa6b63a858ea76e977a7cf1dc42e32af1b0653f88ab75f9dde13f1b6bde0ebc173a54e7ca27f796ee731b8f71d8fb8e6bed6f02f8f749f1c69df68b46f2aea2c09ed98fce87d47aa9ec6bfb53c32f1af2ae2cb60eaaf638ab7c0ddd4bbb83d2fe8f55e6b53f25e21e11c4e59fbd8be6a7dfb7aafd762b6b7f0f34ff13ebe9acf88643730db284b7b41f2c6a3a92ddd893f8577d6f6f05a4296d6d1ac5146a1511061540e0002a6a2bf53c064580c157ab8ac3524aa55779cb7949f9b7ad9745b2e88f9baf8daf5a11a752578c744ba2f90514515eb9ca1451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007ffd7fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800af2ff1efc51d1bc1719b61fe97a8b29290211853d8b9ec3dbad667c5bf88a7c21a70d374b71fda978a761ebe4c7d0be3d7fbb5f145c5c4f7733dc5d48d2cb21dccee72c49ee49afe68f19bc72ff57ea4b24c92cf156f7a6f554efd12eb2b6bae8badde8bf41e13e0efaec563319a53e8bacbfc97e6757e27f1df893c593b49a9dd30889f9608c958947a63bfe35c751457f0ce679ae3331c44b178eab2a9525bb936dfe27ec3430f4a84153a31518ae8828a28ae0360a28a52aca70c083e869d804a28a290051451400514b82393de928b0051451400514514005156d6c6e5ac9f50542608e411337a3302467eb8aa95a54a33828b9ab5d5d79aeff809493bd9851451598c2b5b44d6f51f0f6a50ea9a5ca629e160411d1877047706b268adf0d89ab87ab1af424e338bba6b469ad9a22a538ce2e13574cfd11f04f8bacbc65a143aadb10b2e024f1778e41d47d3b8f6aebebe31f811af4ba7f8a5f4967fdcdfc646dcf1bd3906beceaff4e3c23e369f1470e52c7d7fe346f09ff8a3d7fede4d3f99fcf7c51942cbb1f2a30f85eabd1f4f90514515fa69f3c1451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007ffd0fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803e02f8ad777577e3ed58dd9398a511203da3551b71f51cfe35e795f56fc7ed1741b7d3e0d73c8c6a7732ac2245380c8a324b0ee40e057cb76769717f770d8da21927b891628d07259dce00fcebfcc2f16f86f1395f1762b0b566aa4ea4dcd5aeddaa36e29ff007b5d95cfe88e19c7d3c46594ea4572a8ab7fe02acdfa15a8ae8fc4be15d6bc277c6c35980c6d8ca38e6371eaadd0d7395f9de3f0189c1622784c65370a9176716acd3f347b746b53ab055293bc5ecd057bd7c33f840fe238935bf108786c188314432ad30f5cf0429f6e6b27e15fc35baf15ea0ba9ea71347a55b302c5863ce61ced5f51ea6bed98e38e18d628942a200aaa060003a015fd39e05f82d4f364b3ecfe9de87fcbb83fb7fde7fdd5d17da7e4b5fcfb8c78b2585ff62c14bdff00b4fb792f3fcbf2e534df01f8434a40967a5db8c0c6590313f5dd9e6af4fe14f0d5ca149b4cb52a7b08957f9015d0515fd9b4b87b2aa54bd853c35350eca114beeb58fca258fc4ca5cf2a926fbdd9e6b7bf08bc017c3e6d2d223eb13b21cfe06b92baf801e12998b5bcf7507a287047ea09fd6bdde8af9bcc3c2ee12c6bbe232ea57f28a8bffc96c7a14388f34a5f05797cddff003b9f393fecefa496063d4a60bdc1504d6ad97c01f095b48b35ecf7372a872c85c22b0f7da01fd6bde2bc0fe337c465d0ec5fc35a3ca3edf74844cebc98626e08f6661f90af83e27e00f0f385b2eab9ce37030e586c9b93e69748a4e4d5dbf2b757a1ece5d9de7b99578e128d6777d74565d5b691f39fc42bbd1ee3c4935b787e1486c2c80b6882746d9f79b3df27bd70f4515fe7ee71994f30c6d5c6ce2a2e6dbb2564bb24ba24b45e47edb85a0a8518d24ef656bbddf9bf50a28a7c71c93388e252eec701546493ec2bce49b7646e32a48a29279161854bbb90aaaa32493d0015ebbe15f82de2af102a5d5ea8d36d5b0774dfeb181f441cfe78afa5fc1df0bbc33e0f2b736f17daaf40ff008f99b961ebb4745fe75fb6f04780dc4b9f4a15b114feaf41fda9ab36bfbb0ddf95ecbccf91cdf8cf2fc127084b9e7d97eaf6fcd9cb783be175bc5f0fa7d13588f6dd6a63ce909eb1b81fbbfc56be44d6f47bdd0755b9d23508cc73db3946046323b11ec4722bf4b2bc97e287c3787c6965f6cb10b1ea96eb88d8f1e628e7631fe47b57f4278ade0751c6e41878e410fdf6163ca975a90ddaff0015ef25ddb6baa3e2386f8c274b1b3faebf72a3bdff0095ff0095b4fb8f8668ab57b6577a75d49657d13c13c2c55e37186523daaad7f0855a53a7374ea2b4968d3dd33f648c9495d6c14514540cf5af82fa4dcea3e36b6b8894f95661a59187403181f99afb9abe71fd9deead5f4ad4ed16355b88e657671f79918719fa1afa3abfd1afa3ce494301c1d46bd29f33ace537e4efcb6f972fde7e11c758b9d6cd25092b282497e77fc428a28afdc8f8e0a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd1fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803e72fda2ade47d1f4ab85fb91dc3ab7d5978ae07e02e809a9789e5d6275dc9a6c598f2323cd93807ea173f9e6be86f8a9e1f6f11782afede15dd3dbafda6203a968b923f15cd79bfeced005d1f539f1cbdc28fc02d7f2a712707fb6f1870589c446f4ea45545db9a945ab7c9a8bf99fa4e0334e5e16ab4e0fde8be5f949ff009368f7ad5346d2f5a83ecdaadb47731e72048a0e0fb7a572f6ff000cfc116d29963d2e12c4e7e61b80fa035ddd15fd278de1ecaf1959623178684e6b672845bfbda67c0d1c7e26943d9d2a8d2ec9b4450c30dbc6b0c08b1c6a30aaa3000fa0a968a2bd78c545596c72b77d58514514c414515f3ffc4ff8bf1e82d2683e1a6596ff0005669faa419ecbeaff00a0af98e2de2fcb386f2f96659a54e582d97da93e918aeaff002dde87a395e5788c7d75430d1bbfc12eecdff899f146cfc216efa6e9ccb3eab2ae028391083fc4fefe82be2abdbcbad42ea5bdbd91a59e662eeec724934cb9bab8bdb892eaee46966958b3bb9cb313dc9a82bfce5f12fc4dcc78bf1fedabfbb422df253e915ddf793eafe4b43f78c8387e865747921acdef2eff00f0028a2b5345d26eb5dd52df4ab25dd2dc3841ed9ea7f0afceb0d86ab88ab1a1463cd2934925bb6f448f76738c22e737648ddf06f82757f1aea22cb4e01224c19a7707646bfd4fa0afb2fc1df0cfc39e10855a0845cdd8fbd712805b3ec390bf856f784fc2fa7784f46874ab04036a832be3e691fb926ba6aff447c29f0572ee1bc3431b984154c63d5c9eaa1fdd82daebacb76f6b23f0ce25e2dc463ea4a9506e34bb77f37fe41451457eea7c6051451401c0f8cbe1cf87bc669befa2f2ae94612e23e1ff001f51f5af0abefd9df56494ff0067ea70cb193c79885180f7c120d7d67457e69c53e10f0af105778bcc30dfbd7bca2dc5bf5b68fd5a6cfa1cb78a332c0c3d9d0a9eef67aaf95cf887c73f08af7c17a0c3acb5d8bcfde88e7089b4461bee9ea4919e0d78ed7e8ef8bb478f5ef0dea1a54801f3e060b919c301953f8102bf39a589e195e19061918ab0f70706bf8d7c79f0ef05c2f9a50795c396855868aedda51d25ab6dea9a7ead9faaf0667b5b31c34feb2ef38bf4d1edfa9f40fecf370c9e20d42d81e24b70c47fba7ffaf5f5dd7c8bfb3cdb33ebda85d01f2c76e149ff0078ff00f5abebaafea3fa3aa9ff00a93439f6e6a96f4e67fadcfceb8eedfdaf3b768fe41451457ee47c70514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451401ffd2fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2801080460f435c2f83bc283c2b7babc36ea059dddcfda2003f8438e57f03d3dabbba2bcdc5e5386c462a8632ac7f7949c9c5f6e68f2b5e8d7e291d14b15529d39d28bf7656bfc9dd0514515e91ce1451450014514500794fc5bf1b1f097878c568d8bfbfdd1418eaa07de7fc01e3dcd7c31248f2bb4923167624b31e4927bd7b3fc78d424baf1b7d918929696f1aa8ed97cb1fe62bc56bfcddf1e78bb139cf14d7c349feeb0edd38ae975f13f56eff2491fbe7066594f0996c2697bd35ccdfaecbe4828a28afc54fac0afa8fe00f847093f8baf13ef660b5cfb7df71fc87e35f31db5bc9757315ac432f2baa28f763815fa39e19d222d0741b1d2621816d0aa1f76c7cc7f135fd21f46ae1086679fcf36c446f0c324d7f8e5751fb926fd6c7c1f1fe68f0f8258683d6a7e4b7fbf446ed14515fdf87e261451450014514500145145002119047ad7e7678eac469be2fd5acd780972e47d18e7fad7e8a57cada9fc3cbaf197c54d41dd0a6996ef135c4bd98ed0762fa93dfd057f3a7d22b85f159de072fc36021cd59d6e55e928bbb7d92b5dbe891f79c0998d3c256af52b3b4546efe4d7f99defc0df0d4ba2f858ea774bb66d4dc4aa0f5110e17f3eb5ed951410456d0a5bc0a1238d42aa8e0003802a5afda38478768e439361f28a1aaa514afddeedfcdb6cf92cd31f3c6e2a78a9ef27f8745f70514515f46700514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451401ffd3fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803e28f8ef6125af8dcdd303b2eeda3753dbe4ca9fe55e2d5f68fc74f0dc5aaf85c6b0a5567d31b78278dd1be032ff222be2eaff353c75e1ba994717e25cbe0acfda47d25bfdd2ba3f7fe0ec7c71595d3b6f0f75fcb6fc2c1451457e3c7d49ea7f073433ad78e2d1997743621aea43d86ce13ff001f22beedaf9e3f67ed05ad346bcd7265c35e48123247fcb38c7f524fe95f43d7fa31f479e1bfecbe11a788a91b4ebb751fa6d1fc15fe67e11c738ffac6672845e904a3f3ddfe2edf20a28a2bf743e3828a28a0028a28a0028a28a0029891a2676285dc727031934fa29349bb85c28a28a60145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007ffd4fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803c4be3d5d4b07829628fa4f731a37d064ff00315f26e85e16d6bc48b7474783ed06d10492283f36d271c0ef5f6b7c57f0fcfe22f065ddb5a2ef9e0c4f1a8ea4a7240fa8cd78bfecf0c1357d5607f95cc29c1ebc31c8fc2bf8ebc56e0d59df89782c0e60e51a15e9d949778a9bb2bdd5d4acede67eabc359afd5387ead6a0939c2576bd5afd3f23e7c9f4fbfb690c3716f2c6ea705590839fcabb8f05fc38d7bc5b7d1a2c0f6f661879b71229002f7dbea6beed9f4ad32e983dcdac32b0e4174563fa8ab91c51c2823894228e8146057a593fd16305431eab6618c7528a77e551e56fc9be6765decafe873e2bc47ab3a3cb42972cdf56ee97a2b229695a65ae8da741a6592ec86dd0228fa77fad6851457f5750a14e8d38d1a4ad18a4925b24b647e6939ca727393bb61451456a48514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451401fffd5fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800ebc1af3ab7f00da68fe2f1e29d0f100b9564bbb7fe06ddfc4be873d457a2d15e566792e0f1f2a53c5413953929c1f58c9754ff0006b66b4674e1f19568292a6f492b35d1a0a28a2bd539828a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00fffd6fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd7fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd90a696d6167652f6a70656797060e00000000001976a914e0565a439377674fa2878922ebbc1274dc10897e88ac00000000", + Vout: []chainstate.VoutInfo{ + { + ScriptPubKey: chainstate.ScriptPubKeyInfo{ + Hex: "6a2231394878696756345179427633744870515663554551797131707a5a56646f4175744ea53a0100ffd8ffe000104a46494600010100006000600000ffe1008c4578696600004d4d002a000000080005011200030000000100010000011a0005000000010000004a011b0005000000010000005201280003000000010002000087690004000000010000005a00000000000000600000000100000060000000010003a00100030000000100010000a002000400000001000003b7a0030004000000010000038a00000000ffed003850686f746f73686f7020332e30003842494d04040000000000003842494d0425000000000010d41d8cd98f00b204e9800998ecf8427effc0001108038a03b703012200021101031101ffc4001f0000010501010101010100000000000000000102030405060708090a0bffc400b5100002010303020403050504040000017d01020300041105122131410613516107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728292a3435363738393a434445464748494a535455565758595a636465666768696a737475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000102030405060708090a0bffc400b51100020102040403040705040400010277000102031104052131061241510761711322328108144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a35363738393a434445464748494a535455565758595a636465666768696a737475767778797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f9faffdb004300020202020202030202030403030304050404040405070505050505070807070707070708080808080808080a0a0a0a0a0a0b0b0b0b0b0d0d0d0d0d0d0d0d0d0dffdb004301020202030303060303060d0907090d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0dffdd0004003cffda000c03010002110311003f00fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd0fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd1fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd2fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28af2bf167c5df0bf861ded55cdeddaf063848201f76e82bc5cf788b2cc9b0cf179a578d287793dfc92ddbf257675e0b0188c5d4f65868393f23d528af8f752fda07c493c99d36d60b7419e1c17247e9cd64afc77f1c2b6736c7d8c7c7f3afc6b13f495e0da551c212a925dd434fc5a7f81f594fc3fcd651bbe55eaff00c933ed8a2be49d37f685d66228ba9d84338e373212871ec2bd87c33f17fc25e227581a536570dc08e7e013ecdd2beb7873c66e11ceaa2a385c5a8cded19a706fd1bd1bf24cf331fc279a6122e7529dd775afe5a9eab4522b060194820f208e8696bf523e6c2b2f54d6b4ad161f3f54ba8ad93d6460bfceb52bc1bc4df05ee3c4924d7d79af5d4d76e498fcd0a624cf4014007007a1af92e30cd33bc16139b21c1fb7aaefa3928c55bbdda6dbe897cdaebe9e5786c1d6ab6c6d5e48fa36d9b37df1c3c0968488a79ae71d7ca8cfe9bb6e6a0b4f8ebe07b9204af716e09c6648bff892d5f237893c33aaf857527d33568f648bcab0fbaebeaa6b9fafe31c7fd2378db098c9d1c4d3a70945d9c1c1ab3ecef2bfe27eb14780f28a9494e9ca4d3d9dff00e058fd1bd1fc61e1ad780fecad4219988cec0c030faa9e6ba5afcc48679ada412dbc8d1ba9c8653823f2af63f07fc69f116832a41abb1d46cfa10e7f78a3d9bfc6bf4be0ff00a5060b1338e1f88687b26fedc2ee3f38bf792f4723e7f34f0eead34e7819f3793d1fc9edf91f6c51585e1ff12691e27b14d43489d668d80247f121f461d8d6ed7f53e0f19431742389c34d4e12574d3ba6bba67e715694e94dd3a8acd6e98514579a78ff00e25e95e0644b7910dcdecca5921538c0f563d8570e7b9f60326c14f30cceaaa74a3bb7f82d356df448db0782af8baaa861e3cd27d0f4ba2be446fda175e32ee5b08027f7724feb5d3e91fb4359c8c1359d39a2c9e5e16dc00fa1afcbb03f480e09c4d5f65f5a70f3942497df676f9d8fa3adc119bd38f37b3bfa3573e94a2b8bf0ff00c40f0b789405d36f53cd233e549f238f6c1ebf857695face5b9a60f30a0b1381ab1a907d62d35f7a3e671186ab427eceb45c5f66ac14514577980514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451401fffd3fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28ac9d5b5dd234387cfd5aee2b64edbdb04fd075358e2313470f4dd6af2518addb6925eade85d3a73a925082bb7d8d6a2bc3f53f8f1e12b3731d9a4d7647465185cfe35cc3fed136a1f09a5b95f5dff00fd6afcd31de34705e16a3a7571f16ffbaa525f7c5347d051e12cdaa479a345fcecbf368fa5e8af02b3fda03c37332a5ddb4f0e4f2c30c07f5af51d03c71e18f128034abe8de43ff2c98ed7fc8d7bb91f88bc359c545472ec6c2737b46f693f44ecdfc91c78cc8730c2ae6af49a5ded75f7a3aca28a2bed0f20e1fe20d9f89f51f0fbe9de15611dcdcbaa49216da52239dd83ea7a7d2bc13fe19eb5a7b732cba9c46e0f3b769233fef75fd2beb3ae6f56f17f86b4304ea7a8410b0c8da5c16c8ed81cd7e5bc6fe1cf0c67188799f11cdb518f2ae6a8e108aeeb54aefab6d9f4993e7d98e161f56c02576eeed1bb7ea7c09e25f0c6afe14d45b4dd5e131c98dc8c39475f553deb9eafa2be2cfc44f0878b74a5d3ac22926b98240f0dc6dda17b30e79c11dbe95f3ad7f9f5e2064794e559cd4c2e498955e86f19277b5fecb7b36bbad1ab1fb76498bc4e27091a98ba7c93eabf5f98528241c83823bd2515f147ac7b8fc3af8c1a8f87648f4bd759aef4e242863cc90fb83dc7b57d7f61aae9fa9d9c57f653a4b04ca191c11820d7e67d6841aaea76d01b6b7ba9a388e7e4572179f615fd0de1d7d20f34e1ec2bc06650788a497b9795a51f2e6b3bc7c9edd1db43e1f3de08c363aa7b6a0fd9cbae974fe575a9fa357baee8da77fc7f5ec101f479154fe44d47a7788b43d5dcc7a6df4170ebd551c13f96735f9bb24b2ca732bb39f5624ff3a9ec6fef74cb94bcd3e77b79e339492362ac0fd457dac3e95d8a78a8b96022a95f55cedcade4ec95fe4792fc35a5ecddab3e6f4d3f3fd4fb2be39e87677fe107d55c2adc58bab23f72ac7057f1af8aabb2f1178fbc51e28b64b3d5ef0c9021076280aacc3b903a9ae36bf17f17b8d32ce28cf7fb4f2ca2e11e48a7cd64e4d5f56937d2cb7e87d670be5388cbb05f56c4493776d5b64bb7ea1451457e587d11d6f83fc61aaf83b558f50d3dc98f204d093f2489dc11ebe86bef8f0f6bb65e24d22df57b06cc73a838eeaddc1f706bf36abd13c23f137c47e0eb46b0d38c6f6ecfe66c917383df073c66bf7ff057c61ff55ab4f039aca52c2495d24aee12ee9767d57a3ef7f8ae2de16fed282ad8649555f2baf3f4e87df95cf6abe14f0e6b7751deeada7c1753c6bb55e450c40f4af9a63fda1f5a54c49a6c0cdebb88a9acbf686d4d6e47dbf4e89a1279d8c4301ed5fd2788f1ef8031b18d0c554728b6b49536d27d1b4d3d8f81a7c179dd16e74959aed24991fc61f86167a2c5ff00091f87a1f26db38b8817eea13fc4a3b0f6af9d2bf40e7d6b42f19f82af2f20951eda6b67f3031198db6f2187620d7e7f3001881d0138afe69f1fb85b2acbb34c3e6992b8fb0c545cd28db96ead771b6c9dd3f5b9fa070566389af869e1f177e7a6edaeff003f343a296581c490bb23af2194e08af78f007c69d4f47923d33c4aed796270a263ccb17e3fc43f5af03a2bf2be14e32cdf87718b199556707d57d992ed25b35fd2b1f479965586c75274b131bafc57a33f4d6c2fed353b48afac6559a0994323a9c820d5baf8b3e0ff00c42b9f0f6ab16837f216d36f1c2aee3c43237423d8f7afb4c104647435fe8ff867e21e178bf2958ea2b96a47dda91fe5979793dd3f96e8fc1788722a995e27d8c9de2f58beebfcd750a28ac9d475ed1b49e352bd86dcf5c48e01c7d3ad7df62313470f0756bcd462bab692fbd9e253a739cb960aefc8d6a2b90b7f1f7836ea43141ab5b330edbf1fceba5b5bdb3bd4f36ce78e74e9ba360c3f435cb82cdf018cd3095e13ff000c94bf26cd6b616bd2fe2c1c7d5345aa28a2bd139c28a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00ffd4fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a0f4a00f1df89ff001420f06c5fd99a6ed9b5499738eab0a9e8cdee7b0af8d756d6b54d72edef754b992e2573925db3f90ec2beac87e0b9d675ed435bf17dd19fed12b3451c248c2e78dc4fa0c702b3bc47fb3fd84b6ed2f872e9a29c64ac737287db2391f5afe2ff00137833c44e2d954cc254b970f16f928f3252e55a2938ece4f7d5dfa247eb3c3f9b6459628d052bd476e69db4bf6bf647ca1455ed4f4dbdd1efe7d33508cc5736ee52443d88fe60f6aa35fc8f5a8d4a35254aaa6a517669ee9add33f4d8c94a2a51774c2a68279ada559addda375390ca7041fa8a868a98ca516a5176686d26accfa23c07f1c2eb4c4361e2d325d40abfbb9d066504763d322b535cfda1a46cc7e1fd3f60ed25c1c9ff00be47f8d7cc7457eb585f1cb8cb0f96c72ca58bd23f69a4e76edcceef4e9d7ccf9aa9c21954f10f112a5abe9d3ee477bad7c4bf196bbb96eb5091236ce6388ec5c7a715c3492cb331795d9d8f24b1c9a8e8afce335cf731ccea7b6cc2bcaa4bbca4dfe6cf7b0d83a1878f250828af2560a28a2bca3a028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00b515ede4113c10cce91c9f7d15880df515568a2ae552724a32774b6f2f4128a5aa0a28a2a062ab152194e0839047635f7efc2ff00121f13f83acaf6539b8841b79f3dde2e33f88c1fc6be01af6cf855f13b4ff03d9dee9faa4134b15c48268cc582436369182475c0e6bf71f00f8df0fc3bc412fed0abecf0f560d49bbd935ac5e97f35f33e438d3279e3b02bd846f38bbaf4d9ff009fc8fb52bc93c59f07f40f13dc4da879d3db5e4b925c3965ddeea73c7d2b26cbe3e783ae66114f1dd5ba9fe274047e8c6bd9ac6fecf52b64bbb0992785c655d0e41afed858ee0fe38c3cb08aa53c4c63ab8df55d2f6d24bd4fc8fd8e6b93d45579654dbebd1f9763f3bfc55e18d4fc21ac4ba4ea230cbf32483eec887a30aa1a6ebdac68f2acda6ddcb6eca72363103f2e95f57fc7ed0e1bcf0cc3adaa813d8ccaa5bb98e4e08ffbeb047e35f1d57f03789dc295383b89aa60b0351c61a4e9b4da6a32d95d754d357f2b9fb570f666b34cbe35ab24ded25d2ebfcf73e9ef047c777f323d3bc5ea36b10a2ed074f7703b7b8afa72dee20bb812e6d645962914323a1cab03d0822bf316bdc7e137c4f93c33729a16b2e5b4c9df08e79f21dbbffb87bfa75afd97c21f1ff154f130c9f89ea73539691aaf78be8a6fac5ff33d5756d6df29c51c154e54de2b2e8da4b78ad9fa79f9753ecfa29a8eb2209108656008239041a757f6b269aba3f230a28a298051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451401ffd5fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a2b2356d7b47d0e1f3b55bb8ad97b6f6009fa0eb5e6f7bf1bbc0f69218d27927c7431a12a7f1af9ace78cb22ca5f2e658ba74df694927f76ff0081e861329c66295f0f4a525e4b4fbcf5fa2bc3e3f8f5e0e66c4897083d7666babd2be2a782756d8b16a0913b8fb92fc87f5e2bcbcbfc4ce15c6cfd961b1f4dcbb7325f9d8e9afc3d99515cd5284ade97fc8f45a2a28a78678c4b0baba1e4329c8af30f197c59f0df8551ede2905edf0c810c47214ffb4dd0735ef679c499664f8478eccabc69d3eedefe8b76fc95ce2c1e5f88c555f63878394bfadfb1e9d717105ac4d3dcc8b146832cee42803ea6bc67c51f1c3c33a36fb7d241d4ae4647c9f2c40fbb77fc057cc1e2bf1ff893c5f70cfa95cb2c193b2de3256251f4ee7dcd7155fc85c6df49ec5d694b0dc354b923b7b49abc9f9a8ecbe7cde88fd4328f0f29412a9984b99ff2adbe6f77f2b1b9e23d7af3c4dad5ceb97e144d72c0b0418501405007d00ac3a28afe54c662eb62abcf15889734e6dca4deedb776fe6cfd2695285382a70564b44bc90514515cc585145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140057a3f80be24eafe08b92b1ffa4d8c87f796ee703eaa7b1af38a2bd6c8f3dc7e4f8c866196d574eac766bf27d1a7d53d19cd8bc1d1c552743111e68be87bafc48f8bd0f8c7464d174eb37b68ddd5e66918313b790a30077e735e154515ddc57c5b99f11e3de659b4f9aa3496892492d924b431cb72cc3e028fd5f0d1b477ee1451457cd9de7d83f03bc6efabe9cde1ad424dd7364b98598f2f0fa7fc07f957bfd7e72f83f5e9bc37e24b1d5e124086551201fc51b70c3f235fa296f347730477111ca48a1d48ee08c8aff433e8f3c73533cc81e03172bd6c35a37eae0fe17f2b38fc91f8771ce4f1c1e37dbd2568d4d7d1f5ff003398f1578d345f07470cbacb48ab39210a216191d727a0aa3a2fc4af066bd2086cb518d653d125fdd93f4ddd6babd4f49d3b59b46b2d4e04b885c72ae322be45f8a1f0a0f8555b5dd04bbd86e1e6464e5a127b83dd7f957bde23f1271770eb966f9751a75f07149ca3692a915d5def66badeda75565738f21c06578fb616bca50aaf67a38bf2b77f9ebdcfb24104020e41e41a5af87bc09f1775cf0ac8967a8335fe9d900c6e732463d518ff0023c57d85e1df12e91e28d3d751d22612a1c6e5fe243e8c3b1af4fc3cf16325e2da5cb84972574bdea72f8979afe65e6be691cd9ef0ce2f2c95ea2bc3a496df3eccdfa28a2bf4f3e7428a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00fffd6fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800af9dfe247c665d1e79744f0b9496e53e596e7ef2c6c3a85ec48fcaba3f8cfe339bc33e1f5d3f4f7d979a96e8c30ea9181f311ee73815f1498a729e7b239427ef90719fad7f2b78f3e30e2f29acf87723972d6b27526b78a7b463d9b5ab7d1356d76fd2782f85696261f5fc62bc7ecaefe6fcbc8b9a9eada96b172d79a9dc49712b9c967627f2f4acea28afe21af88ab5ea3ab5a4e527ab6dddbf56cfd76108c22a31564828a28ac8a3a2b0f16788f4cb3974fb1d42786de65dae8ae718f6f4fc2b9f666762ce4927924f24d368aebc463f135e10a55ea4a5186914db692f24f6f919c28c20dca3149bdfcfd428a28ae4340a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800afa3bc35f1e7fb2f4eb3d2eff004f3225b46b1b4aaff310bc6707dabe71a2beb78478df38e19c44b1393d5e4949252d134d277b34d3ff0033cdccf28c2e61054f151ba5b6ebf23f453c2de34d03c5f6de7e91701a45199216e244faafa7bd6ceb3a6c3ac69575a65c2868ee2268c83ee2bf39747d6351d0afe2d4b4c99a19e239054e323d0fa835f57e89f1ebc3d3e9064d6524b7bf89398d54b2c8c3fba474c9f5e95fd93e1ff8fd93e7b83a981e2671a35795a6de909ab6b6becedbc5bd7a765f95677c158ac1d58d6cbaf38dfe69ff009799f245fdab58decf66fd6091a3cfaed38cfe35d67813c6b7fe0bd663bdb762d6d210b730e7e574ff0011d8d72ba95e1d4350b8be231e7cacf8f40c720552afe21c166b5b2ccc963f2b9b8ca12bc1adf47a7deb75d763f5ead86862283a388574d59a3f4cf4dd42d356b18351b1904b05c207461dc1abb5f2a7c05f1a3c5752783ef9c98e60d2da127eebaf2c9f42391f435f55d7fa77e1d71ad0e29c8e966b4b493d271fe59add7a755e4d1fcf19f6513cb7192c34b6dd3ee9ec145782ea1f1966f0df8a2eb42f1369a62b78e52239e12493113f2b107af1d715ed1a56afa66b76897fa55c25cc0e321d0e7afa8ea0fb1e6bb787f8df26ce6bd5c2606b5ead26d4a0d38cd34ecfdd7676bf5574658ec9f1784846ad687bb2d535aa7f3469514515f587981451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007fffd7fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803cb3c6df0d21f1aebda6ea5797452d6cd4ac9005c990139e1b3c67a1e2bb7ff00846b40fb1fd83ec10790176ecf2c6318c7a7a56e579f78f3e21e93e07b2dd3fefef655260b653cb1f56f45f7af89cc32ee1cc896333ec74230f69675272d6f64a296b7eda456efa367b143118fc6ba582a2dbe5d229696ebfd367c81f133c2f0784fc5773a75a716ce04d08feeabf6fc0d79fd6ef88fc47aa78a7549356d5a4df349c0006151474551e82b0abfcc7e28c565f89cdf1388caa0e3425393827d22de8add3d3a1fd0997d3ad4f0d4e188779a4aefcc28a28af04ec0a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a28034748d4ae347d4ed754b562b2db4ab2291fec9e9f88afd1dd1f518757d2ed7538082973124831fed0cd7e68d7ab7833e2df88fc2bf64d3e475b9d2e03b5a0651b821393b5bae4678e715fbff813e29e13853155f0b9ab97b0abcbaa57e59276bb575a59eb6bbd1687c5718f0e55cca9c2a61adcf1befd576fbf63e8af8c1e0a87c49e1f9752b68c7dbac14c8840e5907de5fcabe4ef08f8d75af06dfadd69d213167f7b6ec4f96e3be4763ef5f7d691ac699e23d2e2d434f9567b6b94edcf51cab0ec47422bf3cbc4d69058f88b53b3b63ba282ea6443eaaac40afbbfa43e02196e6181e31c8eaf24eae8e507bb4938c95b7bad1f4692b9e370357788a15b2bc646ea3d1f4beebe4cfbf7c25e2bd33c61a447aae9add7896327e68dfba9fe87bd74f5f9f3e00f1b5ef8275b8ef6225ed25212ea1cf0f19ee3d197a835f7e58dedb6a3670df59b89219d03a30e855b915fb9783de27d2e2ecb1fb7b47154acaa2efda6bc9f55d1e9b58f8ee29e1d965788bc35a72d9fe8fd3f12d514515fb01f2c14514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007fffd0fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2802a5dea1636010dedc47079876a798c1771f419eb56810c0329c83c822be46fda1a7baff0084874d84b110ada9741db717393f5e056b7c1ff8a6eaf0f853c452ee5384b4b873c8f48d8fa7a1fc2bf10a5e3665f4f8c2bf0ae614fd9a8b518546f472b2d24ada5efeebbbf3b5cfb097085696550cca84b99b5771ecbcbbf99f52514515fb79f1e1451450014515e5bf12fe23d9f826c7ecf6e565d52e149862ebb074dedede9eb5e2f1071060324c054ccb32a8a14a0b57f924bab7d11d781c0d6c6568e1f0eaf27fd7dc1f123e2558f826ccdbdbed9f54957f750e72133d19fdbdbbd7c47ab6afa8ebb7f2ea7aaced717131cb3b7f203a003b01c545a8ea179aadecba86a12b4f713b1777739249aa55fe70f8a1e29e61c5f8d6e6dc30d17ee53be9fe297793fc365e7fbd70f70e50cae8d96b51ef2fd17641451457e567d1851451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451400514514005145140051451401b5a5788f5dd0d644d26fa6b559410eb1b90a73edd33efd6b1ddda47691c966624927a9269b4574d5c6e22a528d0a936e11bd936da57decb657f22234a11939c52bbddf70afaa3e03f8dd6585fc1da8c9f3c7992ccb1eabfc49f51d47e35f2bd5dd3b50bad2afa0d46c9cc73dbb891187622bec3c3be35c470b6794b34a3ac56938ff341eebd7aaf348f2f3cca69e6383961a7bee9f67d3faec7e99d15c67813c5f67e33d062d4e060265fdddc459e524039e3d0f506bb3aff0050f2acd30b9960e9e3f053e6a7512716baa7fd6aba33f9d71586a987ab2a1555a517661451457a060145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007ffd1fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803e77fda0f4092ef46b2f1040a58d8ca629703a472f427d830c7e35f24ab32307425594e411d4115fa5babe976bad69973a55e28686ea368d81f7eff00857e7878a3c3d79e17d6ee747bd521a163b1bb3a1fbac3ea2bf853e931c1757079bc388a847f77592527da71565ff81452b79a67ec9e1f66d1ab857819bf7a1aaf34ff00c9fe67d5bf08fe25c5e23b48f40d5df6ea56e8023b1ff5e83b8ff687715ee75f993657b75a75d457b6523433c2c191d4e0822becdf869f162cbc57147a4eb0cb6faaa0c0ec93e3bafa37a8fcabf47f03fc6ba59952a79067b52d885a426f69ae89bfe75ff937aefe0f187094a84a58dc1c6f07ab4ba79af2fcbd0f69a28a2bfa90fce4c3f11deea7a768b7579a3dafdb6f234262841c6e3fd71d71debf3c75cd4b53d5b56b9d4359676bc95c997cc0430238db83d001c01dabf4aabccbc6bf0b3c3de310d72ebf64bfc717310e49edbc746fe75f81f8e3e1966fc5786a5532bafad2bbf64f48c9f74ff9ada7bda5baad6ff6bc1fc4385cb6a4a38887c5f696ebcbd3d3f1e9f06515e85e2bf867e29f09c8c6eadcdc5a83f2dc400b211ee3aa9fad79e904706bf81338c8f30ca712f099951953a8ba495beeeebcd687ed785c5d0c4d35568494a3dd0514515e51d01451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007a57c2df193f843c4b1492b1fb15e620b85cf0013c3fd54fe99afbd5595d43a9c861907d41afcc0afbff00e176b6faf781b4cbc95b74b1c66de43df7424a67f1001fc6bfb27e8b9c61567f58e1baf2ba8af690f2d529afbda7f79f95788b95c57263e0b57eebfd3f26bee3d068a28afec33f2c0a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803fffd2fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800ae23e23c9a8c3e08d5e6d2a478ae63b72eae870c02b02d83fee835dbd41756f1dddb4b6b30dd1ca8c8c0f70c306bccceb032c6e5f5f070972ba9094535ba724d5fe573a3095951af0aad5d45a76f467c97f0f3e35dee9b22695e2c76b9b462025d1e658b3d9bfbcbefd47bf6faced6eadafade3bbb4916586550c8ea720835f9d3e2cd065f0d7886f746941ff47948427f8a33ca9fc4577bf0cbe285e783ee574ed459a7d2a56f990f26127f897dbd457f1c7859e36e3723c6be1ce2c9b74e32705396b2a6d3b5a4f771bf5de3e9b7eadc47c2147194bebf962b49abd96d25e5d9fe67dc34553b0bfb3d4ece2bfb095668265dc8e87208ab95fdad4aac2a4154a6d38bd535aa69f547e4728b8b7192b3415e53f14fe1ec5e33d2bed164a1754b404c2dd3cc5ee8df5ede86bd5a8af2788787f059de5f572ccc21cd4e6acfcbb35d9a7aa7dceac063ab60ebc71141da4bfab1f98f756b71657325a5dc6d14d0b14746182ac3a8351c52c90c8b342c51d08656538208ee0d7da7f14be16dbf8b2ddb57d21162d5a15edc0b851fc2dfed0ec7f03edf18ddd9dd585cc9677b1343344db5d1c61811ec6bfcd7f127c36cc783f31f635af2a3277a7516cd767da4baaf9ad0fdf720cfe8669439e1a497c51edff00fa77e1cfc6b8da38f46f17c9b5d4048aef1c374003e3bff00b5f9d7d2d14b14f1acd0b074700ab29c820d7e61d7ab7817e2beb9e0f65b49c9bdd3fbc3213b93dd0f6fa1e2bf66f0b3e9135706a195f14373a6b48d5de51ff1ff0032f35ef2eb73e5388f81635af89cb95a5d63d1fa76f4dbd0fbaa8ae43c2be38f0f78bedc49a55ca9980cbc0e76ca9ff01ee3dc575f5fd9d96e6784cc30f1c5e06a2a94e5b4a2ee9fdc7e4f88c3d5a151d2ad17192e8c6491c72a18e450ca78208c835e5be28f841e13f12069a387ec17241c4b00c0c9eecbd0d7aad15c79e70e6579ce1de1734a11ab0ed257b7a3dd3f34d336c1e3f11849fb4c34dc5f97f5a9f13f883e0778b74967934e09a840b920a1daf8ff0074ff004af26bfd2f51d2e530ea36d2db3838c48a579fa9e0d7e98d67dee95a6ea51986feda29d181043a86ebf5afe73e25fa2e653886ea64b889517fcb2f7e3f7e925f36cfbbcbfc45c4c2d1c5d3525dd68ff55f91f99f457dc7acfc13f036aa5a486ddec246e8d6cdb547fc00e57f4af31d53f676bc4dcda3ea8920ec93a6d3ff007d2939fc857e199dfd1df8cb00dba34635a2bac24bf2972bfb933ec707c7395565efc9c1f9afd55d1f35515ead7ff05fc7b644ecb25b951d0c32039fc0e2b91bbf04f8bac73f6ad22ed40ea444cc3f35cd7e6998704710e06ff5bc1558dbbc256fbed63dfa19be0ab7f0ab45fcd1cbd153cd6b736edb2789e36f4652a7f5a83a57cd4e9ca0f966accf4134d5d0514515030a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800afb37f67e98c9e0a9e3ff009e57f2afe688dfd6be32afaf7f67766ff846b525ec2fb3f898d6bf7bfa375570e34a715f6a9d45f85ff43e2f8f637ca64fb38fe67d07451457fa227e16145145001451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007fffd3fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803e7df8e7e083aae9cbe28b08f37362bb670a39787ae7ea9fc8d7c835fa7b2469346d14aa191c15653c820f515f0dfc56f87f3f83f5837b6685b4bbd62d0b0e9139e4c6dfcc7a8fa57f167d243c329d2acf8af2e87b92b2aa9747b29fa3d14bcecfab3f5ae02e205287f66d77aaf87cd76f974f2f421f86ff132fbc1175f65b80d71a5ccd9961cf319eee9efea3bd7dafa26b7a678874f8b53d26759e0947054f20f70475047a1afcd5aeb3c27e32d6bc1d7c2ef4b94ec623cc85b9471ee3d7debe2bc24f1cb15c35cb95e6b7a983e9fcd4ffc3de3de3f7767ebf1370752cc2f88c37bb57f097af9f9fde7e8a515e79e07f88fa1f8d6dc2dbb882f90664b67386faaff007866bd0ebfbd326cef039b61218ecbaaaa94a5b34ff0f26baa7aa3f17c5e0eb61aaba35e2e325d18579b78f3e1a68de3687ce702db504188ee50727d9c771fcabd268a9cef22c0671839e0332a4aa5296e9fe6baa6ba35aa1e0f1b5f0b5557c3cb9648fcecf15f82b5ef07dd9b7d5a02232488e7504c4ff43d8fb1ae4abf4d6f6c2cf52b77b4be85278641864750c08fa1af9bfc65f012393ccbef08ca236e5becb29f94fb2b751f8d7f157885f46ecc72f72c670e375a96fc8fe38fa749fe0fc99fad647c7d42bda963d724bbfd97fe5f91f31da5e5d585c25d594cf04d19cac91b15607d88afa03c25f1ef51b211d9f8a61fb64638fb44600940ff0068746fc315e13aa68daae8972d67ab5acb6b329c1591719f707a11ee0d6657e23c39c63c41c298b93cbeaca9493f7a0d68fca507a7e1747d7e3f2ac0e65492af1525d1f5f9347e8bf87fc67e1cf13c424d22f6395b1968c9db22fd54f22ba9afcc48679ade412c0ed1b8e8ca7047e22bd5fc3bf19bc5da18586e6517f0ae3e59bef60760dd6bfa97847e94783aca34388b0ee9cbf9e1ac7e717aaf9391f9ce67e1d558de7819dd76968fefd9fe07dc945784685f1efc317db62d62296c243c17c7991fd78e47e55eb7a5f89bc3fad20934ad42dee4119c248370faa9e47e22bfa2320e3ae1fcee29e578b84df64ed2ff00c05da5f81f0b8ec971d8376c4526bceda7deb43728a28afac3cc0a28a2802bcd696970a527863914f50ca08fd6b9abbf01f83afb3f69d1ed093d4ac4aa7f35c1aeb68ae0c665582c5ae5c5518cd7f7a29fe699bd2c556a4ef4a6d7a368f26bcf82be02ba1f259bdb93de29187f3cd7257dfb3de852926c2fa787d0300d5f43515f1598784dc1f8dbfb6cbe9abff2c797ff0049b1ebd0e27cd68fc15e5f377fcee7c87a8fecf9afc3ce9d7b04fd787050ff005af3fd4fe15f8e74bcb4ba6bca833f34243f03bf15f7e515f9de6ff467e13c526f08ea517e52e65f74937f89eee17c41cca9e955464bd2cff0ff0023f322e6ceeecdf65dc1242de92295fe7d6ab57e98de695a6dfc4d0dedb453230c10ea0f15e6dad7c19f04eadbde2b53652b73badced1ff7cf4c7e15f91e7bf458cde8273cab170aabb49383fce4bf147d3e0fc46c2cf4c4d371f4d57e8cf85e8afa175efd9f75bb5579b40bc8ef14722297f76e7e87ee93f97d6bc5358f0e6bba04c60d62c66b56071975f94fd18654fe06bf09e25f0fb887207ff0ab849423fcd6bc7ff02578fe373ec7019de071abfd9aaa6fb6cfee7a98b451457c69ea851451400514514005145140051451400514a013c0e6a4104edd2373f45355184a5b213696e45455d5d37517fb96b337d2363fd291f4ed423fbf6d32fd5187f4adfea588b737b376f464fb486d729d15298665fbd1b0fa835156128496e8a4efb0514515230a28a2800a28a2800a28a2800a28a2800a28a2800a2a686dee2e5c476f1bcae780a8a589fc0574717823c613c7e6c7a35e95eb93038fc811cd77e0f2ac6e2d3785a329dbf962dfe4998d4c452a7fc4925eaec72d455dbcd3751d39fcbd42d66b66feecb1b21ffc780aa55c95a8d4a5374eac5a6ba35666919292bc5dd051451599414514500145145001451450015f707c0ed31b4ff015bcee30d7d3cb71ff0001cec5fd12be2bd3eca6d4afedf4fb71992e654897eac715fa47a469f0e93a5da69900c476b0a42a3d9140afea7fa2ce412ad9c62b3892f76943917f8a6eff00828bfbcfce7c46c6a86169e156f277f92ff82ff03468a2bc3f55f88cfa5fc53b5f0f7981ac2744b7987f7267fba47e3807eb5fd87c47c4f81c929d1ab8f95a352a469aff0014b45f25d7b23f2ccbf2ead8c94e3456b18b97c91ee1451457d11c01451450014514500145145001451450014514500145145001451450014514500145145001451450014514500145145007ffd4fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800ac8d7743d3bc45a64da4ea71892099707d41ec41ec456bd1586270d4b1146587af15284934d3d534f74cba75254e6a70766b667e7af8ebc11a8f827576b2b9fde5b484b5bce070e9efe8c3b8ae26bf47bc4de18d27c57a649a66ad107461f238fbd1b76653d88af85bc6de07d5bc15a935a5ea992ddc9f22e00f9645fe8dea2bfcf4f18fc1bc4f0c622598e5d172c149e9d5d36feccbcbf965f27aeffb970af1553cc69aa15ddaaaff00c9bcd7ea8e46daeae2ce74b9b491a19632195d0956047a115f46f81be3b4d6c23d37c5e8658800ab7918f9c7fbebdc7b8e7d8f6f9ae8afcd783f8f33ae19c4fd6729ace37de2f58cbfc51dbe7a35d19efe699361330a7ecf130bf67d57a33f4bb4cd5f4dd66d96f34cb88ee627e8d1b035a55f9bba1789f5cf0ddc0b9d1eee480e72541ca37d57a1afa3bc2df1fad26096de28b730bf00cf0f287dcaf51fad7f687037d23b22cd631c3e73fecd5bbbd69bf497d9ff00b7b4f33f27ce380b19866e784fde47ff0026fbbafcbee3e94a2b0b47f13683afc4b2e937b0dc6e19daac370faaf5addafe83c2632862a92af869a9c1ece2d34fe6b43e1ead29d3972544d3ecf432f54d174ad6edcdaeab6b15cc67b48a0e3e87a8af14f107c01d02f8bcda1dcc96121e446c3cc8f3fcc67eb5eff457ce712f02e439fc39736c2c6a3ef6b497a495a4bef3d0cbf39c6e09df0d51af2e9f73d0f84f5df83be35d14965b65bc887f1dbb6efd0e0d79b5d585f58b98ef2de48187512295fe75fa6bd7ad65dfe89a46a8bb350b386e0641f9d01e95fcfbc43f458cb6b375325c5ca9bfe59ae65f7ab35f89f6f81f11ebc6cb17494bcd68feed57e47e69d4914d2c2e2485d918742a4823f2afbc750f847e04d401dda6a44cc725a2250fe8457177dfb3e7866724d95ddd5b7a0dc1c67df7026bf2acc7e8d3c5f857cf84953ab6fe59b8bffc992fccfa4a1c7f95d456a9cd1f557fcae7cefa57c49f1ae8e40b4d526651fc129f317ff1ecd7a169dfb4178a2db6aea1696b76a3ef101a373f88247e95af77fb3aea0a59acf578d973f2ac911ce3dd837f4ae6ae7e02f8d2104c4f6b30edb5d81fc8a8fe75186c93c5dc8972e1d57515d149545f75e4bf02aa63386319acdc2efbae57f7d91e8d61fb44e8f20ff899e957109ffa62eb28fd76576169f1bbc037380f752c0c7b49137f3008fd6be64baf84be3eb452cfa6338033fbb75727f006b99bbf09789ec41377a5dd4607ac4c7f966bd7878cde26e56ad98619cadff3f28b8fe31e5395f09f0f627f813b7f8669fe773eeab4f889e0bbd216df56b62c7f84b807f235d15beb3a4dd1c5b5e4321ebf2b83d7f1afcd69619adced9e378cfa3a953fad2c5737101cc32ba67fbac47f2af5b07f4aacce9be5c76020ff00c32947f06a472d5f0df0ef5a359af549ff0091fa6eb2230cab03f434fafce1b4f16f89ac405b4d4eea303b091b1fcebaab1f8bbe3cb160c3513301c0595430fe95f6997fd2a724a9658cc1d487a38cbff913c9afe1be2d7f0aac5fadd7f99f7a515f23e95fb426b70ed5d5ac61b851f79a32518fe1c8af48d27e3cf846f76adfa4d64c7a965dca3f11cfe95fa564be39705e6568c318a9c9f4a89c3f16b97f13e7f17c1d9b61f574b997f75a7f86ff0081ee14573ba578b7c37ad0074dd42098900ed0e0373ec79ae8bad7ea383c761f174d56c2d4538beb169afbd1f3b568d4a52e5a9169f9ab0555bbb2b3bf85adef618e78db82b228607f3ab5456f529c671709aba7d199c64e2ee9ea789f897e07785f580f3e95bb4db83923cbf9a327dd4ff435f35f8a7e1a78a7c28ecd796c67b604e2e20cba11cf5ee3f1afd00a6491c72a18e550eadc10c320d7e25c6be01f0d67b1955c343eaf5bf9a0bdd6ff00bd0d9fcacfccfafca38db30c1b51a8fda43b3dfe4f7fbee7e61515f6d78d3e0c787bc451bdce92a34dbee4878d7f76e7d1978ebea08c57c97e26f08eb9e12bd6b3d62029c9092af31c83d55b1fa75afe32e3ef0973ee149f3e321cf43a548eb1f9f58bf5f9367eaf92f1360b3256a52b4ff95eff002ee73345751e1ef06788fc51284d1ece4953383291b631f563c57d13e17f801a7db2adc78a2e4dd4bd7c8872b10f627ef37e83dab8f843c2ce24e249279761daa6fedcbdd87def7ffb75366b9a711e0300ad5e7ef765abfbba7cec7cab6f6b73772086d62799c9c05452c73f857a3e87f08bc6daded71682d22600efb83b783df1d6bed3d27c2fe1fd0d02697630418eea833f9f5adfe9d2bfa5386be8b182a56a99ee29cdff002d35cabff02776fee47c063fc47ab2f7707492f396afee5fe6cf99748fd9de0015f5bd4d98e394b750307fde6cff002af45d3fe0cf80ec3696b2372c3a999cb67f0e95eab457edb93f843c1f96a5f57c041b5d66b9dffe4cd9f238ae29cd711f1d66bd34fcac73369e0cf0ad8e3ecba55aa63a7ee94ff306b6a2d3ac21ff00536d1263fba8055ca2beeb0d95e0f0eb970f46315e514bf2478f53135aa3bce6dfab633cb8ff00babf90a3ca8ffb8bf90a7d15d9c91ec637651974cd367e27b5864cff007914ff003158577e06f085f67ed3a4dab67d230bfcb15d5d15c38aca303895cb89a3192f38a7f9a37a78aaf4dde9cdaf46cf22d4be09781afb7186de4b463d0c2e703f03915e79ab7ecedc33e89aa64ff0a5c27f3653fd2bea0a2be0b38f07383b324fdbe02116fac2f07ff92b5f8a3d9c2f15e6b87f82b36bcf5fccf8275bf84be36d103492597da625c9df01de303be3ad79e5c5b5c5ac862b989e2707055d4a9cfe35fa72403c1ae7f55f0af87b5b52ba9d8413e73cb20cf3ef5f8af127d15f0952f5323c5b83fe5a8b997fe04acd7dccfadc0788f517bb8ca49f9c74fc1ff99f9bf457d63e29f801a7dc86b9f0bdc35acbd7c897e789be87ef2fea3dabe72f107847c41e189cc3ac59bc2338593198dbe8c38afe6be31f0bb88f86a4de65877ecff9e3ef43ef5b7a4accfbfcab88b01982ff00679fbdd9e8feeff239ba28a2bf3e3db0a28a2800af7df871f06a5f10dbc7ad78899edecdf06285461e55f527b29eddcd733f08fc18be2cf11096f13758d8e25981e8cdfc2bf8d7dce8891a2c7180aaa0000700015fd4be02f83d86cea9bcff003b873504ed083da4d6f27de29e897577be8b5fceb8d38a6a60dfd4b06ed3eafb2ecbcff230f46f0c683e1f8160d26ca28028c6e551b8fd5ba9adec0a28afedec260e861692a386828416c92492f923f20ab567524e751b6df56676a5a4699abdb9b5d4eda2b989baac8a1bff00d55f2cfc4ef83a9a2dbcbaff00864335aa0dd35b1f98c63fbca7b8f51dabeb7a8e58a39a3686550e8e0ab2919041ea2be278f7c39ca38ab033c3e369a556deed44bde8be9af55dd3d1faea7af92e7d8acb6b29d297bbd63d1ffc1f33f30e8af4bf8a9e105f09789e58ad576d9dd0f3a0f4507aafe06bcd2bfcc9cff24c564f98d6cb318ad529c9c5fcbaaf26b55e47f4260b174f1542388a4fdd92ba0a28a2bc83a428a28a0028a28a00f59f82da40d53c756aee329648f707eaa30bfa9afb9ebe55fd9cec375e6b1a991f7238a01ff03258ff00e822beaaaff443e8dd94c709c1d0c45b5ad39cbe49f22ffd24fc338fb12ea66ae9f48a4bf5fd4ad7b751d95a4d792f090a339edc28cd7e765eeb536a3e297d7198ef92ec4c0fa00f91fa57db5f152f9ac3c05ab4ca769687cb0475064217f99af808120e457e53f4a4e21ab1ccb0395c1d9422eabf572b2fb945fde7d2f873818fd5eb621eedf2fc92bbfccfd3a82412c11caa721d5581fa8a96b82f869af27887c1da7dd86dd245188251dc3c7c73f860d77b5fd7d926694732cbe863e83bc6a46325f3573f2ec661a587af3a13de2dafb828a28af50e60a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2803ffd5fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800ac8d7342d2fc45a7c9a66af02cf0483183d54f62a7a823b115af456189c351c452950c4454a125669aba69f469974ea4a9c94e0ecd6ccf86be20fc29d57c1f2497d641eef4ccf1201978c1e81c0fe7d2bc92bf4f26862b889a19d03c6e0ab2b0c820f50457ce3e3af81705e34da9f84d8432b1dc6d1b88c9efb4ff000fd3a57f1778a5f475af879cb33e158f353ddd2fb51ff05fe25fdddd74bf4fd678738ee1512c3e64ed2e92e8fd7b7aede87ca3455fd4b4bd4747bc7b0d52de4b5b88fef4722ed38f51ea0f62383542bf942b51a946a3a5562e325a34d59a7d9a3f4a8ca324a51774c9edeeae6d6412db4af138e8c8c54fe62bd3b43f8cbe39d176c6f762fe15fe0ba5de71feff000ffad795515ece49c519be4f53dae57899d27fdd9349faad9fcce5c5e5f86c54797134d4979a3eb5d17f685d2270a9ae584b6cfdde13e627e4706bd334cf89fe07d554791aa431b1eab31f2c8faeec0fd6bf3fa8afdab24fa4bf15e0d2863142baef28f2bfbe2d2fc0f92c5f87f965577a5787a3bafc6ff99fa676fa8d85da092d6e22950f46470c0fe20d5c041e8735f98f05d5cdb3892de5789d7a3231523f2ae9ecbc7be32b0e6db57bae3a6f90be3fefacd7e9596fd2bb0b2b2c7e5f25e709a7f834bf33c0afe1ad45fc1ae9faab7e4d9fa25457c2d69f1a7c7b6a30d7693fbc91839fcb15d1db7ed01e2988013dadb4c475e0aff2afb4c1fd267842b2fdeaab0f5827ff00a4c99e4d5f0f7348fc2e2fe7fe691f63515f2841fb44ea23fe3e74b89bfdd723f98ad9b5fda26d19bfd2f4b741fec3ee3fc857d161bc7de07acedf5ce5f584d7feda7054e09ce23ff2eafe8d7f99f4b534aa9ea01af0bb5f8fde1399b13c17108f5201fe55d15a7c65f015d9c7db5a1f7910a8afa5c1f8a5c218ad296634be7251ff00d2ac7055e1bcd29fc5425f257fc8f44b8d274bbb52b736904a0f5df1a9fe95c9dffc32f026a24b5c68d6ea4f53103113f8a106afd978f3c1f7ff00f1ebab5ab7d640bfcf15d25bded9ddaefb59e3997d6370c3f435ec4b0bc3b9cc6ce346baf484ff00cce55531f847bce1f7a3c5effe017832e726ce4bab43d82c9bd47fdf609fd6b87d47f676bb4c9d2f55490761326dfd4135f54d15f219a7825c158ebb9e06317de0dc3f08b4bf03d3c3f17e6f476acdfad9fe7a9f0b6a7f067c73a76592d16e946798581e07b1c579d5fe8daae94e63d46d26b723fe7a215fd4d7e97553bbd3ec6fe230dec11ce87aac8a187eb5f96e7bf459ca2b272ca7153a72e8a494d7e1caff0033e8f07e23e262ed89a4a4bcb47faa3f33e39658983c4ec8c3a153835dde83f13fc6be1e2ab69a8bcb0aff00cb1b8fdea63d3e6e47e0457d43af7c14f076af992d626b094f3ba0381ff7c9e2bc5bc43f017c49a7069b45953508c7f07fab93f0cf07f3afc6f30f07fc40e14a8f1795b9492fb5464eff0038e92f959a3eae8714e49994553c45937d2697e7b7e2773e1cfda0ac2e0a41e25b336cc783341f327d769e47eb5eeda3788b45f105b8b9d1eee2b94c64ec6c95cfa8ea3f1afce6bfd3aff4ab96b3d4ade4b6993aa4aa54fd79ea3dfa5161a8dfe9772979a75c496d321cabc6c548fcabd9e17fa49f10e575161b3ea4abc568eeb92a2fc2cdf938dfcce4cc780703888fb4c1cb91bf9c7faf99fa67457c91e15f8f7a95988ed7c4b00bb8c70678fe5931c751d0d7d2fe1ef14689e28b4179a3dca4cbfc4a0fcea7fda5ea2bfac782fc50e1ee288db2dadfbceb097bb35f2ebeb16d1f9a66dc398ecb9debc3ddeeb55ff03e763a0aced4b49d3358b7fb2ea96d15d424e764aa18647d6b468afbdaf429d6a6e9568a945ee9aba7ea99e242728494a0ecd15edad6dace2582d22486341855450a001ec2ac51455c211845460ac909c9b77614514550828a28a0028a28a0028a28a0028a28a0028a28a0028a28a002a9dee9f65a95bbdadfc11dc42e30c92286047e35728a8ab4a1520e9d449a7ba7aa65464e2f9a2eccf9b3c65f016d27f32fbc2527d9dfa9b5909287fdd63c8fa1af99355d2352d12edec754b77b7990e0ab8c67dc1ee2bf4bab96f14783b42f17599b5d5edc391f7251c3a1f507fa57f35f88df474cb3358cb19c3f6a15f7e5ff009772f97d87e9a79753eff21e3bc461daa58ef7e1dfed2ff3fcfccfce8a2bd63c7bf09f5af0796beb606f74dc9fdf20f9a3ff007d7ae3dc71eb8af2735fc4bc41c3998e498c960334a4e9d45d1f55dd3d9af34ec7eb982c7d0c5d255b0d2528b3edff00821a22697e0986f19712ea2ed3b1efb73b53f415ec35cd783add6d7c2ba4dba0c04b4880ff00be4574b5fea4f04e554f2dc830781a4aca14e0be764dbf9bbb3f9d338c4cb118eab5a5d64ff3d02b805f893e187f148f092ccc6ec929bc0fddf99d766ef5febc575fab5d7d874bbbbcff009e30c8ff00f7ca935f9cf67a9cf1ebb0eaecc4ca2e9672d9e4b6fdc6bf32f18fc54c57096270187c2453f6b2bcefafb89a4d2ecddf7e963e8385786e9e670ad3aadfbaacaddddff23f4a28a86da513dbc538e9222b0fc466a6afdce13538a92d99f1cd59d99f3f7ed05a48b8f0f5a6aca3e6b59f631ff6641fe22be40afbefe2bd98bdf00eacb8c98a212afd5181fe59af812bf803e93795470dc570c5417f1a9c5bf54dc7f248fdb7c3ec4ba9963a6fecc9afbecff561451457f3a1f7414514500145145007d83fb3cdb84f0b5f5c63992f5949ff007117fc6bdfebc2bf67d607c1772bdc6a129fcd23af75aff503c1b8461c179728ff0027e6db7f89fcf1c5726f36aedf7fd0f26f8d9bbfe15fdf63a6f873ff007f16be18afbf3e2b5a7db3c03ab4639d90f998f5f2c861fa8af80ebf94fe947879438a68557b4a8c6df29ccfd27c3a9a796ce3da6ff247b37c1bf1d7fc22fad9d2afdf1a7ea2caac4f48e5e8adf43d0fe1e95f6c8208c8e41afcc0071c8afad7e0efc503aa2c5e15d7e4ff004a8d42dacec7fd6a8fe06ff680e9ebf5afa7fa3b78ab4f0c970b66b3b45bfdcc9ec9bde0fb5deb1f3baea8f3b8eb86e552f996196abe25fafcba9f45d14515fda27e4c14514500145145001451450014514500145145001451450014514500145145001451450014514500145145007fffd6fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28ae73c49e2bd13c296d15d6b53f949348b120032c4b1eb8f41d49ec2b931d8fc360a84b158b9a8538eae4dd925e6d9ad1a352b4d53a516e4f648e8e8aa761a8596a76c979a7cc93c2e01574391cd5cade95585582a94da717aa6b54d7932251716e325668e73c45e13d07c556bf64d66d526033b1f187427bab7515f33f8b3e026ad61beebc3130be8473e44842ca07b1e8dfa1afaee8afcff008d3c2ce1de278b966346d57a548fbb35f3ebe924d1ede53c498ecb9da84fddfe57aaff0081f2b1f99b7fa6ea1a5ced6da8dbc96d2a9c14914a91f9d51afd26d5fc3da2ebb098355b48ae14f775048fa1eb5e19e23fd9fb4ebadd3f876edad5c9e2297e68ff003ea2bf93f8bbe8cd9e6079ab64b516221db48cfee7eebf934fc8fd2f2cf107055ad1c5a707df75fe6beef99f26515e87affc2df19f878b3dcd8b5c423fe5adb7ef17f1180c3f2c579f3a3c6c52452ac3a861823f035fcf99b64798e5759d0cc684a9cbb4a2d7e7b9f7186c650c4479e84d49793b8da28a2bca3a028a28a0028a28a0028a28a00326adc37f7b6ec1a09e48c8e9b588feb5528aba756707cd06d3f2138a6acced34ff00889e33d348fb36ab7181c00edbc63e8d9af46d23e3f789ecf09a9dbc17a83a9c146fcc715e0b457dae4fe25714656d3c163aa24ba39392ff00c06575f81e4e2b21cbb12bf7d462fe567f7ab1f68687f1e7c23a8958f5449b4d90f1975f323ffbe979fd2bd774ed6749d5e113e99770dd46dd1a270dfcabf34aaf69fa9ea1a55c2dd69d7125bcaa721a362a6bf6be1afa51673866a9e754235a3fcd1f725fac5fdc8f92cc3c3bc254bcb093707d9eabfcff00167e99515f1cf863e3c6bfa615835e8975083805c7c928038fa37e38fad7d11e18f895e15f15285b1ba114fde19f08ff0096483f8135fd33c1de30f0c711f2d2c257e4aafec4fdd97cba4be4dfa1f9f669c2b98e02f2a90bc7bad57f9af9a3a2d6fc37a2788ad4da6b1691dcc67a6e1f329f556ea0fd0d7ce1e2ff008077102bdef84e7f39464fd966387fa2b743f43f9d7d57457a7c63e1a70f713536b33a0b9fa4e3eecd7fdbcb7f4775e473e55c438ecbdffb3cfddecf55f77f91f9977da7dee9972f67a840f6f321c3248a548fcea7d2b58d4f43bb4bed2ee1ede64390c8719fa8ee2bef3f1cf86bc29ace952cfe2458e148949fb49f95d3dc1ea7e95f056ad6f636ba8cf0699706eed91c88e62bb0b2fd2bf847c4df0cf17c0d8ea75686294a3277834f96a2b778deff00f6f2d3d363f65e1fe20a59c51929d369add5af17f3fd19f597c3bf8cf69aeb47a47894a5ade9c2c737dd8e53e87b2b1fc8d7be0218654e41ee2bf303e95f437c32f8c5268e23d0bc50ed2d9fdd86e7ef345ecfeabefd47d2bf67f097e90ae728651c553f28d67f82a9ff00c97fe05dcf92e26e07493c565abd63ff00c8ff0097ddd8faea8af10d7be3bf84f4c2f0e9a936a12ae70630163cff00bc4e7f206bcd2eff00686d7a4901b3d36de24eeb23339fcc6dfe55fb4675e39f0665b51d1a98b5392fe44e7f8af77f13e5309c1b9b578f32a565e6d2fc373ebaa2be6ff0efed0563753adb7886c8da2b6079f136f507fda5c0207d335f43d9de5aea16d1de594ab3432a8647539041aface13e3cc8b89693a993e214dade3aa92f58bb3f9ede679999e4b8ccbe5cb8a85afb3dd3f9a2cd14515f60794145145001451450014514500145145001451450014514500145145003248a3991a29543a302195864107d457caff13be0d9b559b5ff000a2168b979ad00fba3a931fb7b57d5748402082320f515f13c73c0395715605e0f3186abe19af8a0fba7dbba7a33d7c9b3bc4e5b5bdad07a755d1ff5dce73c1f234be16d29df826d22cffdf22ba4a8e28a386358a25088a30147000a92beaf2fc33c3e169e1e4eee314afdecad73cdaf5154ab29a5bb6cc6f1145e7e83a8c3fdfb6957f3535f9b5f71b8fe13fcabf4d2f806b29c1e4189f3f91afcceb95d93ca9d76bb0fc89afe3bfa57504b1597565bb5517dce2ff53f54f0d277a75e1e71fd7fc8fd20f0dcdf68f0fe9d3ff7ed623f9a8adbae73c2031e16d241ff009f387ff4115d1d7f5e649272cba849eee11ffd251f976315abcd2eeff3395f1ca07f06eb6a7fe7c2e0fe484d7e74d7e8c78d881e0fd6f3ff003e171ffa2cd7e73d7f1a7d2b52fed5c03ffa772ffd28fd5fc36ff76adfe25f90514515fca27e921451450014515ee3f05fc04be24d51b5dd4d0369f60c36ab0e259ba81f451c9f7c57d270970be3388735a394e057bf37bf48a5bc9f925afe1b9c199e634b03869626b6cbf1ecbe67ab7c02d3f56b0f0edeff00685b496f0cf70b2dbb38c7980a80c40eb8181cf7af79a6aaaa285401540c00380053abfd42e0ee1a870fe4d87c9e9d4735495b99e8dddb7b2d96ba79753f9df36cc1e3b173c54a36e67b7e064ebb682ff46bdb323779b048a07a9c1c7eb5f9b53c4d04f240dd637643f5538afd3a232083debf3afc71a7ff0065f8b354b3c6025c395fa139cfe35fccdf4adcaaf470199456ce707f3b497e4cfd07c35c4fbd5f0efc9fe8ff004394a9609e6b6992e2ddcc7246c19194e0823a1151515fc691938c94a2ecd1faab49ab33ef1f859e3a5f19e823ed440d42cf11dc2ff7bfbae3fdeefef5e9f5f9f7f0e7c58fe10f13dbdfbb116b29115ca8ef1b77ff00809e6bf406391268d658d832380ca472083d08aff483c0df10a5c4d90a862e57c4d0b467de4beccbe6959f9a7dcfc178c723597e379a92fddcf55e5dd7f5d18fa28a2bf6a3e4828a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00fffd7fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a28021b8b886d6092e6e1c47144a5dd98e02aaf249af81fe24f8d65f1a78824ba8c91650131daa1fee0fe223d5bad7b97c78f1a1b2b28fc27612625ba024b92a7958b3c29ff0078fe82be4bafe21fa49788f2c562ff00d56c0cbf774da751aeb3e91f48eeff00bdfe13f5fe01c8552a5fda3597bd2f87c977f9fe5ea75de17f1bf887c237227d26e084fe285fe68d81f51fd457d4de11f8dde1dd7425aeb1ff0012cbb381f39cc2c7d9bb7e35f15d15f9170278bdc43c2cd52c254e7a1ff3ee7ac7feddeb1f969dd33e9f39e17c0e63ef558da7fccb47f3eff33f4f229629e35961757461956539041f435257e7b785fe2178a3c2520fecdbb66b7ce5ade525e23f81e87dc62be8df0d7c7ad03510b0ebd1369f37771f3c47f10323f2afec1e0cfa42f0d672a34b1d2fab567d27f0bf49edff0081729f96e6dc0d98611b9515ed23e5bfddfe573df28aaf6b7505edbc7776ae248a550c8c3a107bd58afde2138ce2a70774f54cf8c945c5d9ee21018608c8ae4f5af037857c400ff69e9d0c8e41f9c285719ee1860e6bada2b8f30caf078fa4e8636946a41f492525f7335a188ab465cf464e2fba763e73d6ff00679d2672d2e83a84b68c7a4730f353e80f0c3f126bcab54f821e39d38b18218af507f140fc9fc1b06bee1a2bf1dcf7e8f5c1b98b73a5465464fad3934bff00017cd1fb923eab07c739ad0569494d7f797eaaccfcded43c2be23d2891a869d730edea5a338fcc71582415386041f7afd3c78e3954ac8a181ea08c8ae5754f02f84b590dfda1a5dbb96eaca9b1bfefa5c1afc9b39fa2949272ca71fe8aa47ff6e8bffdb4fa6c278931db1347e717fa3ff33f3b28afb1b56f801e18bbdcfa65c4f66c4e42e43a0f600f35e5dabfc05f14592b49a74b15e2819da0ed63edcd7e3d9e780dc67965e4f0bed62bad36a5f8692fc0fa9c1f1965588d154e57da5a7e3b7e2785515d56abe09f15e8b9fed0d32745071b9577af1eeb9ae56bf2ac765b8bc154f638ca5284bb4934fee763e92957a7563cd4a49af2770a28a2b88d428a28a0028a28a0029f1c9244e24898a32f20a9c11f8d328a69b4ee80f6df05fc6bd77c3e52cf5acea56438f98fef93e8ddf9ec69be26f8e1e2ad5a775d21869b6a0fc81399481dd98ff21c578a515fa1af1638b565ab298e3a6a9aecfdeb76e7f8ade573c3ff0056f2c788789745733fbbd6db5fcec753acf8d3c51e20b45b2d5f5096e6156de118f19e9cfad72d4515f118fccb178eabedf1b565527b5e4dc9d96caeee7af46853a31e4a5149764ac145145711a85145140057d35f007c572fda2e3c2d74e4a15335be4f43fc4a3f9d7ccb5dafc39d45b4bf1b691720e035cac4c7fd997e53fcebeffc2fe24ab91f13e131b4e568b9a8cbce327677fbefea91e2f1160238ccbaad26b5b36bd56a8fd0ba2901c807d696bfd4d3f9c428a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a0028a28a00cfd5674b5d32eee643858a191c93d80526bf34dbf7929c7f137f335f6b7c6cf1547a1f85a4d2a0702ef53fdd000f2b17f19fc471f8d7c65a65bb5dea36b6a9d659a341ff0002602bf86fe9399d52c767d83c9e83bca945f35ba4aa3565eb649fcd1fb1787984951c155c54f4527a7a47afdedfdc7e8ee811f95a258478c6db6887fe3a2b5aa0b58bc9b68a11ff002cd157f218a9ebfb6b0347d961a9d2fe58a5f723f21ad3e6a929776ce37e2149e578235a7e9fe8720ffbe863fad7e78d7deff176e85afc3fd579c1951221f5671fd057c115fc3ff4a9c42967f84a2b78d2bfdf397f91fb078714dac0d59f797e490514515fcba7e881451450028049c0ea6bf433e1fe871f87bc23a769eaa15fc95965f79241b8e7f3af84bc29a71d5fc4da5e9a0645c5dc487fdddc0b7e99afd1f50140503000c015fd83f454c8e32a98ece26b55cb4e2fd7de97e513f2ef12718d468e153def27f92fd45aab737b6965e5fdae648bce711c7bce373b7403dcd5aaf987e34f8945bf8bb41d3c3623d3e48aea5c1fe22e08cfd1467f1afe98e3de2fa5c35944b34a8af694629376bb9492fc15dfc8fcfb24caa59862961e2eda37f72ff3d0fa7abe21f8e3691db78e65913acf0c7237d718fe95f626b3e20d2bc3fa636adaaceb0dba81f375dc5ba01ea4d7c39f13bc4f65e2bf14cba969c58db8458d0b0c1217dabf17fa4de71977fabd0cbe5522ebb9c24a37f7b96d25cd6edd2e7d6f87b85aff005e957517c9cad37d2fa6879e514515fc1c7eca15f71fc17f129d7bc1f0dacedbae34c22d9f3d4a28fdd9ff00be78fc2be1caf5bf841e34b3f086bb37f6aca63b1bc8b6bb0048575395240fc457ec9e05f18c387f8a29cb13351a155384db764afac5bf4925af44d9f2bc63953c765d254d5e71d577f35f71f72d1583a4789f40d79776937d0dc7b230dc3bf4eb5bd5fe8fe131b87c55255f0b35383d9c5a69fcd687e0b568d4a52e4a9169f67a0514515d26614514500145145001451450014571de27f1e786bc24bb756ba0b315dcb0a7cd21fc07f5ae974fbfb7d4ec60d46d1b743711ac887fd961915e661f3ac057c5d4c050ad1955824e514d3714f6bae87454c2578528d69c1a8bd9db47e85ca28a2bd339c28a28a0028a28a0028a28a0028a28a00fffd0fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800ac7d7f59b5f0f68f77ac5e1c456b19723fbc7b28f7278ad8af973f681f1592f6be12b47e001737583dcfdc53fcff002af86f11f8be9f0cf0fe233597c695a0bbce5a457dfabf24cf6720cade618e861ba6efd16ffe47cf1aeeb37be20d5eeb59d41cbcf752176f61d1547b28c01ec2b268a2bfcb3c5626ae22b4f115e5cd3936db7bb6f56dfab3fa329d38d382841592d1051451581615f4dfc29f84425587c49e2987e5387b7b490751d43480fe8a7f1accf837f0d1b54953c53ad45fe8b1366da271feb187f111fdd1dbd4d7d6e000001c01d2bfaf3c09f0561898d3e24cfe1786f4a9bebda725dbf9575dde963f30e32e2d74dbc060a5afda92e9e4bf57f20555450aa3007000ed4b4515fda07e4c14521214649c01debc13e21fc68b2d09e4d23c37b6eef946249bac511f41fde61dfb0af98e2be30cab87304f1f9b55508f45bca4fb456edfe5d6c8f472ccab138fadec70d1bbfc179b67be515f19e97f1f3c5b6985d422b7bc5c8e4a946c7d477af45d2ff685d0a6c2eab61716c4e06e8c8957ea795207e75f9ee4ff00480e0bc7d94b12e949f49c5afc55e3f89ee62b82336a3aaa7ccbc9dff0d19f43d15c1e95f137c11ab85fb2eab0ab310024a4c4d9f4c3804d7690dddadcaefb7952407bab035faa6599f65b9943da65f888545fdd9297e4d9f3788c162283e5af0717e69a2c514515ea9cc35911c6d750c0f62335c6eb9f0f7c21e21dcda8e9b0b48dff002d506c93fefa5c1fd6bb4a2bceccb27c0e6349d0c7d18d48f6945497e28e8c3e2ab50973d09b8bf2763e67d7bf67ab665693c3b7ec8dda2b81b81ff810c1af0cf10fc3df177864b36a7a7c8615ff0096f10f322fc597a7e38afd0ba465570558020f5079afc3b8a3e8e1c2f99275300a586a9fddd63f38bfd1a3ec32ee3dcc70f68d7b548f9e8fef5faa67e60515f7bf88fe14f83fc47ba492d05adc37fcb5b7f90e7d481c1fcabe76f15fc10f11e881ee7472352b71ced4e2503fdd3d7f0afe64e31f0078a32352af461f58a4bed53bb6979c37fbaebccfd0b2ae35cbb18d4252e497696df7edf91e23454b3dbcf6d2b43731b45229c32382ac0fd0d455f894a328b7192b347d6a69aba0a28a2a461451450014514500145145001451450015734e91e2d42d658c95649e3604750430aa75b3e1db5fb76bfa6d9919135dc287e85c575e029ca78aa7086ee492f5b9956928d3937b599fa4a9f707d29d4d4cec5cf5c0a757faff000bf2ab9fcbaf70a28a2a84145145001451450014514500150cf716f6b134f7522431af2cf2305503dc9e2b90f1c78db4df046926fef3f793c84ac1003f348ffd00ee6be25f15f8e7c41e2fba336a970de4824a5ba1c4683e9dcfb9afc67c4cf1a72ae1297d5147db625abf2276514f67296b6bf44936fc96a7d670f709627335ed5be5a7dfbfa23ecfbff8a5e03d3dcc736af0bb0ed1664fd5411fad5687e2efc3e9b03fb59133fdf8dc7fecb5f04515fced53e94fc46ea73430b4547b3537f8f3afc8fba8f87180e5b3a93bfcbfc8fd13b6f1e7832ef020d6acb9e9ba654ffd088ae9a1bab6b94125bca92a1e8c8c187e62bf31aac45777501cc334919ff6588fe55eee5ff4afc5474c765f197f866e3f838cbf338eb786b49ff06bb5eaaff9347e9c64515f9b90f89fc476f8f2354bc8f1fdd9dc7f5ad0ff0084f3c65b767f6c5e63feba9cd7d252fa56e54d7ef701513f2945fe88e09786d88bfbb597dcff00ccfd11674452cec140e4927000af2df187c5bf0bf862278adee1350bdc1db0c0c1941edb98703f9d7c537baf6b7a88c5fdfdcdc0f4925661f9135935f2bc4bf4a6c657a12a3926115293fb7397335e91492bfab6bc8f4b2ff0e68c26a78babccbb256fc6f7fc8e8bc51e27d4fc59aac9aaea8fb9db8441f7517b2815d67c20d0df5cf1c590db98acb37729c700478dbf9b115e6d04135ccc96f6e8d24b2305545196627a002bed8f83fe03b8f086932deea6a16fb50dace9de345e8bf5e726bf34f08f86330e2ce2da78ec5f34e109fb4ab37addad526fbc9a4addafd11eff0013e63472dcb254a9d936b962bf0fc11ec7451457fa467e06785fc7ebe16fe1086cf3cdcdcaf1ec809feb5f19d7d03f1ff5d5bcd7adb4589b2b651ee703fbefcff2af9fabfcdbf1fb3b8663c678854dde349469fce2bdeffc99b47efbc158474329a7cdbcaf2fbf6fc2c1451457e2e7d5851451401eadf056d56ebe21e9ecc322de39e6fc446547fe855f7557c69fb3ec61bc697321ea961263f174afb2ebfd05fa32e1634b8425556f3ab37f728afd0fc47c42a8e59a28f68afcdbfd42be02f8a9a836a3e3cd5652722397c953ed18007e95f7ed7e6ff8ae43378975291bab5cc9fcebc0fa5563250c9307854f49546dff00dbb17ffc91dbe1bd24f1756a768a5f7bff0080749e2cf8837de29d0b49d16752834f882cad9ff5aea36ab1ff0080feb5e774515fc5d9ce778dcd713f5cc7cdcea5a2aefb4528afc17ea7eb185c252c353f6545596afef7761451457947485145140162d6eeeaca65b8b395e1950e55d18a91f88af7bf077c76d534f31d9f89d4dec1903cf5ff005aa3dfb37f3af9f28aface14e38ceb87311f58ca6bb8778ef197f8a2f47f9f6679b9965184c7d3f678a827e7d57a33f4a343d7f49f11d8a6a3a3dca5c42ddd4f2a7d187507d8d6c57e71786bc55acf84efd6ff00479da36e37a754907a30e86bed1f007c4dd1fc6d6e20ff008f5d4a35fdedbb1fbdeac87b8fd47eb5fdd5e1778e596f1472e031a951c5ff002dfdd9ff0081bebfdd7af66cfc7388f83abe5d7ad47dea5dfaaf5ff3fc8f4ca28a2bf763e3028a2aa5f5da58d94f7b27dd823690fd1466b3ab5634e0ea4dd92577e88a8c5c9a8add906a5ac695a343f68d56ee1b48c9c0699c264fa0c9e4fd2b8bf177c44d1743f0b4baee9b750de3c998ad846e1834a7a671d97a9af8bbc5fe2bd4fc5dac4da96a1212bb888a3cfcb1a0e800fe7ef5cc6f7dbb371da0e71db35fc61c4df4a2c4d49e270b94619460d38c26dfbc9eca76b5bcd2f4bbe87eb397f8754e2a9d4c554bbd1b4969e97dfe65cd4b51bdd5af66d475095a69e762eeec73927fa57df3f0d1655f0268a26ceefb2af5fc71fa57c13a4436571aa5a41a94df67b579904d2e09d9193f31c0f6afd20d2bec1fd9b6c34b647b4112885a320a9403030453fa2ce0ea56cc71f9a55a977caa2d37793727cce4d6f6d37ead8bc46ab1861e8e1e31d2f7db4d15adf8ec5fa28a2bfb48fc9428a28a0028a28a0028a28a0028a28a00ffd1fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2802b5e5d43636935e5c1db1c08d2313e8a326bf393c4bac4de20d7afb589c92d733338cf65ce147e0315f5f7c70f11ff637844e9f0b62e3537f2401d7cb1cb9fe43f1af896bf883e945c57f58cc70f905197bb4973cff00c52f853f48ebff006f1faff875967261e78d92d65a2f45bfe3f90514515fca47e9215e99f0c3c093f8d35b1e7295d3ad087b9931c1f441eedfa0ae2344d1ef75fd56df48b042f35c3851e807727d80e4d7e82f847c3163e12d0e0d1ec97ee0cc8fde490fde63f5afdd7c0cf0c5f13e69f5dc6c7fd928b4e5fdf96ea1e9d65e5a753e3b8c3887fb3b0deca8bfdecf6f25dffcbcfd0dfb6b682cede3b5b6411c5128545518000a9e8a2bfd15842308a8c55923f096db776155eeeeedac2da4bcbc916186252ceee70001eb4979796d616b2dede48b0c10a97777380aa3a926be25f899f13ef3c6376d61a7b341a4c2df22743311fc6ffd076afcdbc4cf1372fe0fc07b6afefd69df921d5beefb45757f25a9eff0f70f57cd2b7243482ddf6ff826d7c49f8c17bafcd2693e1d76b6d3972ad2838926ff00e257dbbf7af082493934515fe71f15f17669c458f96619a547293d9748aed15d12ff0087bb3f79cb72cc3e068aa1868d97e2fcd8514515f3477856a596b7ac69a41b0bc9e0c74092103f2ce2b2e8ada8626ad1973d1938bee9d9fe04ce119ae592ba3d4f48f8c5e36d2b6a9ba1728bc6d997771f5eb5eaba2fed0f6cecb1ebda734609e64b76dd81ebb4e3f9d7caf5a7a3e917daeea50695a746649ee1c2a8ec3d49f61debf4ee19f1738d32fad0a181c5cea5da4a12fde5dbd9252bbd7c9a3e7b30e19ca6bc5ceb524bcd7bbf97ea7df1e1ef885e14f13c8b6fa5de069d86442e0abfe5df1ed5dad79e7807e1e695e09b0511aacfa848bfbfb923927d17d147eb5e875fe8a70955ceaae594eaf104611c43d5c617b2f2776f5ef6d3b1f856671c2471128e05b705b37d7fe00514515f4879e14514500721e23f02f86bc530b47aa59a3391c4a836c8bf461cd7cade39f833adf86449a86924ea1a7a82cc40fdf443fda5ee3dc7e55f69cb343021926758d1464b3100003eb5e1de3ff8cba2e91693e9ba03adf5f480a6f5e618b23924f463ec2bf09f18f84382b1197d4c767ae342aa4f9671b29b7d172fdbbf66be6b73ecf85734cde15e347069ce3d53d97cfa1f1ad14f91da47691fef312c7ea6995fe754ad7d363f750a28a290051451400514514005145140057a3fc27d38ea5e3cd3176e56073393e9e58c83f9e2bce2bdbfe023c0be329164c076b57099f5c8c8fad7dbf86b82a78be2acbf0f55fbaeac2ff277fc6d63c8cfeb4a965b5e71df95fe47da3451457faaa7f370514514005145140051451400567ea9a9d9e8da7cfa9ea1208adedd0bbb1ec07f53dab42be6dfda07c46d0d8d9f876da4ff008f8632cc01fe15e141fc6be3b8ff008ae1c3990e233796b282f757793d22bef7af95cf5724cb1e3f1b4f0ab66f5f4ea7cffe34f16df78c75b9b53bb2562dc4411672238fb0fafa9ae468a2bfcb2ccb32c4e618aa98dc64dcaa4db726fab67f46e1e853a14e34692b4568905765e19f00f8a3c5a73a45a130e706794ec887fc0b1cfe00d759f08bc051f8c3587bbd4509d3ac76b483b4ae7a27d3b9afb6ad6d6daca04b5b48d618a301511061401e8057effe10f8132e26c32cdf36a8e9e19b6a2a3f14eda377774a37d366debb6e7c57147192cbea7d570d1e6a9d6fb2ff00367cb961fb3b5db44ada96aaa8e402cb0c790a7b8cb1e7eb815aedfb3a69db7e4d5a7ddee8b8fe55f4a515fd3787f00b81e94393ea7cde6e73bffe947e7b3e36ce24efed6de897f91f2bdc7ece7739cdaeb2807a490127f30e3f95654dfb3cf8857fd4ea36d27d5197fa9afaf68ae3c47d1db822a6b1c3ca3e9527fab66d0e3bcde3bcd3f58afd123e3c8ff67bf1431fdededaa7d3737f85749a67eceaa1c36afab175fee411ecfc32c5bf90afa7e8a781fa3c704e1e6a72c3ca76fe69cadf726855b8eb37a8aca697a25fadce1fc35f0efc2be15c3e9b66a6618fdf49f3c99fa9e9f857714515faee5593e072cc3ac2e5f4a34e9ae91492fc0f97c4e2eb6227ed2bc9ca5ddbb8562f8875ab6f0f68d75abddb623b78cb7d4f603dc9ad69658e089e79982a46a5998f400724d7c55f15fe26378baeff00b27492534bb66fbc7833b8fe223b28ec3f13e83e13c53f11709c27944f11395ebcd354e3d5cbbff863bbfbb767b3c399154ccf14a097b8b593f2edeafa1e55ac6a973ad6a773aade1dd2dcc8d237b64f03e83a566d1457f98d88af52bd5956acef2936db7bb6f56cfe85842308a8c55920a28a2b12828a28a00f73fd9fa40be35b84fefd849fa3a57d9b5f0d7c109fc9f8876684e3ce86e23fafc85bff0065afb96bfd06fa32e2154e0f70fe5ab35f845fea7e21e214397344fbc57e6d057e7478ded9acfc5bab5b30c14ba907eb5fa2f5f0cfc6bd30e9fe3cba900c25da24ebee587cdff8f66bc9fa52e5b2adc3d86c6457f0ead9fa4a2ff548eaf0e710a38ea949fda8fe4ffe09e4b451457f079fb28514514005145140051451400559b4bcbab0b94bbb295e19a33b91d0ed607ea2ab51574ea4a9c94e0ecd6cd6e8528a6acf63ec6f85ff001721f1084d0fc44eb16a2388a53c24ff00e0ff00a1af7aafcc14778dd648d8ab29c8238208afad7e127c57fed5f2bc33e23947db00db6d3b1c79a07f0b1fef7a7ad7f6b782be3afd75d3c8388e7fbdda151fdaed19ff007bb4bed6cf5dff0025e2de0df64a58dc02f777947b79af2eeba7a6df44d6078a91a4f0d6a91a7de6b4980fa9435bf54b524f334fb98faee8641f9a9afea5cd287b7c1d6a3fcd192fbd347e71869f2558cbb35f99f99a410483d452559bd8bc8bc9e0ff009e72bafe448aad5fe4254838c9c5f43fa853bab857d85fb3e6a17773e19beb49d8b4569720444f60eb92a3d81e7f1af912d6da7bdb98aced90c934eeb1a28e4b331c015f5fea108f847f0b1e08187f685c1085c77b898738ff007541c7b0afde7c01856c066989e25aadac2e1694dd47deebdd8f9b6d5d7a2f23e338d5c2b61a9e5f1d6a549251f2d756751e24f8bfe10f0d5eb69f2cb25d5c270eb6ea1950fa16240cfd3353e87f16bc13ae29db7a2d1d464a5cfeece3eb920fe75f06bbbcaed248c599892c4f2493d4d32bdb5f4a1e2458e956f634dd17b42cee974f7af7bf7d2de4727fc43bcbfd8a8734b9bbff00c03ef1b8f8c5e00b790c675032104825236238f7c74aea741f18f86fc4a08d1afa39dc72533b5ffef93835f9cb5d5786f45f15dd5c47a87872dae1a489b292c4080187bf4af6720fa4d711e271f1a75b030ab07bc69a9f3dbcb595fe6bee3971be1f602145ca159c5f79356f9e88fd16a2b90f056b3a9eb3a246fad5abda5fc3fbb9d1c632c3f887b1eb5d7d7f676599852c7e129e3285f9669357566afd1a7aa6b66ba33f26c4d0950ab2a53dd69a6c1451457718851451401fffd2fdfca28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28a2800a28ac2f136b30f87f40bed667385b5859c7bb7451f8b102b9b198ba585c3cf135dda104e4df6495dbfb8d29529549aa70576dd97ccf8e7e34f885b5bf19cd6b1bee834d5fb3a01d377573f9f1f85791558bab996f6ea5bb9cee9267691c9eecc726abd7f937c559ed5ceb37c4e6959eb566e5e8afa2f92b2f91fd2f9760e384c2d3c34768a4bfcff0010a28af41f86de0f93c63e2586cdc1fb24189ae5bb6c53f77eac78ae5c9326c566d8fa596e0a3cd52a49452f5efe4b76fa235c5e2a9e1a8cabd5768c55d9efbf037c1034ad3dbc517f1e2eaf536c21baa42483f9b11f957d0551c31470449042a1238d42aa8e800e00a92bfd4de0be14c2f0e64f4729c22d20b57fcd27f149fabfb959743f9c737ccea63f153c554ebb2ecba20a8e69a2b789e79dc471c6a59d98e02a8e4926a4271c9af91fe327c4c3aa4f2f857429bfd0e16db752a1e2575eaa0f7507afa9af3fc42e3dc0709e552cc319ac9e908759cbb7a2eafa2f3b27be4792d6ccf12a852d1757d97f5b1cefc55f8993f8b2f1b49d2a429a4c0dc638f3dc7f11ff0067d07e35e335723d3ef25b592f5216fb3c58df2918404f419e993d875aa75fe69715f10e679e6612cd7356dcea6aaf74947a28ff00756cadf9dcfe80cb70387c1d0586c32b28fe7e7e61451457cd1de145145001451450015f51fecfde1a88c577e26b88f2fbbc88188e8072c47f2af972beeff837047078034e318c799e63b7d4b1afdf3e8e19251c7f17c6b5757546129a5fdeba8a7f2e6baf347c5f1e62e7432b7187db697cb56ff23d468a28aff440fc2c2b84d73e25783b4091a1bdd4236994e0c717cec3eb8af0bf8bff001175d9b53b8f0b68be6dbdb5b9d93c91a90f2b639191d147b75af9c645915bf7a1831e4eee0feb5fcabe23fd23bfb2b19532cc86829ca0dc6539df96eb46a2959bb3eada5e5d4fd2721e02fac518e231b3b27aa4b7b79bfd2c7d75a87ed07a04195b0b29ae083c1621011fad79feabfb407896e72ba65b416833c311bdbf5e2bc0a8afe7fce3c79e34cc138bc5fb34fa4128fe3f17e27db61783729a1afb2e67fde77fc36fc0ea75cf1a78a3c467fe26fa84d32768c1d918ff0080ae07e95cb51457e558fcc7178eacf118dab2a937bb93727f7b3e928d0a74a3c94a2a2bb2560a28a2b88d428a29eb14ae09446603a900914d26f6019454eb6d72df76190fd149ad7d3bc2fe22d59f669da75cce7383b626c0faf15d586c062b13354f0f4e5293d924dbfb919d4ad4e9ae69c925e6cc1a2bdc341f811e2cd4595f5668f4e88f5dc43c98fa29c7e66be85f0f7c26f056810c63ec11dedc2f59ee9448c4fa8072abed815fb27097801c559d3f695e9fd5e9f7a974dfa43e2fbecbccf95ccf8d72dc27bb0973cbb475fc76fccf82a8afd0dd73e1ff008475fb57b7bdd36056230b2c4812443ea18007f0e95f1278ebc1f75e0ad7a4d2676f32265f36093fbf19240fc411835c7e24f82d9bf08518e36ace3568376e68a6acfa2927b5fa34daf435c878b30b9a4dd282719ad6cfaaf2671b5b3e1fd6eefc39acdaeb5627f7b6b207c1e8cbfc4a7d88e2b1a8afc8f0b8aab86ad0c450972ce2d34d6e9ad533e9aa538d483a73574f467dfba37c4ff086aba38d5a4bf8ad76a8f36295b12237a63a9fc2b8bd63e3df862c8b269b0cb7ac3a1036a9fc4d7c6d457f4363be937c535b0b0a187853a734ace76bb6fba4fdd5e9667c3d1f0fb2d854739b9497457b25f76acfa7dbf68b3bfe4d270be864e7f956ce99fb4268b3c9b353b096d94e30ca43fe7d2be47a2be730bf483e37a353da4b14a4bb3842df824fee677d4e09c9e51e554ade69bff33f47b40f16681e2687ced1af239f032c80e1d7eaa79ae8abf32ec350bed2ee92f74e9e4b79e339592362ac08fa57d47e0af8ed60fa7b41e30631dcc2bf2cd1a16f3b1db0070dfa57f467877f48ccb3377f54cfd470f56df15ff772b79bd62fc9b69f7be87c1e7bc0788c32f6b816e71edf697f9ff5a1f48d67ea7ab69da3db35e6a7711dbc2bd5a460a3f5eb5f32f88ff683ba977c1e19b2112f459ee3e66c7a841c0fc49af04d6fc47adf88ae4dd6b3792dd3f6dedf2afb2af403e82af8c7e92d90e5f1951c962f1157beb1a6be6f57f2567dc595787f8caed4f16f923db77fe4bfad0f7ff1e7c737943e9be0fca03956bb65e7d3e407a7d4d7cdd7579757d3b5cdeccf3cae4967918b3127d49aad457f1cf1971f675c4f8aface6b55b4b68ad211f48febab7d59faa65592e132ea7ecf0d1b777d5fab0a28a2be30f54fb8fe08dada5bf80eda4b62acf3cb23cc54e4efce307d0818af5daf89fe0b78ca5d03c429a35cc9fe83a93042a4f0937f0b0f4cf435f6c57fa5be07713e0f38e14c3d3c2c7965412a728f6715bffdbcb5f5bae87e01c639755c2e6539547753f793f27d3e5b0514515faf9f2c14514500145145001451450032445951a37019581041e4106bf3cfc7be1f6f0cf8aeff004bdb8896432427d627f997f2ce0fb8afd0faf97ff686d0c7fc4bfc411af2336d21f6e597f239fcebf9dfe927c2eb31e19599417bf87927ff006ecbdd92fbf95fc8fbae00cc7d8661f5793f76a2b7cd6abf547cbf451457f9f67edc145145001524514b3ca90408d2492305445196663c0000ea4d475f59fc11f87d0da59c7e30d5620d713826d1587fab8cff001e3d5bb1f4afb9f0f381319c599bc72cc2be58ef39748c56efcdf44babf2b9e3e799cd2cb70af115757b25dd957e187c1ed4b49d46cfc51ae4ed6d3dbb1923b54c67e652b876e7b1e40fcebe97a28aff0049382b8232ce16cbff00b3b2b8b516ef26dddca5649b7d3a2d1248fc0f37ce31198d7f6f887aecadb25d82be69fda1b45f32d34ed7a35e6266b790ff00b2df32feb9fcebe96ae4bc73e1f5f13f85efb49c0323c65a227b489cafebc571f899c36f3de19c665b0579ca378ff8a3ef47ef6adf336e1ec7fd4f31a55dec9d9fa3d19f9d9452904120f5071495fe57b56d19fd1c145145200a28a2800a28a2800a28a2800a7a3bc4eb246c55d482ac0e0823a106994534da77407d83f09be2b2ebc91f877c43285d4146d8266e04e0763fedff003fad7bbddb05b5998f411b1fc81afccb8659609527858a491b0656538208e4106bec1f037c53b7f11f85efacb57709aa595ac8cd9e04f1aa9f9c7b8fe21f8d7f6d7829e367d7b0af23cfaa7efa117ece6feda4afcb27fcc92d1fda5e7bfe47c5bc23ec6a2c660a3ee37ef25d2fd5797e5e9b7c95abb06d56f187437129fcd8d67d3e573248ee4e4b3139fad32bf8a6bd4f695253eedb3f5a8ab248f68f813a341a9f8d7ed770bb974eb77b8407a79848453f806247b815dc7ed13aca11a56811b65817ba947a71b13f3cb5705f057c45a77877c437f71aa4ab0c0fa7c87731c65a3656da3dc8ce07735c178bfc473f8abc4177ad4f90267c46a7f8635e147e55fbc438af0196f858b28c335f58c4d5973aeaa31716dbf54a315eaedb1f1af2dad5f88feb5517b94e2adead3ff82fee399a28a2bf023ed0b36725bc37904b77179f0248ad2c592bbd011b97239191c66bf47f401a69d1ace5d221586d258239224550b84650471f4af8c3e1e7c2cd57c61711de5ea3da694a416948c34a07f0c79fe7d057dc16b6d0d9db45696ca122811638d47455518007d057f6d7d18386734c150c56658ba2a146aa8f236ad295af769efc9f837aa3f23f11330c3d59d3c3d295e71bdd27a2f5e9727c0a28a2bfac8fccc28a28a0028a28a00fffd3fdfca28a2800a28a2800a28a2800a28a2800a28a4660aa598e001927d850d80b45436f7105d429716ceb24520cab29c823d8d3a693ca85e5c676296c7d0542a9170f689e9bdfc87caefcbd4e6fc49e32f0f78521f3759bb4899865631cc8df451cd79fa7c77f033cbe596b95527018c471f8fa57c87e24d66fb5ed6aef52d42469249657c64f0ab93851e800e2b0ebf86788fe93b9ecb309aca29421422da5cc9ca524bac9dd5afd96ddd9fb1e03c3dc12a0beb526e6f7b3b25e87e9368be22d17c436ff0069d1eee3b94efb0fccbf51d456d57e6d681e21d57c35a8c7a9e933b452c672403f2b8ee187706bef6f0478b6d3c65a0c3ab5bfc927dc9e3cfdc90751f4ee3dabf78f093c67c3717a960b15054b15157e54fdd92eae37d74ea9dedbddeb6f8ce27e13a995dab527cd4df5ea9f67fe675f451457ee27c7057cdffb42788fc8d3acbc31037cd74ff689c03ff2ce3e101fab73ff0001afa3c90a0b1e83935f9f3f11bc407c49e2fbfbf56dd0a486187d3cb8f818faf5afc0be917c55fd95c2d2c15295aa625f27fdbab59fe168bff11f6dc0996fd673155a4bdda6aff3e9fe7f2386a28a2bfcf03f720009381d4d7de1f0a3c19ff088f86a3fb526dbfbe0b35c6472991f2c7ff011d7debe6cf837e12ff848fc511de5ca6eb3d34899f2386907dc5fcf9fc2bee3afec9fa31f01a51a9c538b8eaef0a5ff00b7cbff006d5ff6f1f9578859ceb1cba93f397e8bf5fb828a28afec33f2c3ccbe2e6bf73e1ff05ddcd665926b82b6eaebfc1bfa9cf6e2be79f871f0ca0d76c9bc59e269becfa44259f07832ac7f7989eca0823d4f35f61ea9a569dad59be9faa409736f263746e320e3915f2b7c62f1c5b285f027870ac56566025cf95c2965e918c765efef5fcd1e33e4997e17328f15f10cd54c3d2828d2a1afbf56edebfddeb2f2567a2b3fd0b84b195ea61de5b815cb393bca7da3e5e7dbfab79cf8f3c610788ae92c3468059e8d644adb40a36eeede6301dc8fc8579f51457f0fe759ce2b35c64f1d8c779cbb6892e892d924b4496c8fd7b0985a786a4a8d25a2feaefcdf50a28a2bca3a028a28a0028a29c88f2388e3059988000e49269a4dbb201bd7815f73fc178f5383c116d06a56ef06d7730efe0b46c720e3a8fc6b87f85df0752cd22f10f8ae30d70d8782d1b9118ecd27ab1f4eddf9e9f480014055180380057f707d1f7c28ccf26abfeb1667274e5520e31a7d795d9de7db64d477ef6d8fc838df8970f8a8fd430eb9927772f35d177f5168a28afeab3f3629b69d60f234cd6d1191bef314524fd4e2b3aefc31e1ebe565bad3addc375cc6a0fe62b768ae3ad97e16b45c2ad28c93e8d27f99b4311562ef1935f33c9757f82de08d4f73456ed66e7bc0d803f0e95e57acfecf37f1ab49a26a292ff7639d4afe1b867f91afab372fa8fce94107a57e739ef833c1d9b5dd6c1c6127d69fb8ff00f25b2fbd33dec1f1666b86f86ab6bb4b5fcf53f3a35ff05f89bc32e5758b09624ed281be23f461c7e0707dab97afd3b9a086e2368a78d6446182ac32083ec6bc6fc5bf04bc35af2bdce93ff12bbb2320c633131ff6938c67d457f3af18fd17f1b878cb11c3b5fdaa5f62768cbe52f85fcd44fbacabc44a351a863e1cafbad57ddbafc4f8a28aea7c53e0dd7bc2179f64d660d809fddca9f34720f556fe879ae5abf97331cb71580c44b098da6e1523a38c959af91fa2d0af4eb4155a524e2f6682bd4be17f8ced7c35ab7d8b588639f4cbe6559448a1bca6e81c641e9dc7a5796d15d9c3d9f62b26cc2966583769c1df5d535d535d535a332c760e9e2a84b0f5767fd5cfd2eb4b1d24c493595bc02370195a3450083d08c0ad05445ced5033d702be3df863f17e4f0f2c7a1f888b4ba7f48e6eaf0fb1f55fd457d73657d67a95b25dd84c93c320055d183020f3dabfd2ff000ebc42c9f8a700abe5ed46a25efd3d14a2fd3ac7b4968fc9dd2fe7fcfb23c5e5d59c2bddc5ed2e8ffe0f916e8a2a1b8b882d6269ee6458a341966721401ee4d7e8539c6317293b24784936ec89abe6dfda02e7c3b2585adacb2e75689b7448832446df7b71ec0e38a3c7bf1caded849a5f843134bcabde37dc5ff70773ee78af972f6faef52ba92f6fa569a7958b3bb9c924d7f2478e3e3464f5f2fadc3795255e53d253de11b3bfbafed4935a35a2eef63f4fe0fe13c553af0c7e26f04b65d5faf65f89528a28afe2c3f580a28a2800a28a2800a28a2800a28af68f0cfc0ff137882ce1d467b8b7b1b69d43a1626490a9e876af1f9b0afa1e1ce14cdf3ec43c365142556695ddba2eedbb25f3670e3b32c360a1ed3153515e678bd15f53c3fb395a803ed1adc8c7bec8028fd59aad37ece7a4edf9358b907de3423fa57e970fa3d71cca3ccf0a97fdc4a7ff00c91e03e38c9d3b7b5ffc965fe47c9d457d3d75fb39308d8d96b60be3e5596df827d090fc7e55e65af7c1ef1be828d335aade42bff2d2d5b7f1fee901bf4af9ecefc1ee31caa9bad8ac0c9c5758da76f5e4726be68eec27146578997252acafe775f9d8f328a59209526898aba306561d411c835d9c9f123c712b891b58b8caf4c1031fa5717247244e63954a329c1561820fd2995f0f83cdf30c0a953c2569d34f751938ddadaf66b63d8ab85a159a9558295b6ba4cfa23c01f1b355b7bf834cf15c82e2d666082e48c3c6cc782d8ea3d6beb8073c8afcc0048208ea39afd19f076bd6de24f0e596ab6c47ef2250ebfdd9146197f035fda3f46ef10730cda189c9f35aeea4a9a8ca0e4ef2e5d5495f7693e5b5f557edb7e4dc7d91d0c33a78ac343954aea56dafd3d2fa9d3514515fd507e6e145145001451450015e69f1734a5d53c0da80c65edd44ebf54393fa66bd2eb3f56b45bfd32eacd86e1342e983ea41c7eb5e1713e531ccf28c565d257f690947e6d34bf13b72ec4bc3e2a9d75f65a7f89f99f4559bdb7369793dab7586478ce7fd924556aff24a70709384b747f4d269aba0a28a2a4675be06f0e378abc4f65a39cf9523ef988ed1272df98e3f1afd0f8618ade14b78542471a84451c00aa3000fa0af967f676d2124bbd535b71930aa5ba1f42f966fd00afaaabfd02fa3570cd3c070cbcd24bf79889377feec5b8c57dfccfe67e25e20660eb661f564fdd82fc5eaff0040acb3ad694baaae886e63fb73c6651067e7283a9c5684b2ac313cce70a8a589f60335f21f8075d7d7be3336a73b67cffb4ac79ecaa8768fc857e93c6bc731c8f1b9765f08a94f155630d7a46e94a5ebaa4bd4f0728c99e328d7aedd9538b7eafa2fc0fb02b8af889ae7fc23fe0fd4b5056db2184c517aef93e518f719cd747ab6ada7e87612ea7aa4c20b78465dcf38fc0724d7cc7f1b3c75a4ebda469b61a15da5cc33334f2943d36f0a181e41ce7835cde28f19e1323c83192f6d155fd9be58dd735e5ee2928ef64def6e85f0e65357198da5ee3e4e6d5db4d356ae7cdbd793451457f97a7f440514514005145140051451400514514005145140053d2478cee8d8a9208c838e0f514ca29a6d3ba00a28a29005145140057b27c23f0458f88af67d6f5c655d334d2a5c39015e43c8049ec00c9af1badeff008486fd3414f0edbb98ad7cd79e60a706591b006ef6550001f5afaae0dcc72ccbf328e3f34a5ed614d3928749cfeca7fdd4df33ee95b5b9e766943115f0ee8e1a5cae5a5fb2eb6f3e8bd4facf5bf8dfe0fd05858e951bdf08c6dff4701635c74009c0fcab8d7fda30863b346caf6ccd83ff00a09af9828afd2333fa44719e26ab9e1eb46943a46308d92ed792937f79e061f81b29a71b4e0e4fbb6ff4b23edbf077c67f0ff89eee3d32ea36d3eee4c0412106376f40c3a1f4cf5af65afcc247789d648d8ab29055870411dc57de1f09fc5ade2cf0a4335d386bdb4636f71cf24af2adff0002520fd73e95fd01e06f8cf8ce23af3c9b3b69d74b9a12492e64b74d2d3996eac95d5fb6bf13c63c274b01058bc1fc17b35bdbb3f43d368a28afe9a3f3d0a28a2803ffd4fdfca28a2800a28a2800a28a2800a28a2800aa1aa971a65d98feff009126dfaed359365e2cd1751d7ee3c3b65309aead23df36d395539c6dcf723bfa5746caaea51b90c083f435e7d2c5e1f1f427f55a8a4bde8b69decd68d7aa7bf99bca94e84e3ed636d9ebd8f897e1efc59d47c1ceda6ea4ad79a696384cfcf0b13c9527a8f55ff27da354f8e7e107d1ee5ac9a67b968d96389908f9c8e327d01af9a3e207866ebc2de27bcb19a32b0c9234b6ed8c2bc6c7231f4e95c557f9e783f1738c78529d6e1c94d354dca094e37943a7baee9dbaabdd76d0fdceb70c655994a18f4be2b3d1d93f5fd473b991d9dbab124fe34da28afc41b6ddd9f5c15efbf00bc406c7c4371a1cadfbabf8f7a02781247fe23f957815779f0c673078ef4771fc57013fefa0457dc78699bd5cb38a7018ba2ecd548a7e927cb25f34d9e467f858e232ead4a7fcadfcd6abf147e82514504e3935fea99fcdc79b7c55f140f0bf842ea589f6dd5e29b683d419061987fbab93f5c57c104e7935ebbf187c669e29f111b5b27df63a7ee8a320f0ef9f9987b7615e455fe6ef8efc6f1e21e249c30d2bd0a0b9216d9bfb525eaf4bf5491fbe706e4ef0397a7515a73d5fe8be4bf16c29f1c6f348b1460b3b90aa07524f4a657b1fc16f0a7fc241e2817f709bad74d0256cf4321fb83fafe15f9af0af0f6233dcda865385f8aa492f45d5fa2577f23dfcc71d4f07869e26a6d157ff0025f367d33f0c7c20be10f0c416b281f6bb8fdf5cb7fb6dd07fc04715e894515feabe459361729cbe8e5b838da9d38a8af9757e6f77e6cfe6dc6e2ea62abcf1155fbd277614514c9244891a590854405989e800eb5ea3692bb39923cebe2878c13c23e199a589f6df5d830da8efb8f56ff808e7eb5f04bbbcaed248c599892c4f2493d49af44f89fe3193c63e269ae2363f62b5cc16abdb6a9e5beac79fa6076af39aff00363c6ee3f7c4d9fc961e57c3d1bc21d9ff0034bfede7b7f7523f7ee11c93fb3f04b9d7ef27abfd17cbf3b8514515f8d9f54145145001451450015f4b7c11f879f689078bb598818938b38dc756cf3211edd07e75e4df0efc1973e34d7e3b30a45a42449752765407a7d5ba0afbeed2d2dec6d62b3b5411c50a84451d001c0afea2fa3bf85eb33c57fac99942f4693fdda7b4a6baf9a8fe32f468fcef8eb88beaf4bea1877efcb7f25fe6ff0022c514515fdd47e361589ae788b46f0e5a1bdd62e52de31d371f99be83a9fc2b85f88bf13f4ef05406d2dc2dcea922e521cfca99e8cfededdebe2fd7bc45abf896f9f50d62e1a79589c03c2a8f455e8057e05e2978eb97f0cca597e5e956c5f557f761fe26b77fdd5f368fb6e1ce0daf98255ebbe4a5f8bf4f2f33de7c57f1feee6df6be14b610ae71f699c6e623fd94e83ea49fa57935efc4df1ddfe7cfd62719ff009e588fff004002b84a2bf8c3883c52e29ce6b3ad8bc6cd27f662dc22bc946365f7ddf767eaf81e1dcbb09051a5497ab577f7b3564d735a966f3e4beb96909cee32b673f9d6ee9ff10bc69a64824b4d5ee46de8aefe62ff00df2d91fa571b457ca6173dccb0d53dae1f1138cbba9493fbd33d3a983a1523cb3826bcd23e88f0f7ed05abdaed87c45669768300cb01f2e4c0ee54e558fd36d7bef863e23785bc5788f4eba0b3e326197e47fc8f5fc2bf3eaa48a5960916581da375395653820fb115fb47087d22789f299469e3e5f59a4b753f8ede535adffc5cc7c9e67c0d9762539515ece5e5b7ddfe563f46fc4fe1ad37c57a4cda4ea49b92407638fbd1bf6653ea2be06f16785b52f086b12e93a8af2bf34720fbb221e8c3fa8ed5ed7f0f7e375cdabc5a4f8bdfcd832152ef1f3a7fbf8ea3dfad7a7fc55f09db78d7c2dfda9a5149eead10cf6f2464379a98c950475c8e9ef5fadf1e60722f13320967bc3eff00db6846ee0f49b8ad5c64baf57092babe9d74f99c96b63387f1ab058efe0cde8fa5fbafd51f0fd14515fc4e7eb415d1e83e2df11786a5f3745be96dfd501dd19faa3654fe55ce515d782c7e2707596230951c26b6716d35e8d6a67568d3ab170ab14d3e8f547b3ffc2f8f1d793e566d33ff003d3ca3bbff0043c7e95c06bde34f137895b3ac5fcb32768c1d918ff80ae0572f457d066bc77c459951fabe3f1b5270ece6edf357b3f99c386c9f03879fb4a34629f7495c28a28af943d20a28a2800a28a2800a28a2800a28a2800af54f057c5af11784234b16db7d60bd2094e1933fdc7ea3e8723e95e57457b590711e659262d6372aace9d45d5755d9ad9af2774726370387c5d2747130528f9ff5a1f6c687f1cbc21aa058ef4c96129e3128caff00df4323ad7afda5e5adfc0b75672a4d138caba10c0fe22bf322bd4fe1afc48b8f045ebc779e65c69d329dd0a9e55fb32e7a7bd7f52787df497c5cf194f05c4f18fb3969ed62ace3e724b46bbd92b6f667e779df87f4952955cb9be65f65eb7f47fe67ddb457868f8fde0e29b8c3741bfbbb07f3cd62ea1fb43e8f1a11a6e9b3caff00f4d5822fe9935fd0389f19782e853f693cc20d795e4fee49b3e229f09e6f3972aa0fe765f9b34fe31fc3bb1d574997c45a642b15fda2979360c79d18ea08f51d41af8e2bd63c4ff18bc59e23824b2574b2b5906192118665f42c727eb8af27afe1af19388f8773ccf3fb4387a0e29af7db5caa52fe64b7d56f74aef5b6e7ec3c2b80c760f07ec31d24da7a6b7b2ecd857b77c1af1fc5e18d4df47d5e5d9a75e91b5dbeec537627d03743f85788d5896d2ea08e39a689d1261ba36652038f507bd7c5f08f11e3f20cce9e71977c54dddef669e8d4bc9edff04f5b33c051c6e1e585afb4befbf75e87e9ac7247322cb1307461956539041ee0d3ebf3fbc37f133c5fe17885b69f785edc6310cc048a3e99e47e15e9963fb446b31e05fe9904def1b327f3dd5fdbf917d25b85317462f30e7a153aa71728dfc9c6edaf548fc8319e1fe654a4fd85a71e9ad9fdcffccfad68ae4fc19e2693c59a2a6af2594b63bd885497f880fe25f635d657ef597661431d85a78cc2caf4e6938bb3574f676767f7a3e2f1142742a4a9545692d18514515da62145145007e7a7c44b1fecef1a6ad6ca36afda19947b3735c557aefc6eb7f23c7b74e0604b1c4dff008e8cd79157f941c7d81582e24c7e162b48d59a5e9ccedf81fd2d9356f6b80a351f58c7f20a28a2be48f48fb33f67fb4587c1935d639b8bd9493ea115547f235ee95e59f05e0107c3bd338c7986790ffc0a56af53aff53bc2cc22c3708e5d497fcfa83f9c9733fc59fce5c4957da6695e5fde6bee7638ef881a81d2fc1bab5e29c15b7651ff0003f97f91af87fc09ad2683e2ed37559db6c71ce048de88ff002b7e86beb9f8d571e4780aec7fcf59234fcf35f0bd7f2dfd24b3faf84e2fc0d4a0f5a108cd7f8b9dbffdb51fa3700e0a1532bad19ed36d3f4b25fab3e9cf8ebe35d36fec2cfc3fa4dc2ce5dfed13b46d950a06141c77279fa0f7af98e8249eb457e0bc77c658ae28ce2a66f8b8a8b95928ada292b24bf37e6cfb3c9f2aa797616385a4ee975eed8514515f1e7a81451450014514500145145006de85e1dd5bc4973259e8f0f9f3471994a6403b475c67af5a8752d0f58d1e431ea96735b1071fbc4207e7d2bbaf83da8ff67f8f74f24e16e37c07fe06a7fad7dcf7363677a863bb823994f6750dfcebfa23c33f05b05c63c3f3c751c4ba58884dc5a694a2f44d69a35bf77b6c7c371071655cab1aa8ce9a941a4f7b3eabcd1f993457dd7acfc19f02eb04bad99b2908c6eb56f2f1efb79527ea2bcab58fd9deee305f43d4d641da3b85da7fefa5e3f415e6e7df475e30cbdb961e9c6bc5758495ff00f01972bfbae7460b8eb2aaf6539383f35faaba3e69a2b575bd1eef40d4e7d26f8a19eddb6bf96db973f5acaafc431386ab87ad2c3d78b8ce2da69ee9ad1a7e87d753a919c54e0ee9ec145145605851451400514576de1af879e2bf15812e9764df67271e7cbf247ef827afe19af472bc9f1d996216172fa32a951f48a6dfe1d3ccc3118aa38787b4af2518f76ec713457d083f678f11791b8ea36a25c7ddc363f3c5703aefc29f1b6820c93583dcc43fe5a5b7ef47e43e6fd2bebb34f0b38b72ea3f58c5e02a28774b9adebcb7b7ccf330dc45965797252ad16fd6df99e735b9a0788f58f0cdf26a1a3dc3412291b803f2381d987420d7a2780fe12eb9e23d4629b59b59ac74c43ba579018ddc0fe15079e7d71c0af60f12fc02f0fdddbb49e1a91ec6e147ca8eed244c7df712c3f035f47c2be0ff0019e3705fdbb965274dc1fba9b70a92b6b785eda79b6afd2e70e63c539551adf53c44af7df4bc57933d2fc05e30b7f1ae8116a91a88e753e5dc440e76483ae3d8f515dad7cc3f06acb5af09f8b352f0b6b10bc2d2422500f28db0e37a9e84107ad7d3d5fdd3e1871163739e1ea389cce2e3888b70a89ab3e783b36d746d59b5dd9f8df1160296131d2a78777a6ece3d7461451457e827867fffd5fdfca28a2800a28a64b2c70c6d34cc11101666638000ea49a4da4aec695f443890064f0050086008e41e95f2af8dbe2a5cf89f5b83c27e1894c5653dc25bcb70bf7a6dec1485f45fe75f52dbc4b0c11c2a30114281ec2be238578f72fe22c6e2f0f95fbd4e838c5cfa4a4ef751ee95be2eb7d34d4f6332c96be028d29e27494eeedd5256dfd6fb13578b7c61f1fbf85b4b1a4e9926cd46f54e194fcd147d0b7b13dabd8ee278eda092e262152352cc4f400726bf3a7c5dafdc789bc4379abdc313e6c84460ff000c6bc28fcabf3df1ff00c41abc3b9247078195b1188bc535bc62be292f3d525eadf43dce09c8e38ec5bab595e10b3b777d17eacd8f877e2bff00844fc576faadc3130484c573dcec7ea7df079afbfadee21ba812e6ddc49148a19194e4107a106bf316bdbbe1a7c5cb9f0a88f46d6435c6985b0ae397801f41dd7dabf03f017c5cc3f0fce5926712b61ea4b9a33fe493d1dffbb2d2efa3d766cfb4e34e189e3a2b178557a91566bbaf2f35f89f59788fc2fa278aacbec1ad5bacc83946e8e87d55ba8af0ed43f678d3ddd9b4dd4a48c1c90b2a86c7a0c8c57bfe97ac697addaa5ee95731dcc2e321a36cfe63a83ec6b4abfaeb3fe00e17e27e5c5e61878556d6934ecdae9ef45abaedabf23f30c167798e5d7a5466e3e4ff00c9ec7cb31feceb3ffcb4d5931ed19cff003ad04fd9dad367cfaabeef6418afa5a8af9aa5e02f03c36c15fd6737ff00b71e84b8d7387ff2f7f05fe47cad7bfb3bdd2a16b1d511980e15d319fc735cef873e1a78abc31e39d224d42d4bdb2dcab79f17cd18c64fcd8fbb9c77afb2eb3753d634ad1eddaef55ba8ada24ead2301f97727e95e4661e0170851ad4f31c2a78774a519df9af1f75a7ef29b7a69d1ab1d5438d7349c6542a5a7cc9adb5d55b4b7f91a43818af9fbe327c4b5d1ada4f0be8b2837d709b6e244393046c3eef1d1d87e439ae63c7ff1c4dca49a5783cb2a1cabde30c123fd80791f535f3549249348d2cac5ddc9666639249ea49f5afcebc63f1f30ff0057a99270ccf9a52d27556c975507d5bdb9b64b6bbd57bbc2bc15539e38ccc1592d545fe6ff00cbefec328a28afe303f570fa57dedf0a7c2c3c2fe10b68a55db7779fe937191c8671f2affc05703eb9af92fe17f873fe126f18d9da48bbadedcfda67f4d91e300fd5b03f1afbf400a001c01d2bfb13e8b7c1e9bc4712578edfbb87e0e6d7e0afea8fcb3c45cd6ca197c1ff007a5fa2fcdfdc2d14515fd907e5415e2ff1b3c5dfd81e1afecab57db77aa663183cac43efb7e3d3f1af6591d22469243b5541249ec057e7efc47f13b78afc5577a82b136f19f26dc7611a700fe279afc3bc7ce3879070e4b0f8795abe22f08f751fb72f92d179b4cfb1e0ac9febb8f5526bdca7abf5e8bf5f91c2514515fe721fbb8514514005145140051451401f71fc1683c3f1783e27d15c493c873784f0e26f423d00e9ed5ebb5f9d9e0df17ea5e0dd5d352b16263242cf0e7e5913d0fb8ec6bef6f0e78874ef13e930eada6481e294723ba377561d88aff44bc08f1172dcef26a794422a957a1149c16d28afb71f5fb4b74df99f86719e4588c262a58a6dca1377bf67d9fe9e46ed453894c320808590a9d8586406c7048f4cd4b457ef128f345c4f8b4ecee7e7578d74bf10697e22bb8bc4a59ef2472ed29e44a0f4653e9e83b74ae52bf44bc61e0cd1fc67a69b1d4d30ea098665fbf1b7a83e9ea3bd7c3be31f04eb5e0bd45acf528cb4249f26e141f2e45ed83d8fa8aff39fc5ef08331e1ac5cf30a2dd5c2cdb7cfbca2dbda7e7da5b3f27a1fbc70bf1461f30a6a84ad1a896dd1f9aff002e871f451457e1a7d78514514005145140057aff00c33f89f77e0f9d74dd44b4fa4cadf32753093d597dbd45790515ef70d712e6190e3e1996595392a47ee6baa6baa7d51c78fc050c651787c446f17fd5d799d978fac2c2c7c537874a7592c6e585cdbb272bb25f9b03e8735c6d29627009271d292b8b37c6d3c6636ae2e943914e4e5cab65777b2f25d3c8d70d49d2a51a7295da495fbd828af44f879f0fef7c77a8bc2ae6decedc033cf8ce33d1547727f4aeefc61f02753d1ed5f50f0f4e7508a35dd242c31371d4ae386fa75afabcafc32e25cc728967982c2b9d057d55aeedbb51f89a5dd2fc99e6e23883014314b075aa2537fd6af64780514acac8c55810c0e083c10692be11a69d99ec85145148028a28a0028a28a0028a28a0028a28a0028a5009381d4d6b6a7a0eb3a32c2faa59cb6c970a1e2671f2b83e84706ba29e12bd4a72ad4e0dc636bb49b4afb5decafd2e44aa4232516f57b799914515e81e15f87da9f8af45d5357b1241d3d331c7b72666032541f503a7bf15d993e4b8dcd713f54cbe9b9d4b49d96f68a6dfe0be7b2d4cb158aa587a7ed6b4ad1d15fd5d91e7f45290549561820e083da90024e057996e8740515f5afc28f84f656d6117887c49009eea701e0824195893b120f563fa55af8c1f0ce2d56c8f88742802de5b2fef628d71e6c6bdc01dd47e95fb9c3c01e217c34f881db9edcca959f3b86f7f5b6bcb6bdbcf43e3df1ae07fb43ea3d36e6e97ede9e67c815f67fc33f0d69de22f8676761e21b5171133cad17983e6552c7050f51f857c62ca549561823820f6afd16f045ddbdf784b49b9b5411c6d69100a3800aa807f515f47f464ca30d8dcef19f5ab492a56706aea4a5257ba7a595969e679fe2162aa51c252f67a3e6bdd74b2679a4df007c20f26e866b98d7fbbb837eb8aea343f847e08d0e459e3b21732a1cabdc1f3307e878fd2bd328afebbc0f863c2983aff0059c3e5f494fbf2a76f4bdd2f91f98d6e22cceac3d9d4af2b7a88aa14055180380052d1457dd1e285145140051451401f18fc7e8c278c6161fc76a87f2245786d7bbfed0241f175b7b59a8ffc78d78457f979e3024b8cf314bfe7e3fd0fe8ae1777caa87f850515d9f837c0dadf8daffecba647b208f067b971fbb887d7bb1eca39fc2bebaf0c7c21f08f87a25696dc5fdc81f34d703773c745e839e95e8787fe0de7dc571face1e2a9d0fe79decffc2b797e5e6619df1560b2d7ecea3e69ff002afd7b17fe13aedf879a28f5858fe6ed5e894c8a28e18d6289422200155460003b0a7d7fa3b90658f2ecb30f97b95fd94230bed7e58a57b79d8fc1b1b88f6f88a95ed6e66dfdeee78dfc76cffc203363fe7e60ff00d0abe23afbafe33db7da3c057ad8cf92c927e591fd6be14afe15fa4f519438b69cded2a31fc25347ec7e1e493cb1aed27f920a28a2bf9ccfbb0a28a2800a28a2800a28a2800a28a28037fc2b766c7c49a65d838f2eea224fb6e19fd2bf48148650c3b8cd7e6242e639a391782aea47e06bf4c34f944f616d30fe3891bf3515fd9bf450c6b74731c23e8e9cbef524ff00247e53e2552f7a855ff12fc8b95e73f127c756de0ad11e5460fa85c8296b177dddd8fb28e7eb81debb5d5f54b3d134db8d575090476f6d1991d8fa0ec3d49e83debf3d3c59e25bdf166b771ac5eb1fde3111213911c60fcaa3e83afbd7e97e38f89ef85b2c584c0bff006aac9a8ff723b39faf48f9ebd2c7cff07f0eff0068e23dad65fbb86fe6fb7f9ffc130eeeeee2faea5bcba7324d3397763d4b1eb55e8a2bfcea9ce53939cdddbdd9fbaa492b20a2ba69bc337dff0008ddaf89eda3692d659248662a33e54919e33ecc30735ccd7563b2ec4612518e222d73454979c64ae9af97e3a19d2ad0a89b83bd9b4fd56e15d4787bc1de20f144c23d26d5a44cfcd2921517ea4d72f5345713dbb6e82578cfaa3153fa5565b53070c44658f84a74faa8c945bf9b8cbf2157555c1aa2d2979abafbaebf33ebdf05fc0dd234a297de2365d42e07222ff962bd3a8fe2e73d78af798a28a08d61851638d00555518000e00007615f01683f137c61e1f2ab6d7cf2c4b81e54c77ae076e79afa27c1bf1cb46d6a48b4fd7a3fecfba9085126730b31f53d573efc57f71784be27f00d1a31cb32fa5f54a92b7c76f7df9d4eafb7372f923f20e27e1dcea73788ad2f6b15dba7fdbbfe573de28a404300ca41079047434b5fd367e781451450040d6b6ef70976d1a99a30555f1f300dd467d0e2a7a28a98c2316dc55afabf31b6dee145145508ffd6fdfca28a2800af98fe3a78ede303c1da64bb4b00f78ca79c1e91e7dfa9af7bf156bd0f86b40bcd626ff96119283d5cfdd1f9d7e766a37f75aa5f4fa8deb9927b87691d89ce4b1cd7f337d23fc429e5596c720c14ad56babc9add53dadff6fbd3d13ee7e85c0591ac4d778eacbdd86de72ff81f9d8e8fc011acbe35d151ba0bc89bf153b87ea2bf4440c0c57e7578166fb3f8cb4594f417d083f4660a7f9d7e8a0c9009af37e8a6e3fd8f8e4b7f691ffd274fd4dfc49bfd6a8ff85fe6709f13aee4b2f02eaf3c670de415ff00be8853fa1afcfbafd09f88f62fa8782756b641926dd9b1fee7cdfd2bf3dabf3ffa5446aff6fe11cbe1f65a7af3caff00a1edf870e3f51aa96fcdfa20a28a2bf978fd10d5d2b5cd5f439c5ce93772dac83bc6c403f51d0d7af691f1f3c5d62ab1ea10dbdfa8fe260637fcd78ffc76bc328afabe1fe39cff0024d32ac5ce9aec9fbbff0080bbc7f03cec6e5182c5ff00bcd252f96bf7ee7d3f0fed1c48227d0f69c7056e7393f4318aa973fb46dfba9169a2451376325c1900fa808b9fcebe6ba2bece7e3c71d4a1c8f1efe50a69fdfc973c95c1993277f63f8cbfccf5bd5fe3578e755568e3b88ec91ba8b64da7fefa625bf5af31bdd4b50d4a533ea1732dc487f8a572c7f5354a8af82ceb8b33acde5cd99e2a757ca526d7c96cbee3dac265b85c2ab61e9a8fa20a28a2be78ed0a28ab9a7d9cba85f5bd8c232f7122c6a3dd8e2ae9539549aa70576dd97ab14a4a29b7b1f5b7c02f0e0d3fc3f3ebd32626d464da84f510c7d3f36c9fcabdfab2341d2e1d1746b3d2e0184b68523fc40e7f5ad7aff56f80b86e19070fe172a8ab38457379c9eb27ff0081367f366758f78dc754c4beaf4f4d97e01451457d71e59e49f19bc507c3be1096081b6dd6a4df668f1d42904bb7e0bc7d48af86abd7be34f894ebbe3096ce26cdbe9a3ece801e37f573f9f1f85790d7f9b1e3af187f6f7155654a57a543f771edeefc4fe72bfc923f7ee0ecafea596c3997bd3f79fcf6fc028a28afc68faa0a28a2800a28a2800a28a2800af42f87be3ebef03eaa265dd358ce42dcc19ea3fbcbd830edebd2bcf68af5724ceb1b94e369e6397cdc2ac1dd35faf74f66b668e7c5e1296268ca857578bdcfd2cd1f59d3b5ed3e2d534b9967b79865597f5047623b8ad4af83fe15f89fc47a3788edb4ed1b33c37b2aa4b6cc7e4607ab7b103bd7dde338e7ad7fa4fe14f88f0e30ca5e31d270ab06a335f679ad7bc5f54fb6eb67d1bfc0789721795627d9295e32d577b79ff5a8b59faa695a7eb567269fa9c097104830c8e323ea3d0fbd68515fa5d7a14eb53952ad1528b5669aba6bb347cfc2728494a2ecd1f1f78dfe076ada5ccf7de1706f6cc927c827f7d1fb0fef0fd6bc2aeecaf2c25305ec3241229c15914a9fd6bf457c45e28d17c2d646f759b85857f857abb9f455ea6be5ef187c65d3f5adf0586876d227204b78a1db9ee147f8d7f1278c3e18f04e515a55f0f8efabd596bec6cea2f925ac176be9d8fd7f85788337c54142a51e78afb57e5fcf47f23c068a96797cf99e62891ef39db18daa3e83b0a8abf95669293517747e8cb6d428a28a9185145140051451401f757c19d320d3fc0d672c4a049745a59187724e07e82bd5abc33e04f88a1d47c2e745771f68d3dc8dbdcc6fc83f9e6bdcebfd4ff0bf1584c4709e5f3c15b91538ad3a34ad25ebcc9dcfe72e23a756199d755b7e67f73dbf03e6ef8cff000dace4b19fc5fa3a2c334037ddc6a30b22ff00780fef0efeb5f2857e88f8f8c63c19ac197eefd8e6ebfee1af8b3c27f0dfc4be30b5b8bdd362090c0a4abcb95595c7f0a1c727dfa57f2778fbc05cdc57469e438772a95e0e728c1754dde56e975bf77e6cfd2f82b3ab65b29636a5a30764df67b2381a2b4b55d1f53d12edacb55b692da653f75c633ee0f423e959b5fccf88c3d5a152546bc5c64b469ab34fcd33f4084e338a941dd30a28a2b12828a28a00294024800649e8292bdafe0b78323f116b8daadf461ecf4f20e18655e53d07e1d6be8785786f159fe6b4729c1af7ea3b5fa25bb6fc92bb38b31c7d3c161a789abb457f4be655baf841ac58f821bc5374e52e54095ad71cac3ea4ff007bbe3d2bc7abf437e20dec3a7782f559e6c05fb332283dcb70057e7bc104d75325bdba3492c8c1511464927a002bf53f1c38072ae18cc30781ca6edca9ae64f56e49db9bd65d969a687ce70867589cc6855ad89e92d3d2db7c8dff000868571e24f11d8e916ea58cd282e7b2c6bcb13f402bea7f8ed60abe0580431e45a5cc401c7dd4c15ff0ad6f84ff000f23f08699fda1a8460eab76a3cc27931275083d3dfd4d7a66b1a3e9faf69d3695aa4426b69d76ba9c8fa1047208afde3c39f07b1585e08c6e0f1568e2b191d9fd8497b89f9df57daf6dd1f179ef1553a99c51ab4f5a749f4ebddafd0f813c0fe0ebcf1aeb69a5dbb795128df3cd8c8441eddc9ec2bef0f0e78734cf0be93168fa5c7b6188724f2cec7ab31ee4d54f0bf83740f07db496da25bf95e6b6e776259dbeac79c7a0aea6bedbc1ef09a8f09609d6c628cb193f8a4aed28f48c6f6d3ab76577e491e4714f134f33abc94aea92d9777ddfe9d8f90fe31fc359f4dbe93c4da242d25a5cb16b88d173e539ead81fc2dfa1af3bf867e1897c51e2cb4b53196b6b76135c363e5089ce0ffbc78afbf5e34950c722865618208c822a869fa3e97a5071a6dac56de61cbf94817711eb8af99ce3e8ed9762b8a29e7787a8a141c94e74adbb4ef68be9193dd3db5b797a384e3baf4b2e7849c6f3b594afd3cfcd7434155514228c05180076029d4515fd1e925a23e04f15f1bfc17d1bc4f3b6a3a5c834ebc6c97dab98e427b951d0fb8fcabd1fc25a07fc231e1eb3d13cdf3cdac7b4c98c64939381e9cd747457cae59c0f9265d9a55ce7038750af515a4d5d27addfbb7e5bb6b5692b9e9e2338c657c347095a77845dd5ff00cf70a28a2beacf3028a28a0028a28a0028a28a00f8abe3c4de678db67fcf3b78d7f319feb5e7be0ef0adf78c35d8347b3042b1dd349da3887de63fd3d4d753f196e05c78ff0050dbd2311c7f8aa807f957d1df06fc1e9e1cf0cc7a85c478bdd48095c91f32c67ee2fe5cd7f00e03821f19f8978fa353fdde15672a8ffbb1959453ef26ade977d0fdb6be6ffd95c3f466be3714a3ead5eff23d2341d0b4df0e69b1695a542218621dbab1eecc7b93eb5b145233050598e00e4935fded85c2d1c2d18e1e8454611564968925d11f8ad4a93a93739bbb62d158ba3788748d7c5cb69370b702d6630ca57a071fcc7bd6d5185c551c4d255b0f35283d9a69a7d346b4dc2a529d393854567d99cb78dec3fb53c25aad9019325b3903d4afcc3f957e74b29562adc10706bf4cf51b986cec2e6eae31e54313bbe7fbaa0935f9afa8cd15c5fdc4f0a848e495d95474009e2bf8d7e95d83a2b1597e294bf78e338b5d6c9a69fded9fabf86b566e957a56f7534fe6ff00e18a7451457f229fa7051451400514514005145140051451400671cfa57e91785a5f3fc33a54dfdfb2b76fce306bf374f4afd14f02397f0668ac7fe7c601f92015fd5ff452aad6698fa7de9c5fdd2ff827e6be24c7fd9a8cbfbcff002384f8ef712c3e0468e3c8135d428ffeefccdfcc0af89abf43bc7fe1a3e2cf0adee8f190276512404f412c7cafe07a13d81afcf8bab5b8b2b892d2ee368a6898a3a38c32b0ea08af2be9459562e9f1150c7cd3f653a6a317d2f172baf5d53f99d3e1de2694b013a09fbca4dbf46959fe0414515de7813c09aa78d3548e286264b1471f68b8230a147500f7623b76afe78c9f27c66698ca780c05373a9376497f5b2eafa1f738ac552c3d2956ad2b451f52fc1ad2513e1c5ac57d18912f9e79991c6414772a38f42141fc6b0fc4df01740d4bccb9d0666d3e739223237c24fa63aa8cfa671e95ee565676fa7da43636a823860458d147002a8c01566bfd34878679262721c2e499bd08d55469c60a4f469a4937192b3577ae8cfe7d7c438ba78dab8cc2cdc79e4ddba6fd56ccfcf3f147803c4de129586a76a4c2090b3c7f346c3d73dbf1ae2ebf4e6e6dadef216b7ba8d6589c6191c6411f435f347c4df833025bcbaff8463d8d1e5a7b25e432f768fd08eebd0f6c743fcb7e24fd1cb139551a99970f4dd5a51d5c1fc6975b35a492ed64fd4fd1b20e3ca78992c3e3972c9ecd6cfd7b7e47cbb4529041208c11c10692bf970fd10fa73e0afc4898dc278475c98b07005948e72770ff009664fb8fbbf957d495f9870cd2dbcd1dc40c5248983a329c156539041f506bf473c2baab6b7e1cd3b55720bdcdbc6ee474df8f9bff001ecd7f76fd1b38ff00139ae06ae458f9394e824e127ab706ed67fe176b7934ba1f8df1fe494f0d5a38da2aca7a35e7dfe7f99d0514515fd3e7e7614514500145145007ffd7fdfca28a2803e77fda0f5836fa2d968f1b60dd4be6381fdd8fa7eb5f23d7bffed0972d2789ac6d89e21b6240f4de47f8578057f9a5e3be6b3c771ae3399e94da82f2518afd6ecfe80e0dc32a39452b7dabbfbdff00958b76174f637b6f7919c3412a480fa1420ff4afd27d32ee3bfd3edaf623949e24914fb3006bf336bed9f81fe251ad784869b338373a5388587731b64c6df88047d54d7e8df45ce23861b38c4e4f55dbdb454a3fe285eebe716dfc8f0bc45c03a985a78a8af81d9fa3ff0082bf13d8a7863b98648251b92452ac0f706bf3a3c5be1fbaf0cf882f348ba523ca9098c9fe28c9ca91f857e8e57927c54f8771f8cb4efb6d8a85d4ed54f947a798bfdc27f957ee3e3cf8735b89f278e2700af88a17715fcd176e68af3d135e96ea7c7f05e7d0cbf14e9d776a73b26fb35b3f4ee7c35454f736d71677125addc6d0cd131478dc6d6561c1041a82bfceb9c2509384d59add1fba269aba0a28a2a4614514500145145001570585cfd84ea25088048220c7a33e3381eb81d6bd07e1dfc36d4bc6f7627915adf4b89bf7b70463711d523cf53ea7a0ef4bf13f56d2e4d562f0df8795534bd154c11ede449313fbc7cf7e78cf7c66beda9f0756a190cb88331f729c9f2d24f7a92ead2fe48abb72eaec96f75e4bcd213c62c150d64b59768aff0037dbe6798d7b1fc11f0fff006c78c52f655cc3a6a19ce7a6f3f2aff3cd78e57da1f01f41fecdf0a3ea922e25d4642d9ffa669c2feb9afaaf02f861673c5f878d457a747f792ffb77e1ff00c9b97e479bc61987d532ba8e2fde97babe7bfe173dc28a28aff4b0fe7f0ae73c5daec5e1bf0dea1ad48466da16280ff1487841f8b115d1d7cf5fb42eaad6fa0d8e908d8fb5ce6471eab10e3f53fa57c67887c42f23e1bc66690f8a107cbfe27eec7ff2668f5b22c0ac663e961decdebe8b57f81f244d349712bcf331692462eec7a9663927f3a8ebd03c39e079b53d0b50f14ea4e6df4cb089995ba34d28c0555f6c91cd79fd7f97998e518cc2d1a38bc5c6cab2728df76af6e6b766ef67d6ccfe89a389a55272a74ddf9747e4fb0514515e49d2145145001451450014514500145145007d2dfb3df87127b9bef134eb910116d013fdf61b9c8fa0207e35f5557927c11b68e0f879632a0c35cc9712bfbb095933f920af5baff4f3c19c929659c1d81a74d6b520aa37ddcfdefc134be47f3d716632588cd6b4a5f65f2af96815cd78bfc4b6be13d02eb5bbac1f25711a671be46e1547d4fe95d2d7cadfb41eaf7173a9699e1ab6cb055370d1af5691ced418efc671f5af47c4fe2d9f0e70de2333a3ad4b28c3fc72765f76f6eb639f8772c58fc7d3c3cfe1ddfa2d5ff91e09e21f116abe27d4a5d53569da59643c0fe145ecaa3a002b0ebd9b4ff819e33bdb31752f916cccbb96291b2dcf638e01fceb80f12f83bc45e129c43adda342ac7092afcd13fd1c719f6383ed5fe73f10708f1450a6f37ce30b55466eee728bd5beb27d2fe763f77c1667974e4b0b85a91bad124d74edff0000e628a28af8a3d60a28a2800a28a2800a28a2803a6f0978a2ff00c23ad43abd892761c491e78910f5535f7ef873c43a6789f4a8756d2e512c520f980fbc8e3aab0ec457e6e575fe0ff1aeb5e0bbf377a5c998a423ce81bee4807a8f51d8d7ee9e0d78bf5384f10f058ebcf0751dda5bc1ff003457fe94baeeb5dfe3b8af85e399c155a3a558fe2bb3fd19fa07a969b65abd94ba76a3189ade71b648c92030ce707047153db5adb59c096d6912430c636a246a15540ec00e2b8df0578fb45f1b59f9962fe5dca28335bbfdf43edea3debb9aff0040328c765b99d2866d97ca338cd594d5aed76befa3be8f677d2e7e278aa388c3c9e16ba69a7b3efdff00e0993abe85a3ebd6c6d358b38aea261d245048f707a83ee2bc8b51f803e0eba73258cb75679fe1571220fc186efd6bd9353d4ad348b09f53be6d905ba19246032428ebc573fa6f8fbc1faac4b2da6ab6df363e57708c09ed86c57cdf13e43c2199626386cf69d19566aeb9f954dadb47a4ad7eccf432ec6e6987a6ea60e52505bdaed5ff0023c757f673d377e5b5898afa08541fcf756d59fc02f07da2996fa7bbbbda09da5c46a71ebb467f515eec082010720f208a8e71ba091477461fa579387f06382b0efda52cbe0df9f3497dd26d1d33e2ccdea7bb2aefe565f9247e6a6ad0c36faa5dc16e36c51cf22a0ce70aac40193cf4acfad3d6d4a6b37c87aadc4a0ffdf46b32bfcd0c7c52c4d4495bde7f99fd0149fb8bd02bef0f83fa32691e07b26db892ec1b873dc963c67e82be101d6bf483c2b1087c37a6463a0b58bf5506bfa67e8af9753ab9de2f1935ef53a692f2e696bf846df33f3ef11f1128e0e9525b4a5f92ff008279f7c57f0f78a3c5b6d69a16871a8b667f32795db6af1d07a9f5ab1e00f851a478376dfdc1179a911feb987cb1e7a841dbebd6bd628afeb07e1e64f533e9711e2a0ea6234517277504969c91d977bbbbbbd1a3f35feddc54704b014df2c3adb777eec28a28afb83c62bdd5e5ad8c467bc95218c75772140fc4d3a0b882e6312dbc8b221e8ca720d51d6748b2d774d9f4bd4104904ea5587a67b8f715f0e2ea9e25f857e2abbb1b09d9441290d13e4c5327f092bee3a1afca3c44f126af086270f5b1b86e7c1d47cae717ef425beb1b59a6b55aa7a33e9b22c8219a53a90a552d563aa4f66bd7a7ddd8fbde8ae33c0fe34d3bc6ba3aea168424e985b8833f346ffe07b1aecc90064d7e8f9566b85ccb094f1d829a9d39abc5aeabfadd747a1e06270d570f56546b2b4968d1e15f13357f11780b52b6f1468d70f2d85d3f97776529dd16f1c865ce76161c1231c8f7af42f05f8e746f1b69e6eb4e7d9347813dbbfdf8c9fe60f635e5ff001db5fd264f0d2e976f750cd72f70b98d1c33a85e49201e3a57cc3e1af126a7e15d5a2d5b4b90a4919c32ff000c887aab0ee0d7f2d71378b33e0ee3aab848d4f6b81a9cb29c2fcdece52f89c3b7f338edabd13d4fd1f2fe1959ae4d1aae3cb595d276b7325b5ff24cfd21a2b9ff000bf88acfc53a25b6b3647e49d7e65ee8e3ef29fa1ae82bfaaf058da18cc3c3158692942694935b34d5d33f35ad4674aa3a5515a49d98514515d46614514500145145001451593af5ea69ba2dedf39c086076cfa715862b110c3d19d7a8fdd8a6dfa25765d2a6ea4d423bb763e32b6d3078e3e2d4d6ee37412df3cb30ff00a6511cb0fc48c7e35f70aaaa2845180a3000ec057cadfb3f597db75ad635d9865d54203e8d2b166fe55f55d7e17f47ccad2c8eb679517ef31756736fc949a4befe67f33ecb8e712de321838fc34a297cedfe560af08f8e1e359342d263f0fe9f26cbcd4413215fbc900e09f6dc781f435eeccc114b31c00324fb0afcf3f881af49e23f16ea1a939ca79862887a471fcaa3f4cd5fd2138d2a647c37f55c2cad5710f9135ba8daf36be568ff00dbc2e07ca638cc7fb5a8af1a7afcfa7f9fc8fa73e005b471f82a5b951f3cf7b2973dced0a057b8f4eb5f2e7c0ff1d689a468d77a16b37496ac9399e1690e1595c0dc33ea08fd6b0fe247c63d4351bc7d33c2574d6f631fcad711e56494f7da7aa8f42306bc7e15f15b87b8738070388ad514a6a0a3ece2d73b92bdeeba2beadbefd6e8eaccb86b1d8fceeb4231b26efccef6b74f5f43d7fe357881747f06cd69138136a0c20500f3b7ab7e82be1fabd79a9ea5a8edfed0bb9eeb6676f9d23498cf5c6e2719aa35fc99e2978853e30ce1663ecdd38462a318b77b2576f5b2ddb7d3b1fa5f0e646b2bc2fb0e6e66db6dedfd681451457e6c7be1451450014514500145145001451450007a57e87fc3eff912745ffaf28bff004115f9e07a57e89780976f82f451ff004e50feaa2bfaa7e8a8bfe1671aff00e9d2ff00d2d1f9c7890ffd8e92fef7e875d5f207c7af0b4b63ad47e24813fd1ef408e52070b2a8e33fef0fe55f5fd63788342d3fc49a54fa3ea49be09d707d411c820f620f22bfa7fc51e068f15e41572d4d2aabde837b29adafe4d5d3f5bf43f3be1cce1e5b8d8e21fc3b3f47fe5b9f9d1a4a412ea9691dc8cc2f3c6ae0f752c33fa57e9258595969f691dae9f0c70408a02246a1540fa0af877c6df0b7c43e10b96b8b7864bcd3c36527886e283afce072318ebd2bea0f857e3483c5be1d8924702fec5562b84ee703e571ecc07e75f807d1da94f87f3ac6f0f67347d962a6938732b3928df9945f55aa92b68d26fa1f6fc74d637094b1d849735357bdba5ed66fb763d3a8a28afec33f2b0a4201183d0d2d1401f1d7c6cf012687a88f11e971edb3bd63e722f48e63d48f40de9d8d78257e946bda1d8f88b4ab8d23515dd0dc29538ea0f623dc57c65e21f835e32d26f648f4fb43a85b6e3e5cb132e76f6dca4820fd335fc21e39783d8dc1e6b2cdf22c3ca742aeb28c137c93ebeead795ee9dac9dd69a1fb3707f14d1ab86585c6544a71d136ed75d357d51e495ef5f057c7f368daac7e18d4a42d617adb612c7fd4cc7a63fd96e87df9ae39fe12f8e61d2ee354b8b0f296dc6e3117532151c9202923007bd70166d2c7790b459122caa57d4303c7eb5f916418acfb82f39c2e67528ce949eb6945c79e17b4959a574f6f2767d8fa8c6d3c166d85a9878cd49774d3b3e8cfd36a2aa5834cf636ef71c4ad1217ff0078819fd6add7fa9509f34549753f9ca4acec14514550828a28a00fffd0fdfca28a2803e32f8fc8cbe3285cf47b44c7e04e6bc32be95fda274f65bdd2b5403e5647849f53c11fa57cd55fe6178cf83961b8d73084bacf9be524a5fa9fd0fc295554ca6835d15beed02bb6f0078bee3c17e218b544cb5bb8f2ae631fc711233f883c8ae268af81ca335c56598da59860a5cb529b528bf35fa775d51ed6270d4f114a542aabc64accfd35b0bfb5d4ece1bfb1904b04e81d1d4e4106add7c49f0bbe28cfe109c695aa96974a95bb72d031eebfec9ee3f1afb46caf6d350b68eeeca559a1900657439041aff4c7c34f12f2fe2ecb957a0d46bc52f694fac5f75de2fa3f93d4fe7ee20e1faf95d7e49eb07f0befff0004f3bf1efc2fd17c6c86e73f63d454616e1067763a071fc43f5af8fbc4de03f13784ee1a2d52d1cc409db71182f1301dc30e9f43835fa1f514d0c3711b453a2c88c305580208f706be73c44f02f23e279cb194bf71897bce2aea5fe28e977e69a7ddb3bf22e31c665e95297bf4fb3dd7a3fd363f3128afbb35df83be0bd6d8cab6c6ce56392f01db9fc3a57986a1fb3b49c9d2f551c9e04c9d3fef9eb5fcb59e7d1cb8c30327f55a71af1ef0924fee959fdd73f46c1f1de55597ef24e0fcd7eaae7cc5457d0a3f677f10eec1d4ed31ebb1ff009574da3fecf1669b64d6f51790e3e68e01819ff78f35e1603c09e36c55554fea4e1e7294525f8dfee4cecadc6394538f37b64fd137fa1f2cc104f7322c36f1b4b231c05404927e82be81f017c0fd435178b53f16036b6a0865b5ff0096b20ff6bfba3f5fa57d17e1cf037867c2e83fb2acd1640399586e73f89e95c8fc47f8a7a67846d64b0d3dd6e75691488e35e562ff006a43db1d87535fb564be06643c298479f71b6214d435e45f037d16b694dbe91492ef747c9e2f8c7199954fa96514da6fabdeddfb2f531be2978e2c3c13a22f85fc3db22bd9a3d8a91702de1c633c7427a0fcebe3624b1249c93c9356efefeef54bc96fefe569ae2762eeec7249354ebf9efc48f10311c5799fd6651e4a105cb4e0b68c7f2bbebf25b247dbe4392d3cb70fecd3bcdeb27ddff9762d58da4da85ec1636ca5e5b89522451dd9c8007e66bf48b45d322d1b48b3d2a0fb96b0a463dca8e4fe279af8e7e06f87ff00b5bc62ba8cabba1d2e3337238f35b2a9f9727ea057db55fd41f45be19fabe5589cf2aad6acb923fe186efe7276ff00b74fcefc46cc39f114f0717a455dfabff81f98514515fd527e6e15f2dfed016b7777ac6896f0a332caad1a639cbb3018afa92ab4f676974f1c9730a4ad0b6f8cba8255bd467a1af8af107847fd66c92ae4fed3939dc5ded7f8649bd3cd23d7c8f34fecfc64715cbcd6be9eaac7ccdf17e78bc33e06d17c1967fbb69b6bcca3ba44bce7eae41fc2be61af54f8c7aeff006d78dee911b7436205b27a65796ffc789af2baff003d3c5dce69661c51888e1bf8346d4a0ba28d35cba7cd37f33f71e18c2ca865d0753e29fbcfd65afe560a28a2bf333e8028a28a0028a28a0028a28a0028a28a00fb5be03ea6979e065b207e7b0b996223d9cf980fd0ef23f0af6aaf8a7e0878a4687e273a5dc36db6d4c08cfa0957ee1fe63f1afb5abfd29f023896966fc2186827efd05ece4bb72fc3f7c6df89f81719e5f2c2e6951f49fbcbe7bfe370ac1b9f0ce8579ac45af5d59c72df40bb2395c64a819c7078c8cf07a8adea2bf5ac560b0f898a86260a4934d2693b35b3d7aae8cf98a75674db74db57d34ec159daae93a7eb5652e9da9c0b716f28c3238cfe23d08ec6b468ad2bd0a75a9ca8d68a945ab34d5d35d9a26139424a707668f84be267c38b9f04ea1e7da06974bb863e4c879287fb8c7f91ee2bcb2bf417e245f68363e12be7f104626b77428b1746790fdd0be873ce7b57e7d9c64e381dabfce3f1d38172ee1acf553cb26b92aae7f67d69ebb7f85fd9ebbaecdfef3c1d9cd7cc305cd885ac5daffcdff07b89451457e287d685145140051451400514514017f4dd52ff0047bb4bfd3677b79e339574383ffd715f53f813e39d8df08b4cf1762d6e3855bb03f74e7fdbfee93ebd3d715f24d15f7bc0de24677c2988f6b9654f71fc5096b097aae8fcd599e2e7190e1332a7cb888ebd1add7f5d99f527c6ef885692d88f0a68d32cde7ed7ba9632194275540470727935f2eab323075ea0e47e14da2b9b8f38e31dc539bcb36c62e5764a315b462b64bf16df56cd326ca28e5b86586a3af56fbbee7e907852f86a7e1bd36fc1cf9d6d131faed19fd6ba0ebc5790fc11d4fedfe04b680b65ace492023d00391fcebd7abfd31e0acd16659060b1c9fc74e0dfaf2abfe373f9fb37c37d5f1b568f693fccfcecf1dd89d3fc5faadb118c5cbb7fdf473fd6b92af6ff008efa41b1f172dfa8c25ec21b3fed2f06bc42bfccbf107279657c498dc0c95b96a4ade8ddd7e0d1fd0792e296270146b2eb15f7f5fc4057e8ef84675b9f0be973a9c86b58bf4503fa57e7157ddff077535d4bc0962b9cb5aee818771b49c7e95fb97d15f30853cf7178396f3a69affb764bf491f1fe23d072c153aabeccbf35ff0000f51a88cf00985b19144aca58267e62a38ce3d2a5af0cf8d73ea7a2dae95e2bd1e4314f617063661d0ac83a11dc123906bfb0f8bf887fb0b29ab9b3a6e71a76724b7e5ba526bcd26dfc8fcb32ac0fd73131c37359caf67e76d3ef7a1ee745793f833e2df86fc4b691c77b32d8df850258a53b54b772873c8fd6bd026f106876e9e64d7f6e8bd72645ff001ad327e2dc9f34c1c71d81c4c254dabdf992b7934f54fba64e2b2bc5e1aaba35a9b525e5f977362be2cf8f4b10f1a831e371b68f7fd79c7e95f456aff167c0fa3e565bf13b8fe080190ff87eb5f19f8dbc487c59e24bcd682b2473362256eab1af0a0fbe2bf9d7e91dc6d92e2787e394e0f110a959d48b6a3252e5514eeddaf6e8be67ddf016518ba78d789ab0718f2bdd5af7b1a5f0ebc653782fc450dfb126ce6c457483bc67be3d57a8aed3e207c65d53c42f269da033d969fca971c4b28f73d81f4af0ea2bf95b2ff1133ec0e493e1fc1d770a129393b692d56b14f7517bb4b77eaeff00a457c8f055b16b1b56179a56f2f5b771ccccec59c9627a93c9a6d1457c4b77d59eb1daf85fc7fe26f08298b47b9db0336f685d43213eb8afaabe1dfc5ad3bc64e34cbf45b2d4c0caa67f77363aec27b8f43f857c41535b5c4f69711dd5b398e58983a3a9c156539041afd63c3ff1873ee18af4e9c6aba9865a3a727756ebcb7f85f6b697dd33e6b3be16c166309371e5a8f692defe7dcfd3aa2b88f87be27ff84b3c2f6baa4873381e5cff00f5d13827f1eb5dbd7fa4594e6987ccb054b1f8577a7522a49f93573f03c561a787ad2a1517bd1767f20a28a2bd1300a28a2800aa3a969d69ab58cba75f26f8275db22e48dca7a8e2af515956a34eb53952ab14e324d34f669ee9f932a139424a717668c8d1b40d1bc3f6e6d746b48ad23620b08d70588e993d4fe26b5e8a2a70b85a386a51a187828c16c92492f44b41d4a93a9273a8eedf5661789eecd8f87752bc1c18ad656fc94d7e6f3b167663ce493f9d7e83fc479c5bf81f5890f7b665ff00beb8feb5f9ed5fc53f4acc53966981c35f45093fbe56ff00db4fd73c36a4961ab54ef24bee5ff0428a28afe503f490a28a2800a28a2800a28a2800a28a2800a28a2800a28a28003d2bf46bc18863f08e8c87b58dbffe8b15f9cc06580f538afd28d021fb3e87a7c1ff003ced615fc900afeb4fa28d16f30cc2af68417df26ff43f33f12a5fb8a11f37f9235e8a28afed83f2310a86186008f435970687a45b5f1d4adad228ae9815696340acc0ff00788ebf8d6ad15856c2d1ab28ceac1371775749d9f75d9970ab38a6a2ed70a28a2b72028a28a0028a28a004201183c835f3e689f07a37f1b5e788f52458b4f8ae9a5b4b61d646ce4330eca0f41d4fd2be84a2be5f88b83f2bcf2b61aae654f9fd84b9e2ba5ed6d7baeb6d9b4af7d8f4b019ae23070a91c3cadceacfd3cbfcc28a28afa83cd0a28a2800a28a2803ffd1fdfca28a2803c7fe36e8adaaf82e5b889374b6522cc31d768e1bf4af87ebf4e2eada1bcb696d2e177473294607b835f9efe39f0adc7843c4371a5caa7c9dc5eddcf4689ba7e5d0d7f147d28783aad3c650e24a11bc269427e5257e56fd569f2f347eb9e1de6b19519e026fde4eebd1eff73fcce3e8a28afe4a3f4c0aeebc1ff10bc43e0c9c1d3e5f36d49cbdb4a498dbe9dc1f715c2d15e9e519ce3b2bc5471b975574ea47671767ff0005774f47d4e7c4e168e229ba55e2a517d19f73f85be31f84fc44a90dccbfd9b76d8062b8385cff00b2fd0fe38af558e58a641242eae8c32194e411ec457e61d741a4f8afc49a1e3fb2b52b9b751cec59094cff00b872a7f2afea1e17fa52e328c151cfb0aaa5bedc1f2bf9c5fbadfa38af23f3ccc7c39a336e582a9cbe4f55f7eff99fa41457c356df1b7e205ba0592ee2b823bc90ae4ffdf1b454f2fc73f1e4abb44b6d1fba45cfea4d7e971fa4ff000938733a7593edc91ffe4ec7cf3f0ef33bdb9a1f7bff0023edee9d6b96d7fc69e19f0d46cfabdfc5132ffcb2077487e8a326be1dd4fe2378df575d97bac5ced3c158984208f42230b9fc6b8c777918b48c598f52c726be2f883e9574f91c324c13bf49546b4ffb7637bffe048f5f03e1bbba78bada768afd5ff91f4178cfe3bea5a9abd8785e33636ed90d70f83330ff0064745fd4fd2be7f9a696e2569a77692473966639249f526a3a2bf97f8b38db39e24c4fd6b37ace6d6cb68c7fc315a2fcdf56cfd0f2dca30980a7ecf0b0b777d5fab0a28ad2d234d9f58d52d74bb704c9752ac631db71e4fe039af99a142a56a91a3495e526925ddbd123d09c9462e52d91f617c0bd03fb2bc25fda52ae25d4a432f3d7cb5f957f3c135ed954b4eb1834db0b7d3ed942456f12448a3a0540001f90abb5feb0f06f0f53c8b24c2e534ffe5dc127e72de4fe72bb3f9a735c73c6632a6265f69fe1d3f00a28a2be98f3c2b2f5bd4a2d1f47bdd567e12d209263ce33b149c7e3d2b52bc6be3a6ac74ff03c9668d87d4268e1f7d8a77b7f2c7e35f35c639dac9f23c5e66f7a709497adbdd5f37647a19560feb78ca587fe6697cbafe07c5977732dedd4d7939dd2cf2348e7d59ce4ff003aaf4515fe4dd4a929c9ce6eedeacfe9749256414514540c28a28a0028a28a0028a28a0028a28a009ade796d678ee6062b244c1d5875041c8afd0df04f8920f14f86acb574237ba049973f7655e187e7cfd2bf3b2af5aea9a959218eceee78149c958e46419fa022bf5cf097c54abc198bad39527569554938a76f793d249d9ec9b5b6b7f23e6789787219b528479b96517a3b5f47bae9e47e98ee5f51f9d2d7e6b8f106bd91ff00131bbffbfeff00e35fa07e0e90cde16d2e5662e5ada32598e49247526bfb27c2ef1868f19e26be1e9615d2f67152bb9295eeedd91f95711f0acb29a70a92a9cdccedb5bf5674b451457ece7c91f1cfc7af12497fe234f0fc4c7c8d3d159c0e8659006fd148af05af50f13683ad78afe25eb5a7699119ae4ddca48270151380493d001815e7ba9e9b7ba3df4da6ea31186e206dae8dd8d7f96de24d6ccb32cf3199d6269cbd94aace11934f97dd76514f6ba8a5a1fd199042861f074b094dae6514daebaf5b79b28d14515f9e1ee05145140051451400514514005145140051451401f4efecedaa857d534676fbdb2741f4f94d7d475f04fc26d6868be36b29246db1dc1303fa7cfd3f5afbd81cf22bfd0ffa3867cb1dc231c249fbd42528fc9fbcbf36be47e1bc7b82747337556d349fcd68ff0023c17e3ee886f7c3306af12e5ec251b8ff00d339383f91af8e6bf4a75ed2a1d7346bcd26e0652ea168fe848e0fe079afce3d4ac27d2efee34eb952b2dbc8d1b03eaa715f87fd27f861e173ba39d535ee578d9ff8e1a7e31b7dccfaff000f331557072c249eb0775e8ffe0dca55f4e7ecf3ad857d434091bef62e231fa37f4af98ebb5f87baf1f0ef8b6c3502db623208a53d3e47e0d7e41e16f12ff61714613309bb414b965fe197bafeebdfe47d4f11603eb99755a0b7b5d7aad51fa175c8f8eb435f11785351d2caee7921668ffeba27ccbff8f015d6ab0650cbc823228232083debfd3dcd32fa39860aae0ababc2a45c5fa4958fe78c3d7950ab1ad0de2d3fb8fcc26568dcab65594907d88a719a561867623dc9aeffe28e82da078cafa055db0dc3f9f17a157e4e3f1cd79e57f9319e6575f2bcc2be5d5b4953938bf93b7e27f4c613110c45085786d249fde1451457927485145140051451401b1a3f87f58d7e53068f6cf7522f548f0580f5c67a577faafc1bf1a697a5c7a99b75b8caee96184ee922fa8eff8570de18d6aebc3faf596ad68c55e0990b0071b909c329f6238afd21521941ec457f47f82de17f0f71865f8bfae4aa46bd3b2d1ae55cd76a495b5d9a69b6befd3e0f8b788b1d9557a5ec945c257def7d375f89f39fecf12ceba6ead672865115c2361b8c315c118fc2be8daa169a5e9f6135c5c59c090c974c1e62831bd80c64e3be2afd7f6770170d55e1fc8a864f56a73ba575cdb5d3936b4e9a347e519de611c763678a846ca56d3e4828a28afaf3ca0a28a2800a28a2800a28a2803cd3e2fb32fc3dd54a9c7cb18ff00c88b5f0557e817c50b5379e04d5e1032443bff00ef860dfd2bf3f6bf843e94f4a6b88f0b51ece8a4be539dff00347ecde1c497d42a47af37e8828a28afe623f420a28a2800a28a2800a28a2800a28a2800a28a2800a28a2802d58c26e2f6de01d649517f3602bf4c2d9765bc483f85147e42bf3afc1b6a6f7c57a4db019df77171ec1813fcabf46870315fda7f450c1db099862adbca11fb949ffedc7e4de2555bd4a14fb293fbedfe41451457f5c9f9805145140051451400514514005145140051451400514514005145140051451401ffd2fdfca28a2800af32f89de028fc6da3116fb5351b505ed9cf46f5427d1bf435e9b457919f64783ce72fab9663e1cd4aa2b35faaecd3d53e8ceac1636ae12bc71141da513f31eeed2e6c2e64b3bc89a19e1629246e30cac3a8355ebee2f895f0bac7c636efa8586db7d5635f95f1f2cb8e8aff00d0f6af8bb54d2750d16f24b0d4e078278c9055c63a771ea2bfcd7f12bc2fccf8471ae9", + }, + }, + }, + }, + }, + passFilter: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := PlanariaB(tt.args.tx) + if (err != nil) != tt.wantErr { + t.Errorf("PlanariaB() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.passFilter && got == nil { + t.Errorf("PlanariaB() expected transaction to pass filter and it didn't") + } + }) + } +} diff --git a/engine/chainstate/filters/planaria-d.go b/engine/chainstate/filters/planaria-d.go new file mode 100644 index 000000000..213a57579 --- /dev/null +++ b/engine/chainstate/filters/planaria-d.go @@ -0,0 +1,26 @@ +package filters + +import ( + "strings" + + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" + "github.com/libsv/go-bt" +) + +// PlanariaDTemplate string template for a D transaction +const PlanariaDTemplate = "006a223139694733575459537362796f7333754a373333794b347a45696f69314665734e55" + +// PlanariaDTemplateAlternate alternate string template for a D transaction +const PlanariaDTemplateAlternate = "6a223139694733575459537362796f7333754a373333794b347a45696f69314665734e55" + +// PlanariaD processor +func PlanariaD(tx *chainstate.TxInfo) (*bt.Tx, error) { + // Loop through all of the outputs and check for pubkeyhash output + for _, out := range tx.Vout { + // if any output contains a pubkeyhash output, include this tx in the filter + if strings.HasPrefix(out.ScriptPubKey.Hex, PlanariaDTemplate) || strings.HasPrefix(out.ScriptPubKey.Hex, PlanariaDTemplateAlternate) { + return bt.NewTxFromString(tx.Hex) + } + } + return nil, nil +} diff --git a/engine/chainstate/filters/pubkeyhash.go b/engine/chainstate/filters/pubkeyhash.go new file mode 100644 index 000000000..4b453f061 --- /dev/null +++ b/engine/chainstate/filters/pubkeyhash.go @@ -0,0 +1,19 @@ +package filters + +import ( + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" + "github.com/libsv/go-bt" +) + +// PubKeyHash processor +func PubKeyHash(tx *chainstate.TxInfo) (*bt.Tx, error) { + // log.Printf("Attempting to filter for pubkeyhash: %#v", tx) + // Loop through all the outputs and check for pubkeyhash output + for _, out := range tx.Vout { + // if any output contains a pubkeyhash output, include this tx in the filter + if out.ScriptPubKey.Type == "pubkeyhash" { + return bt.NewTxFromString(tx.Hex) + } + } + return nil, nil +} diff --git a/engine/chainstate/filters/rarecandy.go b/engine/chainstate/filters/rarecandy.go new file mode 100644 index 000000000..cdbd29675 --- /dev/null +++ b/engine/chainstate/filters/rarecandy.go @@ -0,0 +1,23 @@ +package filters + +import ( + "strings" + + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" + "github.com/libsv/go-bt" +) + +// RareCandyFrogCartelScriptTemplate string template for a Rare Candy Frog Cartel NTF +const RareCandyFrogCartelScriptTemplate = "a914179b4c7a45646a509473df5a444b6e18b723bd148876" + +// RareCandyFrogCartel processor +func RareCandyFrogCartel(tx *chainstate.TxInfo) (*bt.Tx, error) { + // Loop through all the outputs and check for pubkeyhash output + for _, out := range tx.Vout { + // if any output contains a pubkeyhash output, include this tx in the filter + if strings.HasPrefix(out.ScriptPubKey.Hex, RareCandyFrogCartelScriptTemplate) { + return bt.NewTxFromString(tx.Hex) + } + } + return nil, nil +} diff --git a/engine/chainstate/interface.go b/engine/chainstate/interface.go new file mode 100644 index 000000000..8872e45b6 --- /dev/null +++ b/engine/chainstate/interface.go @@ -0,0 +1,54 @@ +package chainstate + +import ( + "context" + "net/http" + "time" + + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/tonicpow/go-minercraft/v2" +) + +// HTTPInterface is the HTTP client interface +type HTTPInterface interface { + Do(req *http.Request) (*http.Response, error) +} + +// ChainService is the chain related methods +type ChainService interface { + Broadcast(ctx context.Context, id, txHex string, timeout time.Duration) (string, error) + QueryTransaction( + ctx context.Context, id string, requiredIn RequiredIn, timeout time.Duration, + ) (*TransactionInfo, error) + QueryTransactionFastest( + ctx context.Context, id string, requiredIn RequiredIn, timeout time.Duration, + ) (*TransactionInfo, error) +} + +// ProviderServices is the chainstate providers interface +type ProviderServices interface { + Minercraft() minercraft.ClientInterface + BroadcastClient() broadcast.Client +} + +// HeaderService is header services interface +type HeaderService interface { + VerifyMerkleRoots(ctx context.Context, merkleRoots []MerkleRootConfirmationRequestItem) error +} + +// ClientInterface is the chainstate client interface +type ClientInterface interface { + ChainService + ProviderServices + HeaderService + Close(ctx context.Context) + Debug(on bool) + DebugLog(text string) + HTTPClient() HTTPInterface + IsDebug() bool + IsNewRelicEnabled() bool + Network() Network + QueryTimeout() time.Duration + FeeUnit() *utils.FeeUnit +} diff --git a/engine/chainstate/merkle_root.go b/engine/chainstate/merkle_root.go new file mode 100644 index 000000000..dfd464218 --- /dev/null +++ b/engine/chainstate/merkle_root.go @@ -0,0 +1,31 @@ +package chainstate + +import ( + "context" + "errors" +) + +// VerifyMerkleRoots will try to verify merkle roots with all available providers +// When no error is returned, it means that the block headers service client responded with state: Confirmed or UnableToVerify +func (c *Client) VerifyMerkleRoots(ctx context.Context, merkleRoots []MerkleRootConfirmationRequestItem) error { + pc := c.options.config.blockHedersServiceClient + if pc == nil { + c.options.logger.Warn().Msg("VerifyMerkleRoots is called even though no block headers service client is configured; this likely indicates that the paymail capabilities have been cached.") + return errors.New("no block headers service client found") + } + merkleRootsRes, err := pc.verifyMerkleRoots(ctx, c.options.logger, merkleRoots) + if err != nil { + return err + } + + if merkleRootsRes.ConfirmationState == Invalid { + c.options.logger.Warn().Msg("Not all merkle roots confirmed") + return errors.New("not all merkle roots confirmed") + } + + if merkleRootsRes.ConfirmationState == UnableToVerify { + c.options.logger.Warn().Msg("Some merkle roots were unable to be verified. Proceeding regardless.") + } + + return nil +} diff --git a/engine/chainstate/merkle_root_provider.go b/engine/chainstate/merkle_root_provider.go new file mode 100644 index 000000000..7067707b8 --- /dev/null +++ b/engine/chainstate/merkle_root_provider.go @@ -0,0 +1,109 @@ +package chainstate + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/rs/zerolog" +) + +// MerkleRootConfirmationState represents the state of each Merkle Root verification +// process and can be one of three values: Confirmed, Invalid and UnableToVerify. +type MerkleRootConfirmationState string + +const ( + // Confirmed state occurs when Merkle Root is found in the longest chain. + Confirmed MerkleRootConfirmationState = "CONFIRMED" + // Invalid state occurs when Merkle Root is not found in the longest chain. + Invalid MerkleRootConfirmationState = "INVALID" + // UnableToVerify state occurs when Block Header Service is behind in synchronization with the longest chain. + UnableToVerify MerkleRootConfirmationState = "UNABLE_TO_VERIFY" +) + +// MerkleRootConfirmationRequestItem is a request type for verification +// of Merkle Roots inclusion in the longest chain. +type MerkleRootConfirmationRequestItem struct { + MerkleRoot string `json:"merkleRoot"` + BlockHeight uint64 `json:"blockHeight"` +} + +// MerkleRootConfirmation is a confirmation +// of merkle roots inclusion in the longest chain. +type MerkleRootConfirmation struct { + Hash string `json:"blockHash"` + BlockHeight uint64 `json:"blockHeight"` + MerkleRoot string `json:"merkleRoot"` + Confirmation MerkleRootConfirmationState `json:"confirmation"` +} + +// MerkleRootsConfirmationsResponse is an API response for confirming +// merkle roots inclusion in the longest chain. +type MerkleRootsConfirmationsResponse struct { + ConfirmationState MerkleRootConfirmationState `json:"confirmationState"` + Confirmations []MerkleRootConfirmation `json:"confirmations"` +} + +type blockHeadersServiceClientProvider struct { + url string + authToken string + httpClient *http.Client +} + +func newBlockHeaderServiceClientProvider(url, authToken string) *blockHeadersServiceClientProvider { + return &blockHeadersServiceClientProvider{url: url, authToken: authToken, httpClient: &http.Client{}} +} + +func (p *blockHeadersServiceClientProvider) verifyMerkleRoots( + ctx context.Context, + logger *zerolog.Logger, + merkleRoots []MerkleRootConfirmationRequestItem, +) (*MerkleRootsConfirmationsResponse, error) { + jsonData, err := json.Marshal(merkleRoots) + if err != nil { + return nil, _fmtAndLogError(err, logger, "Error occurred while marshaling merkle roots.") + } + + req, err := http.NewRequestWithContext(ctx, "POST", p.url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, _fmtAndLogError(err, logger, "Error occurred while creating request for the Block Headers Service client.") + } + + if p.authToken != "" { + req.Header.Set("Authorization", "Bearer "+p.authToken) + } + res, err := p.httpClient.Do(req) + if res != nil { + defer func() { + _ = res.Body.Close() + }() + } + if err != nil { + return nil, _fmtAndLogError(err, logger, "Error occurred while sending request to the Block Headers Service service.") + } + + if res.StatusCode != 200 { + return nil, _fmtAndLogError(_statusError(res.StatusCode), logger, "Received unexpected status code from Block Headers Service service.") + } + + // Parse response body. + var merkleRootsRes MerkleRootsConfirmationsResponse + err = json.NewDecoder(res.Body).Decode(&merkleRootsRes) + if err != nil { + return nil, _fmtAndLogError(err, logger, "Error occurred while parsing response from the Block Headers Service service.") + } + + return &merkleRootsRes, nil +} + +// _fmtAndLogError returns brief error for http response message and logs detailed information with original error +func _fmtAndLogError(err error, logger *zerolog.Logger, message string) error { + logger.Error().Err(err).Msg("[verifyMerkleRoots] " + message) + return fmt.Errorf("cannot verify transaction - %s", message) +} + +func _statusError(statusCode int) error { + return fmt.Errorf("block headers service client returned status code %d - check Block Headers Service configuration and service status", statusCode) +} diff --git a/engine/chainstate/merkle_root_test.go b/engine/chainstate/merkle_root_test.go new file mode 100644 index 000000000..77fc6aae5 --- /dev/null +++ b/engine/chainstate/merkle_root_test.go @@ -0,0 +1,127 @@ +package chainstate + +import ( + "bytes" + "context" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func initMockClient(ops ...ClientOps) (*Client, *buffLogger) { + bLogger := newBuffLogger() + ops = append(ops, WithLogger(bLogger.logger)) + c, _ := NewClient( + context.Background(), + ops..., + ) + return c.(*Client), bLogger +} + +func TestVerifyMerkleRoots(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + mockURL := "http://block-headers-service.test/api/v1/chain/merkleroot/verify" + + t.Run("no block headers service client", func(t *testing.T) { + c, _ := initMockClient() + + err := c.VerifyMerkleRoots(context.Background(), []MerkleRootConfirmationRequestItem{}) + + assert.Error(t, err) + }) + + t.Run("block headers service is not online", func(t *testing.T) { + httpmock.Reset() + httpmock.RegisterResponder("POST", mockURL, + httpmock.NewStringResponder(500, `{"error":"Internal Server Error"}`), + ) + c, bLogger := initMockClient(WithConnectionToBlockHeaderService(mockURL, "")) + + err := c.VerifyMerkleRoots(context.Background(), []MerkleRootConfirmationRequestItem{}) + + assert.Error(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, bLogger.contains("block headers service client returned status code 500")) + }) + + t.Run("block headers service wrong auth", func(t *testing.T) { + httpmock.Reset() + httpmock.RegisterResponder("POST", mockURL, + httpmock.NewStringResponder(401, `Unauthorized`), + ) + c, bLogger := initMockClient(WithConnectionToBlockHeaderService(mockURL, "some-token")) + + err := c.VerifyMerkleRoots(context.Background(), []MerkleRootConfirmationRequestItem{}) + + assert.Error(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, bLogger.contains("401")) + }) + + t.Run("block headers service invalid state", func(t *testing.T) { + httpmock.Reset() + httpmock.RegisterResponder("POST", mockURL, + httpmock.NewJsonResponderOrPanic(200, MerkleRootsConfirmationsResponse{ + ConfirmationState: Invalid, + Confirmations: []MerkleRootConfirmation{}, + }), + ) + c, bLogger := initMockClient(WithConnectionToBlockHeaderService(mockURL, "some-token")) + + err := c.VerifyMerkleRoots(context.Background(), []MerkleRootConfirmationRequestItem{}) + + assert.Error(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, bLogger.contains("Not all merkle roots confirmed")) + }) + + t.Run("block headers service confirmedState", func(t *testing.T) { + httpmock.Reset() + httpmock.RegisterResponder("POST", mockURL, + httpmock.NewJsonResponderOrPanic(200, MerkleRootsConfirmationsResponse{ + ConfirmationState: Confirmed, + Confirmations: []MerkleRootConfirmation{ + { + Hash: "some-hash", + BlockHeight: 1, + MerkleRoot: "some-merkle-root", + Confirmation: Confirmed, + }, + }, + }), + ) + c, bLogger := initMockClient(WithConnectionToBlockHeaderService(mockURL, "some-token")) + + err := c.VerifyMerkleRoots(context.Background(), []MerkleRootConfirmationRequestItem{ + { + MerkleRoot: "some-merkle-root", + BlockHeight: 1, + }, + }) + + assert.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.False(t, bLogger.contains("ERR")) + assert.False(t, bLogger.contains("WARN")) + }) +} + +// buffLogger allows to check if a certain string was logged +type buffLogger struct { + logger *zerolog.Logger + buf *bytes.Buffer +} + +func newBuffLogger() *buffLogger { + var buf bytes.Buffer + logger := zerolog.New(&buf).Level(zerolog.DebugLevel).With().Logger() + return &buffLogger{logger: &logger, buf: &buf} +} + +func (l *buffLogger) contains(expected string) bool { + return bytes.Contains(l.buf.Bytes(), []byte(expected)) +} diff --git a/engine/chainstate/minercraft_default.go b/engine/chainstate/minercraft_default.go new file mode 100644 index 000000000..1e6e28a0a --- /dev/null +++ b/engine/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/engine/chainstate/minercraft_init.go b/engine/chainstate/minercraft_init.go new file mode 100644 index 000000000..e5d26a3c3 --- /dev/null +++ b/engine/chainstate/minercraft_init.go @@ -0,0 +1,181 @@ +package chainstate + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/bitcoin-sv/spv-wallet/engine/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/engine/chainstate/mock_const.go b/engine/chainstate/mock_const.go new file mode 100644 index 000000000..9810f4324 --- /dev/null +++ b/engine/chainstate/mock_const.go @@ -0,0 +1,33 @@ +package chainstate + +import "github.com/bitcoin-sv/spv-wallet/engine/utils" + +const ( + // Dummy transaction data + broadcastExample1TxID = "15d31d00ed7533a83d7ab206115d7642812ec04a2cbae4248365febb82576ff3" + broadcastExample1TxHex = "0100000001018d7ab1a0f0253120a0cb284e4170b47e5f83f70faaba5b0b55bbeeef624b45010000006b483045022100d5b0dddf76da9088e21cf1277f064dc7832c3da666732f003ee48f2458142e9a02201fe725a1c455b2bd964779391ae105b87730881f211cd299ca36d70d74d715ab412103673dffd80561b87825658f74076da805c238e8c47f25b5d804893c335514d074ffffffff02c4090000000000001976a914777242b335bc7781f43e1b05c60d8c2f2d08b44c88ac962e0000000000001976a91467d93a70ac575e15abb31bc8272a00ab1495d48388ac00000000" + notFoundExample1TxID = "918c26f8227fa99f1b26f99a19648653a1382fb3b37b03870e9c138894d29b3b" + onChainExample1BlockHash = "0000000000000000015122781ab51d57b26a09518630b882f67f1b08d841979d" + onChainExample1BlockHeight = int64(723229) + onChainExample1Confirmations = int64(314) + onChainExample1TxHex = "01000000025b7439a0c9effa3f19d0e441d2eea596e44a8c49240b6e389c29498285f92ad3010000006a4730440220482c1c896678d7307e1de35cef2aae4907f2684617a26d8abd24c444d527c80d02204c550f8f9d69b9cf65780e2e066041750261702639d02605a2eb694ade4ca1d64121029ce7958b2aa3c627334f50bb810c678e2b284db0ef6f7d067f7fccfa05d0f095ffffffff1998b0e4955e1d8ba976d943c43f32e143ba90e805f0e882d3b8edc0f7473b77020000006a47304402204beb486e5d99a15d4d2267e328abb5466a05fdc20d64903d0ace1c4fabb71a34022024803ae9e18b3c11683b2ff2b5fb4ca973a22fdd390f6ab1f99396604a3f06af4121038ea0f258fb838b5193e9739ddd808bb97aaab52a60ba8a83958b13109ab183ccffffffff030000000000000000fd8901006a0372756e0105004d7d017b22696e223a312c22726566223a5b22653864393134303764643461646164363366333739353032303861383532653562306334383037333563656235346133653334333539346163313839616331625f6f31222c22376135346462326162303030306161303035316134383230343162336135653761636239386333363135363863623334393063666564623066653161356438385f6f33225d2c226f7574223a5b2233356463303036313539393333623438353433343565663663633363366261663165666462353263343837313933386632366539313034343632313562343036225d2c2264656c223a5b5d2c22637265223a5b5d2c2265786563223a5b7b226f70223a2243414c4c222c2264617461223a5b7b22246a6967223a307d2c22757064617465222c5b7b22246a6967223a317d2c7b2267726164756174696f6e506f736974696f6e223a6e756c6c2c226c6576656c223a382c226e616d65223a22e38395e383abe38380222c227870223a373030307d5d5d7d5d7d11010000000000001976a914058cae340a2ef8fd2b43a074b75fb6b38cb2765788acd4020000000000001976a914160381a3811b474ff77f31f64f4e57a5bb5ebf1788ac00000000" + onChainExample1TxID = "908c26f8227fa99f1b26f99a19648653a1382fb3b37b03870e9c138894d29b3b" + onChainExampleArcTxID = "a11b9e1ee08e264f9add02e4afa40dad3c00a23f250ac04449face095c68fab7" + + // API key + // testDummyKey = "test-dummy-api-key-value" //nolint:gosec // this is a dummy key + + // Signatures + matterCloudSig1 = "30450221008003e78e2154e9686a2bb864a811e7ec950093f273f94d222fa87c81a5daf8f6022018e1098ad2a3f1adc431ddd375c06c867d79e484710dc87ac6847b3a2f4909d2" + gorillaPoolSig1 = "3045022100bfa217db8eb1a520db05877c724ce4150d16ff0ff93165e1d2b6498a04a3da3102203cb00b39de31c3ed12456c212cc2e3860928db875eee5f843bce7b9f300beef0" + + // Defaults for request payloads + 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/engine/chainstate/mock_minercraft.go b/engine/chainstate/mock_minercraft.go new file mode 100644 index 000000000..6359cbac0 --- /dev/null +++ b/engine/chainstate/mock_minercraft.go @@ -0,0 +1,534 @@ +package chainstate + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/libsv/go-bk/envelope" + "github.com/libsv/go-bt/v2" + "github.com/tonicpow/go-minercraft/v2" + "github.com/tonicpow/go-minercraft/v2/apis/mapi" +) + +var ( + minerTaal = &minercraft.Miner{ + MinerID: "030d1fe5c1b560efe196ba40540ce9017c20daa9504c4c4cec6184fc702d9f274e", + Name: "Taal", + } + + minerMempool = &minercraft.Miner{ + MinerID: "03e92d3e5c3f7bd945dfbf48e7a99393b1bfb3f11f380ae30d286e7ff2aec5a270", + Name: "Mempool", + } + + minerMatterPool = &minercraft.Miner{ + MinerID: "0253a9b2d017254b91704ba52aad0df5ca32b4fb5cb6b267ada6aefa2bc5833a93", + Name: "Matterpool", + } + + minerGorillaPool = &minercraft.Miner{ + MinerID: "03ad780153c47df915b3d2e23af727c68facaca4facd5f155bf5018b979b9aeb83", + Name: "GorillaPool", + } + + allMiners = []*minercraft.Miner{ + minerTaal, + minerMempool, + minerGorillaPool, + minerMatterPool, + } + + minerAPIs = []*minercraft.MinerAPIs{ + { + MinerID: "03e92d3e5c3f7bd945dfbf48e7a99393b1bfb3f11f380ae30d286e7ff2aec5a270", + APIs: []minercraft.API{ + { + URL: "https://merchantapi.taal.com", + Type: minercraft.MAPI, + }, + { + URL: "https://tapi.taal.com/arc", + Type: minercraft.Arc, + }, + }, + }, + { + MinerID: "03e92d3e5c3f7bd945dfbf48e7a99393b1bfb3f11f380ae30d286e7ff2aec5a270", + APIs: []minercraft.API{ + { + Token: "561b756d12572020ea9a104c3441b71790acbbce95a6ddbf7e0630971af9424b", + URL: "https://www.ddpurse.com/openapi", + Type: minercraft.MAPI, + }, + }, + }, + { + MinerID: "0253a9b2d017254b91704ba52aad0df5ca32b4fb5cb6b267ada6aefa2bc5833a93", + APIs: []minercraft.API{ + { + URL: "https://merchantapi.matterpool.io", + Type: minercraft.MAPI, + }, + }, + }, + { + MinerID: "03ad780153c47df915b3d2e23af727c68facaca4facd5f155bf5018b979b9aeb83", + APIs: []minercraft.API{ + { + URL: "https://merchantapi.gorillapool.io", + Type: minercraft.MAPI, + }, + { + URL: "https://arc.gorillapool.io", + Type: minercraft.Arc, + }, + }, + }, + } +) + +// MinerCraftBase is a mock implementation of the minercraft.MinerCraft interface. +type MinerCraftBase struct{} + +// AddMiner adds a new miner to the list of miners. +func (m *MinerCraftBase) AddMiner(miner minercraft.Miner, apis []minercraft.API) error { + existingMiner := m.MinerByName(miner.Name) + if existingMiner != nil { + return fmt.Errorf("miner %s already exists", miner.Name) + } + // Append the new miner + allMiners = append(allMiners, &miner) + + // Append the new miner APIs + minerAPIs = append(minerAPIs, &minercraft.MinerAPIs{ + MinerID: miner.MinerID, + APIs: apis, + }) + + return nil +} + +// BestQuote returns the best quote for the given fee type and amount. +func (m *MinerCraftBase) BestQuote(context.Context, string, string) (*minercraft.FeeQuoteResponse, error) { + return nil, nil +} + +// FastestQuote returns the fastest quote for the given fee type and amount. +func (m *MinerCraftBase) FastestQuote(context.Context, time.Duration) (*minercraft.FeeQuoteResponse, error) { + return nil, nil +} + +// FeeQuote returns a fee quote for the given miner. +func (m *MinerCraftBase) FeeQuote(context.Context, *minercraft.Miner) (*minercraft.FeeQuoteResponse, error) { + return &minercraft.FeeQuoteResponse{ + Quote: &mapi.FeePayload{ + Fees: []*bt.Fee{ + { + FeeType: bt.FeeTypeData, + MiningFee: bt.FeeUnit(*MockDefaultFee), + }, + }, + }, + }, nil +} + +// MinerByID returns a miner by its ID. +func (m *MinerCraftBase) MinerByID(minerID string) *minercraft.Miner { + for index, miner := range allMiners { + if strings.EqualFold(minerID, miner.MinerID) { + return allMiners[index] + } + } + return nil +} + +// MinerByName returns a miner by its name. +func (m *MinerCraftBase) MinerByName(name string) *minercraft.Miner { + for index, miner := range allMiners { + if strings.EqualFold(name, miner.Name) { + return allMiners[index] + } + } + return nil +} + +// Miners returns all miners. +func (m *MinerCraftBase) Miners() []*minercraft.Miner { + return allMiners +} + +// MinerUpdateToken updates the token for the given miner. +func (m *MinerCraftBase) MinerUpdateToken(name, token string, apiType minercraft.APIType) { + if miner := m.MinerByName(name); miner != nil { + api, _ := m.MinerAPIByMinerID(miner.MinerID, apiType) + api.Token = token + } +} + +// PolicyQuote returns a policy quote for the given miner. +func (m *MinerCraftBase) PolicyQuote(context.Context, *minercraft.Miner) (*minercraft.PolicyQuoteResponse, error) { + return nil, nil +} + +// QueryTransaction returns a transaction for the given miner. +func (m *MinerCraftBase) QueryTransaction(context.Context, *minercraft.Miner, string, ...minercraft.QueryTransactionOptFunc) (*minercraft.QueryTransactionResponse, error) { + return nil, nil +} + +// RemoveMiner removes a miner from the list of miners. +func (m *MinerCraftBase) RemoveMiner(miner *minercraft.Miner) bool { + for i, minerFound := range allMiners { + if miner.Name == minerFound.Name || miner.MinerID == minerFound.MinerID { + allMiners[i] = allMiners[len(allMiners)-1] + allMiners = allMiners[:len(allMiners)-1] + return true + } + } + // Miner not found + return false +} + +// SubmitTransaction submits a transaction to the given miner. +func (m *MinerCraftBase) SubmitTransaction(context.Context, *minercraft.Miner, *minercraft.Transaction) (*minercraft.SubmitTransactionResponse, error) { + return nil, nil +} + +// SubmitTransactions submits transactions to the given miner. +func (m *MinerCraftBase) SubmitTransactions(context.Context, *minercraft.Miner, []minercraft.Transaction) (*minercraft.SubmitTransactionsResponse, error) { + return nil, nil +} + +// APIType will return the API type +func (m *MinerCraftBase) APIType() minercraft.APIType { + return minercraft.MAPI +} + +// MinerAPIByMinerID will return a miner's API given a miner id and API type +func (m *MinerCraftBase) MinerAPIByMinerID(minerID string, apiType minercraft.APIType) (*minercraft.API, error) { + for _, minerAPI := range minerAPIs { + if minerAPI.MinerID == minerID { + for i := range minerAPI.APIs { + if minerAPI.APIs[i].Type == apiType { + return &minerAPI.APIs[i], nil + } + } + } + } + return nil, &minercraft.APINotFoundError{MinerID: minerID, APIType: apiType} +} + +// MinerAPIsByMinerID will return a miner's APIs given a miner id +func (m *MinerCraftBase) MinerAPIsByMinerID(minerID string) *minercraft.MinerAPIs { + for _, minerAPIs := range minerAPIs { + if minerAPIs.MinerID == minerID { + return minerAPIs + } + } + return nil +} + +// UserAgent returns the user agent. +func (m *MinerCraftBase) UserAgent() string { + return "default-user-agent" +} + +type minerCraftTxOnChain struct { + MinerCraftBase +} + +// SubmitTransaction submits a transaction to the given miner. +func (m *minerCraftTxOnChain) 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") +} + +// QueryTransaction mocks the QueryTransaction method of the minercraft API. +func (m *minerCraftTxOnChain) QueryTransaction(_ context.Context, miner *minercraft.Miner, + txID string, _ ...minercraft.QueryTransactionOptFunc, +) (*minercraft.QueryTransactionResponse, error) { + if txID == onChainExample1TxID && miner.Name == minerTaal.Name { + sig := "304402207ede387e82db1ac38e4286b0a967b4fe1c8446c413b3785ccf86b56009439b39022043931eae02d7337b039f109be41dbd44d0472abd10ed78d7e434824ea8ab01da" + pubKey := minerTaal.MinerID + return &minercraft.QueryTransactionResponse{ + JSONEnvelope: minercraft.JSONEnvelope{ + Miner: minerTaal, + Validated: true, + JSONEnvelope: envelope.JSONEnvelope{ + Payload: "{\"apiVersion\":\"1.4.0\",\"timestamp\":\"2022-01-23T19:42:18.6860061Z\",\"txid\":\"908c26f8227fa99f1b26f99a19648653a1382fb3b37b03870e9c138894d29b3b\",\"returnResult\":\"success\",\"blockHash\":\"0000000000000000015122781ab51d57b26a09518630b882f67f1b08d841979d\",\"blockHeight\":723229,\"confirmations\":319,\"minerId\":\"030d1fe5c1b560efe196ba40540ce9017c20daa9504c4c4cec6184fc702d9f274e\",\"txSecondMempoolExpiry\":0}", + Signature: &sig, + PublicKey: &pubKey, + Encoding: utf8Type, + MimeType: applicationJSONType, + }, + }, + Query: &minercraft.QueryTxResponse{ + APIVersion: "1.4.0", + Timestamp: "2022-01-23T19:42:18.6860061Z", + TxID: onChainExample1TxID, + ReturnResult: mAPISuccess, + ResultDescription: "", + BlockHash: onChainExample1BlockHash, + BlockHeight: onChainExample1BlockHeight, + MinerID: minerTaal.MinerID, + Confirmations: onChainExample1Confirmations, + TxSecondMempoolExpiry: 0, + }, + }, nil + } else if txID == onChainExample1TxID && miner.Name == minerMempool.Name { + return &minercraft.QueryTransactionResponse{ + JSONEnvelope: minercraft.JSONEnvelope{ + Miner: minerMempool, + Validated: false, + JSONEnvelope: envelope.JSONEnvelope{ + Payload: "{\"apiVersion\":\"\",\"timestamp\":\"2022-01-23T19:51:10.046Z\",\"txid\":\"908c26f8227fa99f1b26f99a19648653a1382fb3b37b03870e9c138894d29b3b\",\"returnResult\":\"success\",\"resultDescription\":\"\",\"blockHash\":\"0000000000000000015122781ab51d57b26a09518630b882f67f1b08d841979d\",\"blockHeight\":723229,\"confirmations\":321,\"minerId\":null,\"txSecondMempoolExpiry\":0}", + Signature: nil, // NOTE: missing from mempool response + PublicKey: nil, // NOTE: missing from mempool response + Encoding: utf8Type, + MimeType: applicationJSONType, + }, + }, + Query: &minercraft.QueryTxResponse{ + APIVersion: "", // NOTE: missing from mempool response + Timestamp: "2022-01-23T19:51:10.046Z", + TxID: onChainExample1TxID, + ReturnResult: mAPISuccess, + ResultDescription: "", + BlockHash: onChainExample1BlockHash, + BlockHeight: onChainExample1BlockHeight, + MinerID: "", // NOTE: missing from mempool response + Confirmations: onChainExample1Confirmations, + TxSecondMempoolExpiry: 0, + }, + }, nil + } + + return nil, nil +} + +type minerCraftBroadcastSuccess struct { + MinerCraftBase +} + +// SubmitTransaction mocks the SubmitTransaction method of the minercraft API. +func (m *minerCraftBroadcastSuccess) SubmitTransaction(_ context.Context, miner *minercraft.Miner, + _ *minercraft.Transaction, +) (*minercraft.SubmitTransactionResponse, error) { + if miner.Name == minercraft.MinerTaal { + sig := "30440220268ad023bbe03c62a953f907f81c01754f34ffe4822bb9e89c5245613bda7b7602204c201e56b27fd044b3f8ad77ec2c24dc2b9571166a9a998c256d3cbf598fbbda" + pubKey := miner.MinerID + return &minercraft.SubmitTransactionResponse{ + JSONEnvelope: minercraft.JSONEnvelope{ + Miner: miner, + Validated: true, + JSONEnvelope: envelope.JSONEnvelope{ + Payload: "{\"apiVersion\":\"1.4.0\",\"timestamp\":\"2022-02-02T12:12:02.6089293Z\",\"txid\":\"15d31d00ed7533a83d7ab206115d7642812ec04a2cbae4248365febb82576ff3\",\"returnResult\":\"success\",\"resultDescription\":\"\",\"minerId\":\"030d1fe5c1b560efe196ba40540ce9017c20daa9504c4c4cec6184fc702d9f274e\",\"currentHighestBlockHash\":\"000000000000000006e6745f6a57a1da8096faf9f71dd59b2bab3f2b0219b7a0\",\"currentHighestBlockHeight\":724922,\"txSecondMempoolExpiry\":0}", + Signature: &sig, + PublicKey: &pubKey, + Encoding: utf8Type, + MimeType: applicationJSONType, + }, + }, + Results: &minercraft.UnifiedSubmissionPayload{ + APIVersion: "1.4.0", + CurrentHighestBlockHash: "000000000000000006e6745f6a57a1da8096faf9f71dd59b2bab3f2b0219b7a0", + CurrentHighestBlockHeight: 724922, + MinerID: miner.MinerID, + ResultDescription: "", + ReturnResult: mAPISuccess, + Timestamp: "2022-02-02T12:12:02.6089293Z", + TxID: broadcastExample1TxID, + }, + }, 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-02T12:12:02.6089293Z\",\"txid\":\"15d31d00ed7533a83d7ab206115d7642812ec04a2cbae4248365febb82576ff3\",\"returnResult\":\"success\",\"resultDescription\":\"\",\"minerId\":\"03e92d3e5c3f7bd945dfbf48e7a99393b1bfb3f11f380ae30d286e7ff2aec5a270\",\"currentHighestBlockHash\":\"000000000000000006e6745f6a57a1da8096faf9f71dd59b2bab3f2b0219b7a0\",\"currentHighestBlockHeight\":724922,\"txSecondMempoolExpiry\":0}", + Encoding: utf8Type, + MimeType: applicationJSONType, + }, + }, + Results: &minercraft.UnifiedSubmissionPayload{ + APIVersion: "", + CurrentHighestBlockHash: "000000000000000006e6745f6a57a1da8096faf9f71dd59b2bab3f2b0219b7a0", + CurrentHighestBlockHeight: 724922, + MinerID: miner.MinerID, + ResultDescription: "", + ReturnResult: mAPISuccess, + Timestamp: "2022-02-01T17:47:52.518Z", + TxID: broadcastExample1TxID, + }, + }, 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-02T12:12:02.6089293Z\",\"txid\":\"\",\"returnResult\":\"success\",\"resultDescription\":\"\",\"minerId\":\"0253a9b2d017254b91704ba52aad0df5ca32b4fb5cb6b267ada6aefa2bc5833a93\",\"currentHighestBlockHash\":\"000000000000000006e6745f6a57a1da8096faf9f71dd59b2bab3f2b0219b7a0\",\"currentHighestBlockHeight\":724922,\"txSecondMempoolExpiry\":0}", + Signature: &sig, + PublicKey: &pubKey, + Encoding: utf8Type, + MimeType: applicationJSONType, + }, + }, + Results: &minercraft.UnifiedSubmissionPayload{ + APIVersion: "1.1.0-1-g35ba2d3", + CurrentHighestBlockHash: "000000000000000006e6745f6a57a1da8096faf9f71dd59b2bab3f2b0219b7a0", + CurrentHighestBlockHeight: 724922, + MinerID: miner.MinerID, + ResultDescription: "", + ReturnResult: mAPISuccess, + Timestamp: "2022-02-02T12:12:02.6089293Z", + TxID: broadcastExample1TxID, + }, + }, 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-02T12:12:02.6089293Z\",\"txid\":\"\",\"returnResult\":\"success\",\"resultDescription\":\"\",\"minerId\":\"03ad780153c47df915b3d2e23af727c68facaca4facd5f155bf5018b979b9aeb83\",\"currentHighestBlockHash\":\"000000000000000006e6745f6a57a1da8096faf9f71dd59b2bab3f2b0219b7a0\",\"currentHighestBlockHeight\":724922,\"txSecondMempoolExpiry\":0}", + Signature: &sig, + PublicKey: &pubKey, + Encoding: utf8Type, + MimeType: applicationJSONType, + }, + }, + Results: &minercraft.UnifiedSubmissionPayload{ + APIVersion: "", + CurrentHighestBlockHash: "000000000000000006e6745f6a57a1da8096faf9f71dd59b2bab3f2b0219b7a0", + CurrentHighestBlockHeight: 724922, + MinerID: miner.MinerID, + ResultDescription: "", + ReturnResult: mAPISuccess, + Timestamp: "2022-02-02T12:12:02.6089293Z", + TxID: broadcastExample1TxID, + }, + }, nil + } + + return nil, errors.New("missing miner response") +} + +type minerCraftUnreachable struct { + MinerCraftBase +} + +// FeeQuote returns an error. +func (m *minerCraftUnreachable) FeeQuote(context.Context, *minercraft.Miner) (*minercraft.FeeQuoteResponse, error) { + return nil, errors.New("minercraft is unreachable") +} diff --git a/engine/chainstate/network.go b/engine/chainstate/network.go new file mode 100644 index 000000000..d6f13ac6b --- /dev/null +++ b/engine/chainstate/network.go @@ -0,0 +1,30 @@ +package chainstate + +// Network is the supported Bitcoin networks +type Network string + +// Supported networks +const ( + MainNet Network = mainNet // Main public network + StressTestNet Network = stn // Stress Test Network (https://bitcoinscaling.io/) + TestNet Network = testNet // Test public network +) + +// String is the string version of network +func (n Network) String() string { + return string(n) +} + +// Alternate is the alternate string version +func (n Network) Alternate() string { + switch n { + case MainNet: + return mainNetAlt + case TestNet: + return testNetAlt + case StressTestNet: + return stn + default: + return "" + } +} diff --git a/engine/chainstate/network_test.go b/engine/chainstate/network_test.go new file mode 100644 index 000000000..14d2640a9 --- /dev/null +++ b/engine/chainstate/network_test.go @@ -0,0 +1,39 @@ +package chainstate + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestNetwork_String will test the method String() +func TestNetwork_String(t *testing.T) { + t.Parallel() + + t.Run("test all networks", func(t *testing.T) { + assert.Equal(t, mainNet, MainNet.String()) + assert.Equal(t, stn, StressTestNet.String()) + assert.Equal(t, testNet, TestNet.String()) + }) + + t.Run("unknown network", func(t *testing.T) { + un := Network("") + assert.Equal(t, "", un.String()) + }) +} + +// TestNetwork_Alternate will test the method Alternate() +func TestNetwork_Alternate(t *testing.T) { + t.Parallel() + + t.Run("test all networks", func(t *testing.T) { + assert.Equal(t, mainNetAlt, MainNet.Alternate()) + assert.Equal(t, stn, StressTestNet.Alternate()) + assert.Equal(t, testNetAlt, TestNet.Alternate()) + }) + + t.Run("unknown network", func(t *testing.T) { + un := Network("") + assert.Equal(t, "", un.Alternate()) + }) +} diff --git a/engine/chainstate/requirements.go b/engine/chainstate/requirements.go new file mode 100644 index 000000000..e2f63d588 --- /dev/null +++ b/engine/chainstate/requirements.go @@ -0,0 +1,38 @@ +package chainstate + +// RequiredIn is the requirements for querying transaction information +type RequiredIn string + +const ( + // RequiredInMempool is the transaction in mempool? (minimum requirement for a valid response) + RequiredInMempool RequiredIn = requiredInMempool + + // RequiredOnChain is the transaction in on-chain? (minimum requirement for a valid response) + RequiredOnChain RequiredIn = requiredOnChain +) + +// ValidRequirement will return valid if the requirement is known +func (c *Client) validRequirement(requirement RequiredIn) bool { + return requirement == RequiredOnChain || requirement == RequiredInMempool +} + +func checkRequirement(requirement RequiredIn, id string, txInfo *TransactionInfo, onChainCondition bool) bool { + switch requirement { + case RequiredInMempool: + return txInfo.ID == id + case RequiredOnChain: + return onChainCondition + default: + 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/engine/chainstate/requirements_test.go b/engine/chainstate/requirements_test.go new file mode 100644 index 000000000..3ca33adfa --- /dev/null +++ b/engine/chainstate/requirements_test.go @@ -0,0 +1,83 @@ +package chainstate + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_checkRequirement(t *testing.T) { + t.Parallel() + + t.Run("found in mempool - mAPI", func(t *testing.T) { + success := checkRequirementMapi(requiredInMempool, onChainExample1TxID, &TransactionInfo{ + BlockHash: "", + BlockHeight: 0, + Confirmations: 0, + ID: onChainExample1TxID, + MinerID: minerTaal.MinerID, + Provider: "", + }) + assert.Equal(t, true, success) + }) + + t.Run("found in mempool - on-chain - mAPI", func(t *testing.T) { + success := checkRequirementMapi(requiredInMempool, onChainExample1TxID, &TransactionInfo{ + BlockHash: onChainExample1BlockHash, + BlockHeight: onChainExample1BlockHeight, + Confirmations: 1, + ID: onChainExample1TxID, + MinerID: minerTaal.MinerID, + Provider: "", + }) + assert.Equal(t, true, success) + }) + + t.Run("found in mempool - whatsonchain", func(t *testing.T) { + success := checkRequirementMapi(requiredInMempool, onChainExample1TxID, &TransactionInfo{ + BlockHash: "", + BlockHeight: 0, + Confirmations: 0, + ID: onChainExample1TxID, + MinerID: "", + Provider: "whatsonchain", + }) + assert.Equal(t, true, success) + }) + + t.Run("not in mempool - mAPI", func(t *testing.T) { + success := checkRequirementMapi(requiredInMempool, onChainExample1TxID, &TransactionInfo{ + BlockHash: "", + BlockHeight: 0, + Confirmations: 0, + ID: "", + MinerID: minerTaal.MinerID, + Provider: "", + }) + assert.Equal(t, false, success) + }) + + t.Run("found on chain - mAPI", func(t *testing.T) { + success := checkRequirementMapi(requiredOnChain, onChainExample1TxID, &TransactionInfo{ + BlockHash: onChainExample1BlockHash, + BlockHeight: onChainExample1BlockHeight, + Confirmations: 1, + ID: onChainExample1TxID, + MinerID: minerTaal.MinerID, + Provider: "", + }) + assert.Equal(t, true, success) + }) + + t.Run("not on chain - mAPI", func(t *testing.T) { + success := checkRequirementMapi(requiredOnChain, onChainExample1TxID, &TransactionInfo{ + BlockHash: "", + BlockHeight: 0, + Confirmations: 0, + ID: onChainExample1TxID, + MinerID: minerTaal.MinerID, + Provider: "", + }) + assert.Equal(t, false, success) + }) +} diff --git a/engine/chainstate/transaction.go b/engine/chainstate/transaction.go new file mode 100644 index 000000000..4f018ed14 --- /dev/null +++ b/engine/chainstate/transaction.go @@ -0,0 +1,145 @@ +package chainstate + +import ( + "context" + "strings" + "sync" + "time" + + "github.com/libsv/go-bc" + "github.com/tonicpow/go-minercraft/v2" +) + +// query will try ALL providers in order and return the first "valid" response based on requirements +func (c *Client) query(ctx context.Context, id string, requiredIn RequiredIn, + timeout time.Duration, +) *TransactionInfo { + // Create a context (to cancel or timeout) + ctxWithCancel, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + 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 + } + } + } + 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") + } + return nil // No transaction information found +} + +// fastestQuery will try ALL providers on once and return the fastest "valid" response based on requirements +func (c *Client) fastestQuery(ctx context.Context, id string, requiredIn RequiredIn, + timeout time.Duration, +) *TransactionInfo { + // The channel for the internal results + resultsChannel := make( + chan *TransactionInfo, + // len(c.options.config.mAPI.queryMiners)+2, + ) // All miners & WhatsOnChain + + // Create a context (to cancel or timeout) + ctxWithCancel, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + // Loop each miner (break into a Go routine for each query) + var wg sync.WaitGroup + + switch c.ActiveProvider() { + case ProviderMinercraft: + 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 && checkRequirementMapi(requiredIn, id, res) { + resultsChannel <- res + } + }(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 + go func() { + wg.Wait() + close(resultsChannel) + }() + + return <-resultsChannel +} + +// queryMinercraft will submit a query transaction request to a miner using Minercraft(mAPI or Arc) +func queryMinercraft(ctx context.Context, client ClientInterface, miner *minercraft.Miner, id string) (*TransactionInfo, error) { + client.DebugLog("executing request in minercraft using miner: " + miner.Name) + if resp, err := client.Minercraft().QueryTransaction(ctx, miner, id, minercraft.WithQueryMerkleProof()); err != nil { + client.DebugLog("error executing request in minercraft using miner: " + miner.Name + " failed: " + err.Error()) + return nil, err + } else if resp != nil && resp.Query.ReturnResult == mAPISuccess && strings.EqualFold(resp.Query.TxID, id) { + return &TransactionInfo{ + BlockHash: resp.Query.BlockHash, + BlockHeight: resp.Query.BlockHeight, + Confirmations: resp.Query.Confirmations, + ID: resp.Query.TxID, + MinerID: resp.Query.MinerID, + Provider: miner.Name, + MerkleProof: resp.Query.MerkleProof, + }, nil + } + return nil, ErrTransactionIDMismatch +} + +// queryBroadcastClient will submit a query transaction request to a go-broadcast-client +func queryBroadcastClient(ctx context.Context, client ClientInterface, id string) (*TransactionInfo, error) { + client.DebugLog("executing request using " + ProviderBroadcastClient) + if resp, err := client.BroadcastClient().QueryTransaction(ctx, id); err != nil { + 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/engine/chainstate/transaction_info.go b/engine/chainstate/transaction_info.go new file mode 100644 index 000000000..751dd0955 --- /dev/null +++ b/engine/chainstate/transaction_info.go @@ -0,0 +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 + 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 BUMP (from Arc) +func (t *TransactionInfo) Valid() bool { + 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/engine/chainstate/transaction_test.go b/engine/chainstate/transaction_test.go new file mode 100644 index 000000000..c356d93c5 --- /dev/null +++ b/engine/chainstate/transaction_test.go @@ -0,0 +1,321 @@ +package chainstate + +import ( + "context" + "testing" + + broadcast_client_mock "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client-mock" + broadcast_fixtures "github.com/bitcoin-sv/go-broadcast-client/broadcast/broadcast-client-mock/fixtures" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestClient_Transaction will test the method QueryTransaction() +func TestClient_Transaction(t *testing.T) { + t.Parallel() + + t.Run("error - missing id", func(t *testing.T) { + // given + c := NewTestClient(context.Background(), t, WithMinercraft(&minerCraftTxOnChain{})) + + // when + info, err := c.QueryTransaction( + 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(&minerCraftTxOnChain{})) + + // when + info, err := c.QueryTransaction( + context.Background(), onChainExample1TxID, + "", defaultQueryTimeOut, + ) + + // then + require.Error(t, err) + require.Nil(t, info) + assert.ErrorIs(t, err, ErrInvalidRequirements) + }) +} + +func TestClient_Transaction_MAPI(t *testing.T) { + t.Parallel() + + t.Run("query transaction success - mAPI", func(t *testing.T) { + // given + c := NewTestClient( + context.Background(), t, + WithMinercraft(&minerCraftTxOnChain{}), + ) + + // when + info, err := c.QueryTransaction( + 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) + assert.Equal(t, minerTaal.Name, info.Provider) + assert.Equal(t, minerTaal.MinerID, info.MinerID) + }) + + t.Run("valid - test network - mAPI", func(t *testing.T) { + // given + c := NewTestClient( + context.Background(), t, + WithMinercraft(&minerCraftTxOnChain{}), + WithNetwork(TestNet), + ) + + // when + info, err := c.QueryTransaction( + 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) + assert.Equal(t, minerTaal.Name, info.Provider) + assert.Equal(t, minerTaal.MinerID, info.MinerID) + }) +} + +func TestClient_Transaction_BroadcastClient(t *testing.T) { + t.Parallel() + + 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(&MinerCraftBase{}), + WithBroadcastClient(bc), + ) + + // when + info, err := c.QueryTransaction( + context.Background(), onChainExampleArcTxID, + RequiredInMempool, defaultQueryTimeOut, + ) + + // then + require.NoError(t, err) + require.NotNil(t, info) + 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) + }) + + 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(&MinerCraftBase{}), + WithBroadcastClient(bc), + WithNetwork(StressTestNet), + ) + + // when + info, err := c.QueryTransaction( + context.Background(), onChainExampleArcTxID, + RequiredInMempool, defaultQueryTimeOut, + ) + + // then + require.NoError(t, err) + require.NotNil(t, info) + 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) + }) + + 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(&MinerCraftBase{}), + WithBroadcastClient(bc), + WithNetwork(TestNet), + ) + + // when + info, err := c.QueryTransaction( + context.Background(), onChainExampleArcTxID, + RequiredInMempool, defaultQueryTimeOut, + ) + + // then + require.NoError(t, err) + require.NotNil(t, info) + 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_MAPI_Fastest(t *testing.T) { + t.Parallel() + + t.Run("query transaction success - mAPI", func(t *testing.T) { + // given + c := NewTestClient( + context.Background(), t, + 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) + assert.Equal(t, minerTaal.Name, info.Provider) + assert.Equal(t, minerTaal.MinerID, info.MinerID) + }) + + t.Run("valid - test network - mAPI", func(t *testing.T) { + // given + c := NewTestClient( + context.Background(), t, + WithMinercraft(&minerCraftTxOnChain{}), + WithNetwork(TestNet), + ) + + // 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) + assert.Equal(t, minerTaal.Name, info.Provider) + assert.Equal(t, minerTaal.MinerID, info.MinerID) + }) +} + +func TestClient_Transaction_BroadcastClient_Fastest(t *testing.T) { + t.Parallel() + + 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(&MinerCraftBase{}), + WithBroadcastClient(bc), + ) + + // when + info, err := c.QueryTransactionFastest( + 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("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(&MinerCraftBase{}), + WithBroadcastClient(bc), + WithNetwork(StressTestNet), + ) + + // when + info, err := c.QueryTransactionFastest( + 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("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(&MinerCraftBase{}), + WithBroadcastClient(bc), + WithNetwork(TestNet), + ) + + // 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) + }) +} diff --git a/engine/chainstate/types.go b/engine/chainstate/types.go new file mode 100644 index 000000000..234658efa --- /dev/null +++ b/engine/chainstate/types.go @@ -0,0 +1,19 @@ +package chainstate + +// TransactionType tx types +type TransactionType string + +// Metanet type +const Metanet TransactionType = "metanet" + +// PubKeyHash type +const PubKeyHash TransactionType = "pubkeyhash" + +// PlanariaB type +const PlanariaB TransactionType = "planaria-b" + +// PlanariaD type +const PlanariaD TransactionType = "planaria-d" + +// RareCandyFrogCartel type +const RareCandyFrogCartel TransactionType = "rarecandy-frogcartel" diff --git a/engine/client.go b/engine/client.go new file mode 100644 index 000000000..9fbe7e11f --- /dev/null +++ b/engine/client.go @@ -0,0 +1,425 @@ +package engine + +import ( + "context" + "time" + + "github.com/bitcoin-sv/go-paymail" + "github.com/bitcoin-sv/go-paymail/server" + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" + "github.com/bitcoin-sv/spv-wallet/engine/cluster" + "github.com/bitcoin-sv/spv-wallet/engine/logging" + "github.com/bitcoin-sv/spv-wallet/engine/metrics" + "github.com/bitcoin-sv/spv-wallet/engine/notifications" + "github.com/bitcoin-sv/spv-wallet/engine/taskmanager" + "github.com/mrz1836/go-cachestore" + "github.com/mrz1836/go-datastore" + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/rs/zerolog" +) + +type ( + + // Client is the spv wallet engine client & options + Client struct { + options *clientOptions + } + + // clientOptions holds all the configuration for the client + clientOptions struct { + cacheStore *cacheStoreOptions // Configuration options for Cachestore (ristretto, redis, etc.) + cluster *clusterOptions // Configuration options for the cluster coordinator + chainstate *chainstateOptions // Configuration options for Chainstate (broadcast, sync, etc.) + dataStore *dataStoreOptions // Configuration options for the DataStore (MySQL, etc.) + debug bool // If the client is in debug mode + encryptionKey string // Encryption key for encrypting sensitive information (IE: paymail xPub) (hex encoded key) + httpClient HTTPInterface // HTTP interface to use + iuc bool // (Input UTXO Check) True will check input utxos when saving transactions + logger *zerolog.Logger // Internal logging + metrics *metrics.Metrics // Metrics with a collector interface + models *modelOptions // Configuration options for the loaded models + newRelic *newRelicOptions // Configuration options for NewRelic + notifications *notificationsOptions // Configuration options for Notifications + paymail *paymailOptions // Paymail options & client + taskManager *taskManagerOptions // Configuration options for the TaskManager (TaskQ, etc.) + userAgent string // User agent for all outgoing requests + } + + // chainstateOptions holds the chainstate configuration and client + chainstateOptions struct { + chainstate.ClientInterface // Client for Chainstate + options []chainstate.ClientOps // List of options + broadcasting bool // Default value for all transactions + broadcastInstant bool // Default value for all transactions + paymailP2P bool // Default value for all transactions + syncOnChain bool // Default value for all transactions + } + + // cacheStoreOptions holds the cache configuration and client + cacheStoreOptions struct { + cachestore.ClientInterface // Client for Cachestore + options []cachestore.ClientOps // List of options + } + + // clusterOptions holds the cluster configuration for spv wallet engine clusters + // at the moment we only support redis as the cluster coordinator + clusterOptions struct { + cluster.ClientInterface + options []cluster.ClientOps // List of options + } + + // dataStoreOptions holds the data storage configuration and client + dataStoreOptions struct { + datastore.ClientInterface // Client for Datastore + migrationDisabled bool // If the migrations are disabled + options []datastore.ClientOps // List of options + } + + // modelOptions holds the model configuration + modelOptions struct { + migrateModelNames []string // List of models for migration + migrateModels []interface{} // Models for migrations + modelNames []string // List of all models + models []interface{} // Models for use in this engine + } + + // newRelicOptions holds the configuration for NewRelic + newRelicOptions struct { + app *newrelic.Application // NewRelic client application (if enabled) + enabled bool // If NewRelic is enabled for deep Transaction tracing + } + + // notificationsOptions holds the configuration for notifications + notificationsOptions struct { + notifications.ClientInterface // Notifications client + options []notifications.ClientOps // List of options + webhookEndpoint string // Webhook endpoint + } + + // paymailOptions holds the configuration for Paymail + paymailOptions struct { + client paymail.ClientInterface // Paymail client for communicating with Paymail providers + serverConfig *PaymailServerOptions // Server configuration if Paymail is enabled + } + + // PaymailServerOptions is the options for the Paymail server + PaymailServerOptions struct { + *server.Configuration // Server configuration if Paymail is enabled + options []server.ConfigOps // Options for the paymail server + DefaultFromPaymail string // IE: from@domain.com + } + + // taskManagerOptions holds the configuration for taskmanager + taskManagerOptions struct { + taskmanager.TaskEngine // Client for TaskManager + options []taskmanager.TaskManagerOptions // List of options + cronCustomPeriods map[string]time.Duration // will override the default period of cronJob + } +) + +// NewClient creates a new client for all spv wallet engine functionality +// +// If no options are given, it will use the defaultClientOptions() +// ctx may contain a NewRelic txn (or one will be created) +func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error) { + // Create a new client with defaults + client := &Client{options: defaultClientOptions()} + + // Overwrite defaults with any custom options provided by the user + for _, opt := range opts { + opt(client.options) + } + + // Use NewRelic if it's enabled (use existing txn if found on ctx) + ctx = client.GetOrStartTxn(ctx, "new_client") + + // Set the logger (if no custom logger was detected) + if client.options.logger == nil { + client.options.logger = logging.GetDefaultLogger() + } + + // Load the Cachestore client + var err error + if err = client.loadCache(ctx); err != nil { + return nil, err + } + + // Load the cluster coordinator + if err = client.loadCluster(ctx); err != nil { + return nil, err + } + + // Load the Datastore (automatically migrate models) + if err = client.loadDatastore(ctx); err != nil { + return nil, err + } + + // Run custom model datastore migrations (after initializing models) + if err = client.runModelMigrations( + client.options.models.migrateModels..., + ); err != nil { + return nil, err + } + + // Load the Chainstate client + if err = client.loadChainstate(ctx); err != nil { + return nil, err + } + + // Load the Paymail client (if client does not exist) + if err = client.loadPaymailClient(); err != nil { + return nil, err + } + + // Load the Notification client (if client does not exist) + if err = client.loadNotificationClient(); err != nil { + return nil, err + } + + // Load the Taskmanager (automatically start consumers and tasks) + if err = client.loadTaskmanager(ctx); err != nil { + return nil, err + } + + // Register all cron jobs + if err = client.registerCronJobs(); err != nil { + return nil, err + } + + // Default paymail server config (generic capabilities and domain check disabled) + if client.options.paymail.serverConfig.Configuration == nil { + if err = client.loadDefaultPaymailConfig(); err != nil { + return nil, err + } + } + + // Return the client + return client, nil +} + +// AddModels will add additional models to the client +func (c *Client) AddModels(ctx context.Context, autoMigrate bool, models ...interface{}) error { + // Store the models locally in the client + c.options.addModels(modelList, models...) + + // Should we migrate the models? + if autoMigrate { + + // Ensure we have a datastore + d := c.Datastore() + if d == nil { + return ErrDatastoreRequired + } + + // Apply the database migration with the new models + if err := d.AutoMigrateDatabase(ctx, models...); err != nil { + return err + } + + // Add to the list + c.options.addModels(migrateList, models...) + + // Run model migrations + if err := c.runModelMigrations(models...); err != nil { + return err + } + } + + return nil +} + +// Cachestore will return the Cachestore IF: exists and is enabled +func (c *Client) Cachestore() cachestore.ClientInterface { + if c.options.cacheStore != nil && c.options.cacheStore.ClientInterface != nil { + return c.options.cacheStore.ClientInterface + } + return nil +} + +// Cluster will return the cluster coordinator client +func (c *Client) Cluster() cluster.ClientInterface { + if c.options.cluster != nil && c.options.cluster.ClientInterface != nil { + return c.options.cluster.ClientInterface + } + return nil +} + +// Chainstate will return the Chainstate service IF: exists and is enabled +func (c *Client) Chainstate() chainstate.ClientInterface { + if c.options.chainstate != nil && c.options.chainstate.ClientInterface != nil { + return c.options.chainstate.ClientInterface + } + return nil +} + +// Close will safely close any open connections (cache, datastore, etc.) +func (c *Client) Close(ctx context.Context) error { + if txn := newrelic.FromContext(ctx); txn != nil { + defer txn.StartSegment("close_all").End() + } + + // Close Chainstate + ch := c.Chainstate() + if ch != nil { + ch.Close(ctx) + c.options.chainstate.ClientInterface = nil + } + + // Close Datastore + ds := c.Datastore() + if ds != nil { + if err := ds.Close(ctx); err != nil { + return err + } + c.options.dataStore.ClientInterface = nil + } + + // Close Taskmanager + tm := c.Taskmanager() + if tm != nil { + if err := tm.Close(ctx); err != nil { + return err + } + c.options.taskManager.TaskEngine = nil + } + return nil +} + +// Datastore will return the Datastore if it exists +func (c *Client) Datastore() datastore.ClientInterface { + if c.options.dataStore != nil && c.options.dataStore.ClientInterface != nil { + return c.options.dataStore.ClientInterface + } + return nil +} + +// Debug will toggle the debug mode (for all resources) +func (c *Client) Debug(on bool) { + // Set the flag on the current client + c.options.debug = on + + // Set debugging on the Cachestore + if cs := c.Cachestore(); cs != nil { + cs.Debug(on) + } + + // Set debugging on the Chainstate + if ch := c.Chainstate(); ch != nil { + ch.Debug(on) + } + + // Set debugging on the Datastore + if ds := c.Datastore(); ds != nil { + ds.Debug(on) + } + + // Set debugging on the Notifications + if n := c.Notifications(); n != nil { + n.Debug(on) + } +} + +// DefaultSyncConfig will return the default sync config from the client defaults (for chainstate) +func (c *Client) DefaultSyncConfig() *SyncConfig { + return &SyncConfig{ + Broadcast: c.options.chainstate.broadcasting, + BroadcastInstant: c.options.chainstate.broadcastInstant, + PaymailP2P: c.options.chainstate.paymailP2P, + SyncOnChain: c.options.chainstate.syncOnChain, + } +} + +// EnableNewRelic will enable NewRelic tracing +func (c *Client) EnableNewRelic() { + if c.options.newRelic != nil && c.options.newRelic.app != nil { + c.options.newRelic.enabled = true + } +} + +// GetOrStartTxn will check for an existing NewRelic transaction, if not found, it will make a new transaction +func (c *Client) GetOrStartTxn(ctx context.Context, name string) context.Context { + if c.IsNewRelicEnabled() && c.options.newRelic.app != nil { + txn := newrelic.FromContext(ctx) + if txn == nil { + txn = c.options.newRelic.app.StartTransaction(name) + } + ctx = newrelic.NewContext(ctx, txn) + } + return ctx +} + +// GetModelNames will return the model names that have been loaded +func (c *Client) GetModelNames() []string { + return c.options.models.modelNames +} + +// HTTPClient will return the http interface to use in the client +func (c *Client) HTTPClient() HTTPInterface { + return c.options.httpClient +} + +// IsDebug will return the debug flag (bool) +func (c *Client) IsDebug() bool { + return c.options.debug +} + +// IsNewRelicEnabled will return the flag (bool) +func (c *Client) IsNewRelicEnabled() bool { + return c.options.newRelic.enabled +} + +// IsIUCEnabled will return the flag (bool) +func (c *Client) IsIUCEnabled() bool { + return c.options.iuc +} + +// IsEncryptionKeySet will return the flag (bool) if the encryption key has been set +func (c *Client) IsEncryptionKeySet() bool { + return len(c.options.encryptionKey) > 0 +} + +// IsMigrationEnabled will return the flag (bool) +func (c *Client) IsMigrationEnabled() bool { + return !c.options.dataStore.migrationDisabled +} + +// Logger will return the Logger if it exists +func (c *Client) Logger() *zerolog.Logger { + return c.options.logger +} + +// Notifications will return the Notifications if it exists +func (c *Client) Notifications() notifications.ClientInterface { + if c.options.notifications != nil && c.options.notifications.ClientInterface != nil { + return c.options.notifications.ClientInterface + } + return nil +} + +// SetNotificationsClient will overwrite the notification's client with the given client +func (c *Client) SetNotificationsClient(client notifications.ClientInterface) { + c.options.notifications.ClientInterface = client +} + +// Taskmanager will return the Taskmanager if it exists +func (c *Client) Taskmanager() taskmanager.TaskEngine { + if c.options.taskManager != nil && c.options.taskManager.TaskEngine != nil { + return c.options.taskManager.TaskEngine + } + return nil +} + +// UserAgent will return the user agent +func (c *Client) UserAgent() string { + return c.options.userAgent +} + +// Version will return the version +func (c *Client) Version() string { + return version +} + +// Metrics will return the metrics client (if it's enabled) +func (c *Client) Metrics() (metrics *metrics.Metrics, enabled bool) { + return c.options.metrics, c.options.metrics != nil +} diff --git a/engine/client_datastore.go b/engine/client_datastore.go new file mode 100644 index 000000000..abb7e1435 --- /dev/null +++ b/engine/client_datastore.go @@ -0,0 +1,161 @@ +package engine + +import ( + "encoding/json" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/x/bsonx" +) + +const ( + conditionAnd = "$and" +) + +// processCustomFields will process all custom fields +func processCustomFields(conditions *map[string]interface{}) { + // Process the xpub_output_value + _, ok := (*conditions)["xpub_output_value"] + if ok { + processXpubOutputValueConditions(conditions) + } + + // Process the xpub_output_value + _, ok = (*conditions)["xpub_metadata"] + if ok { + processXpubMetadataConditions(conditions) + } +} + +// processXpubOutputValueConditions will process xpub_output_value +func processXpubOutputValueConditions(conditions *map[string]interface{}) { + m, _ := json.Marshal((*conditions)["xpub_output_value"]) //nolint:errchkjson // this check might break the current code + var r map[string]interface{} + _ = json.Unmarshal(m, &r) + + xPubOutputValue := make([]map[string]interface{}, 0) + for xPub, value := range r { + outputKey := "xpub_output_value." + xPub + xPubOutputValue = append(xPubOutputValue, map[string]interface{}{ + outputKey: value, + }) + } + if len(xPubOutputValue) > 0 { + _, ok := (*conditions)[conditionAnd] + if ok { + and := (*conditions)[conditionAnd].([]map[string]interface{}) + and = append(and, xPubOutputValue...) + (*conditions)[conditionAnd] = and + } else { + (*conditions)[conditionAnd] = xPubOutputValue + } + } + + delete(*conditions, "xpub_output_value") +} + +// processXpubMetadataConditions will process xpub_metadata +func processXpubMetadataConditions(conditions *map[string]interface{}) { + // marshal / unmarshal into standard map[string]interface{} + m, _ := json.Marshal((*conditions)["xpub_metadata"]) //nolint:errchkjson // this check might break the current code + var r map[string]interface{} + _ = json.Unmarshal(m, &r) + + for xPub, xr := range r { + xPubMetadata := make([]map[string]interface{}, 0) + for key, value := range xr.(map[string]interface{}) { + xPubMetadata = append(xPubMetadata, map[string]interface{}{ + "xpub_metadata.x": xPub, + "xpub_metadata.k": key, + "xpub_metadata.v": value, + }) + } + if len(xPubMetadata) > 0 { + _, ok := (*conditions)[conditionAnd] + if ok { + and := (*conditions)[conditionAnd].([]map[string]interface{}) + and = append(and, xPubMetadata...) + (*conditions)[conditionAnd] = and + } else { + (*conditions)[conditionAnd] = xPubMetadata + } + } + } + delete(*conditions, "xpub_metadata") +} + +// getMongoIndexes will get indexes from mongo +func getMongoIndexes() map[string][]mongo.IndexModel { + return map[string][]mongo.IndexModel{ + "block_headers": { + mongo.IndexModel{Keys: bsonx.Doc{{ + Key: "height", + Value: bsonx.Int32(1), + }}}, + mongo.IndexModel{Keys: bsonx.Doc{{ + Key: "synced", + Value: bsonx.Int32(1), + }}}, + }, + "destinations": { + mongo.IndexModel{Keys: bsonx.Doc{{ + Key: "address", + Value: bsonx.Int32(1), + }}}, + }, + "draft_transactions": { + mongo.IndexModel{Keys: bsonx.Doc{{ + Key: "status", + Value: bsonx.Int32(1), + }}}, + mongo.IndexModel{Keys: bsonx.Doc{{ + Key: "xpub_id", + Value: bsonx.Int32(1), + }, { + Key: "status", + Value: bsonx.Int32(1), + }}}, + }, + "transactions": { + mongo.IndexModel{Keys: bsonx.Doc{{ + Key: "xpub_metadata.x", + Value: bsonx.Int32(1), + }, { + Key: "xpub_metadata.k", + Value: bsonx.Int32(1), + }, { + Key: "xpub_metadata.v", + Value: bsonx.Int32(1), + }}}, + mongo.IndexModel{Keys: bsonx.Doc{{ + Key: "xpub_in_ids", + Value: bsonx.Int32(1), + }}}, + mongo.IndexModel{Keys: bsonx.Doc{{ + Key: "xpub_out_ids", + Value: bsonx.Int32(1), + }}}, + }, + "utxos": { + mongo.IndexModel{Keys: bsonx.Doc{{ + Key: "transaction_id", + Value: bsonx.Int32(1), + }, { + Key: "output_index", + Value: bsonx.Int32(1), + }}}, + mongo.IndexModel{Keys: bsonx.Doc{{ + Key: "xpub_id", + Value: bsonx.Int32(1), + }, { + Key: "type", + Value: bsonx.Int32(1), + }, { + Key: "draft_id", + Value: bsonx.Int32(1), + }, { + Key: "spending_tx_id", + Value: bsonx.Int32(1), + }}}, + }, + } +} diff --git a/engine/client_internal.go b/engine/client_internal.go new file mode 100644 index 000000000..978db1703 --- /dev/null +++ b/engine/client_internal.go @@ -0,0 +1,183 @@ +package engine + +import ( + "context" + + "github.com/bitcoin-sv/go-paymail" + "github.com/bitcoin-sv/go-paymail/server" + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" + "github.com/bitcoin-sv/spv-wallet/engine/cluster" + "github.com/bitcoin-sv/spv-wallet/engine/notifications" + "github.com/bitcoin-sv/spv-wallet/engine/taskmanager" + "github.com/mrz1836/go-cachestore" + "github.com/mrz1836/go-datastore" +) + +// loadCache will load caching configuration and start the Cachestore client +func (c *Client) loadCache(ctx context.Context) (err error) { + // Load if a custom interface was NOT provided + if c.options.cacheStore.ClientInterface == nil { + c.options.cacheStore.ClientInterface, err = cachestore.NewClient(ctx, c.options.cacheStore.options...) + } + return +} + +// loadCluster will load the cluster coordinator +func (c *Client) loadCluster(ctx context.Context) (err error) { + // Load if a custom interface was NOT provided + if c.options.cluster.ClientInterface == nil { + c.options.cluster.ClientInterface, err = cluster.NewClient(ctx, c.options.cluster.options...) + } + + return +} + +// loadChainstate will load chainstate configuration and start the Chainstate client +func (c *Client) loadChainstate(ctx context.Context) (err error) { + // Load chainstate if a custom interface was NOT provided + if c.options.chainstate.ClientInterface == nil { + c.options.chainstate.options = append(c.options.chainstate.options, chainstate.WithUserAgent(c.UserAgent())) + c.options.chainstate.options = append(c.options.chainstate.options, chainstate.WithHTTPClient(c.HTTPClient())) + c.options.chainstate.options = append(c.options.chainstate.options, chainstate.WithMetrics(c.options.metrics)) + c.options.chainstate.ClientInterface, err = chainstate.NewClient(ctx, c.options.chainstate.options...) + } + + return +} + +// loadDatastore will load the Datastore and start the Datastore client +// +// NOTE: this will run database migrations if the options was set +func (c *Client) loadDatastore(ctx context.Context) (err error) { + // Add the models to migrate (after loading the client options) + if len(c.options.models.migrateModelNames) > 0 { + c.options.dataStore.options = append( + c.options.dataStore.options, + datastore.WithAutoMigrate(c.options.models.migrateModels...), + ) + } + + // Load client (runs ALL options, IE: auto migrate models) + if c.options.dataStore.ClientInterface == nil { + + // Add custom array and object fields + c.options.dataStore.options = append( + c.options.dataStore.options, + datastore.WithCustomFields( + []string{ // Array fields + "xpub_in_ids", + "xpub_out_ids", + }, []string{ // Object fields + "xpub_metadata", + "xpub_output_value", + }, + )) + + // Add custom mongo processor + c.options.dataStore.options = append( + c.options.dataStore.options, + datastore.WithCustomMongoConditionProcessor(processCustomFields), + ) + + // Add custom mongo indexes + c.options.dataStore.options = append( + c.options.dataStore.options, + datastore.WithCustomMongoIndexer(getMongoIndexes), + ) + + // Load the datastore client + c.options.dataStore.ClientInterface, err = datastore.NewClient( + ctx, c.options.dataStore.options..., + ) + } + return +} + +// loadNotificationClient will load the notifications client +func (c *Client) loadNotificationClient() (err error) { + // Load notification if a custom interface was NOT provided + if c.options.notifications.ClientInterface == nil { + c.options.notifications.ClientInterface, err = notifications.NewClient(c.options.notifications.options...) + } + return +} + +// loadPaymailClient will load the Paymail client +func (c *Client) loadPaymailClient() (err error) { + // Only load if it's not set (the client can be overloaded) + if c.options.paymail.client == nil { + c.options.paymail.client, err = paymail.NewClient() + } + return +} + +// loadTaskmanager will load the TaskManager and start the TaskManager client +func (c *Client) loadTaskmanager(ctx context.Context) (err error) { + // Load if a custom interface was NOT provided + if c.options.taskManager.TaskEngine == nil { + c.options.taskManager.TaskEngine, err = taskmanager.NewTaskManager( + ctx, c.options.taskManager.options..., + ) + } + return +} + +// runModelMigrations will run the model Migrate() method for all models +func (c *Client) runModelMigrations(models ...interface{}) (err error) { + // If the migrations are disabled, just return + if c.options.dataStore.migrationDisabled { + return nil + } + + // Migrate the models + d := c.Datastore() + for _, model := range models { + model.(ModelInterface).SetOptions(WithClient(c)) + if err = model.(ModelInterface).Migrate(d); err != nil { + return + } + } + return +} + +func (c *Client) registerCronJobs() error { + cronJobs := c.cronJobs() + + if c.options.taskManager.cronCustomPeriods != nil { + // override the default periods + for name, job := range cronJobs { + if custom, ok := c.options.taskManager.cronCustomPeriods[name]; ok { + job.Period = custom + cronJobs[name] = job + } + } + } + + return c.Taskmanager().CronJobsInit(cronJobs) +} + +// loadDefaultPaymailConfig will load the default paymail server configuration +func (c *Client) loadDefaultPaymailConfig() (err error) { + // Default FROM paymail + if len(c.options.paymail.serverConfig.DefaultFromPaymail) == 0 { + c.options.paymail.serverConfig.DefaultFromPaymail = defaultSenderPaymail + } + + // Set default options if none are found + if len(c.options.paymail.serverConfig.options) == 0 { + c.options.paymail.serverConfig.options = append(c.options.paymail.serverConfig.options, + server.WithP2PCapabilities(), + server.WithDomainValidationDisabled(), + ) + } + + paymailLogger := c.Logger().With().Str("subservice", "go-paymail").Logger() + c.options.paymail.serverConfig.options = append(c.options.paymail.serverConfig.options, server.WithLogger(&paymailLogger)) + + // Create the paymail configuration using the client and default service provider + c.options.paymail.serverConfig.Configuration, err = server.NewConfig( + &PaymailDefaultServiceProvider{client: c}, + c.options.paymail.serverConfig.options..., + ) + return +} diff --git a/engine/client_options.go b/engine/client_options.go new file mode 100644 index 000000000..41c18eabf --- /dev/null +++ b/engine/client_options.go @@ -0,0 +1,685 @@ +package engine + +import ( + "database/sql" + "net/http" + "net/url" + "strings" + "time" + + "github.com/bitcoin-sv/go-broadcast-client/broadcast" + "github.com/bitcoin-sv/go-paymail" + "github.com/bitcoin-sv/go-paymail/server" + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" + "github.com/bitcoin-sv/spv-wallet/engine/cluster" + "github.com/bitcoin-sv/spv-wallet/engine/logging" + "github.com/bitcoin-sv/spv-wallet/engine/metrics" + "github.com/bitcoin-sv/spv-wallet/engine/notifications" + "github.com/bitcoin-sv/spv-wallet/engine/taskmanager" + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/coocood/freecache" + "github.com/go-redis/redis/v8" + "github.com/mrz1836/go-cache" + "github.com/mrz1836/go-cachestore" + "github.com/mrz1836/go-datastore" + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/rs/zerolog" + "github.com/tonicpow/go-minercraft/v2" + taskq "github.com/vmihailenco/taskq/v3" + "go.mongodb.org/mongo-driver/mongo" +) + +// ClientOps allow functional options to be supplied that overwrite default client options. +type ClientOps func(c *clientOptions) + +// defaultClientOptions will return an clientOptions struct with the default settings +// +// Useful for starting with the default and then modifying as needed +func defaultClientOptions() *clientOptions { + defaultLogger := logging.GetDefaultLogger() + + dWarnLogger := defaultLogger.Level(zerolog.WarnLevel) + datastoreLogger := logging.CreateGormLoggerAdapter(&dWarnLogger, "datastore") + // Set the default options + return &clientOptions{ + // By default check input utxos (unless disabled by the user) + iuc: true, + + // Blank chainstate config + chainstate: &chainstateOptions{ + ClientInterface: nil, + options: []chainstate.ClientOps{}, + broadcasting: true, // Enabled by default for new users + broadcastInstant: true, // Enabled by default for new users + paymailP2P: true, // Enabled by default for new users + syncOnChain: true, // Enabled by default for new users + }, + + cluster: &clusterOptions{ + options: []cluster.ClientOps{}, + }, + + // Blank cache config + cacheStore: &cacheStoreOptions{ + ClientInterface: nil, + options: []cachestore.ClientOps{}, + }, + + // Blank Datastore config + dataStore: &dataStoreOptions{ + ClientInterface: nil, + options: []datastore.ClientOps{datastore.WithLogger(&datastore.DatabaseLogWrapper{GormLoggerInterface: datastoreLogger})}, + }, + + // Default http client + httpClient: &http.Client{ + Timeout: defaultHTTPTimeout, + }, + + // Blank model options (use the Base models) + models: &modelOptions{ + modelNames: modelNames(BaseModels...), + models: BaseModels, + migrateModelNames: nil, + migrateModels: nil, + }, + + // Blank NewRelic config + newRelic: &newRelicOptions{}, + + // Blank notifications config + notifications: ¬ificationsOptions{ + ClientInterface: nil, + webhookEndpoint: "", + }, + + // Blank Paymail config + paymail: &paymailOptions{ + client: nil, + serverConfig: &PaymailServerOptions{ + Configuration: nil, + options: []server.ConfigOps{}, + }, + }, + + // Blank TaskManager config + taskManager: &taskManagerOptions{ + TaskEngine: nil, + cronCustomPeriods: map[string]time.Duration{}, + }, + + // Default user agent + userAgent: defaultUserAgent, + } +} + +// modelNames will take a list of models and return the list of names +func modelNames(models ...interface{}) (names []string) { + for _, modelInterface := range models { + names = append(names, modelInterface.(ModelInterface).Name()) + } + return +} + +// modelExists will return true if the model is found +func (o *clientOptions) modelExists(modelName, list string) bool { + m := o.models.modelNames + if list == migrateList { + m = o.models.migrateModelNames + } + for _, name := range m { + if strings.EqualFold(name, modelName) { + return true + } + } + return false +} + +// addModel will add the model if it does not exist already (load once) +func (o *clientOptions) addModel(model interface{}, list string) { + name := model.(ModelInterface).Name() + if !o.modelExists(name, list) { + if list == migrateList { + o.models.migrateModelNames = append(o.models.migrateModelNames, name) + o.models.migrateModels = append(o.models.migrateModels, model) + return + } + o.models.modelNames = append(o.models.modelNames, name) + o.models.models = append(o.models.models, model) + } +} + +// addModels will add the models if they do not exist already (load once) +func (o *clientOptions) addModels(list string, models ...interface{}) { + for _, modelInterface := range models { + o.addModel(modelInterface, list) + } +} + +// DefaultModelOptions will set any default model options (from Client options->model) +func (c *Client) DefaultModelOptions(opts ...ModelOps) []ModelOps { + // Set the Client from the spvwalletengine.Client onto the model + opts = append(opts, WithClient(c)) + + // Set the encryption key (if found) + opts = append(opts, WithEncryptionKey(c.options.encryptionKey)) + + // Return the new options + return opts +} + +// ----------------------------------------------------------------- +// GENERAL +// ----------------------------------------------------------------- + +// WithUserAgent will overwrite the default useragent +func WithUserAgent(userAgent string) ClientOps { + return func(c *clientOptions) { + if len(userAgent) > 0 { + c.userAgent = userAgent + } + } +} + +// WithNewRelic will set the NewRelic application client +func WithNewRelic(app *newrelic.Application) ClientOps { + return func(c *clientOptions) { + // Disregard if the app is nil + if app == nil { + return + } + + // Set the app + c.newRelic.app = app + + // Enable New relic on other services + c.cacheStore.options = append(c.cacheStore.options, cachestore.WithNewRelic()) + c.chainstate.options = append(c.chainstate.options, chainstate.WithNewRelic()) + c.dataStore.options = append(c.dataStore.options, datastore.WithNewRelic()) + c.taskManager.options = append(c.taskManager.options, taskmanager.WithNewRelic()) + // c.notifications.options = append(c.notifications.options, notifications.WithNewRelic()) + + // Enable the service + c.newRelic.enabled = true + } +} + +// WithDebugging will set debugging in any applicable configuration +func WithDebugging() ClientOps { + return func(c *clientOptions) { + c.debug = true + + // Enable debugging on other services + c.cacheStore.options = append(c.cacheStore.options, cachestore.WithDebugging()) + c.chainstate.options = append(c.chainstate.options, chainstate.WithDebugging()) + c.dataStore.options = append(c.dataStore.options, datastore.WithDebugging()) + c.notifications.options = append(c.notifications.options, notifications.WithDebugging()) + } +} + +// WithEncryption will set the encryption key and encrypt values using this key +func WithEncryption(key string) ClientOps { + return func(c *clientOptions) { + if len(key) > 0 { + c.encryptionKey = key + } + } +} + +// WithModels will add additional models (will NOT migrate using datastore) +// +// Pointers of structs (IE: &models.Xpub{}) +func WithModels(models ...interface{}) ClientOps { + return func(c *clientOptions) { + if len(models) > 0 { + c.addModels(modelList, models...) + } + } +} + +// WithIUCDisabled will disable checking the input utxos +func WithIUCDisabled() ClientOps { + return func(c *clientOptions) { + c.iuc = false + } +} + +// WithHTTPClient will set the custom http interface +func WithHTTPClient(httpClient HTTPInterface) ClientOps { + return func(c *clientOptions) { + if httpClient != nil { + c.httpClient = httpClient + } + } +} + +// WithLogger will set the custom logger interface +func WithLogger(customLogger *zerolog.Logger) ClientOps { + return func(c *clientOptions) { + if customLogger != nil { + c.logger = customLogger + + // Enable the logger on all spv wallet engine services + chainstateLogger := customLogger.With().Str("subservice", "chainstate").Logger() + taskManagerLogger := customLogger.With().Str("subservice", "taskManager").Logger() + notificationsLogger := customLogger.With().Str("subservice", "notifications").Logger() + c.chainstate.options = append(c.chainstate.options, chainstate.WithLogger(&chainstateLogger)) + c.taskManager.options = append(c.taskManager.options, taskmanager.WithLogger(&taskManagerLogger)) + c.notifications.options = append(c.notifications.options, notifications.WithLogger(¬ificationsLogger)) + + // Enable the logger on all external services + var datastoreLogger *logging.GormLoggerAdapter + if customLogger.GetLevel() == zerolog.InfoLevel { + warnLvlLogger := customLogger.Level(zerolog.WarnLevel) + datastoreLogger = logging.CreateGormLoggerAdapter(&warnLvlLogger, "datastore") + + } else { + datastoreLogger = logging.CreateGormLoggerAdapter(customLogger, "datastore") + } + c.dataStore.options = append(c.dataStore.options, datastore.WithLogger(&datastore.DatabaseLogWrapper{GormLoggerInterface: datastoreLogger})) + + cachestoreLogger := logging.CreateGormLoggerAdapter(customLogger, "cachestore") + c.cacheStore.options = append(c.cacheStore.options, cachestore.WithLogger(cachestoreLogger)) + } + } +} + +// ----------------------------------------------------------------- +// METRICS +// ----------------------------------------------------------------- + +// WithMetrics will set the metrics with a collector interface +func WithMetrics(collector metrics.Collector) ClientOps { + return func(c *clientOptions) { + if collector != nil { + c.metrics = metrics.NewMetrics(collector) + } + } +} + +// ----------------------------------------------------------------- +// CACHESTORE +// ----------------------------------------------------------------- + +// WithCustomCachestore will set the cachestore +func WithCustomCachestore(cacheStore cachestore.ClientInterface) ClientOps { + return func(c *clientOptions) { + if cacheStore != nil { + c.cacheStore.ClientInterface = cacheStore + } + } +} + +// WithFreeCache will set the cache client for both Read & Write clients +func WithFreeCache() ClientOps { + return func(c *clientOptions) { + c.cacheStore.options = append(c.cacheStore.options, cachestore.WithFreeCache()) + } +} + +// WithFreeCacheConnection will set the cache client to an active FreeCache connection +func WithFreeCacheConnection(client *freecache.Cache) ClientOps { + return func(c *clientOptions) { + if client != nil { + c.cacheStore.options = append( + c.cacheStore.options, + cachestore.WithFreeCacheConnection(client), + ) + } + } +} + +// WithRedis will set the redis cache client for both Read & Write clients +// +// This will load new redis connections using the given parameters +func WithRedis(config *cachestore.RedisConfig) ClientOps { + return func(c *clientOptions) { + if config != nil { + c.cacheStore.options = append(c.cacheStore.options, cachestore.WithRedis(config)) + } + } +} + +// WithRedisConnection will set the cache client to an active redis connection +func WithRedisConnection(activeClient *cache.Client) ClientOps { + return func(c *clientOptions) { + if activeClient != nil { + c.cacheStore.options = append( + c.cacheStore.options, + cachestore.WithRedisConnection(activeClient), + ) + } + } +} + +// ----------------------------------------------------------------- +// DATASTORE +// ----------------------------------------------------------------- + +// WithCustomDatastore will set the datastore +func WithCustomDatastore(dataStore datastore.ClientInterface) ClientOps { + return func(c *clientOptions) { + if dataStore != nil { + c.dataStore.ClientInterface = dataStore + } + } +} + +// WithAutoMigrate will enable auto migrate database mode (given models) +// +// Pointers of structs (IE: &models.Xpub{}) +func WithAutoMigrate(migrateModels ...interface{}) ClientOps { + return func(c *clientOptions) { + if len(migrateModels) > 0 { + c.addModels(modelList, migrateModels...) + c.addModels(migrateList, migrateModels...) + } + } +} + +// WithMigrationDisabled will disable all migrations from running in the Datastore +func WithMigrationDisabled() ClientOps { + return func(c *clientOptions) { + c.dataStore.migrationDisabled = true + } +} + +// WithSQLite will set the Datastore to use SQLite +func WithSQLite(config *datastore.SQLiteConfig) ClientOps { + return func(c *clientOptions) { + if config != nil { + c.dataStore.options = append(c.dataStore.options, datastore.WithSQLite(config)) + } + } +} + +// WithSQL will set the datastore to use the SQL config +func WithSQL(engine datastore.Engine, config *datastore.SQLConfig) ClientOps { + return func(c *clientOptions) { + if config != nil && !engine.IsEmpty() { + c.dataStore.options = append( + c.dataStore.options, + datastore.WithSQL(engine, []*datastore.SQLConfig{config}), + ) + } + } +} + +// WithSQLConfigs will load multiple connections (replica & master) +func WithSQLConfigs(engine datastore.Engine, configs []*datastore.SQLConfig) ClientOps { + return func(c *clientOptions) { + if len(configs) > 0 && !engine.IsEmpty() { + c.dataStore.options = append( + c.dataStore.options, + datastore.WithSQL(engine, configs), + ) + } + } +} + +// WithSQLConnection will set the Datastore to an existing connection for MySQL or PostgreSQL +func WithSQLConnection(engine datastore.Engine, sqlDB *sql.DB, tablePrefix string) ClientOps { + return func(c *clientOptions) { + if sqlDB != nil && !engine.IsEmpty() { + c.dataStore.options = append( + c.dataStore.options, + datastore.WithSQLConnection(engine, sqlDB, tablePrefix), + ) + } + } +} + +// WithMongoDB will set the Datastore to use MongoDB +func WithMongoDB(config *datastore.MongoDBConfig) ClientOps { + return func(c *clientOptions) { + if config != nil { + c.dataStore.options = append(c.dataStore.options, datastore.WithMongo(config)) + } + } +} + +// WithMongoConnection will set the Datastore to an existing connection for MongoDB +func WithMongoConnection(database *mongo.Database, tablePrefix string) ClientOps { + return func(c *clientOptions) { + if database != nil { + c.dataStore.options = append( + c.dataStore.options, + datastore.WithMongoConnection(database, tablePrefix), + ) + } + } +} + +// ----------------------------------------------------------------- +// PAYMAIL +// ----------------------------------------------------------------- + +// WithPaymailClient will set a custom paymail client +func WithPaymailClient(client paymail.ClientInterface) ClientOps { + return func(c *clientOptions) { + if client != nil { + c.paymail.client = client + } + } +} + +// WithPaymailSupport will set the configuration for Paymail support (as a server) +func WithPaymailSupport(domains []string, defaultFromPaymail string, domainValidation, senderValidation bool) ClientOps { + return func(c *clientOptions) { + // Add generic capabilities + c.paymail.serverConfig.options = append(c.paymail.serverConfig.options, server.WithP2PCapabilities()) + + // Add each domain + for _, domain := range domains { + c.paymail.serverConfig.options = append(c.paymail.serverConfig.options, server.WithDomain(domain)) + } + + // Set the sender validation + if senderValidation { + c.paymail.serverConfig.options = append(c.paymail.serverConfig.options, server.WithSenderValidation()) + } + + // Domain validation + if !domainValidation { + c.paymail.serverConfig.options = append(c.paymail.serverConfig.options, server.WithDomainValidationDisabled()) + } + + // Add default values + if len(defaultFromPaymail) > 0 { + c.paymail.serverConfig.DefaultFromPaymail = defaultFromPaymail + } + + // Add the paymail_address model in spv wallet engine + c.addModels(migrateList, newPaymail("")) + } +} + +// WithPaymailBeefSupport will enable Paymail BEEF format support (as a server) and create a Block Headers Service client for Merkle Roots verification. +func WithPaymailBeefSupport(bhsURL, bhsAuthToken string) ClientOps { + return func(c *clientOptions) { + _, err := url.ParseRequestURI(bhsURL) + if err != nil { + panic(err) + } + c.chainstate.options = append(c.chainstate.options, chainstate.WithConnectionToBlockHeaderService(bhsURL, bhsAuthToken)) + c.paymail.serverConfig.options = append(c.paymail.serverConfig.options, server.WithBeefCapabilities()) + } +} + +// WithPaymailServerConfig will set the custom server configuration for Paymail +// +// This will allow overriding the Configuration.actions (paymail service provider) +func WithPaymailServerConfig(config *server.Configuration, defaultFromPaymail string) ClientOps { + return func(c *clientOptions) { + if config != nil { + c.paymail.serverConfig.Configuration = config + } + if len(defaultFromPaymail) > 0 { + c.paymail.serverConfig.DefaultFromPaymail = defaultFromPaymail + } + + // Add the paymail_address model in spv wallet engine + c.addModels(migrateList, newPaymail("")) + } +} + +// ----------------------------------------------------------------- +// TASK MANAGER +// ----------------------------------------------------------------- + +// WithTaskqConfig will set the task manager to use TaskQ & in-memory +func WithTaskqConfig(config *taskq.QueueOptions) ClientOps { + return func(c *clientOptions) { + if config != nil { + c.taskManager.options = append( + c.taskManager.options, + taskmanager.WithTaskqConfig(config), + ) + } + } +} + +// 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 + } + } +} + +// ----------------------------------------------------------------- +// CLUSTER +// ----------------------------------------------------------------- + +// WithClusterRedis will set the cluster coordinator to use redis +func WithClusterRedis(redisOptions *redis.Options) ClientOps { + return func(c *clientOptions) { + if redisOptions != nil { + c.cluster.options = append(c.cluster.options, cluster.WithRedis(redisOptions)) + } + } +} + +// WithClusterKeyPrefix will set the cluster key prefix to use for all keys in the cluster coordinator +func WithClusterKeyPrefix(prefix string) ClientOps { + return func(c *clientOptions) { + if prefix != "" { + c.cluster.options = append(c.cluster.options, cluster.WithKeyPrefix(prefix)) + } + } +} + +// WithClusterClient will set the cluster options on the client +func WithClusterClient(clusterClient cluster.ClientInterface) ClientOps { + return func(c *clientOptions) { + if clusterClient != nil { + c.cluster.ClientInterface = clusterClient + } + } +} + +// ----------------------------------------------------------------- +// CHAIN-STATE +// ----------------------------------------------------------------- + +// WithCustomChainstate will set the chainstate +func WithCustomChainstate(chainState chainstate.ClientInterface) ClientOps { + return func(c *clientOptions) { + if chainState != nil { + c.chainstate.ClientInterface = chainState + } + } +} + +// WithChainstateOptions will set chainstate defaults +func WithChainstateOptions(broadcasting, broadcastInstant, paymailP2P, syncOnChain bool) ClientOps { + return func(c *clientOptions) { + c.chainstate.broadcasting = broadcasting + c.chainstate.broadcastInstant = broadcastInstant + c.chainstate.paymailP2P = paymailP2P + c.chainstate.syncOnChain = syncOnChain + } +} + +// WithExcludedProviders will set a list of excluded providers +func WithExcludedProviders(providers []string) ClientOps { + return func(c *clientOptions) { + if len(providers) > 0 { + c.chainstate.options = append(c.chainstate.options, chainstate.WithExcludedProviders(providers)) + } + } +} + +// ----------------------------------------------------------------- +// NOTIFICATIONS +// ----------------------------------------------------------------- + +// WithNotifications will set the notifications config +func WithNotifications(webhookEndpoint string) ClientOps { + return func(c *clientOptions) { + if len(webhookEndpoint) > 0 { + c.notifications.webhookEndpoint = webhookEndpoint + } + } +} + +// WithCustomNotifications will set a custom notifications interface +func WithCustomNotifications(customNotifications notifications.ClientInterface) ClientOps { + return func(c *clientOptions) { + if customNotifications != nil { + c.notifications.ClientInterface = customNotifications + } + } +} + +// 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.WithFeeQuotes(enabled)) + } +} + +// 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.WithFeeUnit(feeUnit)) + } +} + +// WithMAPI will specify Arc as an API for minercraft client +func WithMAPI() ClientOps { + return func(c *clientOptions) { + c.chainstate.options = append(c.chainstate.options, chainstate.WithMAPI()) + } +} + +// WithMinercraft will set custom minercraft client +func WithMinercraft(minercraft minercraft.ClientInterface) ClientOps { + return func(c *clientOptions) { + if minercraft != nil { + c.chainstate.options = append(c.chainstate.options, chainstate.WithMinercraft(minercraft)) + } + } +} + +// WithMinercraftAPIs set custom MinerAPIs for minercraft +func WithMinercraftAPIs(miners []*minercraft.MinerAPIs) ClientOps { + return func(c *clientOptions) { + c.chainstate.options = append(c.chainstate.options, chainstate.WithMinercraftAPIs(miners)) + } +} + +// WithBroadcastClient will set broadcast client +func WithBroadcastClient(broadcastClient broadcast.Client) ClientOps { + return func(c *clientOptions) { + c.chainstate.options = append(c.chainstate.options, chainstate.WithBroadcastClient(broadcastClient)) + } +} + +// WithCallback set callback settings +func WithCallback(callbackURL string, callbackToken string) ClientOps { + return func(c *clientOptions) { + c.chainstate.options = append(c.chainstate.options, chainstate.WithCallback(callbackURL, callbackToken)) + } +} diff --git a/engine/client_options_test.go b/engine/client_options_test.go new file mode 100644 index 000000000..a3386b617 --- /dev/null +++ b/engine/client_options_test.go @@ -0,0 +1,860 @@ +package engine + +import ( + "context" + "net/http" + "os" + "testing" + "time" + + "github.com/bitcoin-sv/go-paymail" + "github.com/bitcoin-sv/spv-wallet/engine/chainstate" + "github.com/bitcoin-sv/spv-wallet/engine/logging" + "github.com/bitcoin-sv/spv-wallet/engine/taskmanager" + "github.com/bitcoin-sv/spv-wallet/engine/tester" + "github.com/bitcoin-sv/spv-wallet/engine/utils" + "github.com/coocood/freecache" + "github.com/mrz1836/go-cachestore" + "github.com/mrz1836/go-datastore" + "github.com/newrelic/go-agent/v3/newrelic" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestNewRelicOptions will test the method enable() +func Test_newRelicOptions_enable(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("enable with valid app", func(t *testing.T) { + app, err := tester.GetNewRelicApp(defaultNewRelicApp) + require.NoError(t, err) + require.NotNil(t, app) + + opts := DefaultClientOpts(false, true) + opts = append(opts, WithNewRelic(app)) + opts = append(opts, WithLogger(&testLogger)) + + var tc ClientInterface + tc, err = NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + opts..., + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + tc.EnableNewRelic() + assert.Equal(t, true, tc.IsNewRelicEnabled()) + }) + + t.Run("enable with invalid app", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithNewRelic(nil)) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + tc.EnableNewRelic() + assert.Equal(t, false, tc.IsNewRelicEnabled()) + }) +} + +// Test_newRelicOptions_getOrStartTxn will test the method getOrStartTxn() +func Test_newRelicOptions_getOrStartTxn(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("Get a valid ctx and txn", func(t *testing.T) { + app, err := tester.GetNewRelicApp(defaultNewRelicApp) + require.NoError(t, err) + require.NotNil(t, app) + + opts := DefaultClientOpts(false, true) + opts = append(opts, WithNewRelic(app), WithLogger(&testLogger)) + + var tc ClientInterface + tc, err = NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + opts..., + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + ctx := tc.GetOrStartTxn(context.Background(), "test-name") + assert.NotNil(t, ctx) + + txn := newrelic.FromContext(ctx) + assert.NotNil(t, txn) + }) + + t.Run("invalid ctx and txn", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithNewRelic(nil), WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + ctx := tc.GetOrStartTxn(context.Background(), "test-name") + assert.NotNil(t, ctx) + + txn := newrelic.FromContext(ctx) + assert.Nil(t, txn) + }) +} + +// TestClient_defaultModelOptions will test the method DefaultModelOptions() +func TestClient_defaultModelOptions(t *testing.T) { + t.Parallel() + + t.Run("default options", func(t *testing.T) { + dco := defaultClientOptions() + require.NotNil(t, dco) + + require.NotNil(t, dco.cacheStore) + require.Nil(t, dco.cacheStore.ClientInterface) + require.NotNil(t, dco.cacheStore.options) + assert.Equal(t, 0, len(dco.cacheStore.options)) + + require.NotNil(t, dco.dataStore) + require.Nil(t, dco.dataStore.ClientInterface) + require.NotNil(t, dco.dataStore.options) + assert.Equal(t, 1, len(dco.dataStore.options)) + + require.NotNil(t, dco.newRelic) + + require.NotNil(t, dco.paymail) + + assert.Equal(t, defaultUserAgent, dco.userAgent) + + require.NotNil(t, dco.taskManager) + + assert.Nil(t, dco.logger) + }) +} + +// TestWithUserAgent will test the method WithUserAgent() +func TestWithUserAgent(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("check type", func(t *testing.T) { + opt := WithUserAgent("") + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("empty user agent", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithUserAgent(""), WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.NotEqual(t, "", tc.UserAgent()) + assert.Equal(t, defaultUserAgent, tc.UserAgent()) + }) + + t.Run("custom user agent", func(t *testing.T) { + customAgent := "custom-user-agent" + + opts := DefaultClientOpts(false, true) + opts = append(opts, WithUserAgent(customAgent), WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.NotEqual(t, defaultUserAgent, tc.UserAgent()) + assert.Equal(t, customAgent, tc.UserAgent()) + }) +} + +// TestWithNewRelic will test the method WithNewRelic() +func TestWithNewRelic(t *testing.T) { + t.Parallel() + + t.Run("check type", func(t *testing.T) { + opt := WithNewRelic(nil) + assert.IsType(t, *new(ClientOps), opt) + }) +} + +// TestWithDebugging will test the method WithDebugging() +func TestWithDebugging(t *testing.T) { + t.Parallel() + + t.Run("check type", func(t *testing.T) { + opt := WithDebugging() + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("set debug (with cache and Datastore)", func(t *testing.T) { + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + DefaultClientOpts(true, true)..., + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, true, tc.IsDebug()) + assert.Equal(t, true, tc.Cachestore().IsDebug()) + assert.Equal(t, true, tc.Datastore().IsDebug()) + }) +} + +// TestWithEncryption will test the method WithEncryption() +func TestWithEncryption(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("check type", func(t *testing.T) { + opt := WithEncryption("") + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("empty key", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithEncryption("")) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, false, tc.IsEncryptionKeySet()) + }) + + t.Run("custom encryption key", func(t *testing.T) { + key, _ := utils.RandomHex(32) + opts := DefaultClientOpts(false, true) + opts = append(opts, WithEncryption(key)) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, true, tc.IsEncryptionKeySet()) + }) +} + +// TestWithRedis will test the method WithRedis() +func TestWithRedis(t *testing.T) { + testLogger := zerolog.Nop() + + t.Run("check type", func(t *testing.T) { + opt := WithRedis(nil) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("using valid config", func(t *testing.T) { + if testing.Short() { + t.Skip("skipping live local redis tests") + } + + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + WithTaskqConfig(taskmanager.DefaultTaskQConfig(tester.RandomTablePrefix())), + WithRedis(&cachestore.RedisConfig{ + URL: cachestore.RedisPrefix + "localhost:6379", + }), + WithSQLite(tester.SQLiteTestConfig(false, true)), + WithMinercraft(&chainstate.MinerCraftBase{}), + WithLogger(&testLogger), + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + cs := tc.Cachestore() + require.NotNil(t, cs) + assert.Equal(t, cachestore.Redis, cs.Engine()) + }) + + t.Run("missing redis prefix", func(t *testing.T) { + if testing.Short() { + t.Skip("skipping live local redis tests") + } + + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + WithTaskqConfig(taskmanager.DefaultTaskQConfig(tester.RandomTablePrefix())), + WithRedis(&cachestore.RedisConfig{ + URL: "localhost:6379", + }), + WithSQLite(tester.SQLiteTestConfig(false, true)), + WithMinercraft(&chainstate.MinerCraftBase{}), + WithLogger(&testLogger), + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + cs := tc.Cachestore() + require.NotNil(t, cs) + assert.Equal(t, cachestore.Redis, cs.Engine()) + }) +} + +// TestWithRedisConnection will test the method WithRedisConnection() +func TestWithRedisConnection(t *testing.T) { + testLogger := zerolog.Nop() + + t.Run("check type", func(t *testing.T) { + opt := WithRedisConnection(nil) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("using a nil connection", func(t *testing.T) { + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + WithTaskqConfig(taskmanager.DefaultTaskQConfig(tester.RandomTablePrefix())), + WithRedisConnection(nil), + WithSQLite(tester.SQLiteTestConfig(false, true)), + WithMinercraft(&chainstate.MinerCraftBase{}), + WithLogger(&testLogger), + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + cs := tc.Cachestore() + require.NotNil(t, cs) + assert.Equal(t, cachestore.FreeCache, cs.Engine()) + }) + + t.Run("using an existing connection", func(t *testing.T) { + client, conn := tester.LoadMockRedis(10*time.Second, 10*time.Second, 10, 10) + require.NotNil(t, client) + require.NotNil(t, conn) + + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + WithTaskqConfig(taskmanager.DefaultTaskQConfig(tester.RandomTablePrefix())), + WithRedisConnection(client), + WithSQLite(tester.SQLiteTestConfig(false, true)), + WithMinercraft(&chainstate.MinerCraftBase{}), + WithLogger(&testLogger), + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + cs := tc.Cachestore() + require.NotNil(t, cs) + assert.Equal(t, cachestore.Redis, cs.Engine()) + }) +} + +// TestWithFreeCache will test the method WithFreeCache() +func TestWithFreeCache(t *testing.T) { + t.Parallel() + + t.Run("check type", func(t *testing.T) { + opt := WithFreeCache() + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("using FreeCache", func(t *testing.T) { + testLogger := zerolog.Nop() + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + WithFreeCache(), + WithTaskqConfig(taskmanager.DefaultTaskQConfig(testQueueName)), + WithSQLite(&datastore.SQLiteConfig{Shared: true}), + WithMinercraft(&chainstate.MinerCraftBase{}), + WithLogger(&testLogger)) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + cs := tc.Cachestore() + require.NotNil(t, cs) + assert.Equal(t, cachestore.FreeCache, cs.Engine()) + }) +} + +// TestWithFreeCacheConnection will test the method WithFreeCacheConnection() +func TestWithFreeCacheConnection(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("check type", func(t *testing.T) { + opt := WithFreeCacheConnection(nil) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("using a nil client", func(t *testing.T) { + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + WithFreeCacheConnection(nil), + WithTaskqConfig(taskmanager.DefaultTaskQConfig(testQueueName)), + WithSQLite(&datastore.SQLiteConfig{Shared: true}), + WithMinercraft(&chainstate.MinerCraftBase{}), + WithLogger(&testLogger), + ) + require.NoError(t, err) + require.NotNil(t, tc) + + defer CloseClient(context.Background(), t, tc) + + cs := tc.Cachestore() + require.NotNil(t, cs) + assert.Equal(t, cachestore.FreeCache, cs.Engine()) + }) + + t.Run("using an existing connection", func(t *testing.T) { + fc := freecache.NewCache(cachestore.DefaultCacheSize) + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + WithFreeCacheConnection(fc), + WithTaskqConfig(taskmanager.DefaultTaskQConfig(testQueueName)), + WithSQLite(&datastore.SQLiteConfig{Shared: true}), + WithMinercraft(&chainstate.MinerCraftBase{}), + WithLogger(&testLogger), + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + cs := tc.Cachestore() + require.NotNil(t, cs) + assert.Equal(t, cachestore.FreeCache, cs.Engine()) + }) +} + +// TestWithPaymailClient will test the method WithPaymailClient() +func TestWithPaymailClient(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("using a nil driver, automatically makes paymail client", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithPaymailClient(nil)) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.NotNil(t, tc.PaymailClient()) + }) + + t.Run("custom paymail client", func(t *testing.T) { + p, err := paymail.NewClient() + require.NoError(t, err) + require.NotNil(t, p) + + opts := DefaultClientOpts(false, true) + opts = append(opts, WithPaymailClient(p)) + opts = append(opts, WithLogger(&testLogger)) + + var tc ClientInterface + tc, err = NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.NotNil(t, tc.PaymailClient()) + assert.Equal(t, p, tc.PaymailClient()) + }) +} + +// TestWithTaskQ will test the method WithTaskQ() +func TestWithTaskQ(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + // todo: test cases where config is nil, or cannot load TaskQ + + t.Run("using taskq using memory", func(t *testing.T) { + tcOpts := DefaultClientOpts(true, true) + tcOpts = append(tcOpts, WithLogger(&testLogger)) + + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + tcOpts..., + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + tm := tc.Taskmanager() + require.NotNil(t, tm) + assert.Equal(t, taskmanager.FactoryMemory, tm.Factory()) + }) + + t.Run("using taskq using redis", func(t *testing.T) { + if testing.Short() { + t.Skip("skipping live local redis tests") + } + + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + WithTaskqConfig( + taskmanager.DefaultTaskQConfig(tester.RandomTablePrefix(), taskmanager.WithRedis("localhost:6379")), + ), + WithRedis(&cachestore.RedisConfig{ + URL: cachestore.RedisPrefix + "localhost:6379", + }), + WithSQLite(tester.SQLiteTestConfig(false, true)), + WithMinercraft(&chainstate.MinerCraftBase{}), + WithLogger(&testLogger), + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + tm := tc.Taskmanager() + require.NotNil(t, tm) + assert.Equal(t, taskmanager.FactoryRedis, tm.Factory()) + }) +} + +// TestWithLogger will test the method WithLogger() +func TestWithLogger(t *testing.T) { + t.Parallel() + + t.Run("check type", func(t *testing.T) { + opt := WithLogger(nil) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("test applying nil", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithLogger(nil)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.NotNil(t, tc.Logger()) + assert.Equal(t, logging.GetDefaultLogger(), tc.Logger()) + }) + + t.Run("test applying option", func(t *testing.T) { + customLogger := zerolog.Nop() + opts := DefaultClientOpts(false, true) + opts = append(opts, WithLogger(&customLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, &customLogger, tc.Logger()) + }) +} + +// TestWithModels will test the method WithModels() +func TestWithModels(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("check type", func(t *testing.T) { + opt := WithModels() + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("empty models - returns default models", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithModels()) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, []string{ + ModelXPub.String(), ModelAccessKey.String(), + ModelDraftTransaction.String(), ModelTransaction.String(), + ModelSyncTransaction.String(), ModelDestination.String(), + ModelUtxo.String(), + }, tc.GetModelNames()) + }) + + t.Run("add custom models", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithModels(newPaymail(testPaymail))) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, []string{ + ModelXPub.String(), ModelAccessKey.String(), + ModelDraftTransaction.String(), ModelTransaction.String(), + ModelSyncTransaction.String(), ModelDestination.String(), + ModelUtxo.String(), ModelPaymailAddress.String(), + }, tc.GetModelNames()) + }) +} + +// TestWithIUCDisabled will test the method WithIUCDisabled() +func TestWithIUCDisabled(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("check type", func(t *testing.T) { + opt := WithIUCDisabled() + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("default options", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, true, tc.IsIUCEnabled()) + }) + + t.Run("iuc disabled", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithIUCDisabled()) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, false, tc.IsIUCEnabled()) + }) +} + +// TestWithHTTPClient will test the method WithHTTPClient() +func TestWithHTTPClient(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("check type", func(t *testing.T) { + opt := WithHTTPClient(nil) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("test applying nil", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithHTTPClient(nil)) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.NotNil(t, tc.HTTPClient()) + }) + + t.Run("test applying option", func(t *testing.T) { + customClient := &http.Client{} + opts := DefaultClientOpts(false, true) + opts = append(opts, WithHTTPClient(customClient)) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, customClient, tc.HTTPClient()) + }) +} + +// TestWithCustomCachestore will test the method WithCustomCachestore() +func TestWithCustomCachestore(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("check type", func(t *testing.T) { + opt := WithCustomCachestore(nil) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("test applying nil", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithCustomCachestore(nil)) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.NotNil(t, tc.Cachestore()) + }) + + t.Run("test applying option", func(t *testing.T) { + customCache, err := cachestore.NewClient(context.Background()) + require.NoError(t, err) + + opts := DefaultClientOpts(false, true) + opts = append(opts, WithCustomCachestore(customCache)) + opts = append(opts, WithLogger(&testLogger)) + + var tc ClientInterface + tc, err = NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, customCache, tc.Cachestore()) + }) +} + +// TestWithCustomDatastore will test the method WithCustomDatastore() +func TestWithCustomDatastore(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("check type", func(t *testing.T) { + opt := WithCustomDatastore(nil) + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("test applying nil", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithCustomDatastore(nil)) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.NotNil(t, tc.Datastore()) + }) + + t.Run("test applying option", func(t *testing.T) { + customData, err := datastore.NewClient(context.Background()) + require.NoError(t, err) + + opts := DefaultClientOpts(false, true) + opts = append(opts, WithCustomDatastore(customData)) + opts = append(opts, WithLogger(&testLogger)) + + var tc ClientInterface + tc, err = NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, customData, tc.Datastore()) + }) + + // Attempt to remove a file created during the test + t.Cleanup(func() { + _ = os.Remove("datastore.db") + }) +} + +// TestWithAutoMigrate will test the method WithAutoMigrate() +func TestWithAutoMigrate(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("check type", func(t *testing.T) { + opt := WithAutoMigrate() + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("no additional models, just base models", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithAutoMigrate()) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, []string{ + ModelXPub.String(), + ModelAccessKey.String(), + ModelDraftTransaction.String(), + ModelTransaction.String(), + ModelSyncTransaction.String(), + ModelDestination.String(), + ModelUtxo.String(), + }, tc.GetModelNames()) + }) + + t.Run("one additional model", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithAutoMigrate(newPaymail(testPaymail))) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, []string{ + ModelXPub.String(), + ModelAccessKey.String(), + ModelDraftTransaction.String(), + ModelTransaction.String(), + ModelSyncTransaction.String(), + ModelDestination.String(), + ModelUtxo.String(), + ModelPaymailAddress.String(), + }, tc.GetModelNames()) + }) +} + +// TestWithMigrationDisabled will test the method WithMigrationDisabled() +func TestWithMigrationDisabled(t *testing.T) { + t.Parallel() + testLogger := zerolog.Nop() + + t.Run("check type", func(t *testing.T) { + opt := WithMigrationDisabled() + assert.IsType(t, *new(ClientOps), opt) + }) + + t.Run("default options", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, true, tc.IsMigrationEnabled()) + }) + + t.Run("migration disabled", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithMigrationDisabled()) + opts = append(opts, WithLogger(&testLogger)) + + tc, err := NewClient(tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), opts...) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, false, tc.IsMigrationEnabled()) + }) +} diff --git a/engine/client_paymail.go b/engine/client_paymail.go new file mode 100644 index 000000000..f00b33bd8 --- /dev/null +++ b/engine/client_paymail.go @@ -0,0 +1,39 @@ +package engine + +import ( + "github.com/bitcoin-sv/go-paymail" +) + +// PaymailClient will return the Paymail if it exists +func (c *Client) PaymailClient() paymail.ClientInterface { + if c.options.paymail != nil && c.options.paymail.client != nil { + return c.options.paymail.Client() + } + return nil +} + +// GetPaymailConfig will return the Paymail server config if it exists +func (c *Client) GetPaymailConfig() *PaymailServerOptions { + if c.options.paymail != nil && c.options.paymail.serverConfig != nil { + return c.options.paymail.serverConfig + } + return nil +} + +// Client will return the paymail client from the options struct +func (p *paymailOptions) Client() paymail.ClientInterface { + return p.client +} + +// FromSender will return either the configuration value or the application default +func (p *paymailOptions) FromSender() string { + if len(p.serverConfig.DefaultFromPaymail) > 0 { + return p.serverConfig.DefaultFromPaymail + } + return defaultSenderPaymail +} + +// ServerConfig will return the Paymail Server configuration from the options struct +func (p *paymailOptions) ServerConfig() *PaymailServerOptions { + return p.serverConfig +} diff --git a/engine/client_test.go b/engine/client_test.go new file mode 100644 index 000000000..099851b5b --- /dev/null +++ b/engine/client_test.go @@ -0,0 +1,251 @@ +package engine + +import ( + "context" + "testing" + + "github.com/bitcoin-sv/go-paymail" + "github.com/bitcoin-sv/spv-wallet/engine/tester" + "github.com/mrz1836/go-cachestore" + "github.com/mrz1836/go-datastore" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// todo: finish unit tests! + +// TestClient_Debug will test the method Debug() +func TestClient_Debug(t *testing.T) { + t.Parallel() + + t.Run("load basic with debug", func(t *testing.T) { + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + DefaultClientOpts(false, true)..., + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, false, tc.IsDebug()) + + tc.Debug(true) + + assert.Equal(t, true, tc.IsDebug()) + assert.Equal(t, true, tc.Cachestore().IsDebug()) + assert.Equal(t, true, tc.Datastore().IsDebug()) + assert.Equal(t, true, tc.Notifications().IsDebug()) + }) +} + +// TestClient_IsDebug will test the method IsDebug() +func TestClient_IsDebug(t *testing.T) { + t.Parallel() + + t.Run("basic debug checks", func(t *testing.T) { + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + DefaultClientOpts(false, true)..., + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, false, tc.IsDebug()) + + tc.Debug(true) + + assert.Equal(t, true, tc.IsDebug()) + }) +} + +// TestClient_Version will test the method Version() +func TestClient_Version(t *testing.T) { + t.Parallel() + + t.Run("check version", func(t *testing.T) { + tc, err := NewClient( + tester.GetNewRelicCtx(t, defaultNewRelicApp, defaultNewRelicTx), + DefaultClientOpts(false, true)..., + ) + require.NoError(t, err) + require.NotNil(t, tc) + defer CloseClient(context.Background(), t, tc) + + assert.Equal(t, version, tc.Version()) + }) +} + +// TestClient_Cachestore will test the method Cachestore() +func TestClient_Cachestore(t *testing.T) { + t.Parallel() + + t.Run("no options, panic", func(t *testing.T) { + assert.Panics(t, func() { + c := new(Client) + assert.Nil(t, c.Cachestore()) + }) + }) + + t.Run("valid cachestore", func(t *testing.T) { + tc, err := NewClient( + context.Background(), + DefaultClientOpts(false, true)..., + ) + require.NoError(t, err) + defer CloseClient(context.Background(), t, tc) + + assert.NotNil(t, tc.Cachestore()) + assert.IsType(t, &cachestore.Client{}, tc.Cachestore()) + }) +} + +// TestClient_Datastore will test the method Datastore() +func TestClient_Datastore(t *testing.T) { + t.Parallel() + + t.Run("no options, panic", func(t *testing.T) { + assert.Panics(t, func() { + c := new(Client) + assert.Nil(t, c.Datastore()) + }) + }) + + t.Run("valid datastore", func(t *testing.T) { + tc, err := NewClient( + context.Background(), + DefaultClientOpts(false, true)..., + ) + require.NoError(t, err) + defer CloseClient(context.Background(), t, tc) + + assert.NotNil(t, tc.Datastore()) + assert.IsType(t, &datastore.Client{}, tc.Datastore()) + }) +} + +// TestClient_PaymailClient will test the method PaymailClient() +func TestClient_PaymailClient(t *testing.T) { + t.Parallel() + + t.Run("no options, panic", func(t *testing.T) { + assert.Panics(t, func() { + c := new(Client) + assert.Nil(t, c.PaymailClient()) + }) + }) + + t.Run("valid paymail client", func(t *testing.T) { + tc, err := NewClient( + context.Background(), + DefaultClientOpts(false, true)..., + ) + require.NoError(t, err) + defer CloseClient(context.Background(), t, tc) + + assert.NotNil(t, tc.PaymailClient()) + assert.IsType(t, &paymail.Client{}, tc.PaymailClient()) + }) +} + +// TestClient_GetPaymailConfig will test the method GetPaymailConfig() +func TestClient_GetPaymailConfig(t *testing.T) { + t.Parallel() + + t.Run("no options, panic", func(t *testing.T) { + assert.Panics(t, func() { + c := new(Client) + assert.Nil(t, c.GetPaymailConfig()) + }) + }) + + t.Run("valid paymail server config", func(t *testing.T) { + opts := DefaultClientOpts(false, true) + opts = append(opts, WithPaymailSupport( + []string{testDomain}, + defaultSenderPaymail, + false, false, + )) + + tc, err := NewClient(context.Background(), opts...) + require.NoError(t, err) + defer CloseClient(context.Background(), t, tc) + + assert.NotNil(t, tc.GetPaymailConfig()) + assert.IsType(t, &PaymailServerOptions{}, tc.GetPaymailConfig()) + }) +} + +// TestPaymailOptions_Client will test the method Client() +func TestPaymailOptions_Client(t *testing.T) { + t.Parallel() + + t.Run("no client", func(t *testing.T) { + p := new(paymailOptions) + assert.Nil(t, p.Client()) + }) + + t.Run("valid paymail client", func(t *testing.T) { + tc, err := NewClient( + context.Background(), + DefaultClientOpts(false, true)..., + ) + require.NoError(t, err) + assert.NotNil(t, tc.PaymailClient()) + defer CloseClient(context.Background(), t, tc) + + assert.IsType(t, &paymail.Client{}, tc.PaymailClient()) + assert.NotNil(t, tc.PaymailClient()) + assert.IsType(t, &paymail.Client{}, tc.PaymailClient()) + }) +} + +// TestPaymailOptions_FromSender will test the method FromSender() +func TestPaymailOptions_FromSender(t *testing.T) { + t.Parallel() + + t.Run("no sender, use default", func(t *testing.T) { + p := &paymailOptions{ + serverConfig: &PaymailServerOptions{}, + } + assert.Equal(t, defaultSenderPaymail, p.FromSender()) + }) + + t.Run("custom sender set", func(t *testing.T) { + p := &paymailOptions{ + serverConfig: &PaymailServerOptions{ + DefaultFromPaymail: "from@domain.com", + }, + } + assert.Equal(t, "from@domain.com", p.FromSender()) + }) +} + +// TestPaymailOptions_ServerConfig will test the method ServerConfig() +func TestPaymailOptions_ServerConfig(t *testing.T) { + // t.Parallel() + + t.Run("no server config", func(t *testing.T) { + p := new(paymailOptions) + assert.Nil(t, p.ServerConfig()) + }) + + t.Run("valid server config", func(t *testing.T) { + logger := zerolog.Nop() + opts := DefaultClientOpts(false, true) + opts = append(opts, WithPaymailSupport( + []string{testDomain}, + defaultSenderPaymail, + false, false, + ), + WithLogger(&logger)) + + tc, err := NewClient(context.Background(), opts...) + require.NoError(t, err) + defer CloseClient(context.Background(), t, tc) + + assert.NotNil(t, tc.GetPaymailConfig()) + assert.IsType(t, &PaymailServerOptions{}, tc.GetPaymailConfig()) + }) +} diff --git a/engine/cluster/client.go b/engine/cluster/client.go new file mode 100644 index 000000000..c7230d603 --- /dev/null +++ b/engine/cluster/client.go @@ -0,0 +1,81 @@ +package cluster + +import ( + "context" + + "github.com/bitcoin-sv/spv-wallet/engine/logging" + "github.com/go-redis/redis/v8" + "github.com/rs/zerolog" +) + +type ( + + // Client is the client (configuration) + Client struct { + pubSubService + options *clientOptions + } + + // clientOptions holds all the configuration for the client + clientOptions struct { + coordinator Coordinator // which coordinator to use, either 'memory' or 'redis' + debug bool // For extra logs and additional debug information + logger *zerolog.Logger // Internal logger interface + newRelicEnabled bool // Whether to use New Relic + prefix string // the cluster key prefix to use before all keys + redisOptions *redis.Options + } +) + +// NewClient create new cluster client +func NewClient(ctx context.Context, opts ...ClientOps) (*Client, error) { + // Create a new client with defaults + client := &Client{options: defaultClientOptions()} + + // Overwrite defaults with any set by user + for _, opt := range opts { + opt(client.options) + } + + // Use NewRelic if it's enabled (use existing txn if found on ctx) + ctx = client.options.getTxnCtx(ctx) + + // Set logger if not set + if client.options.logger == nil { + client.options.logger = logging.GetDefaultLogger() + } + + if client.options.coordinator == CoordinatorRedis { + pubSubClient, err := NewRedisPubSub(ctx, client.options.redisOptions) + if err != nil { + return nil, err + } + pubSubClient.debug = client.IsDebug() + pubSubClient.logger = client.options.logger + pubSubClient.prefix = client.GetClusterPrefix() + client.pubSubService = pubSubClient + } else { + pubSubClient, err := NewMemoryPubSub(ctx) + if err != nil { + return nil, err + } + + pubSubClient.debug = client.IsDebug() + pubSubClient.logger = client.options.logger + pubSubClient.prefix = client.GetClusterPrefix() + client.pubSubService = pubSubClient + } + + // Return the client + return client, nil +} + +// IsDebug returns whether debugging is on or off +func (c *Client) IsDebug() bool { + return c.options.debug +} + +// GetClusterPrefix returns the cluster key prefix that can be used in things like Redis +func (c *Client) GetClusterPrefix() string { + return c.options.prefix +} diff --git a/engine/cluster/client_options.go b/engine/cluster/client_options.go new file mode 100644 index 000000000..37efe2150 --- /dev/null +++ b/engine/cluster/client_options.go @@ -0,0 +1,66 @@ +package cluster + +import ( + "context" + + "github.com/go-redis/redis/v8" + "github.com/newrelic/go-agent/v3/newrelic" +) + +const clientOptsPrefix = "bsv_" + +// ClientOps allow functional options to be supplied +// that overwrite default client options. +type ClientOps func(c *clientOptions) + +// defaultClientOptions will return an clientOptions struct with the default settings +// +// Useful for starting with the default and then modifying as needed +func defaultClientOptions() *clientOptions { + // Set the default options + return &clientOptions{ + debug: false, + coordinator: CoordinatorMemory, + prefix: clientOptsPrefix, + } +} + +// getTxnCtx will check for an existing transaction +func (c *clientOptions) getTxnCtx(ctx context.Context) context.Context { + if c.newRelicEnabled { + txn := newrelic.FromContext(ctx) + if txn != nil { + ctx = newrelic.NewContext(ctx, txn) + } + } + return ctx +} + +// WithNewRelic will enable the NewRelic wrapper +func WithNewRelic() ClientOps { + return func(c *clientOptions) { + c.newRelicEnabled = true + } +} + +// WithDebugging will enable debugging mode +func WithDebugging() ClientOps { + return func(c *clientOptions) { + c.debug = true + } +} + +// WithRedis will enable redis cluster coordinator +func WithRedis(redisOptions *redis.Options) ClientOps { + return func(c *clientOptions) { + c.coordinator = CoordinatorRedis + c.redisOptions = redisOptions + } +} + +// WithKeyPrefix will set the prefix to use for all keys in the cluster coordinator +func WithKeyPrefix(prefix string) ClientOps { + return func(c *clientOptions) { + c.prefix = prefix + } +} diff --git a/engine/cluster/cluster.go b/engine/cluster/cluster.go new file mode 100644 index 000000000..fbdf87900 --- /dev/null +++ b/engine/cluster/cluster.go @@ -0,0 +1,2 @@ +// Package cluster is for clustering spv wallet engine(s) or servers to work together for chainstate monitoring and other tasks +package cluster diff --git a/engine/cluster/interface.go b/engine/cluster/interface.go new file mode 100644 index 000000000..bebdc17ea --- /dev/null +++ b/engine/cluster/interface.go @@ -0,0 +1,35 @@ +package cluster + +import "github.com/rs/zerolog" + +// Coordinator the coordinators supported in cluster mode +type Coordinator string + +var ( + // CoordinatorRedis definition + CoordinatorRedis Coordinator = "redis" + + // CoordinatorMemory definition - use only in single server setups of spv wallet engine! + CoordinatorMemory Coordinator = "memory" +) + +// Channel all keys used in cluster coordinator +type Channel string + +var ( + // DestinationNew is a message sent when a new destination is created + DestinationNew Channel = "new-destination" +) + +// ClientInterface interface for the internal pub/sub functionality for clusters +type ClientInterface interface { + pubSubService + IsDebug() bool + GetClusterPrefix() string +} + +type pubSubService interface { + Logger() *zerolog.Logger + Subscribe(channel Channel, callback func(data string)) (func() error, error) + Publish(channel Channel, data string) error +} diff --git a/engine/cluster/memory-pub-sub.go b/engine/cluster/memory-pub-sub.go new file mode 100644 index 000000000..7fbdcfcfb --- /dev/null +++ b/engine/cluster/memory-pub-sub.go @@ -0,0 +1,56 @@ +package cluster + +import ( + "context" + + "github.com/rs/zerolog" +) + +// MemoryPubSub struct +type MemoryPubSub struct { + ctx context.Context + callbacks map[string]func(data string) + debug bool + logger *zerolog.Logger + prefix string +} + +// NewMemoryPubSub create a new memory pub/sub client +// this is the default (mock) implementation for the internal pub/sub communications on standalone servers +// for clusters, use another solution, like RedisPubSub +func NewMemoryPubSub(ctx context.Context) (*MemoryPubSub, error) { + + return &MemoryPubSub{ + ctx: ctx, + callbacks: make(map[string]func(data string)), + }, nil +} + +// Logger returns the logger to use +func (m *MemoryPubSub) Logger() *zerolog.Logger { + return m.logger +} + +// Subscribe to a channel +func (m *MemoryPubSub) Subscribe(channel Channel, callback func(data string)) (func() error, error) { + + channelName := m.prefix + string(channel) + m.callbacks[channelName] = callback + + return func() error { + delete(m.callbacks, channelName) + return nil + }, nil +} + +// Publish to a channel +func (m *MemoryPubSub) Publish(channel Channel, data string) error { + + channelName := m.prefix + string(channel) + callback, ok := m.callbacks[channelName] + if ok { + callback(data) + } + + return nil +} diff --git a/engine/cluster/redis-pub-sub.go b/engine/cluster/redis-pub-sub.go new file mode 100644 index 000000000..2b177aa55 --- /dev/null +++ b/engine/cluster/redis-pub-sub.go @@ -0,0 +1,80 @@ +package cluster + +import ( + "context" + + "github.com/go-redis/redis/v8" + "github.com/rs/zerolog" +) + +// RedisPubSub struct +type RedisPubSub struct { + ctx context.Context + client *redis.Client + debug bool + logger *zerolog.Logger + options *redis.Options + prefix string + subscriptions map[string]*redis.PubSub +} + +// NewRedisPubSub create a new redis pub/sub client +func NewRedisPubSub(ctx context.Context, options *redis.Options) (*RedisPubSub, error) { + client := redis.NewClient(options) + + return &RedisPubSub{ + ctx: ctx, + client: client, + options: options, + subscriptions: make(map[string]*redis.PubSub), + }, nil +} + +// Logger returns the logger to use +func (r *RedisPubSub) Logger() *zerolog.Logger { + return r.logger +} + +// Subscribe to a channel +func (r *RedisPubSub) Subscribe(channel Channel, callback func(data string)) (func() error, error) { + channelName := r.prefix + string(channel) + + if r.debug { + r.Logger().Debug().Msgf("NEW SUBSCRIPTION: %s -> %s", channel, channelName) + } + sub := r.client.Subscribe(r.ctx, channelName) + + go func(ch <-chan *redis.Message) { + if r.debug { + r.Logger().Info().Msgf("START CHANNEL LISTENER: %s", channelName) + } + for msg := range ch { + if r.debug { + r.Logger().Info().Msgf("NEW PUBLISH MESSAGE: %s -> %v", channelName, msg) + } + callback(msg.Payload) + } + }(sub.Channel()) + + return func() error { + if r.debug { + r.Logger().Info().Msgf("CLOSE PUBLICATION: %s", channelName) + } + return sub.Close() + }, nil +} + +// Publish to a channel +func (r *RedisPubSub) Publish(channel Channel, data string) error { + + channelName := r.prefix + string(channel) + if r.debug { + r.Logger().Info().Msgf("PUBLISH: %s -> %s", channelName, data) + } + err := r.client.Publish(r.ctx, channelName, data) + if err != nil { + return err.Err() + } + + return nil +} diff --git a/engine/codecov.yml b/engine/codecov.yml new file mode 100644 index 000000000..21745834b --- /dev/null +++ b/engine/codecov.yml @@ -0,0 +1,42 @@ +# Reference: https://docs.codecov.com/docs/codecovyml-reference +# ---------------------- +codecov: + require_ci_to_pass: true + +# Coverage configuration +# ---------------------- +coverage: + status: + patch: false + range: 70..90 # First number represents red, and second represents green + # (default is 70..100) + round: down # up, down, or nearest + precision: 2 # Number of decimal places, between 0 and 5 + +# Ignoring Paths +# -------------- +# which folders/files to ignore +ignore: + - "*/.make/.*" + - "*/.github/.*" + - "*/examples/.*" + - "*/tools/.*" + +# Parsers +# -------------- +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +# Pull request comments: +# ---------------------- +# Diff is the Coverage Diff of the pull request. +# Files are the files impacted by the pull request +comment: + layout: "reach,diff,flags,files,footer" + behavior: default + require_changes: false \ No newline at end of file diff --git a/engine/cron_job_declarations.go b/engine/cron_job_declarations.go new file mode 100644 index 000000000..226e3cdd9 --- /dev/null +++ b/engine/cron_job_declarations.go @@ -0,0 +1,69 @@ +package engine + +import ( + "context" + "time" + + "github.com/bitcoin-sv/spv-wallet/engine/taskmanager" +) + +// Cron job names to be used in WithCronCustomPeriod +const ( + CronJobNameDraftTransactionCleanUp = "draft_transaction_clean_up" + CronJobNameSyncTransactionBroadcast = "sync_transaction_broadcast" + CronJobNameSyncTransactionSync = "sync_transaction_sync" + CronJobNameCalculateMetrics = "calculate_metrics" +) + +type cronJobHandler func(ctx context.Context, client *Client) error + +// here is where we define all the cron jobs for the client +func (c *Client) cronJobs() taskmanager.CronJobs { + jobs := taskmanager.CronJobs{} + + addJob := func(name string, period time.Duration, task cronJobHandler) { + // handler adds the client pointer to the cronJobTask by using a closure + handler := func(ctx context.Context) (err error) { + if metrics, enabled := c.Metrics(); enabled { + end := metrics.TrackCron(name) + defer func() { + success := err == nil + end(success) + }() + } + err = task(ctx, c) + return + } + + jobs[name] = taskmanager.CronJob{ + Handler: handler, + Period: period, + } + } + + addJob( + CronJobNameDraftTransactionCleanUp, + 60*time.Second, + taskCleanupDraftTransactions, + ) + addJob( + CronJobNameSyncTransactionBroadcast, + 2*time.Minute, + taskBroadcastTransactions, + ) + addJob( + CronJobNameSyncTransactionSync, + 5*time.Minute, + taskSyncTransactions, + ) + + if _, enabled := c.Metrics(); enabled { + addJob( + CronJobNameCalculateMetrics, + 15*time.Second, + taskCalculateMetrics, + ) + } + + return jobs +} diff --git a/engine/cron_job_definitions.go b/engine/cron_job_definitions.go new file mode 100644 index 000000000..3f34abc0b --- /dev/null +++ b/engine/cron_job_definitions.go @@ -0,0 +1,128 @@ +package engine + +import ( + "context" + "errors" + "time" + + "github.com/mrz1836/go-datastore" +) + +// taskCleanupDraftTransactions will clean up all old expired draft transactions +func taskCleanupDraftTransactions(ctx context.Context, client *Client) error { + client.Logger().Info().Msg("running cleanup draft transactions task...") + + // Construct an empty model + var models []DraftTransaction + conditions := map[string]interface{}{ + statusField: DraftStatusDraft, + // todo: add DB condition for date "expires_at": map[string]interface{}{"$lte": time.Now()}, + } + + queryParams := &datastore.QueryParams{ + Page: 1, + PageSize: 20, + OrderByField: idField, + SortDirection: datastore.SortAsc, + } + + // Get the records + if err := getModels( + ctx, client.Datastore(), + &models, conditions, queryParams, defaultDatabaseReadTimeout, + ); err != nil { + if errors.Is(err, datastore.ErrNoResults) { + return nil + } + return err + } + + // Loop and update + var err error + timeNow := time.Now().UTC() + for index := range models { + if timeNow.After(models[index].ExpiresAt) { + models[index].enrich(ModelDraftTransaction, WithClient(client)) + models[index].Status = DraftStatusExpired + if err = models[index].Save(ctx); err != nil { + return err + } + } + } + + return nil +} + +// taskBroadcastTransactions will broadcast any transactions +func taskBroadcastTransactions(ctx context.Context, client *Client) error { + client.Logger().Info().Msg("running broadcast transaction(s) task...") + + err := processBroadcastTransactions(ctx, 1000, WithClient(client)) + if err == nil || errors.Is(err, datastore.ErrNoResults) { + return nil + } + return err +} + +// taskSyncTransactions will sync any transactions +func taskSyncTransactions(ctx context.Context, client *Client) error { + logClient := client.Logger() + logClient.Info().Msg("running sync transaction(s) task...") + + // Prevent concurrent running + unlock, err := newWriteLock( + ctx, lockKeyProcessSyncTx, client.Cachestore(), + ) + defer unlock() + if err != nil { + logClient.Warn().Msg("cannot run sync transaction(s) task, previous run is not complete yet...") + return nil //nolint:nilerr // previous run is not complete yet + } + + err = processSyncTransactions(ctx, 100, WithClient(client)) + if err == nil || errors.Is(err, datastore.ErrNoResults) { + return nil + } + return err +} + +func taskCalculateMetrics(ctx context.Context, client *Client) error { + m, enabled := client.Metrics() + if !enabled { + return errors.New("metrics are not enabled") + } + + modelOpts := client.DefaultModelOptions() + + if xpubsCount, err := getXPubsCount(ctx, nil, nil, modelOpts...); err != nil { + client.options.logger.Error().Err(err).Msg("error getting xpubs count") + } else { + m.SetXPubCount(xpubsCount) + } + + if utxosCount, err := getUtxosCount(ctx, nil, nil, modelOpts...); err != nil { + client.options.logger.Error().Err(err).Msg("error getting utxos count") + } else { + m.SetUtxoCount(utxosCount) + } + + if paymailsCount, err := getPaymailAddressesCount(ctx, nil, nil, modelOpts...); err != nil { + client.options.logger.Error().Err(err).Msg("error getting paymails count") + } else { + m.SetPaymailCount(paymailsCount) + } + + if destinationsCount, err := getDestinationsCount(ctx, nil, nil, modelOpts...); err != nil { + client.options.logger.Error().Err(err).Msg("error getting destinations count") + } else { + m.SetDestinationCount(destinationsCount) + } + + if accessKeysCount, err := getAccessKeysCount(ctx, nil, nil, modelOpts...); err != nil { + client.options.logger.Error().Err(err).Msg("error getting access keys count") + } else { + m.SetAccessKeyCount(accessKeysCount) + } + + return nil +} diff --git a/engine/db_model_transactions.go b/engine/db_model_transactions.go new file mode 100644 index 000000000..67825e11f --- /dev/null +++ b/engine/db_model_transactions.go @@ -0,0 +1,228 @@ +package engine + +import ( + "context" + + "github.com/bitcoin-sv/spv-wallet/engine/notifications" + "github.com/mrz1836/go-datastore" +) + +// GetModelTableName will get the db table name of the current model +func (m *Transaction) GetModelTableName() string { + return tableTransactions +} + +// Save will save the model into the Datastore +func (m *Transaction) Save(ctx context.Context) (err error) { + // Prepare the metadata + if len(m.Metadata) > 0 { + // set the metadata to be xpub specific, but only if we have a valid xpub ID + if m.XPubID != "" { + // was metadata set via opts ? + if m.XpubMetadata == nil { + m.XpubMetadata = make(XpubMetadata) + } + if _, ok := m.XpubMetadata[m.XPubID]; !ok { + m.XpubMetadata[m.XPubID] = make(Metadata) + } + for key, value := range m.Metadata { + m.XpubMetadata[m.XPubID][key] = value + } + } else { + m.Client().Logger().Debug(). + Str("txID", m.ID). + Msg("xPub id is missing from transaction, cannot store metadata") + } + } + + return Save(ctx, m) +} + +// BeforeCreating will fire before the model is being inserted into the Datastore +func (m *Transaction) BeforeCreating(_ context.Context) error { + if m.beforeCreateCalled { + m.Client().Logger().Debug(). + Str("txID", m.ID). + Msgf("skipping: %s BeforeCreating hook, because already called", m.Name()) + return nil + } + + m.Client().Logger().Debug(). + Str("txID", m.ID). + Msgf("starting: %s BeforeCreating hook...", m.Name()) + + // Test for required field(s) + if len(m.Hex) == 0 { + return ErrMissingFieldHex + } + + // Set the xPubID + m.setXPubID() + + // Set the ID - will also parse and verify the tx + err := m.setID() + if err != nil { + return err + } + + m.Client().Logger().Debug(). + Str("txID", m.ID). + Msgf("end: %s BeforeCreating hook", m.Name()) + m.beforeCreateCalled = true + return nil +} + +// AfterCreated will fire after the model is created in the Datastore +func (m *Transaction) AfterCreated(ctx context.Context) error { + m.Client().Logger().Debug(). + Str("txID", m.ID). + Msgf("starting: %s AfterCreated hook...", m.Name()) + + // Pre-build the options + opts := m.GetOptions(false) + + // update the xpub balances + for xPubID, balance := range m.XpubOutputValue { + // todo: run this in a go routine? (move this into a function on the xpub model?) + xPub, err := getXpubWithCache(ctx, m.Client(), "", xPubID, opts...) + if err != nil { + return err + } else if xPub == nil { + return ErrMissingRequiredXpub + } + if err = xPub.incrementBalance(ctx, balance); err != nil { + return err + } + } + + // Update the draft transaction, process broadcasting + // todo: go routine (however it's not working, panic in save for missing datastore) + if m.draftTransaction != nil { + m.draftTransaction.Status = DraftStatusComplete + m.draftTransaction.FinalTxID = m.ID + if err := m.draftTransaction.Save(ctx); err != nil { + return err + } + } + + // Fire notifications (this is already in a go routine) + notify(notifications.EventTypeCreate, m) + + m.Client().Logger().Debug(). + Str("txID", m.ID). + Msgf("end: %s AfterCreated hook...", m.Name()) + return nil +} + +// AfterUpdated will fire after the model is updated in the Datastore +func (m *Transaction) AfterUpdated(_ context.Context) error { + m.Client().Logger().Debug(). + Str("txID", m.ID). + Msgf("starting: %s AfterUpdated hook...", m.Name()) + + // Fire notifications (this is already in a go routine) + notify(notifications.EventTypeUpdate, m) + + m.Client().Logger().Debug(). + Str("txID", m.ID). + Msgf("end: %s AfterUpdated hook", m.Name()) + return nil +} + +// AfterDeleted will fire after the model is deleted in the Datastore +func (m *Transaction) AfterDeleted(_ context.Context) error { + m.Client().Logger().Debug().Msgf("starting: %s AfterDeleted hook...", m.Name()) + + // Fire notifications (this is already in a go routine) + notify(notifications.EventTypeDelete, m) + + m.Client().Logger().Debug().Msgf("end: %s AfterDeleted hook", m.Name()) + return nil +} + +// ChildModels will get any related sub models +func (m *Transaction) ChildModels() (childModels []ModelInterface) { + // Add the UTXOs if found + for index := range m.utxos { + childModels = append(childModels, &m.utxos[index]) + } + + // Add the broadcast transaction record + if m.syncTransaction != nil { + childModels = append(childModels, m.syncTransaction) + } + + return +} + +// Migrate model specific migration on startup +func (m *Transaction) Migrate(client datastore.ClientInterface) error { + tableName := client.GetTableName(tableTransactions) + if client.Engine() == datastore.MySQL { + if err := m.migrateMySQL(client, tableName); err != nil { + return err + } + } else if client.Engine() == datastore.PostgreSQL { + if err := m.migratePostgreSQL(client, tableName); err != nil { + return err + } + } + + return client.IndexMetadata(tableName, xPubMetadataField) +} + +// migratePostgreSQL is specific migration SQL for Postgresql +func (m *Transaction) migratePostgreSQL(client datastore.ClientInterface, tableName string) error { + tx := client.Execute(`CREATE INDEX IF NOT EXISTS idx_` + tableName + `_xpub_in_ids ON ` + + tableName + ` USING gin (xpub_in_ids jsonb_ops)`) + if tx.Error != nil { + return tx.Error + } + + if tx = client.Execute(`CREATE INDEX IF NOT EXISTS idx_` + tableName + `_xpub_out_ids ON ` + + tableName + ` USING gin (xpub_out_ids jsonb_ops)`); tx.Error != nil { + return tx.Error + } + + return nil +} + +// migrateMySQL is specific migration SQL for MySQL +func (m *Transaction) migrateMySQL(client datastore.ClientInterface, tableName string) error { + idxName := "idx_" + tableName + "_xpub_in_ids" + idxExists, err := client.IndexExists(tableName, idxName) + if err != nil { + return err + } + if !idxExists { + tx := client.Execute("ALTER TABLE `" + tableName + "`" + + " ADD INDEX " + idxName + " ( (CAST(xpub_in_ids AS CHAR(64) ARRAY)) )") + if tx.Error != nil { + m.Client().Logger().Error().Msg("failed creating json index on mysql: " + tx.Error.Error()) + return nil //nolint:nolintlint,nilerr // error is not needed + } + } + + idxName = "idx_" + tableName + "_xpub_out_ids" + if idxExists, err = client.IndexExists( + tableName, idxName, + ); err != nil { + return err + } + if !idxExists { + tx := client.Execute("ALTER TABLE `" + tableName + "`" + + " ADD INDEX " + idxName + " ( (CAST(xpub_out_ids AS CHAR(64) ARRAY)) )") + if tx.Error != nil { + m.Client().Logger().Error().Msg("failed creating json index on mysql: " + tx.Error.Error()) + return nil //nolint:nolintlint,nilerr // error is not needed + } + } + + tx := client.Execute("ALTER TABLE `" + tableName + "` MODIFY COLUMN hex longtext") + if tx.Error != nil { + m.Client().Logger().Error().Msg("failed changing hex type to longtext in MySQL: " + tx.Error.Error()) + return nil //nolint:nolintlint,nilerr // error is not needed + } + + return nil +} diff --git a/engine/definitions.go b/engine/definitions.go new file mode 100644 index 000000000..1180d1b08 --- /dev/null +++ b/engine/definitions.go @@ -0,0 +1,170 @@ +package engine + +import ( + "time" +) + +// Defaults for engine functionality +const ( + changeOutputSize = uint64(35) // Average size in bytes of a change output + databaseLongReadTimeout = 30 * time.Second // For all "GET" or "SELECT" methods + defaultBroadcastTimeout = 25 * time.Second // Default timeout for broadcasting + defaultCacheLockTTL = 20 // in Seconds + defaultCacheLockTTW = 10 // in Seconds + defaultDatabaseReadTimeout = 20 * time.Second // For all "GET" or "SELECT" methods + defaultDraftTxExpiresIn = 20 * time.Second // Default TTL for draft transactions + defaultHTTPTimeout = 20 * time.Second // Default timeout for HTTP requests + defaultOverheadSize = uint64(8) // 8 bytes is the default overhead in a transaction = 4 bytes version + 4 bytes nLockTime + defaultQueryTxTimeout = 10 * time.Second // Default timeout for syncing on-chain information + defaultUserAgent = "spv-wallet: " + version // Default user agent + 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.14.2" // spv wallet engine version +) + +// All the base models +const ( + ModelAccessKey ModelName = "access_key" + ModelDestination ModelName = "destination" + ModelDraftTransaction ModelName = "draft_transaction" + ModelMetadata ModelName = "metadata" + ModelNameEmpty ModelName = "empty" + ModelPaymailAddress ModelName = "paymail_address" + ModelSyncTransaction ModelName = "sync_transaction" + ModelTransaction ModelName = "transaction" + ModelUtxo ModelName = "utxo" + ModelXPub ModelName = "xpub" +) + +// AllModelNames is a list of all models +var AllModelNames = []ModelName{ + ModelAccessKey, + ModelDestination, + ModelMetadata, + ModelPaymailAddress, + ModelPaymailAddress, + ModelSyncTransaction, + ModelTransaction, + ModelUtxo, + ModelXPub, +} + +// Internal table names +const ( + tableAccessKeys = "access_keys" + tableDestinations = "destinations" + tableDraftTransactions = "draft_transactions" + tablePaymailAddresses = "paymail_addresses" + tableSyncTransactions = "sync_transactions" + tableTransactions = "transactions" + tableUTXOs = "utxos" + tableXPubs = "xpubs" +) + +const ( + // ReferenceIDField is used for Paymail + ReferenceIDField = "reference_id" + + // Internal field names + aliasField = "alias" + broadcastStatusField = "broadcast_status" + createdAtField = "created_at" + currentBalanceField = "current_balance" + domainField = "domain" + draftIDField = "draft_id" + idField = "id" + metadataField = "metadata" + nextExternalNumField = "next_external_num" + nextInternalNumField = "next_internal_num" + p2pStatusField = "p2p_status" + satoshisField = "satoshis" + spendingTxIDField = "spending_tx_id" + statusField = "status" + syncStatusField = "sync_status" + typeField = "type" + xPubIDField = "xpub_id" + xPubMetadataField = "xpub_metadata" + blockHeightField = "block_height" + blockHashField = "block_hash" + merkleProofField = "merkle_proof" + bumpField = "bump" + + // Universal statuses + statusCanceled = "canceled" + statusComplete = "complete" + statusDraft = "draft" + statusError = "error" + statusExpired = "expired" + statusPending = "pending" + statusProcessing = "processing" + statusReady = "ready" + statusSkipped = "skipped" + + // Paymail / Handles + cacheKeyAddressResolution = "paymail-address-resolution-" + cacheKeyCapabilities = "paymail-capabilities-" + cacheTTLAddressResolution = 2 * time.Minute + cacheTTLCapabilities = 60 * time.Minute + defaultSenderPaymail = "example@example.com" + handleHandcashPrefix = "$" + handleMaxLength = 25 + handleRelayPrefix = "1" + p2pMetadataField = "p2p_tx_metadata" + + // Misc + gormTypeText = "text" + migrateList = "migrate" + modelList = "models" +) + +// Cache keys for model caching +const ( + cacheKeyDestinationModel = "destination-id-%s" // model-id- + cacheKeyDestinationModelByAddress = "destination-address-%s" // model-address-
+ cacheKeyDestinationModelByLockingScript = "destination-locking-script-%s" // model-locking-script-