From 79b7b7cd46d7dff865efc10aafe3fc4be0f26d13 Mon Sep 17 00:00:00 2001 From: jeffyanta Date: Sun, 25 Jan 2026 17:08:51 -0500 Subject: [PATCH] Improve USD market value calculations --- ocp/currency/exchange_rate.go | 12 ++--- ocp/currency/time.go | 2 + ocp/currency/usd_market_value.go | 64 +++++++++++++++++++-------- ocp/rpc/transaction/airdrop.go | 2 +- ocp/rpc/transaction/intent_handler.go | 18 ++------ ocp/worker/account/gift_card.go | 8 +--- ocp/worker/account/testutil.go | 2 +- ocp/worker/geyser/external_deposit.go | 2 +- ocp/worker/swap/util.go | 48 ++++++++++++++++---- 9 files changed, 100 insertions(+), 58 deletions(-) diff --git a/ocp/currency/exchange_rate.go b/ocp/currency/exchange_rate.go index c69567b..6ea74a6 100644 --- a/ocp/currency/exchange_rate.go +++ b/ocp/currency/exchange_rate.go @@ -7,16 +7,16 @@ import ( ) // CalculateExchangeRate calculates the exchange rate for a crypto value exchange. -func CalculateExchangeRate(mintAccount *common.Account, nativeAmount float64, quarks uint64) float64 { +func CalculateExchangeRate(mintAccount *common.Account, quarks uint64, nativeAmount float64) float64 { quarksPerUnit := common.GetMintQuarksPerUnit(mintAccount) - tokenUnits := new(big.Float).Quo( + tokenUnitsBig := new(big.Float).Quo( big.NewFloat(float64(quarks)).SetPrec(defaultPrecision), big.NewFloat(float64(quarksPerUnit)).SetPrec(defaultPrecision), ) - exchangeRate := new(big.Float).Quo( + exchangeRateBig := new(big.Float).Quo( big.NewFloat(float64(nativeAmount)).SetPrec(defaultPrecision), - tokenUnits, + tokenUnitsBig, ) - res, _ := exchangeRate.Float64() - return res + exchangeRate, _ := exchangeRateBig.Float64() + return exchangeRate } diff --git a/ocp/currency/time.go b/ocp/currency/time.go index d2d7941..b209889 100644 --- a/ocp/currency/time.go +++ b/ocp/currency/time.go @@ -12,6 +12,8 @@ const ( // GetLatestExchangeRateTime gets the latest time for fetching an exchange rate. // By synchronizing on a time, we can eliminate the amount of perceived volatility // over short time spans. +// +// Deprecated: Use real-time data and favour volatility func GetLatestExchangeRateTime() time.Time { // Standardize to concrete 15 minute intervals to reduce perceived volatility. // Notably, don't fall exactly on the 15 minute interval, so we remove 1 second. diff --git a/ocp/currency/usd_market_value.go b/ocp/currency/usd_market_value.go index 3b699b7..3d16dfe 100644 --- a/ocp/currency/usd_market_value.go +++ b/ocp/currency/usd_market_value.go @@ -2,40 +2,67 @@ package currency import ( "context" + "errors" + "math/big" "time" currency_lib "github.com/code-payments/ocp-server/currency" "github.com/code-payments/ocp-server/ocp/common" ocp_data "github.com/code-payments/ocp-server/ocp/data" + "github.com/code-payments/ocp-server/ocp/data/currency" "github.com/code-payments/ocp-server/solana/currencycreator" ) -// CalculateUsdMarketValue calculates the current USD market value of a crypto -// amount in quarks. -func CalculateUsdMarketValue(ctx context.Context, data ocp_data.Provider, mint *common.Account, quarks uint64, at time.Time) (float64, float64, error) { +// CalculateUsdMarketValueFromFiatAmount calculates the USD market value for a given +// fiat value and exchange rate. +func CalculateUsdMarketValueFromFiatAmount(fiatAmount, fiatToUsdRate float64) (float64, error) { + if !common.IsCoreMintUsdStableCoin() { + return 0, errors.New("non-usd stable coin not implemented") + } + + fiatAmountBig := big.NewFloat(fiatAmount).SetPrec(defaultPrecision) + fiatToUsdRateBig := big.NewFloat(fiatToUsdRate).SetPrec(defaultPrecision) + usdMarketValue, _ := new(big.Float).Quo(fiatAmountBig, fiatToUsdRateBig).Float64() + return usdMarketValue, nil +} + +// CalculateUsdMarketValueFromTokenAmount calculates the current USD market value +// of a crypto amount in quarks. +func CalculateUsdMarketValueFromTokenAmount(ctx context.Context, data ocp_data.Provider, mint *common.Account, quarks uint64, at time.Time) (float64, error) { isSupportedMint, err := common.IsSupportedMint(ctx, data, mint) if err != nil { - return 0, 0, err + return 0, err } else if !isSupportedMint { - return 0, 0, common.ErrUnsupportedMint + return 0, common.ErrUnsupportedMint } - usdExchangeRecord, err := data.GetExchangeRate(ctx, currency_lib.USD, at) - if err != nil { - return 0, 0, err - } + coreMintQuarksPerUnitBig := big.NewFloat(float64(common.GetMintQuarksPerUnit(common.CoreMintAccount))).SetPrec(defaultPrecision) - coreMintQuarksPerUnit := common.GetMintQuarksPerUnit(common.CoreMintAccount) + var exchangeRateRecord *currency.ExchangeRateRecord + if common.IsCoreMintUsdStableCoin() { + exchangeRateRecord = ¤cy.ExchangeRateRecord{ + Symbol: string(currency_lib.USD), + Rate: 1.0, + Time: at, + } + } else { + exchangeRateRecord, err = data.GetExchangeRate(ctx, currency_lib.USD, at) + if err != nil { + return 0, err + } + } + exchangeRateBig := big.NewFloat(exchangeRateRecord.Rate).SetPrec(defaultPrecision) if common.IsCoreMint(mint) { - units := float64(quarks) / float64(coreMintQuarksPerUnit) - marketValue := usdExchangeRecord.Rate * units - return marketValue, usdExchangeRecord.Rate, nil + quarksBig := big.NewFloat(float64(quarks)).SetPrec(defaultPrecision) + unitsBig := new(big.Float).Quo(quarksBig, coreMintQuarksPerUnitBig) + marketValue, _ := new(big.Float).Mul(exchangeRateBig, unitsBig).Float64() + return marketValue, nil } reserveRecord, err := data.GetCurrencyReserveAtTime(ctx, mint.PublicKey().ToBase58(), at) if err != nil { - return 0, 0, err + return 0, err } coreMintSellValueInQuarks, _ := currencycreator.EstimateSell(¤cycreator.EstimateSellArgs{ @@ -44,10 +71,9 @@ func CalculateUsdMarketValue(ctx context.Context, data ocp_data.Provider, mint * ValueMintDecimals: uint8(common.CoreMintDecimals), SellFeeBps: 0, }) + coreMintSellValueInQuarksBig := big.NewFloat(float64(coreMintSellValueInQuarks)).SetPrec(defaultPrecision) - coreMintSellValueInUnits := float64(coreMintSellValueInQuarks) / float64(coreMintQuarksPerUnit) - otherMintUnits := float64(quarks) / float64(common.GetMintQuarksPerUnit(mint)) - marketValue := usdExchangeRecord.Rate * coreMintSellValueInUnits - rate := marketValue / otherMintUnits - return marketValue, rate, nil + coreMintSellValueInUnitsBig := new(big.Float).Quo(coreMintSellValueInQuarksBig, coreMintQuarksPerUnitBig) + marketValue, _ := new(big.Float).Mul(exchangeRateBig, coreMintSellValueInUnitsBig).Float64() + return marketValue, nil } diff --git a/ocp/rpc/transaction/airdrop.go b/ocp/rpc/transaction/airdrop.go index 87af475..d9c9224 100644 --- a/ocp/rpc/transaction/airdrop.go +++ b/ocp/rpc/transaction/airdrop.go @@ -214,7 +214,7 @@ func (s *transactionServer) airdrop(ctx context.Context, intentId string, owner coreMintAmount := nativeAmount / exchangeRateRecord.Rate quarkAmount := uint64(coreMintAmount*float64(common.CoreMintQuarksPerUnit)) + additionalQuarks - usdMarketValue, _, err := currency_util.CalculateUsdMarketValue(ctx, s.data, common.CoreMintAccount, quarkAmount, currency_util.GetLatestExchangeRateTime()) + usdMarketValue, err := currency_util.CalculateUsdMarketValueFromTokenAmount(ctx, s.data, common.CoreMintAccount, quarkAmount, time.Now()) if err != nil { log.With(zap.Error(err)).Warn("failure calculating usd market value") return nil, err diff --git a/ocp/rpc/transaction/intent_handler.go b/ocp/rpc/transaction/intent_handler.go index b651e80..ebe157e 100644 --- a/ocp/rpc/transaction/intent_handler.go +++ b/ocp/rpc/transaction/intent_handler.go @@ -407,7 +407,7 @@ func (h *SendPublicPaymentIntentHandler) PopulateMetadata(ctx context.Context, i if common.IsCoreMint(mint) { exchangeRate = typed.ClientExchangeData.CoreMintFiatExchangeRate.ExchangeRate.ExchangeRate } else { - exchangeRate = currency_util.CalculateExchangeRate(mint, typed.ClientExchangeData.NativeAmount, typed.ClientExchangeData.Quarks) + exchangeRate = currency_util.CalculateExchangeRate(mint, typed.ClientExchangeData.Quarks, typed.ClientExchangeData.NativeAmount) } quarks = typed.ClientExchangeData.Quarks case *transactionpb.SendPublicPaymentMetadata_ServerExchangeData: // todo: deprecate this flow @@ -419,7 +419,7 @@ func (h *SendPublicPaymentIntentHandler) PopulateMetadata(ctx context.Context, i return NewIntentDeniedError("client exchange data not provided") } - usdMarketValue, _, err := currency_util.CalculateUsdMarketValue(ctx, h.data, mint, quarks, currency_util.GetLatestExchangeRateTime()) + usdMarketValue, err := currency_util.CalculateUsdMarketValueFromFiatAmount(nativeAmount, exchangeRate) if err != nil { return err } @@ -999,11 +999,6 @@ func (h *ReceivePaymentsPubliclyIntentHandler) PopulateMetadata(ctx context.Cont } h.cachedGiftCardIssuedIntentRecord = giftCardIssuedIntentRecord - usdMarketValue, _, err := currency_util.CalculateUsdMarketValue(ctx, h.data, mint, typedProtoMetadata.Quarks, currency_util.GetLatestExchangeRateTime()) - if err != nil { - return err - } - intentRecord.IntentType = intent.ReceivePaymentsPublicly intentRecord.MintAccount = mint.PublicKey().ToBase58() intentRecord.ReceivePaymentsPubliclyMetadata = &intent.ReceivePaymentsPubliclyMetadata{ @@ -1018,7 +1013,7 @@ func (h *ReceivePaymentsPubliclyIntentHandler) PopulateMetadata(ctx context.Cont OriginalExchangeRate: giftCardIssuedIntentRecord.SendPublicPaymentMetadata.ExchangeRate, OriginalNativeAmount: giftCardIssuedIntentRecord.SendPublicPaymentMetadata.NativeAmount, - UsdMarketValue: usdMarketValue, + UsdMarketValue: giftCardIssuedIntentRecord.SendPublicPaymentMetadata.UsdMarketValue, } return nil @@ -1332,17 +1327,12 @@ func (h *PublicDistributionIntentHandler) PopulateMetadata(ctx context.Context, totalQuarks += distribution.Quarks } - usdMarketValue, _, err := currency_util.CalculateUsdMarketValue(ctx, h.data, mint, totalQuarks, currency_util.GetLatestExchangeRateTime()) - if err != nil { - return err - } - intentRecord.IntentType = intent.PublicDistribution intentRecord.MintAccount = mint.PublicKey().ToBase58() intentRecord.PublicDistributionMetadata = &intent.PublicDistributionMetadata{ Source: source.PublicKey().ToBase58(), Quantity: totalQuarks, - UsdMarketValue: usdMarketValue, + UsdMarketValue: 0, // todo: Sum USD market values of each funding into pool } destinationTokenAddresses := make([]string, len(typedProtoMetadata.Distributions)) diff --git a/ocp/worker/account/gift_card.go b/ocp/worker/account/gift_card.go index ae7352d..038d839 100644 --- a/ocp/worker/account/gift_card.go +++ b/ocp/worker/account/gift_card.go @@ -17,7 +17,6 @@ import ( "github.com/code-payments/ocp-server/metrics" "github.com/code-payments/ocp-server/ocp/balance" "github.com/code-payments/ocp-server/ocp/common" - currency_util "github.com/code-payments/ocp-server/ocp/currency" ocp_data "github.com/code-payments/ocp-server/ocp/data" "github.com/code-payments/ocp-server/ocp/data/account" "github.com/code-payments/ocp-server/ocp/data/action" @@ -309,11 +308,6 @@ func insertAutoReturnIntentRecord(ctx context.Context, data ocp_data.Provider, g return err } - usdMarketValue, _, err := currency_util.CalculateUsdMarketValue(ctx, data, mintAccount, giftCardIssuedIntent.SendPublicPaymentMetadata.Quantity, time.Now()) - if err != nil { - return err - } - // We need to insert a faked completed public receive intent so it can appear // as a return in the user's payment history. Think of it as a server-initiated // intent on behalf of the user based on pre-approved conditional actions. @@ -337,7 +331,7 @@ func insertAutoReturnIntentRecord(ctx context.Context, data ocp_data.Provider, g OriginalExchangeRate: giftCardIssuedIntent.SendPublicPaymentMetadata.ExchangeRate, OriginalNativeAmount: giftCardIssuedIntent.SendPublicPaymentMetadata.NativeAmount, - UsdMarketValue: usdMarketValue, + UsdMarketValue: giftCardIssuedIntent.SendPublicPaymentMetadata.UsdMarketValue, }, State: intent.StateConfirmed, diff --git a/ocp/worker/account/testutil.go b/ocp/worker/account/testutil.go index 0850447..d8fe628 100644 --- a/ocp/worker/account/testutil.go +++ b/ocp/worker/account/testutil.go @@ -215,7 +215,7 @@ func (e *testEnv) assertGiftCardAutoReturned(t *testing.T, giftCard *testGiftCar assert.Equal(t, giftCard.issuedIntentRecord.SendPublicPaymentMetadata.ExchangeCurrency, historyRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeCurrency) assert.Equal(t, giftCard.issuedIntentRecord.SendPublicPaymentMetadata.ExchangeRate, historyRecord.ReceivePaymentsPubliclyMetadata.OriginalExchangeRate) assert.Equal(t, giftCard.issuedIntentRecord.SendPublicPaymentMetadata.NativeAmount, historyRecord.ReceivePaymentsPubliclyMetadata.OriginalNativeAmount) - assert.Equal(t, 1234.5, historyRecord.ReceivePaymentsPubliclyMetadata.UsdMarketValue) + assert.Equal(t, giftCard.issuedIntentRecord.SendPublicPaymentMetadata.UsdMarketValue, historyRecord.ReceivePaymentsPubliclyMetadata.UsdMarketValue) assert.Equal(t, intent.StateConfirmed, historyRecord.State) } diff --git a/ocp/worker/geyser/external_deposit.go b/ocp/worker/geyser/external_deposit.go index 7405a45..8a4060c 100644 --- a/ocp/worker/geyser/external_deposit.go +++ b/ocp/worker/geyser/external_deposit.go @@ -313,7 +313,7 @@ func processPotentialExternalDepositIntoVm(ctx context.Context, data ocp_data.Pr return errors.Wrap(err, "invalid owner account") } - usdMarketValue, _, err := currency_util.CalculateUsdMarketValue(ctx, data, mint, uint64(deltaQuarksIntoOmnibus), time.Now()) + usdMarketValue, err := currency_util.CalculateUsdMarketValueFromTokenAmount(ctx, data, mint, uint64(deltaQuarksIntoOmnibus), time.Now()) if err != nil { return errors.Wrap(err, "error calculating usd market value") } diff --git a/ocp/worker/swap/util.go b/ocp/worker/swap/util.go index c848092..7a4e912 100644 --- a/ocp/worker/swap/util.go +++ b/ocp/worker/swap/util.go @@ -131,13 +131,18 @@ func (p *runtime) submitTransaction(ctx context.Context, record *swap.Record) er return nil } -func (p *runtime) updateBalancesForFinalizedSwap(ctx context.Context, record *swap.Record) (uint64, error) { - owner, err := common.NewAccountFromPublicKeyString(record.Owner) +func (p *runtime) updateBalancesForFinalizedSwap(ctx context.Context, swapRecord *swap.Record) (uint64, error) { + owner, err := common.NewAccountFromPublicKeyString(swapRecord.Owner) if err != nil { return 0, err } - toMint, err := common.NewAccountFromPublicKeyString(record.ToMint) + fromMint, err := common.NewAccountFromPublicKeyString(swapRecord.FromMint) + if err != nil { + return 0, err + } + + toMint, err := common.NewAccountFromPublicKeyString(swapRecord.ToMint) if err != nil { return 0, err } @@ -152,7 +157,7 @@ func (p *runtime) updateBalancesForFinalizedSwap(ctx context.Context, record *sw return 0, err } - tokenBalances, err := p.data.GetBlockchainTransactionTokenBalances(ctx, record.TransactionSignature) + tokenBalances, err := p.data.GetBlockchainTransactionTokenBalances(ctx, swapRecord.TransactionSignature) if err != nil { return 0, err } @@ -165,15 +170,40 @@ func (p *runtime) updateBalancesForFinalizedSwap(ctx context.Context, record *sw return 0, errors.New("delta quarks into destination vm omnibus is not positive") } - usdMarketValue, _, err := currency_util.CalculateUsdMarketValue(ctx, p.data, toMint, uint64(deltaQuarksIntoOmnibus), time.Now()) - if err != nil { - return 0, err + var usdMarketValue float64 + switch swapRecord.FundingSource { + case swap.FundingSourceSubmitIntent: + fundingIntentRecord, err := p.data.GetIntent(ctx, swapRecord.FundingId) + if err != nil { + return 0, err + } + + if fundingIntentRecord.IntentType != intent.SendPublicPayment { + return 0, errors.New("unexpected intent type") + } + + usdMarketValue = fundingIntentRecord.SendPublicPaymentMetadata.UsdMarketValue + case swap.FundingSourceExternalWallet: + if !common.IsCoreMint(fromMint) { + return 0, errors.New("unexpected source mint") + } + + if !common.IsCoreMintUsdStableCoin() { + return 0, errors.New("core mint is not a usd stable coin") + } + + usdMarketValue, err = currency_util.CalculateUsdMarketValueFromTokenAmount(ctx, p.data, common.CoreMintAccount, swapRecord.Amount, time.Now()) + if err != nil { + return 0, err + } + default: + return 0, errors.New("unsupported funding source") } err = p.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { // For transaction history intentRecord := &intent.Record{ - IntentId: getSwapDepositIntentID(record.TransactionSignature, ownerDestinationTimelockVault), + IntentId: getSwapDepositIntentID(swapRecord.TransactionSignature, ownerDestinationTimelockVault), IntentType: intent.ExternalDeposit, MintAccount: toMint.PublicKey().ToBase58(), @@ -196,7 +226,7 @@ func (p *runtime) updateBalancesForFinalizedSwap(ctx context.Context, record *sw // For tracking in cached balances externalDepositRecord := &deposit.Record{ - Signature: record.TransactionSignature, + Signature: swapRecord.TransactionSignature, Destination: ownerDestinationTimelockVault.PublicKey().ToBase58(), Amount: uint64(deltaQuarksIntoOmnibus), UsdMarketValue: usdMarketValue,