gnoswap_list.gno
19.07 Kb · 616 lines
1package padv3
2
3import (
4 "chain"
5 "strconv"
6 "strings"
7 "time"
8
9 "gno.land/p/gnoswap/consts/v1"
10 u256 "gno.land/p/gnoswap/uint256/v1"
11 "gno.land/r/gnoland/wugnot"
12 "gno.land/r/gnoswap/gns"
13 gnspool "gno.land/r/gnoswap/pool"
14 "gno.land/r/gnoswap/position"
15 "gno.land/r/gnoswap/router"
16)
17
18// Sapphire Gnoswap registry keys / role addresses (sapphire-1).
19const (
20 wugnotTokenKey = "gno.land/r/gnoland/wugnot.wugnot"
21 gnsTokenKey = "gno.land/r/gnoswap/gns.GNS"
22 gnoswapPoolAddrStr = "g1dexaf6aqkkyr9yfy9d5up69lsn7ra80af34g5v"
23 gnoswapRouterAddrStr = "g1vc883gshu5z7ytk5cdynhc8c2dh67pdp4cszkp"
24)
25
26// listFund tracks assets pulled from the listing caller for refund / LP ugnot reimburse.
27type listFund struct {
28 ok bool
29 caller address
30 pulledLpWugnot int64 // reimbursed as ugnot after successful list
31 pulledFeeWugnot int64 // leftover WUGNOT refunded after list; full refund on fail
32 pulledGns int64 // CreatePool fee; consumed on success
33}
34
35// tryListOnGnoswap seeds a Gnoswap CL pool with remaining meme tokens + raised-sized
36// WUGNOT as LP. CreatePool fee (fixed GNS amount) is paid from:
37// 1) pre-funded GNS on pad (preferred - immune to GNOT/GNS price moves), else
38// 2) ExactOut WUGNOT->GNS using SURPLUS inventory only (above raised), so LP depth
39// stays equal to raised even when GNS is expensive.
40//
41// Returns true on success. Soft failure sets l.GnoswapNote and returns false
42// (caller keeps internal CPMM). Does not pull from user - see prepareCallerListFunding.
43func tryListOnGnoswap(cur realm, l *Launch, raisedUgnot, remainingTokens int64) bool {
44 if raisedUgnot <= 0 || remainingTokens <= 0 {
45 l.GnoswapNote = "list skip: empty capital"
46 return false
47 }
48 padAddr := cur.Address()
49 wBal := wugnot.BalanceOf(padAddr)
50 // Full reservation backing: prevent launch A from Minting with launch B's PoolUgnot.
51 // reservedWugnot includes this launch's PoolUgnot (graduated, seed not released).
52 needReserve := reservedWugnot()
53 if wBal < needReserve {
54 l.GnoswapNote = "list skip: WUGNOT under-backed vs reserved; have " +
55 strconv.FormatInt(wBal, 10) + " reserved " + strconv.FormatInt(needReserve, 10)
56 return false
57 }
58 if wBal < raisedUgnot {
59 l.GnoswapNote = "list skip: need WUGNOT inventory >= raised for LP; have " +
60 strconv.FormatInt(wBal, 10) + " need " + strconv.FormatInt(raisedUgnot, 10)
61 return false
62 }
63
64 feeNeed := gnspool.GetPoolCreationFee()
65 if feeNeed <= 0 {
66 feeNeed = 100_000_000 // 100 GNS default (6 decimals)
67 }
68
69 // --- GNS for CreatePool fee ---
70 // Require GNS already on pad. Do NOT Approve+router ExactOut mid-list:
71 // realm spender frame often panics "insufficient allowance" and reverts the tx.
72 // UI: gns.Transfer(pad, feeNeed) then RetryListGnoswap.
73 gnsBal := gns.BalanceOf(padAddr)
74 feeWugnot := int64(0)
75 if gnsBal < feeNeed {
76 l.GnoswapNote = "list skip: need " + strconv.FormatInt(feeNeed, 10) +
77 " GNS base units on pad (Transfer GNS then RetryList). have " +
78 strconv.FormatInt(gnsBal, 10) +
79 ". No mid-list WUGNOT->GNS swap (avoids allowance panic)."
80 return false
81 }
82
83 liqWugnot := raisedUgnot
84 if wugnot.BalanceOf(padAddr) < liqWugnot {
85 l.GnoswapNote = "list skip: WUGNOT below raised after fee path"
86 return false
87 }
88
89 // LP token amount: prefer PoolToken already sized in graduate() to curve spot.
90 // Defensive resize if Virtuals are somehow still set (pre-graduate test paths):
91 // tokensForLP = raisedUgnot * VirtualToken / VirtualUgnot
92 liqTokens := remainingTokens
93 if l.VirtualUgnot > 0 && l.VirtualToken > 0 && liqWugnot > 0 {
94 needed := liqWugnot * l.VirtualToken / l.VirtualUgnot
95 if needed > 0 && needed < remainingTokens {
96 liqTokens = needed
97 }
98 }
99 if liqTokens <= 0 {
100 l.GnoswapNote = "list skip: zero LP tokens at curve spot"
101 return false
102 }
103
104 // Mint LP slice + leftover inventory (LeftoverTokens set at graduate) onto pad.
105 mintAmt := remainingTokens
106 if l.LeftoverTokens > 0 {
107 mintAmt = remainingTokens + l.LeftoverTokens
108 }
109 if mintAmt < liqTokens {
110 mintAmt = liqTokens
111 }
112 addBal(l, padAddr, mintAmt)
113
114 tokenKey := adenaKeyFromTokenID(l.TokenID, l.Symbol)
115 if tokenKey == "" {
116 l.GnoswapNote = "list skip: empty token registry key"
117 return false
118 }
119
120 // Sort token0 < token1 (Gnoswap pool key order) BEFORE sqrtPrice + CreatePool + Mint.
121 // CreatePool reorders tokens and INVERTS sqrtPriceX96 when args are out of order
122 // (newSqrt = Q192/oldSqrt). If we pass unsorted keys with a price already computed
123 // for the sorted pair, the pool is born with inverted price → Mint only takes a
124 // tiny sliver of one side (e.g. ~4 GNOT of a 10k WUGNOT raise). Always call
125 // CreatePool(t0, t1, fee, sqrt) with the same sorted pair as Mint.
126 t0, t1 := wugnotTokenKey, tokenKey
127 amt0, amt1 := liqWugnot, liqTokens
128 if strings.Compare(t0, t1) > 0 {
129 t0, t1 = t1, t0
130 amt0, amt1 = amt1, amt0
131 }
132
133 sqrtPrice := computeSqrtPriceX96(amt0, amt1)
134 if sqrtPrice == "" || sqrtPrice == "0" {
135 l.GnoswapNote = "list skip: bad sqrtPriceX96"
136 return false
137 }
138
139 poolAddr := address(gnoswapPoolAddrStr)
140 gns.Approve(cross(cur), poolAddr, feeNeed)
141 wugnot.Approve(cross(cur), poolAddr, liqWugnot)
142 if err := l.ledger.Approve(padAddr, poolAddr, liqTokens); err != nil {
143 l.GnoswapNote = "list skip: token approve failed: " + err.Error()
144 return false
145 }
146
147 // CRITICAL: sorted (t0,t1) + matching sqrtPrice (do NOT pass original unsorted keys)
148 gnspool.CreatePool(cross(cur), t0, t1, GnoswapFeeTier, sqrtPrice)
149
150 tickLower, tickUpper := alignedFullRangeTicks(GnoswapTickSpacing)
151 deadline := time.Now().Unix() + 600
152
153 posID, liqStr, a0, a1 := position.Mint(
154 cross(cur),
155 t0,
156 t1,
157 GnoswapFeeTier,
158 tickLower,
159 tickUpper,
160 strconv.FormatInt(amt0, 10),
161 strconv.FormatInt(amt1, 10),
162 "0",
163 "0",
164 deadline,
165 padAddr, // permanent lock: pad owns position NFT
166 "",
167 )
168
169 // Actual WUGNOT deposited (whichever side is WUGNOT after sort)
170 used0, _ := strconv.ParseInt(a0, 10, 64)
171 used1, _ := strconv.ParseInt(a1, 10, 64)
172 wugnotUsed := int64(0)
173 if t0 == wugnotTokenKey {
174 wugnotUsed = used0
175 } else if t1 == wugnotTokenKey {
176 wugnotUsed = used1
177 }
178 // Sanity: if Mint took << intended LP WUGNOT, mark note (still listed — pool exists)
179 if wugnotUsed > 0 && liqWugnot > 0 && wugnotUsed*2 < liqWugnot {
180 // keep going but surface under-deposit for ops
181 chain.Emit("ListUnderDeposit",
182 "id", l.ID,
183 "wantWugnot", strconv.FormatInt(liqWugnot, 10),
184 "gotWugnot", strconv.FormatInt(wugnotUsed, 10),
185 )
186 }
187
188 poolPath := t0 + ":" + t1 + ":" + strconv.FormatUint(uint64(GnoswapFeeTier), 10)
189 l.GnoswapListed = true
190 l.GnoswapPoolPath = poolPath
191 l.GnoswapPositionID = posID
192 l.ListVenue = VenueGnoswap
193 l.FeeWugnotSpent = feeWugnot
194 if wugnotUsed > 0 {
195 l.LiqWugnotUsed = wugnotUsed
196 } else {
197 l.LiqWugnotUsed = liqWugnot
198 }
199 // Release Create-time GNS escrow (fee left pad via CreatePool).
200 consumeListFeeEscrow(l)
201 leftoverTok := l.LeftoverTokens
202 if leftoverTok < 0 {
203 leftoverTok = 0
204 }
205 if remLeft := remainingTokens - liqTokens; remLeft > leftoverTok {
206 leftoverTok = remLeft
207 }
208 l.GnoswapNote = "listed pool=" + poolPath + " pos=" + strconv.FormatUint(posID, 10) +
209 " liq=" + liqStr + " a0=" + a0 + " a1=" + a1 +
210 " wugnotUsed=" + strconv.FormatInt(wugnotUsed, 10) +
211 " feeWugnot=" + strconv.FormatInt(feeWugnot, 10) +
212 " lpTokens=" + strconv.FormatInt(liqTokens, 10) +
213 " leftoverTokens=" + strconv.FormatInt(leftoverTok, 10)
214
215 chain.Emit("GnoswapListed",
216 "id", l.ID,
217 "poolPath", poolPath,
218 "positionId", strconv.FormatUint(posID, 10),
219 "feeWugnot", strconv.FormatInt(feeWugnot, 10),
220 "liqWugnot", strconv.FormatInt(liqWugnot, 10),
221 "wugnotUsed", strconv.FormatInt(wugnotUsed, 10),
222 "lpTokens", strconv.FormatInt(liqTokens, 10),
223 "leftoverTokens", strconv.FormatInt(leftoverTok, 10),
224 "tokens", strconv.FormatInt(remainingTokens, 10),
225 )
226 return true
227}
228
229// prepareCallerListFunding fills pad inventory shortfall from the EOA caller via TransferFrom.
230//
231// padv14+ WUGNOT raise: Buy already TransferFrom WUGNOT to pad, so wBal >= raised
232// at graduate in the normal case — only GNS fee (or small fee WUGNOT budget) may be short.
233//
234// Legacy ugnot-raise pads: wugnot.Deposit is EOA-only; caller wraps temp LP then reimbursed.
235func prepareCallerListFunding(cur realm, l *Launch, raisedUgnot int64) listFund {
236 caller := cur.Previous().Address()
237 padA := cur.Address()
238 out := listFund{ok: true, caller: caller}
239
240 if raisedUgnot <= 0 {
241 l.GnoswapNote = "list skip: empty raise"
242 out.ok = false
243 return out
244 }
245
246 feeNeed := gnspool.GetPoolCreationFee()
247 if feeNeed <= 0 {
248 feeNeed = 100_000_000
249 }
250
251 wBal := wugnot.BalanceOf(padA)
252 // Pull up to full reservation backing (and raised LP), so tryList does not
253 // soft-fail under-backed / steal other launches' PoolUgnot.
254 needW := raisedUgnot
255 if res := reservedWugnot(); res > needW {
256 needW = res
257 }
258 if wBal < needW {
259 short := needW - wBal
260 if !pullWugnotFrom(cur, caller, padA, short) {
261 l.GnoswapNote = "list skip: need temp WUGNOT wrap " +
262 strconv.FormatInt(short, 10) +
263 " ugnot units (Deposit+Approve pad) for reserved backing / LP. " +
264 "Fee: " + strconv.FormatInt(feeNeed, 10) + " GNS or WUGNOT ExactOut budget."
265 out.ok = false
266 return out
267 }
268 out.pulledLpWugnot = short
269 wBal += short
270 }
271
272 gnsBal := gns.BalanceOf(padA)
273 if gnsBal < feeNeed {
274 gShort := feeNeed - gnsBal
275 if pullGnsFrom(cur, caller, padA, gShort) {
276 out.pulledGns = gShort
277 } else {
278 // No GNS from caller - pull modest WUGNOT surplus for ExactOut → GNS.
279 // Do NOT pull full GnoswapMaxFeeWugnot (5000 GNOT) — that causes
280 // InsufficientCoins for normal wallets. MaxFee still caps ExactOut spend.
281 surplus := wBal - raisedUgnot
282 if surplus < 1 {
283 feeBudget := listFeeWugnotPull()
284 if !pullWugnotFrom(cur, caller, padA, feeBudget) {
285 l.GnoswapNote = "list skip: need GNS fee " +
286 strconv.FormatInt(feeNeed, 10) +
287 " (Approve pad) OR WUGNOT fee budget " +
288 strconv.FormatInt(feeBudget, 10) +
289 " ugnot for ExactOut. Prefer holding 100 GNS."
290 // Roll back LP pull.
291 if out.pulledLpWugnot > 0 {
292 safeWugnotTransfer(cur, caller, out.pulledLpWugnot)
293 out.pulledLpWugnot = 0
294 }
295 out.ok = false
296 return out
297 }
298 out.pulledFeeWugnot = feeBudget
299 }
300 }
301 }
302 return out
303}
304
305// listFeeWugnotPull is the WUGNOT amount pulled from the listing caller for
306// ExactOut → GNS when they do not hold CreatePool fee in GNS.
307// Kept well below GnoswapMaxFeeWugnot so List does not demand 5k+ GNOT wrap.
308func listFeeWugnotPull() int64 {
309 const defaultPull int64 = 1_500_000_000 // 1500 GNOT — enough for ~100 GNS at ≤15 GNOT/GNS
310 if GnoswapMaxFeeWugnot > 0 && GnoswapMaxFeeWugnot < defaultPull {
311 return GnoswapMaxFeeWugnot
312 }
313 return defaultPull
314}
315
316// pullWugnotFrom is intentionally a no-op pull.
317//
318// GRC20 TransferFrom from a realm often panics "insufficient allowance" even when
319// Allowance(owner, pad) is set (spender frame resolves to EOA, not pad). Soft-fail
320// instead so list can fall back to notes / surplus / pre-funded pad inventory.
321//
322// UI must pre-fund pad via wugnot.Transfer(pad, amount) and gns.Transfer(pad, fee)
323// before RetryListGnoswap — never rely on Approve+TransferFrom in the same flow.
324func pullWugnotFrom(cur realm, from, to address, amount int64) bool {
325 if amount <= 0 {
326 return true
327 }
328 // Already on pad? treat as satisfied without TransferFrom.
329 if wugnot.BalanceOf(to) >= amount && from != to {
330 // Not a full check for "raised" accounting — prepareCallerListFunding
331 // already compared pad balance to raised before calling shortfall pulls.
332 }
333 _ = cur
334 _ = from
335 _ = to
336 return false
337}
338
339func pullGnsFrom(cur realm, from, to address, amount int64) bool {
340 if amount <= 0 {
341 return true
342 }
343 _ = cur
344 _ = from
345 _ = to
346 // Never TransferFrom GNS either (same spender-frame issue).
347 return false
348}
349
350func safeWugnotTransfer(cur realm, to address, amount int64) {
351 if amount <= 0 {
352 return
353 }
354 have := wugnot.BalanceOf(cur.Address())
355 if have < amount {
356 amount = have
357 }
358 if amount > 0 {
359 wugnot.Transfer(cross(cur), to, amount)
360 }
361}
362
363func safeGnsTransfer(cur realm, to address, amount int64) {
364 if amount <= 0 {
365 return
366 }
367 have := gns.BalanceOf(cur.Address())
368 if have < amount {
369 amount = have
370 }
371 if amount > 0 {
372 gns.Transfer(cross(cur), to, amount)
373 }
374}
375
376func refundCallerListFunding(cur realm, fund listFund) {
377 if !fund.caller.IsValid() {
378 return
379 }
380 safeWugnotTransfer(cur, fund.caller, fund.pulledLpWugnot+fund.pulledFeeWugnot)
381 safeGnsTransfer(cur, fund.caller, fund.pulledGns)
382}
383
384// settleCallerListFunding returns unused fee-budget WUGNOT to the caller.
385// LP shortfall pull is rare on WUGNOT-raise pads (raised already on pad); if it
386// happened, reimburse in WUGNOT from any leftover pad balance after list.
387func settleCallerListFunding(cur realm, fund listFund) {
388 if !fund.caller.IsValid() {
389 return
390 }
391 if fund.pulledLpWugnot > 0 {
392 safeWugnotTransfer(cur, fund.caller, fund.pulledLpWugnot)
393 }
394 // Leftover fee-budget WUGNOT (ExactOut may not consume full maxIn).
395 if fund.pulledFeeWugnot > 0 {
396 safeWugnotTransfer(cur, fund.caller, fund.pulledFeeWugnot)
397 }
398}
399
400// listOnGnoswapWithFunding is the full auto path: pull inventory from caller if needed,
401// CreatePool+Mint, reimburse LP ugnot / refund on failure.
402func listOnGnoswapWithFunding(cur realm, l *Launch, raisedUgnot, remainingTokens int64) bool {
403 fund := prepareCallerListFunding(cur, l, raisedUgnot)
404 if !fund.ok {
405 return false
406 }
407 ok := tryListOnGnoswap(cur, l, raisedUgnot, remainingTokens)
408 if ok {
409 settleCallerListFunding(cur, fund)
410 if fund.pulledLpWugnot > 0 || fund.pulledFeeWugnot > 0 || fund.pulledGns > 0 {
411 chain.Emit("ListFunding",
412 "id", l.ID,
413 "caller", fund.caller.String(),
414 "lpWugnot", strconv.FormatInt(fund.pulledLpWugnot, 10),
415 "feeWugnot", strconv.FormatInt(fund.pulledFeeWugnot, 10),
416 "gns", strconv.FormatInt(fund.pulledGns, 10),
417 )
418 }
419 return true
420 }
421 refundCallerListFunding(cur, fund)
422 return false
423}
424
425// listNeedOf: poolU|wHave|wNeedLp|gnsHave|gnsNeed|feeGns|feeWugnotBudget|padAddr
426func listNeedOf(l *Launch) string {
427 poolU := int64(0)
428 if l != nil && l.Status == StatusGraduated && !l.GnoswapListed {
429 poolU = l.PoolUgnot
430 }
431 padA := padAddr
432 wHave := wugnot.BalanceOf(padA)
433 gnsHave := gns.BalanceOf(padA)
434 feeGns := gnspool.GetPoolCreationFee()
435 if feeGns <= 0 {
436 feeGns = 100_000_000
437 }
438 wNeed := poolU - wHave
439 if wNeed < 0 {
440 wNeed = 0
441 }
442 gNeed := feeGns - gnsHave
443 if gNeed < 0 {
444 gNeed = 0
445 }
446 feeBud := listFeeWugnotPull()
447 // If GNS already covered, fee WUGNOT budget need is 0 for the wizard.
448 if gNeed == 0 {
449 feeBud = 0
450 } else if wHave > poolU {
451 // Existing surplus reduces fee budget to pull
452 surp := wHave - poolU
453 if surp >= feeBud {
454 feeBud = 0
455 } else {
456 feeBud = feeBud - surp
457 }
458 }
459 return strconv.FormatInt(poolU, 10) + "|" +
460 strconv.FormatInt(wHave, 10) + "|" +
461 strconv.FormatInt(wNeed, 10) + "|" +
462 strconv.FormatInt(gnsHave, 10) + "|" +
463 strconv.FormatInt(gNeed, 10) + "|" +
464 strconv.FormatInt(feeGns, 10) + "|" +
465 strconv.FormatInt(feeBud, 10) + "|" +
466 padA.String()
467}
468
469// ListNeed is the public query for the Token list wizard (default venue).
470func ListNeed(id string) string {
471 return listNeedOf(mustLaunch(id))
472}
473
474// ListNeedFor is venue-aware; only gnoswap has a Need adapter today.
475func ListNeedFor(id, venueId string) string {
476 l := mustLaunch(id)
477 vid := normalizeVenueID(venueId)
478 if vid != VenueGnoswap {
479 poolU := int64(0)
480 if l != nil && l.Status == StatusGraduated && !l.GnoswapListed {
481 poolU = l.PoolUgnot
482 }
483 return strconv.FormatInt(poolU, 10) + "|0|0|0|0|0|0|" + padAddr.String()
484 }
485 return listNeedOf(l)
486}
487
488// ListedOf reports whether the launch is listed on any venue.
489func ListedOf(id string) bool {
490 l := mustLaunch(id)
491 return l.GnoswapListed || l.ListVenue != ""
492}
493
494// ListPoolPathOf returns the pool path after listing.
495func ListPoolPathOf(id string) string {
496 return mustLaunch(id).GnoswapPoolPath
497}
498
499// ListNoteOf returns the listing status note.
500func ListNoteOf(id string) string {
501 return mustLaunch(id).GnoswapNote
502}
503
504func swapWugnotForGNS(cur realm, amountOutGNS, maxInWugnot int64) (spent int64, ok bool) {
505 if amountOutGNS <= 0 || maxInWugnot <= 0 {
506 return 0, false
507 }
508 routerAddr := address(gnoswapRouterAddrStr)
509 before := wugnot.BalanceOf(cur.Address())
510 if before < maxInWugnot {
511 maxInWugnot = before
512 }
513 if maxInWugnot <= 0 {
514 return 0, false
515 }
516 wugnot.Approve(cross(cur), routerAddr, maxInWugnot)
517
518 route := wugnotTokenKey + ":" + gnsTokenKey + ":" + strconv.FormatUint(uint64(GnoswapFeeTier), 10)
519 deadline := time.Now().Unix() + 600
520 inStr, outStr := router.ExactOutSwapRoute(
521 cross(cur),
522 wugnotTokenKey,
523 gnsTokenKey,
524 strconv.FormatInt(amountOutGNS, 10),
525 route,
526 "100",
527 strconv.FormatInt(maxInWugnot, 10),
528 deadline,
529 "",
530 )
531 _ = outStr
532 spentIn, err := strconv.ParseInt(inStr, 10, 64)
533 if err != nil || spentIn <= 0 {
534 after := wugnot.BalanceOf(cur.Address())
535 if after >= before {
536 return 0, false
537 }
538 return before - after, true
539 }
540 return spentIn, true
541}
542
543func computeSqrtPriceX96(amount0, amount1 int64) string {
544 if amount0 <= 0 || amount1 <= 0 {
545 return ""
546 }
547 a0 := u256.MustFromDecimal(strconv.FormatInt(amount0, 10))
548 a1 := u256.MustFromDecimal(strconv.FormatInt(amount1, 10))
549 num := u256.Zero().Mul(a1, consts.Q192())
550 ratio := u256.Zero().Div(num, a0)
551 sqrt := u256Sqrt(ratio)
552 if sqrt.Lt(consts.MinSqrtRatio()) {
553 return consts.MinSqrtRatio().ToString()
554 }
555 if sqrt.Gte(consts.MaxSqrtRatio()) {
556 return u256.Zero().Sub(consts.MaxSqrtRatio(), u256.One()).ToString()
557 }
558 return sqrt.ToString()
559}
560
561func u256Sqrt(x *u256.Uint) *u256.Uint {
562 if x == nil || x.IsZero() {
563 return u256.Zero()
564 }
565 if x.Eq(u256.One()) {
566 return u256.One()
567 }
568 lo := u256.Zero()
569 hi := u256.Zero().Add(x, u256.One())
570 cap128 := u256.Zero().Lsh(u256.One(), 128)
571 if hi.Gt(cap128) {
572 hi = cap128.Clone()
573 }
574 for lo.Lt(u256.Zero().Sub(hi, u256.One())) {
575 mid := u256.Zero().Div(u256.Zero().Add(lo, hi), u256.NewUint(2))
576 if mid.IsZero() {
577 lo = u256.One()
578 continue
579 }
580 sq, overflow := u256.Zero().MulOverflow(mid, mid)
581 if overflow || sq.Gt(x) {
582 hi = mid
583 } else {
584 lo = mid
585 }
586 }
587 return lo
588}
589
590func alignedFullRangeTicks(spacing int32) (int32, int32) {
591 if spacing <= 0 {
592 spacing = 60
593 }
594 tl := (GnoswapMinTick / spacing) * spacing
595 tu := (GnoswapMaxTick / spacing) * spacing
596 if tl >= tu {
597 tl = -spacing
598 tu = spacing
599 }
600 return tl, tu
601}
602
603// GnoswapListedOf reports whether a launch was auto-listed on Gnoswap.
604func GnoswapListedOf(id string) bool {
605 return mustLaunch(id).GnoswapListed
606}
607
608// GnoswapPoolPathOf returns the Gnoswap pool path after listing (or empty).
609func GnoswapPoolPathOf(id string) string {
610 return mustLaunch(id).GnoswapPoolPath
611}
612
613// GnoswapNoteOf returns the listing status note.
614func GnoswapNoteOf(id string) string {
615 return mustLaunch(id).GnoswapNote
616}