Skip to content

Commit e4e3581

Browse files
authored
Add minimum dollar value for a holder to be included in count (#164)
1 parent 4996708 commit e4e3581

5 files changed

Lines changed: 631 additions & 83 deletions

File tree

ocp/worker/currency/holder/runtime.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@ import (
1616
"github.com/code-payments/ocp-server/ocp/data/account"
1717
"github.com/code-payments/ocp-server/ocp/data/currency"
1818
"github.com/code-payments/ocp-server/ocp/worker"
19+
"github.com/code-payments/ocp-server/solana/currencycreator"
1920
)
2021

2122
const (
2223
historicalUpdateTimeInterval = time.Hour
2324
)
2425

26+
var (
27+
minHoldingValue = common.ToCoreMintQuarks(10)
28+
)
29+
2530
type holderRuntime struct {
2631
log *zap.Logger
2732
data ocp_data.Provider
@@ -59,16 +64,16 @@ func (p *holderRuntime) Start(runtimeCtx context.Context, interval time.Duration
5964
}
6065

6166
func (p *holderRuntime) UpdateAllLaunchpadCurrencyHolderCounts(ctx context.Context) {
62-
liveHolderRecordsByMint, err := p.data.GetAllLiveCurrencyHolderCounts(ctx)
67+
liveReserveRecordsByMint, err := p.data.GetAllLiveCurrencyReserves(ctx)
6368
if err != nil {
6469
p.log.With(zap.Error(err)).Warn("failed getting all available currencies")
6570
return
6671
}
6772

68-
for mint := range liveHolderRecordsByMint {
73+
for mint, reserveRecord := range liveReserveRecordsByMint {
6974
log := p.log.With(zap.String("mint", mint))
7075

71-
holderCount, err := p.countHoldersForMint(ctx, mint)
76+
holderCount, err := p.countHoldersForMint(ctx, mint, reserveRecord.SupplyFromBonding)
7277
if err != nil {
7378
log.With(zap.Error(err)).Warn("failed counting holders for mint")
7479
continue
@@ -112,7 +117,16 @@ func (p *holderRuntime) UpdateAllLaunchpadCurrencyHolderCounts(ctx context.Conte
112117
}
113118
}
114119

115-
func (p *holderRuntime) countHoldersForMint(ctx context.Context, mint string) (uint64, error) {
120+
func (p *holderRuntime) countHoldersForMint(ctx context.Context, mint string, currentSupply uint64) (uint64, error) {
121+
minHoldings := currencycreator.EstimateValueExchange(&currencycreator.EstimateValueExchangeArgs{
122+
CurrentSupplyInQuarks: currentSupply,
123+
ValueInQuarks: minHoldingValue,
124+
ValueMintDecimals: uint8(common.CoreMintDecimals),
125+
})
126+
if minHoldings == 0 {
127+
return 0, nil
128+
}
129+
116130
accountRecords, err := p.data.GetAccountInfosByMintAndType(ctx, mint, commonpb.AccountType_PRIMARY)
117131
if err == account.ErrAccountInfoNotFound {
118132
return 0, nil
@@ -139,7 +153,7 @@ func (p *holderRuntime) countHoldersForMint(ctx context.Context, mint string) (u
139153

140154
var count uint64
141155
for _, bal := range balances {
142-
if bal > 0 {
156+
if bal >= minHoldings {
143157
count++
144158
}
145159
}

solana/currencycreator/discrete_exponential_curve.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,104 @@ func (c *DiscreteExponentialCurve) ValueToTokens(currentSupply, value *big.Float
205205

206206
return totalTokens
207207
}
208+
209+
// TokensForValueExchange calculates the number of tokens that should be exchanged
210+
// (sold) starting at `currentSupply` to receive `value` in return.
211+
// This is equivalent to "How many tokens do I need to sell to get Y value?"
212+
// Returns nil if the supply is beyond table bounds or if there aren't enough tokens.
213+
// Supports fractional tokens - does not round up or down.
214+
func (c *DiscreteExponentialCurve) TokensForValueExchange(currentSupply, value *big.Float) *big.Float {
215+
zero := big.NewFloat(0)
216+
if value.Cmp(zero) == 0 {
217+
return big.NewFloat(0)
218+
}
219+
220+
stepSizeFloat := big.NewFloat(DiscretePricingStepSize)
221+
scaleFloat := new(big.Float).SetPrec(defaultCurvePrec).SetInt(c.scale)
222+
223+
// Calculate start step
224+
startStepFloat := new(big.Float).SetPrec(defaultCurvePrec).Quo(currentSupply, stepSizeFloat)
225+
startStepInt, _ := startStepFloat.Int(nil)
226+
startStep := startStepInt.Int64()
227+
228+
if startStep < 0 || int(startStep) >= len(DiscretePricingTable) {
229+
return nil
230+
}
231+
232+
// Convert value to scaled float for comparison with scaled prices
233+
valueScaled := new(big.Float).SetPrec(defaultCurvePrec).Mul(value, scaleFloat)
234+
235+
// Calculate tokens available in the current partial step (from step boundary to currentSupply)
236+
startStepBoundary := new(big.Float).SetPrec(defaultCurvePrec).Mul(big.NewFloat(float64(startStep)), stepSizeFloat)
237+
tokensInCurrentPartialStep := new(big.Float).SetPrec(defaultCurvePrec).Sub(currentSupply, startStepBoundary)
238+
startPrice := new(big.Float).SetPrec(defaultCurvePrec).SetInt(DiscretePricingTable[startStep])
239+
costOfCurrentPartialStep := new(big.Float).SetPrec(defaultCurvePrec).Mul(tokensInCurrentPartialStep, startPrice)
240+
241+
// If the value fits within the current partial step, divide value by price for fractional tokens
242+
if valueScaled.Cmp(costOfCurrentPartialStep) <= 0 {
243+
tokens := new(big.Float).SetPrec(defaultCurvePrec).Quo(valueScaled, startPrice)
244+
return tokens
245+
}
246+
247+
// We can at least consume the entire current partial step
248+
remainingValueScaled := new(big.Float).SetPrec(defaultCurvePrec).Sub(valueScaled, costOfCurrentPartialStep)
249+
250+
// baseCumulative is the total value of all complete steps below startStep (steps 0..startStep-1)
251+
baseCumulative := new(big.Float).SetPrec(defaultCurvePrec).SetInt(DiscreteCumulativeValueTable[startStep])
252+
253+
// Check if there's enough value in the entire supply below
254+
if remainingValueScaled.Cmp(baseCumulative) > 0 {
255+
return nil
256+
}
257+
258+
// Target cumulative: find endStep where cumulative[endStep] >= baseCumulative - remainingValueScaled
259+
targetCumulative := new(big.Float).SetPrec(defaultCurvePrec).Sub(baseCumulative, remainingValueScaled)
260+
261+
// Convert to big.Int for binary search (use ceiling to avoid overshooting complete steps)
262+
targetCumulativeInt, accuracy := targetCumulative.Int(nil)
263+
if accuracy == big.Below {
264+
targetCumulativeInt.Add(targetCumulativeInt, big.NewInt(1))
265+
}
266+
267+
// Binary search for the smallest endStep where cumulative[endStep] >= targetCumulativeInt
268+
low := 0
269+
high := int(startStep)
270+
271+
for low < high {
272+
mid := (low + high) / 2
273+
if DiscreteCumulativeValueTable[mid].Cmp(targetCumulativeInt) >= 0 {
274+
high = mid
275+
} else {
276+
low = mid + 1
277+
}
278+
}
279+
280+
endStep := low
281+
282+
// Calculate tokens from complete steps (selling steps startStep-1 down to endStep)
283+
endStepSupply := new(big.Float).SetPrec(defaultCurvePrec).Mul(big.NewFloat(float64(endStep)), stepSizeFloat)
284+
tokensFromCompleteSteps := new(big.Float).SetPrec(defaultCurvePrec).Sub(startStepBoundary, endStepSupply)
285+
286+
// Calculate remaining value after complete steps
287+
cumulativeAtEndStep := new(big.Float).SetPrec(defaultCurvePrec).SetInt(DiscreteCumulativeValueTable[endStep])
288+
valueUsedForCompleteSteps := new(big.Float).SetPrec(defaultCurvePrec).Sub(baseCumulative, cumulativeAtEndStep)
289+
remainingValue := new(big.Float).SetPrec(defaultCurvePrec).Sub(remainingValueScaled, valueUsedForCompleteSteps)
290+
291+
// Sell fractional tokens in the step below endStep with remaining value
292+
var tokensInEndPartialStep *big.Float
293+
if remainingValue.Cmp(zero) > 0 {
294+
if endStep == 0 {
295+
return nil
296+
}
297+
endPrice := new(big.Float).SetPrec(defaultCurvePrec).SetInt(DiscretePricingTable[endStep-1])
298+
tokensInEndPartialStep = new(big.Float).SetPrec(defaultCurvePrec).Quo(remainingValue, endPrice)
299+
} else {
300+
tokensInEndPartialStep = new(big.Float).SetPrec(defaultCurvePrec).SetFloat64(0)
301+
}
302+
303+
// Total tokens = partial top step + complete middle steps + fractional bottom step
304+
totalTokens := new(big.Float).SetPrec(defaultCurvePrec).Add(tokensInCurrentPartialStep, tokensFromCompleteSteps)
305+
totalTokens.Add(totalTokens, tokensInEndPartialStep)
306+
307+
return totalTokens
308+
}

solana/currencycreator/discrete_exponential_curve_test.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,211 @@ func TestDiscreteFractionalSupply(t *testing.T) {
443443
assertApproxEq(t, tokensBack, big.NewFloat(10), 0.0000000001, "Tokens from fractional supply")
444444
}
445445

446+
func TestDiscreteTokensForValueExchangeZeroValue(t *testing.T) {
447+
curve := DefaultDiscreteExponentialCurve()
448+
449+
// Test with 0 value from various supplies
450+
for _, supplyVal := range []float64{0, 50, 100, 1000, 10000} {
451+
supply := big.NewFloat(supplyVal)
452+
value := big.NewFloat(0)
453+
tokens := curve.TokensForValueExchange(supply, value)
454+
if tokens.Cmp(big.NewFloat(0)) != 0 {
455+
t.Errorf("0 value from supply %f should yield 0 tokens, got %s",
456+
supplyVal, tokens.Text('f', 18))
457+
}
458+
}
459+
}
460+
461+
func TestDiscreteTokensForValueExchangeWithinSingleStep(t *testing.T) {
462+
curve := DefaultDiscreteExponentialCurve()
463+
464+
// Supply at 50, sell tokens within the same step (step 0)
465+
supply := big.NewFloat(50)
466+
price0 := big.NewFloat(0.01)
467+
468+
// Value for 25 tokens at price0 = 0.25
469+
valueFor25 := new(big.Float).Mul(price0, big.NewFloat(25))
470+
tokens := curve.TokensForValueExchange(supply, valueFor25)
471+
assertApproxEq(t, tokens, big.NewFloat(25), 0.0000000001, "Tokens for value within single step")
472+
}
473+
474+
func TestDiscreteTokensForValueExchangeCrossingBoundary(t *testing.T) {
475+
curve := DefaultDiscreteExponentialCurve()
476+
477+
// Supply at 250, sell tokens crossing step boundaries
478+
supply := big.NewFloat(250)
479+
sellTokens := big.NewFloat(150)
480+
481+
// Calculate the value of selling 150 tokens from supply 250
482+
// (that's TokensToValue from supply 100 to 250)
483+
newSupply := new(big.Float).Sub(supply, sellTokens)
484+
value := curve.TokensToValue(newSupply, sellTokens)
485+
486+
// Now TokensForValueExchange should return 150
487+
tokensResult := curve.TokensForValueExchange(supply, value)
488+
assertApproxEq(t, tokensResult, sellTokens, 1, "Tokens for value crossing boundary")
489+
}
490+
491+
func TestDiscreteTokensForValueExchangeRoundtrip(t *testing.T) {
492+
curve := DefaultDiscreteExponentialCurve()
493+
494+
testCases := []struct {
495+
supply float64
496+
tokens float64
497+
}{
498+
{100, 50},
499+
{100, 100},
500+
{250, 150},
501+
{500, 250},
502+
{1000, 500},
503+
{1000, 1000},
504+
{10000, 5000},
505+
{50000, 25000},
506+
}
507+
508+
for _, tc := range testCases {
509+
supply := big.NewFloat(tc.supply)
510+
sellTokens := big.NewFloat(tc.tokens)
511+
512+
// Calculate value of selling these tokens
513+
newSupply := new(big.Float).Sub(supply, sellTokens)
514+
value := curve.TokensToValue(newSupply, sellTokens)
515+
if value == nil {
516+
t.Errorf("TokensToValue returned nil for supply=%f, tokens=%f", tc.supply, tc.tokens)
517+
continue
518+
}
519+
520+
// Roundtrip: value -> tokens should give back the original tokens
521+
tokensBack := curve.TokensForValueExchange(supply, value)
522+
if tokensBack == nil {
523+
t.Errorf("TokensForValueExchange returned nil for supply=%f, value=%s", tc.supply, value.Text('f', 18))
524+
continue
525+
}
526+
527+
assertApproxEq(t, tokensBack, sellTokens, 1,
528+
fmt.Sprintf("Roundtrip value->tokens for supply=%f, tokens=%f", tc.supply, tc.tokens))
529+
}
530+
}
531+
532+
func TestDiscreteTokensForValueExchangeFractionalRoundtrip(t *testing.T) {
533+
curve := DefaultDiscreteExponentialCurve()
534+
535+
testCases := []struct {
536+
supply float64
537+
tokens float64
538+
}{
539+
{50.5, 25.3},
540+
{100.25, 50.75},
541+
{250.123, 100.456},
542+
{1000.5, 500.25},
543+
{10000.5, 1234.567},
544+
}
545+
546+
for _, tc := range testCases {
547+
supply := big.NewFloat(tc.supply)
548+
sellTokens := big.NewFloat(tc.tokens)
549+
550+
// Calculate value of selling these tokens
551+
newSupply := new(big.Float).Sub(supply, sellTokens)
552+
value := curve.TokensToValue(newSupply, sellTokens)
553+
if value == nil {
554+
t.Errorf("TokensToValue returned nil for supply=%f, tokens=%f", tc.supply, tc.tokens)
555+
continue
556+
}
557+
558+
// Roundtrip: value -> tokens should give back the original tokens
559+
tokensBack := curve.TokensForValueExchange(supply, value)
560+
if tokensBack == nil {
561+
t.Errorf("TokensForValueExchange returned nil for supply=%f, value=%s", tc.supply, value.Text('f', 18))
562+
continue
563+
}
564+
565+
assertApproxEq(t, tokensBack, sellTokens, 0.0000000001,
566+
fmt.Sprintf("Fractional roundtrip value->tokens for supply=%f, tokens=%f", tc.supply, tc.tokens))
567+
}
568+
}
569+
570+
func TestDiscreteTokensForValueExchangeEntireSupply(t *testing.T) {
571+
curve := DefaultDiscreteExponentialCurve()
572+
573+
// Selling the entire supply should return all tokens
574+
supply := big.NewFloat(1000)
575+
zero := big.NewFloat(0)
576+
totalValue := curve.TokensToValue(zero, supply)
577+
578+
tokens := curve.TokensForValueExchange(supply, totalValue)
579+
assertApproxEq(t, tokens, supply, 1, "Selling entire supply should return all tokens")
580+
}
581+
582+
func TestDiscreteTokensForValueExchangeExceedsSupplyReturnsNil(t *testing.T) {
583+
curve := DefaultDiscreteExponentialCurve()
584+
585+
supply := big.NewFloat(100)
586+
zero := big.NewFloat(0)
587+
totalValue := curve.TokensToValue(zero, supply)
588+
589+
// Try to extract more value than the entire supply is worth
590+
excessValue := new(big.Float).Add(totalValue, big.NewFloat(1))
591+
tokens := curve.TokensForValueExchange(supply, excessValue)
592+
if tokens != nil {
593+
t.Errorf("Value exceeding supply should return nil, got %s", tokens.Text('f', 18))
594+
}
595+
}
596+
597+
func TestDiscreteTokensForValueExchangeZeroSupplyReturnsNil(t *testing.T) {
598+
curve := DefaultDiscreteExponentialCurve()
599+
600+
// At zero supply, any positive value should return nil
601+
supply := big.NewFloat(0)
602+
value := big.NewFloat(1)
603+
tokens := curve.TokensForValueExchange(supply, value)
604+
if tokens != nil {
605+
t.Errorf("Zero supply with positive value should return nil, got %s", tokens.Text('f', 18))
606+
}
607+
}
608+
609+
func TestDiscreteTokensForValueExchangeSellingInPartsEqualsSellingAllAtOnce(t *testing.T) {
610+
curve := DefaultDiscreteExponentialCurve()
611+
612+
supply := big.NewFloat(500)
613+
614+
// Sell 300 tokens total: first sell for some value, then sell more for more value
615+
sellTokens := big.NewFloat(300)
616+
newSupply := new(big.Float).Sub(supply, sellTokens)
617+
totalValue := curve.TokensToValue(newSupply, sellTokens)
618+
619+
// Split the value into two parts
620+
value1 := new(big.Float).Quo(totalValue, big.NewFloat(3))
621+
value2 := new(big.Float).Sub(totalValue, value1)
622+
623+
// Sell for value1 first
624+
tokens1 := curve.TokensForValueExchange(supply, value1)
625+
supplyAfterFirstSell := new(big.Float).Sub(supply, tokens1)
626+
627+
// Then sell for value2
628+
tokens2 := curve.TokensForValueExchange(supplyAfterFirstSell, value2)
629+
630+
// Total tokens should equal selling all at once
631+
tokensAll := curve.TokensForValueExchange(supply, totalValue)
632+
tokensParts := new(big.Float).Add(tokens1, tokens2)
633+
634+
assertApproxEq(t, tokensParts, tokensAll, 1,
635+
"Selling in parts should equal selling all at once")
636+
}
637+
638+
func TestDiscreteTokensForValueExchangeLargeAcrossManySteps(t *testing.T) {
639+
curve := DefaultDiscreteExponentialCurve()
640+
641+
supply := big.NewFloat(1234567)
642+
sellTokens := big.NewFloat(10000)
643+
644+
newSupply := new(big.Float).Sub(supply, sellTokens)
645+
value := curve.TokensToValue(newSupply, sellTokens)
646+
647+
tokensBack := curve.TokensForValueExchange(supply, value)
648+
assertApproxEq(t, tokensBack, sellTokens, 1, "Large exchange across many steps roundtrip")
649+
}
650+
446651
func TestGenerateDiscreteCurveTable(t *testing.T) {
447652
t.Skip()
448653

0 commit comments

Comments
 (0)