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