Skip to content

Show the wallets market performance - don't merge yet #3410

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,7 @@ tags
!/frontends/web/src/locales/sl/
!/frontends/web/src/locales/tr/
!/frontends/web/src/locales/zh/
!/frontends/web/src/locales/cs/
!/frontends/web/src/locales/cs/
.aider*
build-errors.log
servewallet
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ gomobileinit:
gomobile init
servewallet:
go run -mod=vendor ./cmd/servewallet
servewallet-reload:
@echo "Starting servewallet with auto-reload on Go file changes..."
@command -v air >/dev/null 2>&1 || { echo "Installing air for auto-reload..."; go install github.com/air-verse/air@latest; }
air -c scripts/air.toml
servewallet-mainnet:
go run -mod=vendor ./cmd/servewallet -mainnet
servewallet-regtest:
Expand Down
104 changes: 99 additions & 5 deletions backend/accounts/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,15 @@ func NewOrderedTransactions(txs []*TransactionData) OrderedTransactions {

// TimeseriesEntry contains the balance of the account at the given time.
type TimeseriesEntry struct {
Time time.Time
Value coin.Amount
Time time.Time
Value coin.Amount
SumFiatAtTime *big.Rat
}

// RatesProvider provides access to current and historical exchange rates.
type RatesProvider interface {
LatestPrice() map[string]map[string]float64
HistoricalPriceAt(coinCode string, fiat string, timestamp time.Time) float64
}

// MarshalJSON serializes the entry as JSON.
Expand Down Expand Up @@ -238,12 +245,84 @@ func (txs OrderedTransactions) EarliestTime() (time.Time, error) {
// Timeseries chunks the time between `start` and `end` into steps of `interval` duration, and
// provides the balance of the account at each step.
func (txs OrderedTransactions) Timeseries(
start, end time.Time, interval time.Duration) ([]TimeseriesEntry, error) {
start, end time.Time,
interval time.Duration,
ratesProvider RatesProvider,
coinCode string,
fiat string,
coinDecimals *big.Int) ([]TimeseriesEntry, error) {

for _, tx := range txs {
if tx.isConfirmed() && tx.Timestamp == nil {
return nil, errp.WithStack(errors.ErrNotAvailable)
}
}

// Calculate sumFiatAtTime using historical prices (like in chart.go) - only if all parameters are provided
fiatTimeMap := make(map[int64]*big.Rat) // Map: timestamp -> SumFiatAtTime
if len(txs) > 0 && ratesProvider != nil && coinCode != "" && fiat != "" && coinDecimals != nil {
cumulativeFiatAtTime := new(big.Rat)

// Iterate through transactions from oldest to newest (reverse order)
for i := len(txs) - 1; i >= 0; i-- {
tx := txs[i]

// Skip unconfirmed transactions
if !tx.isConfirmed() || tx.Timestamp == nil {
continue
}

// Get historical price at transaction time (like in chart.go)
historicalPrice := ratesProvider.HistoricalPriceAt(coinCode, fiat, *tx.Timestamp)

// Calculate coin amount change for this transaction
var coinAmountChange *big.Rat
switch tx.Type {
case TxTypeReceive:
if tx.Status != TxStatusFailed {
coinAmountChange = new(big.Rat).SetFrac(tx.Amount.BigInt(), coinDecimals)
} else {
coinAmountChange = new(big.Rat)
}
case TxTypeSend:
if tx.Status != TxStatusFailed {
coinAmountChange = new(big.Rat).SetFrac(tx.Amount.BigInt(), coinDecimals)
coinAmountChange.Neg(coinAmountChange) // Negative for send
} else {
coinAmountChange = new(big.Rat)
}
case TxTypeSendSelf:
coinAmountChange = new(big.Rat)
default:
coinAmountChange = new(big.Rat)
}

// Calculate fiat value at historical price (sumFiatAtTime)
fiatValueAtTime := new(big.Rat).Mul(coinAmountChange, new(big.Rat).SetFloat64(historicalPrice))

// Add to cumulative sumFiatAtTime
cumulativeFiatAtTime.Add(cumulativeFiatAtTime, fiatValueAtTime)

// Rundungskorrektur: Wenn Coin-Balance praktisch 0 ist, setze auch Fiat auf 0
currentBalance := txs[i].Balance
balanceInt, _ := currentBalance.Int64()
if balanceInt == 0 {
// Wenn die aktuelle Balance 0 ist, sollte auch der Fiat-Wert 0 sein
cumulativeFiatAtTime.SetInt64(0)
} else {
// Kleine Rundungsfehler korrigieren (< 1 Cent)
absValue := new(big.Rat).Abs(cumulativeFiatAtTime)
if absValue.Cmp(big.NewRat(1, 100)) < 0 {
cumulativeFiatAtTime.SetInt64(0)
}
}

// Store in map for later lookup
// fmt.Printf("DEBUG Timeseries - timestamp: %v, SumFiatAtTime: %v\n", tx.Timestamp.UTC(), cumulativeFiatAtTime)
fiatTimeMap[tx.Timestamp.Unix()] = new(big.Rat).Set(cumulativeFiatAtTime)
}
}

currentTime := start
if currentTime.IsZero() {
return nil, nil
Expand All @@ -260,21 +339,36 @@ func (txs OrderedTransactions) Timeseries(
return tx.Timestamp.Before(currentTime) || tx.Timestamp.Equal(currentTime)
})
var value coin.Amount
var sumFiatAtTime *big.Rat

if nextIndex == len(txs) {
value = coin.NewAmountFromInt64(0)
sumFiatAtTime = new(big.Rat) // Zero value
} else {
value = txs[nextIndex].Balance
// Find the corresponding SumFiatAtTime value
if txs[nextIndex].Timestamp != nil {
if fiatValue, exists := fiatTimeMap[txs[nextIndex].Timestamp.Unix()]; exists {
sumFiatAtTime = new(big.Rat).Set(fiatValue)
} else {
sumFiatAtTime = new(big.Rat) // Zero if not found
}
} else {
sumFiatAtTime = new(big.Rat) // Zero for unconfirmed
}
}

result = append(result, TimeseriesEntry{
Time: currentTime,
Value: value,
Time: currentTime,
Value: value,
SumFiatAtTime: sumFiatAtTime,
})

currentTime = currentTime.Add(interval)
if currentTime.After(end) {
break
}
}

return result, nil
}
70 changes: 44 additions & 26 deletions backend/accounts/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package accounts
import (
"testing"
"time"
"math/big"

"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -110,60 +111,77 @@ func TestOrderedTransactions(t *testing.T) {
time.Date(2020, 9, 9, 13, 0, 0, 0, time.UTC),
time.Date(2020, 9, 21, 13, 0, 0, 0, time.UTC),
24*time.Hour,
nil, // ratesProvider - nil für Tests
"", // coinCode - leer für Tests
"", // fiat - leer für Tests
nil, // coinDecimals - nil für Tests
)
require.NoError(t, err)
require.Equal(t, []TimeseriesEntry{
{
Time: time.Date(2020, 9, 9, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(0),
Time: time.Date(2020, 9, 9, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(0),
SumFiatAtTime: new(big.Rat),
},
{
Time: time.Date(2020, 9, 10, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(200),
Time: time.Date(2020, 9, 10, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(200),
SumFiatAtTime: new(big.Rat),
},
{
Time: time.Date(2020, 9, 11, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(190),
Time: time.Date(2020, 9, 11, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(190),
SumFiatAtTime: new(big.Rat),
},
{
Time: time.Date(2020, 9, 12, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(190),
Time: time.Date(2020, 9, 12, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(190),
SumFiatAtTime: new(big.Rat),
},
{
Time: time.Date(2020, 9, 13, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(190),
Time: time.Date(2020, 9, 13, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(190),
SumFiatAtTime: new(big.Rat),
},
{
Time: time.Date(2020, 9, 14, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(190),
Time: time.Date(2020, 9, 14, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(190),
SumFiatAtTime: new(big.Rat),
},
{
Time: time.Date(2020, 9, 15, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(290),
Time: time.Date(2020, 9, 15, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(290),
SumFiatAtTime: new(big.Rat),
},
{
Time: time.Date(2020, 9, 16, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(290),
Time: time.Date(2020, 9, 16, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(290),
SumFiatAtTime: new(big.Rat),
},
{
Time: time.Date(2020, 9, 17, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(290),
Time: time.Date(2020, 9, 17, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(290),
SumFiatAtTime: new(big.Rat),
},
{
Time: time.Date(2020, 9, 18, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(290),
Time: time.Date(2020, 9, 18, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(290),
SumFiatAtTime: new(big.Rat),
},
{
Time: time.Date(2020, 9, 19, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(290),
Time: time.Date(2020, 9, 19, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(290),
SumFiatAtTime: new(big.Rat),
},
{
Time: time.Date(2020, 9, 20, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(590),
Time: time.Date(2020, 9, 20, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(590),
SumFiatAtTime: new(big.Rat),
},
{
Time: time.Date(2020, 9, 21, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(589),
Time: time.Date(2020, 9, 21, 13, 0, 0, 0, time.UTC),
Value: coin.NewAmountFromInt64(589),
SumFiatAtTime: new(big.Rat),
},
}, timeseries)
}
Expand Down
Loading