Skip to content

Conversation

rickyrombo
Copy link
Contributor

Main changes:

  • Adds new tables to track DAMM V2 pools and positions
  • Adds SQL function that notifies on new migrations
  • Adds parsing of DBC instruction to index migrations
  • Adds new SolanaIndexer DAMM V2 Indexer that listens for new migration notification and restarts, subscribing to all DAMM v2 pools and positions
  • Adds SQL function that calculates total and unclaimed fees
  • Adds fee struct to coins/coin/cointicker endpoints

Extras:

  • Adds a withRetries helper that doesn't have a result
  • Adds a Uint256LE helper struct for deserializing 256-bit unsigned integers in little endian
  • Fixes bug with ProcessSignature skipping failed transactions on retry because they were in the fetch cache
  • Makes coin and coinTicker requests share the same sql (will prevent bugs in the future imo)
  • Makes SolanaIndexer check more frequently for unprocessed transactions in the deadletter queue in dev environment (helps testing - just insert a signature and see if it can process it)
  • Fixes transaction retry ticker job from not running (it was exiting and tearing down the ticker)

TODO:

  • Remove wip code that isn't needed
  • Add tests
  • Restructure so that the files all make sense (may need to rename some existing SolanaIndexer things to make more sense)
  • Make SDK types

'totalTradingQuoteFee', COALESCE(artist_coin_pools.total_trading_quote_fee, 0),
'creatorWalletAddress', COALESCE(artist_coin_pools.creator_wallet_address, '')
) AS dynamic_bonding_curve,
ROW_TO_JSON(calculate_artist_coin_fees(artist_coins.mint)) AS artist_fees,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: kinda yucky but because I'm doing ROW_TO_JSON here, the struct deserializes using the JSON struct tag names. Thus the JSON struct tags need to be underscores. To my knowledge, it's not possible to have one JSON struct tag for serializing and another for deserializing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead could we just select the fields off of the resulting row from calculate_artist_coin_fees?

i think this is also okay though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah and use JSON_BUILD_OBJECT? we could do that. I could also make that function return camelCased columns...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should live somewhere higher up i think

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking for ideas on how to organize now that there will be likely multiple indexers in this folder. I think the current model isn't great of having everything dumped in this one package. One thing I'm considering is separating out the processors and indexers perhaps... then there could be indexer.ArtistCoins and indexer.DammV2

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's possible we support a coin that's launched outside of our dbc, so i like having indexer.Dbc and indexer.DammV2

I don't hate what you've done here though tbh, it's not bad to follow!

return grpcClients, nil
}

func watchPgNotification(ctx context.Context, pool database.DbPool, notification string, callback notificationCallback, logger *zap.Logger) error {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

planning to move this to utils or somewhere and apply a similar strategy to the existing indexer.

Copy link
Contributor

@dylanjeffers dylanjeffers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking great

* (
position.unlocked_liquidity + position.vested_liquidity + position.permanent_locked_liquidity
)
/ POWER (2, 128)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the liquidity is represented as an enormous integer but is actually shifted 128 bits - AI says its a method for doing precise math w/ large fixed fractional components (In this case, 128bits for the whole component, and 128bits for the fractional component?)

See: https://github.com/MeteoraAg/damm-v2-sdk/blob/70d1af59689039a1dc700dee8f741db48024d02d/src/helpers/utils.ts#L190-L191

pretty interesting.. wish they had better explainers

+ position.fee_b_pending
) AS total_damm_v2_fees,
(
(pool.fee_b_per_liquidity - position.fee_b_per_token_checkpoint)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is fee_b_per_token_checkpoint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is updated as you claim and marks the amount you've already claimed basically

pool TEXT PRIMARY KEY REFERENCES sol_meteora_damm_v2_pools(address) ON DELETE CASCADE,
protocol_fee_percent SMALLINT NOT NULL,
partner_fee_percent SMALLINT NOT NULL,
referral_fee_percent SMALLINT NOT NULL,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's referral fee?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't actually know! Would be curious to learn (it comes from the fee configuration on the Pool struct on chain: https://explorer.solana.com/address/9avVRGRvPsSYiXKBMHnC6RNPbwN5yE3v7fD8FibgScwA/anchor-account looks like ours is 20%?)

"total_claimed_a_fee": metrics.TotalClaimedAFee,
"total_claimed_b_fee": metrics.TotalClaimedBFee,
})
return err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good

statsCtx := context.WithoutCancel(ctx)
statsJob.ScheduleEvery(statsCtx, 5*time.Minute)
go statsJob.Run(statsCtx)
// statsJob := jobs.NewCoinStatsJob(s.config, s.pool)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch - yeah had these turned off for testing

Copy link
Member

@raymondjacobson raymondjacobson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

things look pretty great here to me. my only concern is the amount of code we've written, especially for the instruction.go files - i really think we should use their packages for these if we can. i feel like there should be ways to not have to implement a lot of this ourselves. especially stuff like Uint256LE

SELECT
pool.token_a_mint AS mint,
(
pool.fee_b_per_liquidity
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirming that b is always what we care about (I think so) and that a is our "base" mint

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's possible we support a coin that's launched outside of our dbc, so i like having indexer.Dbc and indexer.DammV2

I don't hate what you've done here though tbh, it's not bad to follow!

subscription.Slots = make(map[string]*pb.SubscribeRequestFilterSlots)
subscription.Slots["checkpoints"] = &pb.SubscribeRequestFilterSlots{}

// fromSlot := uint64(372380625)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👻

}

switch inst.TypeID {
case meteora_dbc.InstructionImplDef.TypeID(meteora_dbc.Instruction_MigrationDammV2):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

'totalTradingQuoteFee', COALESCE(artist_coin_pools.total_trading_quote_fee, 0),
'creatorWalletAddress', COALESCE(artist_coin_pools.creator_wallet_address, '')
) AS dynamic_bonding_curve,
ROW_TO_JSON(calculate_artist_coin_fees(artist_coins.mint)) AS artist_fees,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead could we just select the fields off of the resulting row from calculate_artist_coin_fees?

i think this is also okay though

@rickyrombo
Copy link
Contributor Author

rickyrombo commented Oct 13, 2025

@raymondjacobson

i really think we should use their packages for these if we can

I'm not aware of any of their packages doing the deserializing of instructions. Most of their SDKs are focused on building the instructions...

They do have deserializing of state that we could be using, but they use different packages etc and it honestly seems more manual than what I have here. I have copied the structs nearly verbatim but using different types for eg UInt128 that are native to the binary deserializer that we use, and worked to make these fit well with our current patterns rather than adopting a new one. I think the state gives me a lot of confidence actually because it's all just using the borsh decoder directly rather than handrolling, and it's easily tested against. It might be nice actually to have something that converts IDL files into go structs for us, but that's probably unnecessary. One concern I have bringing in the package they have is that we will have mismatching versions of things and increased bloat, especially when all we really need are the types in the account state themselves, and the patterns for things will be different per program. Having one common place and pattern for communicating with programs and chain state makes sense to me.

As far as Uint256LE, yeah agree that's a bit of a smell... it should map 1:1 to what they do though, and requires less manual logic to work with...

@rickyrombo rickyrombo closed this Oct 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants