memepad.gno
23.03 Kb · 841 lines
1// Package pad is gnomemepad: a self-contained meme launchpad for gno.land.
2//
3// Direction A — factory IS the market (no external AMM required for MVP):
4//
5// Create → GRC20 token + bonding curve → Graduate (atomic locked CPMM)
6// → GRC20 is listable on Gnoswap (external DEX); pad keeps locked pool too
7//
8// Tokens are real GRC20 (mint on buy, burn on sell) so holders can transfer and
9// later provide liquidity on Gnoswap. Hybrid of Pump.fun + permanent LP lock.
10package padv3
11
12import (
13 "chain"
14 "chain/banker"
15 "chain/runtime"
16 "chain/runtime/unsafe"
17 "strconv"
18 "strings"
19
20 "gno.land/p/demo/tokens/grc20"
21 "gno.land/p/g1mv0052e7r6s09f5t9xsqf00nj3tqsgt9dg52jr/gnomemepad/ammmath"
22 "gno.land/p/nt/avl/v0"
23 "gno.land/p/nt/seqid/v0"
24 "gno.land/r/demo/defi/grc20reg"
25)
26
27var (
28 launches avl.Tree // id -> *Launch
29 bySymbol avl.Tree // symbol -> id string
30 nextID seqid.ID
31 nextTokenID seqid.ID // GRC20 identity sequence (shared for all launches)
32 protocolAddr address
33 protocolFees int64
34 inited bool
35 // testSkipBanker: when true, sendUgnot is a no-op (unit tests without funded realm bank).
36 // Always false in production.
37 testSkipBanker bool
38)
39
40// Trade is one price sample for charts (capped history per launch).
41type Trade struct {
42 Height int64
43 Side int // TradeSideBuy | TradeSideSell | TradeSideOpen
44 Ugnot int64
45 Tokens int64
46 Price int64 // ugnot per token * 1e6 after the trade
47}
48
49// Launch is one meme market: curve phase then locked pool phase.
50type Launch struct {
51 ID string
52 Name string
53 Symbol string
54 URI string
55 Creator address
56 Status int
57 Created int64 // block height
58
59 // GRC20 (mint/burn via pad; transferable off-pad after buy)
60 Token *grc20.Token
61 Ledger *grc20.PrivateLedger
62 TokenID string // Token.ID() — registry / Gnoswap identity
63
64 // Virtual curve reserves
65 VirtualUgnot int64
66 VirtualToken int64
67 RealSold int64 // tokens sold on curve (≤ CurveSupply)
68 RaisedUgnot int64 // net ugnot collateral in curve (excl. fee vaults)
69
70 // Real pool (post-grad); LP permanently locked — no remove path
71 // PoolToken is pad-internal reserve (not the same as GRC20 total supply).
72 PoolUgnot int64
73 PoolToken int64
74
75 CreatorFees int64
76 BondUgnot int64
77 BondRefunded bool
78 UniqueBuyers avl.Tree
79 BuyerCount int
80
81 // Gnoswap listing readiness (GRC20 exists from Create; pool on Gnoswap is external)
82 GnoswapReady bool // true after graduate — token may be paired on Gnoswap
83 GnoswapNote string
84
85 // Chart history (ordered AVL keys)
86 Trades avl.Tree // tradeKey -> *Trade
87 NextTrade int64
88}
89
90// Init sets the protocol treasury. First EOA caller becomes fee recipient.
91func Init(cur realm) {
92 if inited {
93 panic("pad: already initialized")
94 }
95 if !cur.Previous().IsUserCall() {
96 panic("pad: EOA only")
97 }
98 protocolAddr = cur.Previous().Address()
99 inited = true
100 chain.Emit("Init", "protocol", protocolAddr.String())
101}
102
103func requireInit() {
104 if !inited {
105 panic("pad: call Init first")
106 }
107}
108
109func mustLaunch(id string) *Launch {
110 // Sapphire avl.Tree.Get returns a single any (nil if missing).
111 l, ok := launches.Get(id).(*Launch)
112 if !ok {
113 panic("pad: unknown launch")
114 }
115 return l
116}
117
118func balOf(l *Launch, addr address) int64 {
119 if l == nil || l.Token == nil {
120 return 0
121 }
122 return l.Token.BalanceOf(addr)
123}
124
125// addBal mints (delta>0) or burns (delta<0) GRC20 via pad-owned PrivateLedger.
126func addBal(l *Launch, addr address, delta int64) {
127 if l == nil || l.Ledger == nil {
128 panic("pad: missing GRC20 ledger")
129 }
130 if delta == 0 {
131 return
132 }
133 if delta > 0 {
134 if err := l.Ledger.Mint(addr, delta); err != nil {
135 panic("pad: mint: " + err.Error())
136 }
137 return
138 }
139 if err := l.Ledger.Burn(addr, -delta); err != nil {
140 panic("pad: burn: " + err.Error())
141 }
142}
143
144func sendUgnot(cur realm, to address, amount int64) {
145 if amount <= 0 {
146 return
147 }
148 if testSkipBanker {
149 return
150 }
151 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
152 bk.SendCoins(cur.Address(), to, chain.Coins{{Denom: DenomUgnot, Amount: amount}})
153}
154
155func requireUserPayment(cur realm) int64 {
156 if !cur.Previous().IsUserCall() {
157 panic("pad: must be EOA MsgCall")
158 }
159 sent := unsafe.OriginSend().AmountOf(DenomUgnot)
160 if sent <= 0 {
161 panic("pad: need ugnot -send")
162 }
163 return sent
164}
165
166func noteBuyer(l *Launch, buyer address) {
167 k := buyer.String()
168 if l.UniqueBuyers.Has(k) {
169 return
170 }
171 l.UniqueBuyers.Set(k, true)
172 l.BuyerCount++
173}
174
175func tradeKey(n int64) string {
176 s := strconv.FormatInt(n, 10)
177 for len(s) < 12 {
178 s = "0" + s
179 }
180 return s
181}
182
183// spotPriceScaled returns ugnot/token * 1e6 from current curve or pool reserves.
184func spotPriceScaled(l *Launch) int64 {
185 if l.Status == StatusGraduated {
186 if l.PoolToken <= 0 {
187 return 0
188 }
189 return l.PoolUgnot * 1000000 / l.PoolToken
190 }
191 if l.VirtualToken <= 0 {
192 return 0
193 }
194 return l.VirtualUgnot * 1000000 / l.VirtualToken
195}
196
197func recordTrade(l *Launch, side int, ugnot, tokens int64) {
198 l.NextTrade++
199 t := &Trade{
200 Height: runtime.ChainHeight(),
201 Side: side,
202 Ugnot: ugnot,
203 Tokens: tokens,
204 Price: spotPriceScaled(l),
205 }
206 l.Trades.Set(tradeKey(l.NextTrade), t)
207 // Ring buffer: drop oldest while over cap.
208 for l.Trades.Size() > MaxTradeHistory {
209 oldest := ""
210 l.Trades.Iterate("", "", func(k string, _ any) bool {
211 oldest = k
212 return true // stop
213 })
214 if oldest == "" {
215 break
216 }
217 l.Trades.Remove(oldest)
218 }
219}
220
221func maybeRefundBond(cur realm, l *Launch) {
222 if l.BondRefunded || l.BondUgnot <= 0 {
223 return
224 }
225 if l.BuyerCount < BondRefundBuyers {
226 return
227 }
228 if runtime.ChainHeight()-l.Created > BondRefundMaxHeights {
229 return
230 }
231 amt := l.BondUgnot
232 l.BondUgnot = 0
233 l.BondRefunded = true
234 sendUgnot(cur, l.Creator, amt)
235 chain.Emit("BondRefund", "id", l.ID, "amount", strconv.FormatInt(amt, 10))
236}
237
238// Create deploys a fair-launch meme. Requires CreateBondUgnot via -send.
239// No pre-mint; all tradeable float starts on the bonding curve.
240func Create(cur realm, name, symbol, uri string) string {
241 requireInit()
242 sent := requireUserPayment(cur)
243 if sent < CreateBondUgnot {
244 panic("pad: create bond underpaid")
245 }
246 if name == "" || symbol == "" {
247 panic("pad: name and symbol required")
248 }
249 if len(symbol) > 12 {
250 panic("pad: symbol too long")
251 }
252 if bySymbol.Has(symbol) {
253 panic("pad: symbol taken")
254 }
255 extra := sent - CreateBondUgnot
256 if extra > 0 {
257 protocolFees += extra
258 }
259
260 creator := cur.Previous().Address()
261 id := nextID.Next().String()
262
263 // Real GRC20 bound to this pad realm (mint/burn only via pad ledger).
264 // Decimals=0: whole-token units (matches existing trade amounts).
265 token, ledger := grc20.NewToken(name, symbol, 0, nextTokenID.Next(), cur)
266
267 // Adena (and Gnoswap registries) resolve tokens ONLY via grc20reg under key
268 // packagePath.SYMBOL — Token.ID() itself is packagePath.SYMBOL.seq and is
269 // rejected as "Invalid path" if pasted into Adena without registration.
270 // Skip in unit tests (testSkipBanker); production always registers.
271 regKey := ""
272 if !testSkipBanker {
273 regKey = grc20reg.Register(cross(cur), token, symbol)
274 }
275
276 l := &Launch{
277 ID: id,
278 Name: name,
279 Symbol: symbol,
280 URI: uri,
281 Creator: creator,
282 Status: StatusCurve,
283 Created: runtime.ChainHeight(),
284 Token: token,
285 Ledger: ledger,
286 TokenID: token.ID(),
287 VirtualUgnot: VirtualUgnot0,
288 VirtualToken: VirtualToken0,
289 UniqueBuyers: avl.Tree{},
290 BondUgnot: CreateBondUgnot,
291 Trades: avl.Tree{},
292 }
293 // Open mark for charts (initial virtual spot).
294 recordTrade(l, TradeSideOpen, 0, 0)
295 launches.Set(id, l)
296 bySymbol.Set(symbol, id)
297
298 chain.Emit("Created",
299 "id", id,
300 "symbol", symbol,
301 "creator", creator.String(),
302 "token", l.TokenID,
303 "reg", regKey,
304 )
305 return id
306}
307
308// AdenaPathOf returns the grc20reg / Adena token key: packagePath.SYMBOL
309// (Token.ID is packagePath.SYMBOL.seq — Adena rejects that form).
310func AdenaPathOf(id string) string {
311 l := mustLaunch(id)
312 return adenaKeyFromTokenID(l.TokenID, l.Symbol)
313}
314
315// adenaKeyFromTokenID strips the trailing .seq from Token.ID when present.
316func adenaKeyFromTokenID(tokenID, symbol string) string {
317 if tokenID == "" {
318 return ""
319 }
320 // Token.ID = packagePath.symbol.seq → registry key = packagePath.symbol
321 suffix := "." + symbol + "."
322 if i := strings.LastIndex(tokenID, suffix); i >= 0 {
323 // packagePath + "." + symbol
324 return tokenID[:i] + "." + symbol
325 }
326 // Already packagePath.symbol or unknown layout
327 if strings.HasSuffix(tokenID, "."+symbol) {
328 return tokenID
329 }
330 return tokenID
331}
332
333// Buy spends -send ugnot on the bonding curve; credits tokens to the caller.
334// Auto-graduates when RaisedUgnot >= GraduationThreshold.
335func Buy(cur realm, id string) int64 {
336 requireInit()
337 sent := requireUserPayment(cur)
338 l := mustLaunch(id)
339 if l.Status != StatusCurve {
340 panic("pad: not on curve (use SwapBuy)")
341 }
342 buyer := cur.Previous().Address()
343 height := runtime.ChainHeight()
344
345 remaining := CurveSupply - l.RealSold
346 if remaining <= 0 {
347 panic("pad: curve sold out — call Graduate")
348 }
349
350 fee := ammmath.ApplyFee(sent, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS)
351 // Net enters curve; remainder boosts virtual ugnot (stays as collateral).
352 netIn := fee.Net + fee.Remainder
353
354 tokensOut, newVU, newVT := ammmath.BuyTokens(l.VirtualUgnot, l.VirtualToken, netIn)
355 // Cap at remaining curve float (large buys can outrun CurveSupply on virtual CPMM).
356 if tokensOut > remaining {
357 lo, hi := int64(1), netIn
358 bestTok, bestVU, bestVT, bestNet := int64(0), l.VirtualUgnot, l.VirtualToken, int64(0)
359 for lo <= hi {
360 mid := (lo + hi) / 2
361 tok, vu, vt := ammmath.BuyTokens(l.VirtualUgnot, l.VirtualToken, mid)
362 if tok <= remaining {
363 bestTok, bestVU, bestVT, bestNet = tok, vu, vt, mid
364 lo = mid + 1
365 } else {
366 hi = mid - 1
367 }
368 }
369 if bestTok <= 0 {
370 panic("pad: buy too large for remaining curve supply")
371 }
372 tokensOut, newVU, newVT, netIn = bestTok, bestVU, bestVT, bestNet
373 // MVP: full `sent` still paid; excess stays in realm as protocol dust.
374 _ = sent
375 }
376 if height-l.Created < AntiSnipeHeights {
377 maxTok := TotalSupply * AntiSnipeMaxBuyBPS / 10000
378 if tokensOut > maxTok {
379 panic("pad: anti-snipe max buy exceeded")
380 }
381 }
382
383 // Mutate only after all checks pass.
384 l.CreatorFees += fee.Creator
385 protocolFees += fee.Protocol
386 l.VirtualUgnot = newVU
387 l.VirtualToken = newVT
388 l.RealSold += tokensOut
389 raisedAdd := netIn
390 if raisedAdd > fee.Net+fee.Remainder {
391 raisedAdd = fee.Net + fee.Remainder
392 }
393 l.RaisedUgnot += raisedAdd
394
395 addBal(l, buyer, tokensOut)
396 noteBuyer(l, buyer)
397 maybeRefundBond(cur, l)
398 recordTrade(l, TradeSideBuy, sent, tokensOut)
399
400 chain.Emit("Buy",
401 "id", id,
402 "buyer", buyer.String(),
403 "ugnot", strconv.FormatInt(sent, 10),
404 "tokens", strconv.FormatInt(tokensOut, 10),
405 )
406
407 if ammmath.CanGraduate(l.RaisedUgnot, GraduationThreshold) || l.RealSold >= CurveSupply {
408 if l.RaisedUgnot >= GraduationThreshold {
409 graduate(cur, l)
410 }
411 }
412 return tokensOut
413}
414
415// Sell burns curve tokens and pays ugnot (fee on output).
416func Sell(cur realm, id string, tokensIn int64) int64 {
417 requireInit()
418 if !cur.Previous().IsUserCall() {
419 panic("pad: must be EOA MsgCall")
420 }
421 if tokensIn <= 0 {
422 panic("pad: tokensIn must be positive")
423 }
424 l := mustLaunch(id)
425 if l.Status != StatusCurve {
426 panic("pad: not on curve (use SwapSell)")
427 }
428 seller := cur.Previous().Address()
429 if balOf(l, seller) < tokensIn {
430 panic("pad: insufficient token balance")
431 }
432
433 gross, newVU, newVT := ammmath.SellTokens(l.VirtualUgnot, l.VirtualToken, tokensIn)
434 fee := ammmath.ApplyFeeOnOutput(gross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS)
435
436 // Full gross left virtual reserves; retain fee in virtual ugnot (cash stays in realm).
437 l.VirtualUgnot = newVU + fee.Fee
438 l.VirtualToken = newVT
439 l.RealSold -= tokensIn
440 if l.RealSold < 0 {
441 l.RealSold = 0
442 }
443
444 // User receives net; creator+protocol become fee liabilities (leave Raised).
445 payOut := fee.Net + fee.Creator + fee.Protocol
446 if l.RaisedUgnot >= payOut {
447 l.RaisedUgnot -= payOut
448 } else {
449 l.RaisedUgnot = 0
450 }
451 // remainder stays inside Raised/virtual (already in fee.Fee re-add portion for remainder+vaults)
452 // Raised should still include remainder: payOut excludes remainder ✓
453 l.CreatorFees += fee.Creator
454 protocolFees += fee.Protocol
455
456 addBal(l, seller, -tokensIn)
457 sendUgnot(cur, seller, fee.Net)
458 recordTrade(l, TradeSideSell, fee.Net, tokensIn)
459
460 chain.Emit("Sell",
461 "id", id,
462 "seller", seller.String(),
463 "tokens", strconv.FormatInt(tokensIn, 10),
464 "ugnot", strconv.FormatInt(fee.Net, 10),
465 )
466 return fee.Net
467}
468
469// Graduate permissionlessly moves a ready curve into a permanently locked CPMM.
470func Graduate(cur realm, id string) {
471 requireInit()
472 l := mustLaunch(id)
473 if l.Status != StatusCurve {
474 panic("pad: already graduated")
475 }
476 if !ammmath.CanGraduate(l.RaisedUgnot, GraduationThreshold) {
477 panic("pad: threshold not met")
478 }
479 graduate(cur, l)
480}
481
482func graduate(cur realm, l *Launch) {
483 if l.Status != StatusCurve {
484 return
485 }
486 // Seed pool: all curve collateral + remaining pool seed tokens.
487 // Curve unsold tokens are not minted; only PoolSeed enters the pool.
488 // Raised ugnot becomes pool ugnot (fee vaults stay separate liabilities).
489 poolU := l.RaisedUgnot
490 if poolU <= 0 {
491 panic("pad: empty pool ugnot")
492 }
493 poolT := PoolSeed
494 // Optional: if curve sold less than CurveSupply, unsold is never minted (dead supply).
495 // Real circulating = RealSold + PoolSeed after minting pool to realm accounting.
496
497 l.PoolUgnot = poolU
498 l.PoolToken = poolT
499 l.RaisedUgnot = 0
500 l.VirtualUgnot = 0
501 l.VirtualToken = 0
502 l.Status = StatusGraduated
503
504 // Pool tokens are held by the launch (not an address) — PoolToken reserve.
505 // Circulating user balances remain; total effective supply = sum(balances)+PoolToken.
506
507 // Forfeit unrefunded bond to protocol at graduation if still locked.
508 if !l.BondRefunded && l.BondUgnot > 0 {
509 protocolFees += l.BondUgnot
510 l.BondUgnot = 0
511 l.BondRefunded = true
512 }
513
514 // Mark graduation on chart at pool spot.
515 recordTrade(l, TradeSideOpen, poolU, poolT)
516
517 // GRC20 already exists and is transferable. Mark ready for external Gnoswap pool.
518 // Pad keeps a locked internal CPMM; Gnoswap listing is a separate permissionless pool
519 // seeded with GNOT + this GRC20 by the community/creator (see Guide).
520 l.GnoswapReady = true
521 l.GnoswapNote = "create GNOT/" + l.Symbol + " pool on Gnoswap with Token.ID=" + l.TokenID
522
523 chain.Emit("Graduated",
524 "id", l.ID,
525 "poolUgnot", strconv.FormatInt(poolU, 10),
526 "poolToken", strconv.FormatInt(poolT, 10),
527 "token", l.TokenID,
528 "gnoswap_ready", "1",
529 )
530 _ = cur // banker not needed; ugnot already on realm
531}
532
533// TokenIDOf returns the GRC20 Token.ID() for a launch.
534func TokenIDOf(id string) string {
535 return mustLaunch(id).TokenID
536}
537
538// GRC20Bank returns the underlying *grc20.Token for interop (metadata / external DEX).
539func GRC20Bank(id string) *grc20.Token {
540 l := mustLaunch(id)
541 if l.Token == nil {
542 panic("pad: no token")
543 }
544 return l.Token
545}
546
547// SwapBuy buys tokens from the graduated pool with -send ugnot.
548func SwapBuy(cur realm, id string) int64 {
549 requireInit()
550 sent := requireUserPayment(cur)
551 l := mustLaunch(id)
552 if l.Status != StatusGraduated {
553 panic("pad: not graduated (use Buy)")
554 }
555 buyer := cur.Previous().Address()
556
557 fee := ammmath.ApplyFee(sent, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS)
558 l.CreatorFees += fee.Creator
559 protocolFees += fee.Protocol
560
561 tokensOut, newPU, newPT := ammmath.PoolSwapUgnotForToken(
562 l.PoolUgnot, l.PoolToken, fee.Net, fee.Remainder,
563 )
564 l.PoolUgnot = newPU
565 l.PoolToken = newPT
566 addBal(l, buyer, tokensOut)
567 noteBuyer(l, buyer)
568 recordTrade(l, TradeSideBuy, sent, tokensOut)
569
570 chain.Emit("SwapBuy",
571 "id", id,
572 "buyer", buyer.String(),
573 "ugnot", strconv.FormatInt(sent, 10),
574 "tokens", strconv.FormatInt(tokensOut, 10),
575 )
576 return tokensOut
577}
578
579// SwapSell sells tokens into the graduated pool for ugnot.
580func SwapSell(cur realm, id string, tokensIn int64) int64 {
581 requireInit()
582 if !cur.Previous().IsUserCall() {
583 panic("pad: must be EOA MsgCall")
584 }
585 if tokensIn <= 0 {
586 panic("pad: tokensIn must be positive")
587 }
588 l := mustLaunch(id)
589 if l.Status != StatusGraduated {
590 panic("pad: not graduated (use Sell)")
591 }
592 seller := cur.Previous().Address()
593 if balOf(l, seller) < tokensIn {
594 panic("pad: insufficient token balance")
595 }
596
597 gross, newPU, newPT := ammmath.PoolSwapTokenForUgnot(l.PoolUgnot, l.PoolToken, tokensIn)
598 fee := ammmath.ApplyFeeOnOutput(gross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS)
599
600 // Retain fee in pool ugnot (cash stays); user gets net.
601 l.PoolUgnot = newPU + fee.Fee
602 l.PoolToken = newPT
603 l.CreatorFees += fee.Creator
604 protocolFees += fee.Protocol
605
606 addBal(l, seller, -tokensIn)
607 sendUgnot(cur, seller, fee.Net)
608 recordTrade(l, TradeSideSell, fee.Net, tokensIn)
609
610 chain.Emit("SwapSell",
611 "id", id,
612 "seller", seller.String(),
613 "tokens", strconv.FormatInt(tokensIn, 10),
614 "ugnot", strconv.FormatInt(fee.Net, 10),
615 )
616 return fee.Net
617}
618
619// Transfer moves GRC20 tokens between addresses (user-initiated).
620func Transfer(cur realm, id string, to address, amount int64) {
621 requireInit()
622 if !cur.Previous().IsUserCall() {
623 panic("pad: must be EOA MsgCall")
624 }
625 if amount <= 0 {
626 panic("pad: amount must be positive")
627 }
628 if !to.IsValid() {
629 panic("pad: invalid to")
630 }
631 l := mustLaunch(id)
632 from := cur.Previous().Address()
633 if from == to {
634 panic("pad: self transfer")
635 }
636 if l.Ledger == nil {
637 panic("pad: no GRC20 ledger")
638 }
639 // Proper GRC20 transfer (not mint/burn).
640 if err := l.Ledger.Transfer(from, to, amount); err != nil {
641 panic("pad: transfer: " + err.Error())
642 }
643 chain.Emit("Transfer", "id", id, "from", from.String(), "to", to.String(),
644 "amount", strconv.FormatInt(amount, 10))
645}
646
647// Approve sets GRC20 allowance so DEX/contracts can TransferFrom.
648func Approve(cur realm, id string, spender address, amount int64) {
649 requireInit()
650 if !cur.Previous().IsUserCall() {
651 panic("pad: must be EOA MsgCall")
652 }
653 if !spender.IsValid() {
654 panic("pad: invalid spender")
655 }
656 l := mustLaunch(id)
657 if l.Ledger == nil {
658 panic("pad: no GRC20 ledger")
659 }
660 owner := cur.Previous().Address()
661 if err := l.Ledger.Approve(owner, spender, amount); err != nil {
662 panic("pad: approve: " + err.Error())
663 }
664 chain.Emit("Approval", "id", id, "owner", owner.String(), "spender", spender.String(),
665 "amount", strconv.FormatInt(amount, 10))
666}
667
668// ClaimCreatorFees withdraws accrued creator fees for a launch.
669func ClaimCreatorFees(cur realm, id string) int64 {
670 requireInit()
671 if !cur.Previous().IsUserCall() {
672 panic("pad: must be EOA MsgCall")
673 }
674 l := mustLaunch(id)
675 caller := cur.Previous().Address()
676 if caller != l.Creator {
677 panic("pad: not creator")
678 }
679 amt := l.CreatorFees
680 if amt <= 0 {
681 return 0
682 }
683 l.CreatorFees = 0
684 sendUgnot(cur, caller, amt)
685 chain.Emit("ClaimCreator", "id", id, "amount", strconv.FormatInt(amt, 10))
686 return amt
687}
688
689// ClaimProtocolFees withdraws protocol treasury fees.
690func ClaimProtocolFees(cur realm) int64 {
691 requireInit()
692 if !cur.Previous().IsUserCall() {
693 panic("pad: must be EOA MsgCall")
694 }
695 caller := cur.Previous().Address()
696 if caller != protocolAddr {
697 panic("pad: not protocol")
698 }
699 amt := protocolFees
700 if amt <= 0 {
701 return 0
702 }
703 protocolFees = 0
704 sendUgnot(cur, caller, amt)
705 chain.Emit("ClaimProtocol", "amount", strconv.FormatInt(amt, 10))
706 return amt
707}
708
709// --- read helpers (non-crossing) ---
710
711func BalanceOf(id string, owner address) int64 {
712 return balOf(mustLaunch(id), owner)
713}
714
715func GetStatus(id string) int {
716 return mustLaunch(id).Status
717}
718
719func GetRaised(id string) int64 {
720 return mustLaunch(id).RaisedUgnot
721}
722
723func GetPool(id string) (ugnot, token int64) {
724 l := mustLaunch(id)
725 return l.PoolUgnot, l.PoolToken
726}
727
728func GetCreatorFees(id string) int64 {
729 return mustLaunch(id).CreatorFees
730}
731
732func ProtocolFees() int64 {
733 return protocolFees
734}
735
736func LaunchCount() int {
737 return launches.Size()
738}
739
740func ResolveSymbol(symbol string) string {
741 s, ok := bySymbol.Get(symbol).(string)
742 if !ok {
743 return ""
744 }
745 return s
746}
747
748// ListIDs returns newline-separated launch IDs (sorted by AVL key / creation order).
749func ListIDs() string {
750 out := ""
751 launches.Iterate("", "", func(key string, _ any) bool {
752 if out != "" {
753 out += "\n"
754 }
755 out += key
756 return false
757 })
758 return out
759}
760
761// LaunchInfo returns a single-line pipe-delimited summary for UIs/indexers:
762//
763// id|name|symbol|status|raised|sold|buyers|creatorFees|poolUgnot|poolToken|uri|creator|virtualUgnot|virtualToken|created|tokenID|gnoswapReady
764//
765// status: 0=curve 1=graduated; gnoswapReady: 0|1
766func LaunchInfo(id string) string {
767 l := mustLaunch(id)
768 gs := "0"
769 if l.GnoswapReady {
770 gs = "1"
771 }
772 return l.ID + "|" +
773 l.Name + "|" +
774 l.Symbol + "|" +
775 strconv.Itoa(l.Status) + "|" +
776 strconv.FormatInt(l.RaisedUgnot, 10) + "|" +
777 strconv.FormatInt(l.RealSold, 10) + "|" +
778 strconv.Itoa(l.BuyerCount) + "|" +
779 strconv.FormatInt(l.CreatorFees, 10) + "|" +
780 strconv.FormatInt(l.PoolUgnot, 10) + "|" +
781 strconv.FormatInt(l.PoolToken, 10) + "|" +
782 l.URI + "|" +
783 l.Creator.String() + "|" +
784 strconv.FormatInt(l.VirtualUgnot, 10) + "|" +
785 strconv.FormatInt(l.VirtualToken, 10) + "|" +
786 strconv.FormatInt(l.Created, 10) + "|" +
787 l.TokenID + "|" +
788 gs
789}
790
791// ParamsInfo returns fixed MVP parameters for UI display.
792// total|curve|poolSeed|gradThreshold|feeBps|createBond
793func ParamsInfo() string {
794 return strconv.FormatInt(TotalSupply, 10) + "|" +
795 strconv.FormatInt(CurveSupply, 10) + "|" +
796 strconv.FormatInt(PoolSeed, 10) + "|" +
797 strconv.FormatInt(GraduationThreshold, 10) + "|" +
798 strconv.FormatInt(FeeBPS, 10) + "|" +
799 strconv.FormatInt(CreateBondUgnot, 10)
800}
801
802// TradeHistory returns newline-separated chart points:
803//
804// height|side|ugnot|tokens|priceScaled
805//
806// side: 0=buy 1=sell 2=open/graduate. Ordered oldest → newest.
807func TradeHistory(id string) string {
808 l := mustLaunch(id)
809 out := ""
810 l.Trades.Iterate("", "", func(_ string, value any) bool {
811 t := value.(*Trade)
812 line := strconv.FormatInt(t.Height, 10) + "|" +
813 strconv.Itoa(t.Side) + "|" +
814 strconv.FormatInt(t.Ugnot, 10) + "|" +
815 strconv.FormatInt(t.Tokens, 10) + "|" +
816 strconv.FormatInt(t.Price, 10)
817 if out != "" {
818 out += "\n"
819 }
820 out += line
821 return false
822 })
823 return out
824}
825
826// TradeCount returns number of stored chart samples for a launch.
827func TradeCount(id string) int {
828 return mustLaunch(id).Trades.Size()
829}
830
831// resetForTest clears package state between unit tests.
832func resetForTest() {
833 launches = avl.Tree{}
834 bySymbol = avl.Tree{}
835 nextID = 0
836 var zero address
837 protocolAddr = zero
838 protocolFees = 0
839 inited = false
840 testSkipBanker = true // unit tests skip banker; integration/chain tests leave false
841}