package ucs03_zkgm
import (
"strings"
"gno.land/p/demo/tokens/grc20"
"gno.land/p/nt/fqname/v0"
z "gno.land/p/onbloc/ibc/union/zkgm"
u256 "gno.land/p/onbloc/math/uint256"
"gno.land/r/demo/defi/grc20reg"
zkgm "gno.land/r/onbloc/ibc/union/apps/ucs03_zkgm"
)
// canonicalDecimals is the ledger precision a wrapped voucher is capped to.
// A source token with more decimals overflows the grc20 int64 ledger,
// so its amounts are downscaled to this precision on the way in,
// and upscaled back on the way out.
const canonicalDecimals = 6
// mintVoucher mints amount of the wrapped voucher for ibcDenom to receiver,
// creating the token on first use.
// The voucher is created before the mint,
// so a first-use mint already has the origin decimals bundled in;
// MintScaled then downscales the origin-precision amount to the ledger.
func (v *ucs03ZkgmV1) mintVoucher(_ int, rlm realm, ibcDenom string, image [32]byte, name, symbol string, decimals uint8, receiver address, amount *u256.Uint) error {
vou, err := v.getOrCreateVoucher(0, rlm, ibcDenom, image, name, symbol, decimals)
if err != nil {
return err
}
return vou.MintScaled(receiver, amount)
}
// burnVoucher burns amount of the ibc/ wrapped voucher held by sender.
// Used on send-side UNESCROW (destroy wrapped supply before remote unwrap) and
// maker burn-address ack (burnEscrowedV2). GRC20 and native coins are never
// passed here — they cannot be supply-burned by this realm.
// BurnScaled downscales the origin-precision amount to the ledger.
func (v *ucs03ZkgmV1) burnVoucher(_ int, rlm realm, ibcDenom string, sender address, amount *u256.Uint) error {
vou, ok := v.store.GetVoucher(ibcDenom)
if !ok {
return makeError(errVoucherNotFound, ibcDenom)
}
return vou.BurnScaled(sender, amount)
}
// transferVoucher moves amount of the wrapped voucher between holders (market-maker fill).
// TransferScaled downscales the origin-precision amount to the ledger.
func (v *ucs03ZkgmV1) transferVoucher(_ int, rlm realm, ibcDenom string, from, to address, amount *u256.Uint) error {
vou, ok := v.store.GetVoucher(ibcDenom)
if !ok {
return makeError(errVoucherNotFound, ibcDenom)
}
return vou.TransferScaled(from, to, amount)
}
// VoucherApprove lets the caller (rlm.Previous(), the address that called the
// proxy) grant spender an allowance on their own wrapped-voucher balance.
// Delegates to Voucher.Approve so mint/burn stay reachable exclusively via
// mintVoucher/burnVoucher above; this never touches Ledger() directly.
func (v *ucs03ZkgmV1) VoucherApprove(_ int, rlm realm, ibcDenom string, spender address, amount int64) error {
assertIsRlmCurrent(0, rlm)
vou, ok := v.store.GetVoucher(ibcDenom)
if !ok {
return makeError(errVoucherNotFound, ibcDenom)
}
return vou.Approve(0, rlm, spender, amount)
}
// VoucherTransfer lets the caller (rlm.Previous(), the address that called the
// proxy) move their own wrapped-voucher balance directly to another address.
// Same capability scope as VoucherApprove: Transfer only, no mint/burn.
func (v *ucs03ZkgmV1) VoucherTransfer(_ int, rlm realm, ibcDenom string, to address, amount int64) error {
assertIsRlmCurrent(0, rlm)
vou, ok := v.store.GetVoucher(ibcDenom)
if !ok {
return makeError(errVoucherNotFound, ibcDenom)
}
return vou.Transfer(0, rlm, to, amount)
}
// getOrCreateVoucher returns the wrapped grc20 voucher for ibcDenom, creating and registering it on first use.
func (v *ucs03ZkgmV1) getOrCreateVoucher(_ int, rlm realm, ibcDenom string, image [32]byte, name, symbol string, decimals uint8) (*zkgm.Voucher, error) {
if vou, ok := v.store.GetVoucher(ibcDenom); ok {
return vou, nil
}
if name == "" {
name = ibcDenom
}
symbol = sanitizeVoucherSymbol(symbol, ibcDenom)
// grc20reg keys its registry by (registering realm, token symbol) and
// panics on a repeat key. Every voucher this realm registers shares the
// same realm path, so a symbol collision here always means a different
// ibcDenom already claimed this symbol. Check the prospective key
// ourselves so that case fails with a controlled error instead of a panic.
key := fqname.Construct(rlm.PkgPath(), symbol)
if grc20reg.Get(key) != nil {
return nil, makeError(errVoucherSymbolAlreadyRegistered, symbol)
}
// The grc20 ledger is int64,
// so a source token above canonicalDecimals is created at canonicalDecimals.
// The origin decimals are bundled into the voucher (see Voucher.ScaleExp),
// so only the local ledger is downscaled; the packet keeps origin precision.
ledgerDecimals := decimals
if scaleExpForDecimals(decimals) > 0 {
ledgerDecimals = canonicalDecimals
}
token, ledger := grc20.NewToken(name, symbol, int(ledgerDecimals), 0, rlm)
grc20reg.Register(cross(rlm), token, strings.TrimPrefix(ibcDenom, ibcDenomPrefix))
// originDecimals travels with the token,
// so every mint/burn/transfer derives the same scale from it and the ledger token,
// with no parallel map to keep in sync.
voucher := zkgm.NewVoucher(token, ledger, decimals)
v.store.SetVoucher(0, rlm, ibcDenom, voucher)
return voucher, nil
}
// ledgerAmountOf downscales an origin-precision amount to the ledger amount for a denom.
// It delegates to Voucher.ToLedgerAmount when a voucher exists,
// otherwise it is an identity narrow for native/low-decimal denoms.
// Used by the token-order pre-flight checks, which hold only a denom string.
func (v *ucs03ZkgmV1) ledgerAmountOf(ibcDenom string, amount *u256.Uint) (int64, error) {
if vou, ok := v.store.GetVoucher(ibcDenom); ok {
return vou.ToLedgerAmount(amount)
}
return amountInt64(amount)
}
// scaleExpOf returns the origin->ledger downscale exponent registered for a
// denom, or 0 when no voucher exists for it (native and low-decimal tokens).
func (v *ucs03ZkgmV1) scaleExpOf(ibcDenom string) int {
if vou, ok := v.store.GetVoucher(ibcDenom); ok {
return vou.ScaleExp()
}
return 0
}
// sanitizeVoucherSymbol derives a grc20-legal symbol (<=MaxSymbolLen, [A-Za-z0-9_-])
// from the candidate, falling back to a prefix of the IBC hash when empty.
func sanitizeVoucherSymbol(candidate, ibcDenom string) string {
out := make([]byte, 0, grc20.MaxSymbolLen)
for i := 0; i < len(candidate) && len(out) < grc20.MaxSymbolLen; i++ {
c := candidate[i]
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' {
out = append(out, c)
}
}
if len(out) == 0 {
fallback := strings.TrimPrefix(ibcDenom, ibcDenomPrefix)
if len(fallback) > grc20.MaxSymbolLen {
fallback = fallback[:grc20.MaxSymbolLen]
}
return fallback
}
return string(out)
}
// scaleExpForDecimals returns the origin->ledger downscale exponent for `decimals`:
// the low-order digits dropped so the ledger holds canonicalDecimals.
// Tokens at or below canonicalDecimals return 0.
// Used at creation to cap the ledger,
// and by the INITIALIZE pre-flight check before a voucher exists.
func scaleExpForDecimals(decimals uint8) int {
if int(decimals) > canonicalDecimals {
return int(decimals) - canonicalDecimals
}
return 0
}
// amountInt64 narrows a uint256 to int64 for the native banker/grc20 paths (no scaling).
// It is the scaleExp-0 case of the shared downscaler,
// kept as a named helper for the native call sites (funds.sub, sendNative).
func amountInt64(amount *u256.Uint) (int64, error) {
return z.ScaleDownToInt64(amount, 0)
}