To support multiple token standards, we recommend using a SendManager module with alias denom routing. It automatically routes transactions to the appropriate handler based on denomination prefixes.
For complete implementation details, see the source code. We recommend simply copying / importing the implementation and using it directly if all you need is x/badges and x/bank.
It is intended to be a drop-in replacement for the BankKeeper interface. Simply replace anywhere you call k.bankKeeper.SendCoins() with k.sendManagerKeeper.SendCoinsWithAliasRouting() (or whatever other functions you need) and get support for multiple token standards at once via dynamic routing.
SendManager uses prefix-based pattern matching to route denoms. It checks each registered prefix against the denom and routes to the first matching handler:
// Example: denom = "badgeslp:1:utoken"func(k Keeper)getRouterForDenom(denomstring)(types.AliasDenomRouter,bool){// Check registered prefixes in order (e.g., ["badgeslp:", "badges:"])for_,prefix:=rangek.registeredPrefixes{// Does "badgeslp:1:utoken" start with "badgeslp:"? Yes!ifstrings.HasPrefix(denom,prefix){// Return the router registered for this prefixreturnk.prefixToRouter[prefix],true}}// No prefix matched - will use bank keeper for standard coinsreturnnil,false}
Examples:
badgeslp:1:utoken β matches badgeslp: prefix β routes to x/badges
uatom β no prefix match β routes to x/bank (standard Cosmos SDK coin)
Alias Denoms
BitBadges format: badgeslp:COLLECTION_ID:denom (e.g., badgeslp:1:utoken). The integer amount converts to Balances[] via collection's cosmosCoinWrapperPaths field (where the conversion rate is defined). Note that this process has no wrapping involved. The conversion is simply an alias for the full Balances[] field for compatibility with other environments.
Auto-Scan Mode
All sends through SendManager use auto-scan mode (no prioritized approvals). The underlying transfer is executed via MsgTransferTokens:
User-Level Approvals
SendManager does not manage user-level approvals automatically. If they need to be handled, you must set them as needed elsewhere before / after calling SendManager. Note: All x/badges transfers require approvals to be satisfied on the collection, sender, and recipient level, where applicable.
This may be especially important for module addresses or special non-user addresses. They automatically inherit the defaults for the collection.
Example: Setting approvals before sending (from FundCommunityPoolViaAliasDenom)
For example, the community pool address (depending on the defaults) may not accept incoming approvals by default which is needed to transfer tokens to it in the x/badges module.
// Example: Sometimes, you may need both pre and post approval updates to make stuff work and clean up.
preUpdateApprovalsMsg := &badgestypes.MsgUpdateUserApprovals{ ... }
badgesMsgServer.UpdateUserApprovals(ctx, preUpdateApprovalsMsg)
sendManagerKeeper.SendCoinsWithAliasRouting(ctx, from, to, coins)
postUpdateApprovalsMsg := &badgestypes.MsgUpdateUserApprovals{ ... }
badgesMsgServer.UpdateUserApprovals(ctx, postUpdateApprovalsMsg)
func (k Keeper) SendCoinsWithAliasRouting(ctx sdk.Context, from, to sdk.AccAddress, coins sdk.Coins) error {
for _, coin := range coins {
router, found := k.getRouterForDenom(coin.Denom)
if found {
// Alias denom - use custom router
amountUint := sdkmath.NewUintFromBigInt(coin.Amount.BigInt())
return router.SendNativeTokensViaAliasDenom(ctx, from.String(), to.String(), coin.Denom, amountUint)
}
// Standard coin - use bank keeper
return k.bankKeeper.SendCoins(ctx, from, to, sdk.NewCoins(coin))
}
}
// Sending - works for both standard coins ("uatom") and alias denoms ("badgeslp:1:utoken")
err := sendManagerKeeper.SendCoinsWithAliasRouting(ctx, from, to, coins)
// Querying - automatically routes to correct handler
balance, err := sendManagerKeeper.GetBalanceWithAliasRouting(ctx, address, denom)