From 5bbec69fd39309d4b1205012674a6a2023889f3f Mon Sep 17 00:00:00 2001 From: Augustyn Chmiel Date: Thu, 25 Sep 2025 07:49:39 +0200 Subject: [PATCH 1/2] [FEATURE] implement getFiatExchangeRate on WalletServices --- .../fiat_exhange_rate/fiat_exchange_rate.go | 61 ++++++++++ .../fiat_exhange_rate/fiat_exchange_rate.md | 107 ++++++++++++++++++ .../nlock_time_is_final.go | 33 ------ .../nlock_time_is_final.md | 97 ++++++++++++++++ pkg/services/services.go | 25 +++- pkg/services/services_fiat_exchange_test.go | 99 ++++++++++++++++ 6 files changed, 384 insertions(+), 38 deletions(-) create mode 100644 examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.go create mode 100644 examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.md create mode 100644 examples/services_examples/nlock_time_is_final/nlock_time_is_final.md create mode 100644 pkg/services/services_fiat_exchange_test.go diff --git a/examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.go b/examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.go new file mode 100644 index 00000000..2b53a4c7 --- /dev/null +++ b/examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "log/slog" + + "github.com/bsv-blockchain/go-wallet-toolbox/examples/internal/show" + "github.com/bsv-blockchain/go-wallet-toolbox/pkg/defs" + "github.com/bsv-blockchain/go-wallet-toolbox/pkg/services" +) + +func exampleFiatExchangeRate(srv *services.WalletServices) { + examples := []struct { + currency defs.Currency + base *defs.Currency + }{ + {currency: defs.EUR, base: ptr(defs.USD)}, + {currency: defs.GBP, base: ptr(defs.EUR)}, + {currency: defs.GBP, base: nil}, // defaults to USD + {currency: "ABC", base: ptr(defs.USD)}, // invalid case + } + + for _, ex := range examples { + base := "" + if ex.base != nil { + base = string(*ex.base) + } + step := fmt.Sprintf("Getting fiat rate for %s per %s", ex.currency, base) + show.Step("FiatExchangeRate", step) + + rate := srv.FiatExchangeRate(ex.currency, ex.base) + if rate == 0 { + show.WalletError("FiatExchangeRate", fmt.Sprintf("%s/%s", ex.currency, base), fmt.Errorf("rate not found")) + } else { + show.WalletSuccess("FiatExchangeRate", fmt.Sprintf("%s/%s", ex.currency, base), rate) + } + } +} + +func ptr(c defs.Currency) *defs.Currency { + return &c +} + +func main() { + show.ProcessStart("Fiat Exchange Rate Conversion") + + cfg := defs.DefaultServicesConfig(defs.NetworkMainnet) + cfg.FiatExchangeRates = defs.FiatExchangeRates{ + Rates: map[defs.Currency]float64{ + defs.USD: 1.0, + defs.EUR: 0.85, + defs.GBP: 0.65, + }, + } + + srv := services.New(slog.Default(), cfg) + + exampleFiatExchangeRate(srv) + + show.ProcessComplete("Fiat Exchange Rate Conversion") +} diff --git a/examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.md b/examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.md new file mode 100644 index 00000000..07466776 --- /dev/null +++ b/examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.md @@ -0,0 +1,107 @@ +# Fiat Exchange Rate + +This example demonstrates how to retrieve and convert fiat currency exchange rates using the Go Wallet Toolbox SDK. It supports converting one fiat currency to another based on the latest rates (e.g., EUR to USD, GBP to EUR). + +## Overview + +The process involves: +1. Setting up the service with a mock configuration including fiat exchange rates. +2. Calling the `FiatExchangeRate()` method to retrieve the exchange rate of one currency relative to another. +3. Processing the returned `float64` result representing the conversion rate. +4. Handling invalid currencies or missing data. + +This showcases a simplified currency conversion mechanism used internally within the wallet toolbox. + +## Code Walkthrough + +### Configuration + +The fiat exchange rates are mocked for testing or example purposes using a `map[defs.Currency]float64`: + +```go +{ + USD: 1.0, + EUR: 0.85, + GBP: 0.65, +} +``` + +You can replace or update this map with actual rates from a provider or service. + +### Method Signature + +```go +func (s *WalletServices) FiatExchangeRate(ctx context.Context, currency defs.Currency, base *defs.Currency) (float64, error) +``` + +- **`currency`**: Target fiat currency (e.g., EUR). +- **`base`**: Base fiat currency to convert against (e.g., USD). Defaults to USD if `nil`. +- **Returns**: The conversion rate from `currency` to `base`. + +### Example Scenarios + +- `FiatExchangeRate(EUR, USD)` → 0.85 (EUR to USD) +- `FiatExchangeRate(GBP, EUR)` → 0.65 / 0.85 ≈ 0.7647 +- `FiatExchangeRate(GBP, nil)` → 0.65 (GBP to default USD) +- `FiatExchangeRate(ABC, USD)` → Error (unknown currency) + +## Running the Example + +```bash +go run ./examples/services_examples/fiat_exchange_rate/fiat_exchange_rate.go +``` + +## Expected Output + +```text +🚀 STARTING: Fiat Exchange Rate Conversion +============================================================ + +=== STEP === +FiatExchangeRate is performing: Getting fiat rate for EUR per USD +-------------------------------------------------- + + WALLET CALL: FiatExchangeRate +Args: EUR/USD +✅ Result: 0.85 + +=== STEP === +FiatExchangeRate is performing: Getting fiat rate for GBP per EUR +-------------------------------------------------- + + WALLET CALL: FiatExchangeRate +Args: GBP/EUR +✅ Result: 0.7647058823529412 + +=== STEP === +FiatExchangeRate is performing: Getting fiat rate for GBP per +-------------------------------------------------- + + WALLET CALL: FiatExchangeRate +Args: GBP/ +✅ Result: 0.65 + +=== STEP === +FiatExchangeRate is performing: Getting fiat rate for ABC per USD +-------------------------------------------------- + + WALLET CALL: FiatExchangeRate +Args: ABC/USD +❌ Error: rate not found +============================================================ +🎉 COMPLETED: Fiat Exchange Rate Conversion +``` + +## Integration Steps + +1. **Add fiat exchange rates** to your service configuration or implement a live provider. +2. **Invoke `FiatExchangeRate()`** with the desired currency and optional base. +3. **Use the returned value** to display or convert currency values in your app. +4. **Handle errors gracefully** if a currency is missing or unknown. +5. **Cache frequently-used rates** to avoid redundant calls in performance-critical paths. + +## Additional Resources + +- [FiatExchangeRate Code](./fiat_exchange_rate.go) - Main example implementation +- [BSVExchangeRate](../bsv_exchange_rate/bsv_exchange_rate.go) - Check BSV/USD rate +- [Currency Definitions](../../pkg/defs/currency.go) - Enum of supported fiat currencies diff --git a/examples/services_examples/nlock_time_is_final/nlock_time_is_final.go b/examples/services_examples/nlock_time_is_final/nlock_time_is_final.go index ddf78e67..8ad9533c 100644 --- a/examples/services_examples/nlock_time_is_final/nlock_time_is_final.go +++ b/examples/services_examples/nlock_time_is_final/nlock_time_is_final.go @@ -78,36 +78,3 @@ func main() { show.ProcessComplete("Check nLockTime Finality") } - -/* Output: - -🚀 STARTING: Check nLockTime Finality -============================================================ - -=== STEP === -Wallet-Services is performing: Checking finality for past timestamp locktime: 1754897347 --------------------------------------------------- - - WALLET CALL: NLockTimeIsFinal -Args: 1754897347 -✅ Result: true - -=== STEP === -Wallet-Services is performing: Checking finality for future timestamp locktime: 1754904547 --------------------------------------------------- - - WALLET CALL: NLockTimeIsFinal -Args: 1754904547 -✅ Result: false - -=== STEP === -Wallet-Services is performing: Checking finality for block height locktime: 800000 --------------------------------------------------- - - WALLET CALL: NLockTimeIsFinal -Args: 800000 -✅ Result: true -============================================================ -🎉 COMPLETED: Check nLockTime Finality - -*/ diff --git a/examples/services_examples/nlock_time_is_final/nlock_time_is_final.md b/examples/services_examples/nlock_time_is_final/nlock_time_is_final.md new file mode 100644 index 00000000..2b34bd74 --- /dev/null +++ b/examples/services_examples/nlock_time_is_final/nlock_time_is_final.md @@ -0,0 +1,97 @@ +# NLockTime Finality + +This example demonstrates how to check whether a given `nLockTime` value is considered **final** on the BSV blockchain using the Go Wallet Toolbox SDK. It supports evaluating both timestamp-based and block-height-based locktimes and determines their finality status based on current blockchain state. + +## Overview + +The `NLockTimeIsFinal` method determines whether a transaction can be accepted for mining based on its `nLockTime` value. A transaction is considered final if: +- It has a locktime of 0 +- The locktime is less than the current block height (for block-based locktime) +- The locktime is less than the current UNIX timestamp (for time-based locktime) +- All input sequence numbers are `0xffffffff` (final) + +## Code Walkthrough + +The example demonstrates three scenarios: + +### 1. Timestamp LockTime (Past) +```go +lockTime := uint32(time.Now().Unix() - 3600) +isFinal, err := srv.NLockTimeIsFinal(ctx, lockTime) +// ✅ Result: true +``` + +### 2. Timestamp LockTime (Future) +```go +lockTime := uint32(time.Now().Unix() + 3600) +isFinal, err := srv.NLockTimeIsFinal(ctx, lockTime) +// ❌ Result: false +``` + +### 3. Block Height LockTime +```go +lockTime := uint32(800000) +isFinal, err := srv.NLockTimeIsFinal(ctx, lockTime) +// ✅ Result: depends on current blockchain height +``` + +## Method Signature + +```go +func (s *WalletServices) NLockTimeIsFinal(ctx context.Context, txOrLockTime any) (bool, error) +``` + +- **`txOrLockTime`**: Can be a `uint32`, `int`, transaction hex string, `sdk.Transaction`, etc. +- **Returns**: Whether the locktime is final and the transaction is ready to be accepted into a block. + +## Running the Example + +```bash +go run ./examples/services_examples/nlocktime_finality/nlocktime_finality.go +``` + +## Expected Output + +```text +🚀 STARTING: Check nLockTime Finality +============================================================ + +=== STEP === +Wallet-Services is performing: Checking finality for past timestamp locktime: 1754897347 +-------------------------------------------------- + + WALLET CALL: NLockTimeIsFinal +Args: 1754897347 +✅ Result: true + +=== STEP === +Wallet-Services is performing: Checking finality for future timestamp locktime: 1754904547 +-------------------------------------------------- + + WALLET CALL: NLockTimeIsFinal +Args: 1754904547 +✅ Result: false + +=== STEP === +Wallet-Services is performing: Checking finality for block height locktime: 800000 +-------------------------------------------------- + + WALLET CALL: NLockTimeIsFinal +Args: 800000 +✅ Result: true +============================================================ +🎉 COMPLETED: Check nLockTime Finality +``` + +## Integration Steps + +1. **Import Wallet Toolbox** and initialize `WalletServices` with proper network config. +2. **Pass a locktime or transaction** into `NLockTimeIsFinal()`. +3. **Check returned boolean** to determine if the transaction is final. +4. **Handle errors gracefully**, especially with malformed inputs or failed service lookups. + +## Additional Resources + +- [NLockTimeIsFinal Code](./nlocktime_finality.go) - Full example implementation +- [Go-SDK Transaction Type](https://pkg.go.dev/github.com/bsv-blockchain/go-sdk/transaction) - For parsing raw transactions +- [Bitcoin nLockTime Reference](https://en.bitcoin.it/wiki/NLockTime) - Understanding nLockTime usage diff --git a/pkg/services/services.go b/pkg/services/services.go index 5c98b31b..f92decaf 100644 --- a/pkg/services/services.go +++ b/pkg/services/services.go @@ -349,11 +349,6 @@ func (s *WalletServices) BsvExchangeRate(ctx context.Context) (float64, error) { return bsvExchangeRate, nil } -// FiatExchangeRate returns approximate exchange rate currency per base. -func (s *WalletServices) FiatExchangeRate(currency defs.Currency, base *defs.Currency) float64 { - panic("Not implemented yet") -} - // MerklePath attempts to obtain the merkle proof associated with a 32 byte transaction hash (txid). func (s *WalletServices) MerklePath(ctx context.Context, txid string) (*wdk.MerklePathResult, error) { result, err := s.merklePathServices.OneByOne(ctx, txid) @@ -579,3 +574,23 @@ func (s *WalletServices) HashOutputScript(scriptHex string) (string, error) { } return outputScript, nil } + +// FiatExchangeRate returns approximate exchange rate currency per base. +// Uses config.FiatExchangeRates as the source. +func (s *WalletServices) FiatExchangeRate(currency defs.Currency, base *defs.Currency) float64 { + rates := s.config.FiatExchangeRates.Rates + + baseCurrency := defs.USD + if base != nil { + baseCurrency = *base + } + + currencyRate, ok1 := rates[currency] + baseRate, ok2 := rates[baseCurrency] + + if !ok1 || !ok2 || baseRate == 0 { + return 0 + } + + return currencyRate / baseRate +} diff --git a/pkg/services/services_fiat_exchange_test.go b/pkg/services/services_fiat_exchange_test.go new file mode 100644 index 00000000..dbd10c86 --- /dev/null +++ b/pkg/services/services_fiat_exchange_test.go @@ -0,0 +1,99 @@ +package services_test + +import ( + "testing" + + "github.com/bsv-blockchain/go-wallet-toolbox/pkg/defs" + "github.com/bsv-blockchain/go-wallet-toolbox/pkg/internal/testabilities/testservices" + "github.com/stretchr/testify/assert" +) + +func TestWalletServices_FiatExchangeRate(t *testing.T) { + t.Run("returns 1 when same currency used for base", func(t *testing.T) { + // given: + given := testservices.GivenServices(t) + + usd := defs.USD + services := given.Services().Config(func(cfg *defs.WalletServices) { + cfg.FiatExchangeRates = defs.FiatExchangeRates{ + Rates: map[defs.Currency]float64{ + usd: 1.0, + }, + } + }).New() + + // when: + rate := services.FiatExchangeRate(usd, &usd) + + // then: + assert.Equal(t, 1.0, rate) + }) + + t.Run("converts correctly from EUR to USD", func(t *testing.T) { + // given: + given := testservices.GivenServices(t) + + eur := defs.EUR + usd := defs.USD + + services := given.Services().Config(func(cfg *defs.WalletServices) { + cfg.FiatExchangeRates = defs.FiatExchangeRates{ + Rates: map[defs.Currency]float64{ + usd: 1.0, + eur: 0.85, + }, + } + }).New() + + // when: + rate := services.FiatExchangeRate(eur, &usd) + + // then: + assert.Equal(t, 0.85, rate) + }) + + t.Run("converts correctly from GBP to EUR", func(t *testing.T) { + // given: + given := testservices.GivenServices(t) + + gbp := defs.GBP + eur := defs.EUR + + services := given.Services().Config(func(cfg *defs.WalletServices) { + cfg.FiatExchangeRates = defs.FiatExchangeRates{ + Rates: map[defs.Currency]float64{ + gbp: 0.6, + eur: 0.9, + }, + } + }).New() + + // when: + rate := services.FiatExchangeRate(gbp, &eur) + + // then: + assert.InDelta(t, 0.6666, rate, 0.0001) + }) + + t.Run("returns 0 when currency is missing", func(t *testing.T) { + // given: + given := testservices.GivenServices(t) + + usd := defs.USD + gbp := defs.GBP + + services := given.Services().Config(func(cfg *defs.WalletServices) { + cfg.FiatExchangeRates = defs.FiatExchangeRates{ + Rates: map[defs.Currency]float64{ + usd: 1.0, + }, + } + }).New() + + // when: + rate := services.FiatExchangeRate(gbp, &usd) + + // then: + assert.Equal(t, 0.0, rate) + }) +} From 2b0c62f5f5cf57295e9d3bd63b0ddf0ec3ee0f0d Mon Sep 17 00:00:00 2001 From: Augustyn Chmiel Date: Thu, 25 Sep 2025 07:49:39 +0200 Subject: [PATCH 2/2] [FEATURE] implement getFiatExchangeRate on WalletServices --- .../fiat_exchange_rate.go | 0 .../fiat_exchange_rate.md | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename examples/services_examples/{fiat_exhange_rate => fiat_exchange_rate}/fiat_exchange_rate.go (100%) rename examples/services_examples/{fiat_exhange_rate => fiat_exchange_rate}/fiat_exchange_rate.md (93%) diff --git a/examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.go b/examples/services_examples/fiat_exchange_rate/fiat_exchange_rate.go similarity index 100% rename from examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.go rename to examples/services_examples/fiat_exchange_rate/fiat_exchange_rate.go diff --git a/examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.md b/examples/services_examples/fiat_exchange_rate/fiat_exchange_rate.md similarity index 93% rename from examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.md rename to examples/services_examples/fiat_exchange_rate/fiat_exchange_rate.md index 07466776..a8a6591a 100644 --- a/examples/services_examples/fiat_exhange_rate/fiat_exchange_rate.md +++ b/examples/services_examples/fiat_exchange_rate/fiat_exchange_rate.md @@ -31,7 +31,7 @@ You can replace or update this map with actual rates from a provider or service. ### Method Signature ```go -func (s *WalletServices) FiatExchangeRate(ctx context.Context, currency defs.Currency, base *defs.Currency) (float64, error) +func (s *WalletServices) FiatExchangeRate(currency defs.Currency, base *defs.Currency) float64 ``` - **`currency`**: Target fiat currency (e.g., EUR). @@ -104,4 +104,4 @@ Args: ABC/USD - [FiatExchangeRate Code](./fiat_exchange_rate.go) - Main example implementation - [BSVExchangeRate](../bsv_exchange_rate/bsv_exchange_rate.go) - Check BSV/USD rate -- [Currency Definitions](../../pkg/defs/currency.go) - Enum of supported fiat currencies +- [Currency Definitions](../../../pkg/defs/currency.go) - Enum of supported fiat currencies