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