gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/gnomi/pad⧉ Realm Open in gnoweb ↗
Kind Realm (renderable)
Name pad
Namespace g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt / gnomi
Exported functions 76
Module gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/gnomi/pad
gno 0.9 memepad.gno gno
⧉
1 // Package pad is gnomemepad: a self-contained meme launchpad for gno.land.
2 //
3 // CreateWithVenue (bond + venue-aware GNS escrow) -> GRC20 + WUGNOT curve -> Graduate
4 // -> internal CPMM; then either:
5 // gnoswap: RetryList / RetryListGnoswap (escrowed GNS + WUGNOT inventory)
6 // zdex: ReleaseListSeed -> EOA CreatePool -> ConfirmZdexList
7 AdenaPathOf (id string) string
AdminInfo () string
Approve (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, spender string, amount int64)
BalanceOf (id string, owner string) int64
Buy (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, amountWugnot int64, minTokensOut int64) int64
ClaimCreatorFees (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string) int64
ClaimListFee (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string) int64
ClaimProtocolFees (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) int64
ClaimWugnot (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) int64
ConfirmZdexList (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, poolId string)
Create (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, symbol string, uri string) string
CreateBondRequired () int64
CreateWithVenue (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, name string, symbol string, uri string, venueId string) string
DefaultListVenue () string
FeeInfo () string
FreeGns () string
FreeWugnot () string
GetCreatorFees (id string) int64
GetPool (id string) (ugnot int64, token int64)
GetRaised (id string) int64
GetStatus (id string) int
GnoswapListedOf (id string) bool
GnoswapNoteOf (id string) string
GnoswapPoolPathOf (id string) string
Graduate (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string)
GraduationThresholdLive () int64
GRC20Bank (id string) *gno.land/p/nt/grc20/v0.Token
Init (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
IsProtocol (addr string) bool
LaunchCount () int
LaunchInfo (id string) string
ListBuyers (id string) string
ListedOf (id string) bool
ListFeeRequired () int64
ListFeeRequiredFor (venueId string) int64
ListIDs () string
ListNeed (id string) string
ListNeedFor (id string, venueId string) string
ListNoteOf (id string) string
ListPoolPathOf (id string) string
ListSeedReleasedOf (id string) bool
ListVenueOf (id string) string
ListVenues () string
PadAddress () string
ParamsInfo () string
PointsEnabled () bool
PreferredVenueOf (id string) string
PrepaidBalance (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) int64
PrepaidOf (addr string) int64
ProtocolAddress () string
ProtocolFees () int64
ProtocolFeesPaid () int64
PushProtocolFees (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) int64
ReleaseListSeed (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string)
RemainingRaiseUgnot (id string) int64
Render (path string) string
ReservedUgnot () int64
ResolveSymbol (symbol string) string
RetryList (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, venueId string) bool
RetryListGnoswap (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string) bool
Sell (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, tokensIn int64, minWugnotOut int64) int64
SetDefaultListVenue (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, venueId string)
SetGraduationThreshold (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, ugnot int64)
SetListFeeGns (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, gns int64)
SetListVenue (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, label string, enabled bool, feeAssetHint string)
SetPointsEnabled (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, on bool)
SetPreferredVenue (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, venueId string)
SwapBuy (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, amountWugnot int64, minTokensOut int64) int64
SwapSell (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, tokensIn int64, minWugnotOut int64) int64
TokenIDOf (id string) string
TradeCount (id string) int
TradeHistory (id string) string
Transfer (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, to string, amount int64)
TransferFrom (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, from string, to string, amount int64)
TransferProtocol (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newAddr string)
WithdrawProtocolUgnot (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, amount int64) int64
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.
//
8 // Production: Buy/Sell/Swap* use WUGNOT (push-pay + prepaid credits).
9 // Payment: user wugnot.Transfer(pad, amount) then Buy.
10 // Create fee: gnoswap preference locks ListFeeGns; zdex preference locks 0.
11 // DefaultListVenue source = zdex; Pearl may SetDefaultListVenue(gnoswap) after deploy.
12 // Unit tests (testSkipBanker): OriginSend ugnot as collateral units.
13 // Tokens are real GRC20 (mint on buy, burn on sell).
14 package pad
15
16 import (
17 "chain"
18 "chain/banker"
19 "chain/runtime"
20 "chain/runtime/unsafe"
21 "strconv"
22 "strings"
23
24 "gno.land/p/nt/grc20/v0"
25 "gno.land/p/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/gnomi/ammmath"
26 "gno.land/p/nt/avl/v0"
27 "gno.land/p/nt/seqid/v0"
28 "gno.land/r/nt/grc20reg/v0"
29 "gno.land/r/gnoland/wugnot"
30 "gno.land/r/gnoswap/gns"
31 // bond: create-bond policy (promo / normal). Separate package - pad upgrades
32 // do not replace bond schedule. Deploy prepare rewrites to personal path.
33 createbond "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/gnomi/bond"
34 // pointsv2: optional trade/create awards (off by default until SetPointsEnabled).
35 // Deploy prepare rewrites this import to the Sapphire personal-namespace path.
36 pointsv2 "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/gnomi/pointsv2"
37 )
38
39 var (
40 launches avl.Tree // id -> *Launch
41 bySymbol avl.Tree // symbol -> id string
42 nextID seqid.ID
43 nextTokenID seqid.ID // GRC20 identity sequence (shared for all launches)
44 // padAddr: this realm's package address (set in init) - for inventory funding.
45 padAddr address
46 // protocolAddr: treasury set by Init (first EOA). Receives protocol fee share.
47 protocolAddr address
48 // protocolFees: ugnot still on pad, claimable / pushable to protocolAddr.
49 protocolFees int64
50 // protocolFeesPaid: lifetime ugnot already sent to protocolAddr (stats only).
51 protocolFeesPaid int64
52 inited bool
53 // testSkipBanker: when true, sendUgnot is a no-op (unit tests without funded realm bank).
54 // Always false in production.
55 testSkipBanker bool
56 // testForceGnoswapList: unit tests only - stub tryListOnGnoswap succeeds without Sapphire deps.
57 // Always false in production (gnoswap_list_full ignores it).
58 testForceGnoswapList bool
59 // testForceZdexList: unit tests only - stub tryListOnZdex succeeds.
60 testForceZdexList bool
61 // pointsEnabled: when true, notify pointsv2 after Create / Buy / Sell / Swap*.
62 // Admin must also AllowPad(this package) on pointsv2.
63 pointsEnabled bool
64 // Live mutable economics (protocol-gated). Seeded from consts in Init.
65 // Changing graduation mid-flight affects open curve launches' ready/remaining.
66 graduationUgnot int64
67 listFeeGnsLive int64
68 // wugnotCredit: EOA address string -> int64 prepaid WUGNOT claimable by user.
69 // totalWugnotCredit: sum of all credits (included in reservedWugnot).
70 // Flow: Transfer WUGNOT to pad (raises free) -> Buy auto-locks free into credit
71 // and spends; overpay refund goes back to credit; ClaimWugnot withdraws credit.
72 wugnotCredit avl.Tree
73 totalWugnotCredit int64
74 // totalListFeeGns: sum of per-launch ListFeeGns escrow (reserved GNS on pad).
75 // freeGns = gns.BalanceOf(pad) - totalListFeeGns.
76 totalListFeeGns int64
77 )
78
79 func init(cur realm) {
80 padAddr = cur.Address()
81 }
82
83 // Trade is one price sample for charts (capped history per launch).
84 type Trade struct {
85 Height int64
86 Side int // TradeSideBuy | TradeSideSell | TradeSideOpen
87 Ugnot int64
88 Tokens int64
89 Price int64 // ugnot per token * 1e6 after the trade
90 }
91
92 // Launch is one meme market: curve phase then locked pool phase.
93 // token/ledger are unexported so external packages cannot Mint/Burn via field access.
94 type Launch struct {
95 ID string
96 Name string
97 Symbol string
98 URI string
99 Creator address
100 Status int
101 Created int64 // block height
102
103 // GRC20 (mint/burn only via pad-owned private ledger)
104 token *grc20.Token
105 ledger *grc20.PrivateLedger
106 TokenID string // Token.ID() - registry / Gnoswap identity
107
108 // Virtual curve reserves
109 VirtualUgnot int64
110 VirtualToken int64
111 RealSold int64 // tokens sold on curve (<= CurveSupply)
112 RaisedUgnot int64 // net ugnot collateral in curve (excl. fee vaults)
113
114 // Real pool (post-grad); LP permanently locked - no remove path
115 // PoolToken is pad-internal reserve sized to curve spot (not always all unsold).
116 // LeftoverTokens = (TotalSupply - RealSold) - PoolToken; minted to pad at list, not LP'd.
117 PoolUgnot int64
118 PoolToken int64
119 LeftoverTokens int64
120
121 CreatorFees int64
122 BondUgnot int64
123 BondRefunded bool
124 UniqueBuyers avl.Tree // address -> true
125 BuyerCount int
126 // snipeBought: address -> cumulative tokens bought during anti-snipe window
127 snipeBought avl.Tree
128
129 // Gnoswap listing state
130 GnoswapReady bool // graduated; token is listable / listed
131 GnoswapListed bool // true when CreatePool+Mint succeeded on Gnoswap
132 GnoswapNote string // human status / failure reason
133 GnoswapPoolPath string
134 GnoswapPositionID uint64
135 // FeeWugnotSpent / LiqWugnotUsed: inventory spent at graduate (1:1 vs raised ugnot notionally)
136 FeeWugnotSpent int64
137 LiqWugnotUsed int64
138
139 // ListFeeGns: Create-time GNS escrow for Gnoswap CreatePool fee (padv20+).
140 // Consumed on successful list; ClaimListFee refunds creator if still unlisted.
141 ListFeeGns int64
142 ListFeeConsumed bool // true after list paid CreatePool fee from escrow
143
144 // ListVenue: which DEX adapter succeeded (e.g. "zdex" / "gnoswap"). Empty if unlisted.
145 ListVenue string
146 // PreferredVenue: chosen at Create (or SetPreferredVenue). Used when RetryList venueId == "".
147 PreferredVenue string
148 // ListSeedReleased: PoolUgnot (WUGNOT) + PoolToken (minted GRC20) sent to creator for EOA zdex CreatePool.
149 ListSeedReleased bool
150
151 // Chart history (ordered AVL keys)
152 Trades avl.Tree // tradeKey -> *Trade
153 NextTrade int64
154 }
155
156 // Init sets the protocol treasury. First EOA caller becomes fee recipient
157 // (protocolAddr). Protocol trade fees accrue on-pad until ClaimProtocolFees
158 // (treasury only) or PushProtocolFees (anyone may push to treasury).
159 // Creator fees always need ClaimCreatorFees by the token creator.
160 //
161 // Deploy note: call Init with the wallet that should receive protocol fees
162 // (or TransferProtocol later). Gnoswap CreatePool GNS fee is paid to Gnoswap,
163 // not to this treasury.
164 func Init(cur realm) {
165 if inited {
166 panic( "pad: already initialized" )
167 }
168 if !cur.Previous().IsUserCall() {
169 panic( "pad: EOA only" )
170 }
171 protocolAddr = cur.Previous().Address()
172 graduationUgnot = GraduationThreshold // seed from const default
173 listFeeGnsLive = ListFeeGns
174 if graduationUgnot <= 0 {
175 panic( "pad: default graduation misconfigured" )
176 }
177 ensureListVenuesSeeded()
178 inited = true
179 chain.Emit( "Init" ,
180 "protocol" , protocolAddr.String(),
181 "graduationUgnot" , strconv.FormatInt(graduationUgnot, 10 ),
182 "listFeeGns" , strconv.FormatInt(listFeeGnsLive, 10 ),
183 "defaultListVenue" , DefaultListVenue(),
184 )
185 }
186
187 func requireProtocol(cur realm) {
188 requireInit()
189 if !cur.Previous().IsUserCall() {
190 panic( "pad: EOA only" )
191 }
192 if cur.Previous().Address() != protocolAddr {
193 panic( "pad: not protocol" )
194 }
195 }
196
197 // graduationThreshold is the live raise target (ugnot). Falls back to const if unset.
198 func graduationThreshold() int64 {
199 if graduationUgnot > 0 {
200 return graduationUgnot
201 }
202 return GraduationThreshold
203 }
204
205 // SetGraduationThreshold updates the live raise target (ugnot, 1 GNOT = 1e6).
206 // Protocol/deploy wallet only. Affects open curve launches (remaining raise / ready).
207 func SetGraduationThreshold(cur realm, ugnot int64 ) {
208 requireProtocol(cur)
209 if ugnot <= 0 {
210 panic( "pad: graduation must be positive" )
211 }
212 old := graduationThreshold()
213 graduationUgnot = ugnot
214 chain.Emit( "SetGraduationThreshold" ,
215 "from" , strconv.FormatInt(old, 10 ),
216 "to" , strconv.FormatInt(ugnot, 10 ),
217 )
218 }
219
220 // GraduationThresholdLive returns the live raise target (ugnot) for UIs/qeval.
221 func GraduationThresholdLive() int64 {
222 return graduationThreshold()
223 }
224
225 // SetListFeeGns updates Create-time GNS escrow required for new launches.
226 // Protocol/deploy wallet only. Does not change already-escrowed launches.
227 func SetListFeeGns(cur realm, gns int64 ) {
228 requireProtocol(cur)
229 if gns < 0 {
230 panic( "pad: list fee must be non-negative" )
231 }
232 old := requiredListFeeGns()
233 listFeeGnsLive = gns
234 chain.Emit( "SetListFeeGns" ,
235 "from" , strconv.FormatInt(old, 10 ),
236 "to" , strconv.FormatInt(gns, 10 ),
237 )
238 }
239
240 // creditProtocol accrues protocol ugnot liability on the pad realm.
241 // Cash stays in pad until ClaimProtocolFees / PushProtocolFees.
242 func creditProtocol(amt int64 ) {
243 if amt <= 0 {
244 return
245 }
246 protocolFees += amt
247 }
248
249 func requireInit() {
250 if !inited {
251 panic( "pad: call Init first" )
252 }
253 }
254
255 // SetPointsEnabled toggles pointsv2 notifications (protocol admin only).
256 // pointsv2 must AllowPad(this package path) or OnTrade/OnCreate will panic and revert the trade.
257 func SetPointsEnabled(cur realm, on bool ) {
258 requireProtocol(cur)
259 pointsEnabled = on
260 chain.Emit( "SetPointsEnabled" , "on" , strconv.FormatBool(on))
261 }
262
263 // PointsEnabled reports whether pad notifies pointsv2 after trades/creates.
264 func PointsEnabled() bool {
265 return pointsEnabled
266 }
267
268 func notifyTrade(cur realm, trader address, id string , side int64 , volumeUgnot int64 ) {
269 if !pointsEnabled || testSkipBanker {
270 return
271 }
272 _ = pointsv2.OnTrade(cross(cur), trader, id, side, volumeUgnot)
273 }
274
275 func notifyCreate(cur realm, creator address, id string ) {
276 if !pointsEnabled || testSkipBanker {
277 return
278 }
279 _ = pointsv2.OnCreate(cross(cur), creator, id)
280 }
281
282 func mustLaunch(id string ) *Launch {
283 // Sapphire avl.Tree.Get returns a single any (nil if missing).
284 l, ok := launches.Get(id).(*Launch)
285 if !ok {
286 panic( "pad: unknown launch" )
287 }
288 return l
289 }
290
291 func balOf(l *Launch, addr address) int64 {
292 if l == nil || l.token == nil {
293 return 0
294 }
295 return l.token.BalanceOf(addr)
296 }
297
298 // addBal mints (delta>0) or burns (delta<0) GRC20 via pad-owned PrivateLedger.
299 func addBal(l *Launch, addr address, delta int64 ) {
300 if l == nil || l.ledger == nil {
301 panic( "pad: missing GRC20 ledger" )
302 }
303 if delta == 0 {
304 return
305 }
306 if delta > 0 {
307 if err := l.ledger.Mint(addr, delta); err != nil {
308 panic( "pad: mint: " + err.Error())
309 }
310 return
311 }
312 if err := l.ledger.Burn(addr, -delta); err != nil {
313 panic( "pad: burn: " + err.Error())
314 }
315 }
316
317 func requireMinOut(got, minOut int64 , what string ) {
318 if minOut < 0 {
319 panic( "pad: minOut must be non-negative" )
320 }
321 if minOut > 0 && got < minOut {
322 panic( "pad: " + what + " below minOut (slippage)" )
323 }
324 }
325
326 func snipeBoughtOf(l *Launch, buyer address) int64 {
327 v := l.snipeBought.Get(buyer.String())
328 if v == nil {
329 return 0
330 }
331 n, ok := v.( int64 )
332 if !ok {
333 return 0
334 }
335 return n
336 }
337
338 func checkAndAddSnipe(l *Launch, buyer address, tokensOut int64 ) {
339 height := runtime.ChainHeight()
340 if height-l.Created >= AntiSnipeHeights {
341 return
342 }
343 maxTok := TotalSupply * AntiSnipeMaxBuyBPS / 10000
344 prev := snipeBoughtOf(l, buyer)
345 if prev+tokensOut > maxTok {
346 panic( "pad: anti-snipe cumulative max buy exceeded" )
347 }
348 l.snipeBought.Set(buyer.String(), prev+tokensOut)
349 }
350
351 func sendUgnot(cur realm, to address, amount int64 ) {
352 if amount <= 0 {
353 return
354 }
355 if testSkipBanker {
356 return
357 }
358 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
359 bk.SendCoins(cur.Address(), to, chain.Coins{{Denom: DenomUgnot, Amount: amount}})
360 }
361
362 func requireUserPayment(cur realm) int64 {
363 if !cur.Previous().IsUserCall() {
364 panic( "pad: must be EOA MsgCall" )
365 }
366 sent := unsafe.OriginSend().AmountOf(DenomUgnot)
367 if sent <= 0 {
368 panic( "pad: need ugnot -send" )
369 }
370 return sent
371 }
372
373 // --- WUGNOT collateral (production curve / internal CPMM) ---
374
375 // reservedWugnot is WUGNOT already committed (markets/fees/user prepaid credits).
376 // Excludes bank-only liabilities (create bond ugnot).
377 func reservedWugnot() int64 {
378 reserved := protocolFees + totalWugnotCredit
379 launches.Iterate( "" , "" , func (_ string , value any ) bool {
380 l := value.(*Launch)
381 reserved += l.CreatorFees
382 if l.Status == StatusCurve {
383 reserved += l.RaisedUgnot
384 }
385 // Internal CPMM holds WUGNOT when not listed and seed not released to EOA.
386 if l.Status == StatusGraduated && !l.GnoswapListed && !l.ListSeedReleased {
387 reserved += l.PoolUgnot
388 }
389 return false
390 })
391 return reserved
392 }
393
394 // freeWugnotOnPad is WUGNOT on pad above reserved commitments.
395 // After user wugnot.Transfer(pad, amt), free rises; Buy locks free into caller credit.
396 func freeWugnotOnPad() int64 {
397 have := wugnot.BalanceOf(padAddr)
398 free := have - reservedWugnot()
399 if free < 0 {
400 return 0
401 }
402 return free
403 }
404
405 // FreeWugnot is free|have|reserved|totalCredit for UI preflight.
406 func FreeWugnot() string {
407 have := wugnot.BalanceOf(padAddr)
408 res := reservedWugnot()
409 free := have - res
410 if free < 0 {
411 free = 0
412 }
413 return strconv.FormatInt(free, 10 ) + "|" +
414 strconv.FormatInt(have, 10 ) + "|" +
415 strconv.FormatInt(res, 10 ) + "|" +
416 strconv.FormatInt(totalWugnotCredit, 10 )
417 }
418
419 func prepaidOf(addr address) int64 {
420 if !addr.IsValid() {
421 return 0
422 }
423 v := wugnotCredit.Get(addr.String())
424 if v == nil {
425 return 0
426 }
427 n, ok := v.( int64 )
428 if !ok {
429 return 0
430 }
431 return n
432 }
433
434 func addPrepaid(addr address, amount int64 ) {
435 if amount <= 0 || !addr.IsValid() {
436 return
437 }
438 next := prepaidOf(addr) + amount
439 wugnotCredit.Set(addr.String(), next)
440 totalWugnotCredit += amount
441 }
442
443 func subPrepaid(addr address, amount int64 ) {
444 if amount <= 0 {
445 return
446 }
447 have := prepaidOf(addr)
448 if have < amount {
449 panic( "pad: prepaid WUGNOT underfunded" )
450 }
451 next := have - amount
452 if next == 0 {
453 wugnotCredit.Remove(addr.String())
454 } else {
455 wugnotCredit.Set(addr.String(), next)
456 }
457 totalWugnotCredit -= amount
458 }
459
460 // PrepaidBalance returns caller's claimable WUGNOT credit on this pad.
461 func PrepaidBalance(cur realm) int64 {
462 if !cur.Previous().IsUserCall() {
463 panic( "pad: must be EOA MsgCall" )
464 }
465 return prepaidOf(cur.Previous().Address())
466 }
467
468 // PrepaidOf returns claimable credit for an address (read helper).
469 func PrepaidOf(addr string ) int64 {
470 return prepaidOf(address(addr))
471 }
472
473 // ClaimWugnot withdraws all of the caller's prepaid WUGNOT credit to their wallet.
474 // Safe after Buy overpay or after graduate/list — excess stays claimable until claimed.
475 func ClaimWugnot(cur realm) int64 {
476 requireInit()
477 if !cur.Previous().IsUserCall() {
478 panic( "pad: must be EOA MsgCall" )
479 }
480 user := cur.Previous().Address()
481 amt := prepaidOf(user)
482 if amt <= 0 {
483 return 0
484 }
485 subPrepaid(user, amt)
486 payWugnotOut(cur, user, amt)
487 chain.Emit( "ClaimWugnot" ,
488 "user" , user.String(),
489 "amount" , strconv.FormatInt(amt, 10 ),
490 )
491 return amt
492 }
493
494 // takeWugnotIn collects amount WUGNOT as payment for Buy/SwapBuy.
495 //
496 // 1) Auto-lock free float into caller's prepaid (user must Transfer to pad first).
497 // 2) Spend amount from prepaid (overpay stays as credit via creditRefund).
498 // No TransferFrom.
499 //
500 // Tests (testSkipBanker): OriginSend ugnot as synthetic collateral units.
501 func takeWugnotIn(cur realm, amount int64 ) int64 {
502 if !cur.Previous().IsUserCall() {
503 panic( "pad: must be EOA MsgCall" )
504 }
505 if testSkipBanker {
506 // Local tests: OriginSend ugnot stands in for WUGNOT units.
507 return requireUserPayment(cur)
508 }
509 if amount <= 0 {
510 panic( "pad: amountWugnot must be positive" )
511 }
512 user := cur.Previous().Address()
513 have := prepaidOf(user)
514 if have < amount {
515 need := amount - have
516 free := freeWugnotOnPad()
517 if free < need {
518 havePad := wugnot.BalanceOf(padAddr)
519 res := reservedWugnot()
520 panic( "pad: Transfer " + strconv.FormatInt(need, 10 ) +
521 " more WUGNOT to pad then Buy (free=" + strconv.FormatInt(free, 10 ) +
522 " prepaid=" + strconv.FormatInt(have, 10 ) +
523 " have=" + strconv.FormatInt(havePad, 10 ) +
524 " reserved=" + strconv.FormatInt(res, 10 ) +
525 " needTotal=" + strconv.FormatInt(amount, 10 ) + ")" )
526 }
527 // Lock free into this caller's prepaid (raises reserved, lowers free).
528 addPrepaid(user, need)
529 }
530 subPrepaid(user, amount)
531 return amount
532 }
533
534 // creditRefund keeps unused Buy WUGNOT as claimable prepaid (not instant wallet refund).
535 // User calls ClaimWugnot after buy / after list when convenient.
536 func creditRefund(buyer address, amount int64 ) {
537 if testSkipBanker || amount <= 0 {
538 return
539 }
540 addPrepaid(buyer, amount)
541 }
542
543 // payWugnotOut sends WUGNOT from pad to user (production).
544 // Tests: pay ugnot via banker when testSkipBanker.
545 func payWugnotOut(cur realm, to address, amount int64 ) {
546 if amount <= 0 {
547 return
548 }
549 if testSkipBanker {
550 sendUgnot(cur, to, amount)
551 return
552 }
553 have := wugnot.BalanceOf(cur.Address())
554 if have < amount {
555 amount = have
556 }
557 if amount > 0 {
558 wugnot.Transfer(cross(cur), to, amount)
559 }
560 }
561
562 func noteBuyer(l *Launch, buyer address) {
563 k := buyer.String()
564 if l.UniqueBuyers.Has(k) {
565 return
566 }
567 l.UniqueBuyers.Set(k, true )
568 l.BuyerCount++
569 }
570
571 func tradeKey(n int64 ) string {
572 s := strconv.FormatInt(n, 10 )
573 for len(s) < 12 {
574 s = "0" + s
575 }
576 return s
577 }
578
579 // spotPriceScaled returns ugnot/token * 1e6 from current curve or pool reserves.
580 func spotPriceScaled(l *Launch) int64 {
581 if l.Status == StatusGraduated {
582 if l.PoolToken <= 0 {
583 return 0
584 }
585 return l.PoolUgnot * 1000000 / l.PoolToken
586 }
587 if l.VirtualToken <= 0 {
588 return 0
589 }
590 return l.VirtualUgnot * 1000000 / l.VirtualToken
591 }
592
593 func recordTrade(l *Launch, side int , ugnot, tokens int64 ) {
594 l.NextTrade++
595 t := &Trade{
596 Height: runtime.ChainHeight(),
597 Side: side,
598 Ugnot: ugnot,
599 Tokens: tokens,
600 Price: spotPriceScaled(l),
601 }
602 l.Trades.Set(tradeKey(l.NextTrade), t)
603 // Ring buffer: drop oldest while over cap.
604 for l.Trades.Size() > MaxTradeHistory {
605 oldest := ""
606 l.Trades.Iterate( "" , "" , func (k string , _ any ) bool {
607 oldest = k
608 return true // stop
609 })
610 if oldest == "" {
611 break
612 }
613 l.Trades.Remove(oldest)
614 }
615 }
616
617 func maybeRefundBond(cur realm, l *Launch) {
618 if l.BondRefunded || l.BondUgnot <= 0 {
619 return
620 }
621 if l.BuyerCount < BondRefundBuyers {
622 return
623 }
624 // Quality gate: pure sybil micro-buys cannot refund bond.
625 if l.RaisedUgnot < BondRefundMinRaised {
626 return
627 }
628 if runtime.ChainHeight()-l.Created > BondRefundMaxHeights {
629 return
630 }
631 amt := l.BondUgnot
632 l.BondUgnot = 0
633 l.BondRefunded = true
634 sendUgnot(cur, l.Creator, amt)
635 chain.Emit( "BondRefund" , "id" , l.ID, "amount" , strconv.FormatInt(amt, 10 ))
636 }
637
638 // requiredCreateBond returns ugnot the creator must send.
639 // Production: createbond.CurrentBondUgnot() (promo or normal).
640 // Unit tests (testSkipBanker): local CreateBondUgnot constant.
641 func requiredCreateBond() int64 {
642 if testSkipBanker {
643 return CreateBondUgnot
644 }
645 return createbond.CurrentBondUgnot()
646 }
647
648 // CreateBondRequired is a public alias for UIs / qeval (same as requiredCreateBond).
649 func CreateBondRequired() int64 {
650 return requiredCreateBond()
651 }
652
653 // requiredListFeeGns returns GNS base units the creator must pre-fund (free on pad).
654 // Live value from SetListFeeGns; falls back to ListFeeGns const.
655 func requiredListFeeGns() int64 {
656 if listFeeGnsLive > 0 {
657 return listFeeGnsLive
658 }
659 if ListFeeGns > 0 {
660 return ListFeeGns
661 }
662 return 100_000_000
663 }
664
665 // ListFeeRequired is a public alias for UIs / qeval.
666 func ListFeeRequired() int64 {
667 return requiredListFeeGns()
668 }
669
670 // freeGnsOnPad is GNS on pad above per-launch list-fee escrow.
671 func freeGnsOnPad() int64 {
672 if testSkipBanker {
673 // Tests skip GNS inventory checks.
674 return requiredListFeeGns()
675 }
676 have := gns.BalanceOf(padAddr)
677 free := have - totalListFeeGns
678 if free < 0 {
679 return 0
680 }
681 return free
682 }
683
684 // FreeGns returns free|have|reserved for UI preflight (GNS list fee).
685 func FreeGns() string {
686 have := int64 ( 0 )
687 if !testSkipBanker {
688 have = gns.BalanceOf(padAddr)
689 }
690 free := freeGnsOnPad()
691 return strconv.FormatInt(free, 10 ) + "|" +
692 strconv.FormatInt(have, 10 ) + "|" +
693 strconv.FormatInt(totalListFeeGns, 10 )
694 }
695
696 // lockListFeeEscrow earmarks free GNS for this launch's CreatePool fee.
697 func lockListFeeEscrow(l *Launch, amt int64 ) {
698 if l == nil || amt <= 0 {
699 return
700 }
701 if freeGnsOnPad() < amt {
702 have := int64 ( 0 )
703 if !testSkipBanker {
704 have = gns.BalanceOf(padAddr)
705 }
706 panic( "pad: Transfer " + strconv.FormatInt(amt, 10 ) +
707 " GNS to pad then Create (free=" + strconv.FormatInt(freeGnsOnPad(), 10 ) +
708 " have=" + strconv.FormatInt(have, 10 ) +
709 " reserved=" + strconv.FormatInt(totalListFeeGns, 10 ) + ")" )
710 }
711 totalListFeeGns += amt
712 l.ListFeeGns = amt
713 l.ListFeeConsumed = false
714 }
715
716 // listFeeForVenue: gnoswap → ListFeeRequired; zdex / other → 0.
717 func listFeeForVenue(venueId string ) int64 {
718 if normalizeVenueID(venueId) == VenueGnoswap {
719 return requiredListFeeGns()
720 }
721 return 0
722 }
723
724 // ListFeeRequiredFor is venue-aware Create fee for UI preflight.
725 func ListFeeRequiredFor(venueId string ) int64 {
726 ensureListVenuesSeeded()
727 return listFeeForVenue(venueId)
728 }
729
730 // unlockListFeeEscrow clears launch GNS escrow; refund sends GNS back to creator.
731 func unlockListFeeEscrow(cur realm, l *Launch, refund bool ) {
732 if l == nil || l.ListFeeGns <= 0 || l.ListFeeConsumed {
733 return
734 }
735 amt := l.ListFeeGns
736 l.ListFeeGns = 0
737 totalListFeeGns -= amt
738 if totalListFeeGns < 0 {
739 totalListFeeGns = 0
740 }
741 if refund && !testSkipBanker && amt > 0 {
742 have := gns.BalanceOf(padAddr)
743 if have < amt {
744 amt = have
745 }
746 if amt > 0 {
747 gns.Transfer(cross(cur), l.Creator, amt)
748 }
749 }
750 chain.Emit( "ListFeeUnlocked" ,
751 "id" , l.ID,
752 "amount" , strconv.FormatInt(amt, 10 ),
753 "refund" , strconv.FormatBool(refund),
754 )
755 }
756
757 // syncListFeeForVenue adjusts escrow after PreferredVenue change (pre-list only).
758 func syncListFeeForVenue(cur realm, l *Launch, vid string ) {
759 if l == nil || l.ListFeeConsumed || l.GnoswapListed || l.ListVenue != "" {
760 return
761 }
762 want := listFeeForVenue(vid)
763 if l.ListFeeGns == want {
764 return
765 }
766 if l.ListFeeGns > 0 {
767 unlockListFeeEscrow(cur, l, true )
768 }
769 lockListFeeEscrow(l, want)
770 }
771
772 // consumeListFeeEscrow clears launch escrow after successful Gnoswap list
773 // (CreatePool spent feeNeed GNS from pad balance).
774 func consumeListFeeEscrow(l *Launch) {
775 if l == nil || l.ListFeeGns <= 0 {
776 return
777 }
778 amt := l.ListFeeGns
779 totalListFeeGns -= amt
780 if totalListFeeGns < 0 {
781 totalListFeeGns = 0
782 }
783 l.ListFeeGns = 0
784 l.ListFeeConsumed = true
785 chain.Emit( "ListFeeConsumed" , "id" , l.ID, "amount" , strconv.FormatInt(amt, 10 ))
786 }
787
788 // ClaimListFee refunds Create-time GNS escrow to the token creator if still unlisted.
789 // Use when graduate/list will not complete or creator abandons listing.
790 func ClaimListFee(cur realm, id string ) int64 {
791 requireInit()
792 if !cur.Previous().IsUserCall() {
793 panic( "pad: must be EOA MsgCall" )
794 }
795 l := mustLaunch(id)
796 caller := cur.Previous().Address()
797 if caller != l.Creator {
798 panic( "pad: only creator may claim list fee" )
799 }
800 if l.GnoswapListed {
801 panic( "pad: already listed - list fee spent" )
802 }
803 if l.ListFeeConsumed {
804 panic( "pad: list fee already consumed" )
805 }
806 amt := l.ListFeeGns
807 if amt <= 0 {
808 return 0
809 }
810 l.ListFeeGns = 0
811 totalListFeeGns -= amt
812 if totalListFeeGns < 0 {
813 totalListFeeGns = 0
814 }
815 if !testSkipBanker && amt > 0 {
816 have := gns.BalanceOf(padAddr)
817 if have < amt {
818 amt = have
819 }
820 if amt > 0 {
821 gns.Transfer(cross(cur), l.Creator, amt)
822 }
823 }
824 chain.Emit( "ClaimListFee" ,
825 "id" , id,
826 "creator" , l.Creator.String(),
827 "amount" , strconv.FormatInt(amt, 10 ),
828 )
829 return amt
830 }
831
832 // Create deploys a fair-launch meme. Bond amount from bond realm (or fallback const).
833 // Also locks ListFeeGns free GNS (creator must Transfer GNS to pad first).
834 // No pre-mint; all tradeable float starts on the bonding curve.
835 func Create(cur realm, name, symbol, uri string ) string {
836 requireInit()
837 sent := requireUserPayment(cur)
838 bondNeed := requiredCreateBond()
839 if bondNeed <= 0 {
840 panic( "pad: create bond misconfigured" )
841 }
842 if sent < bondNeed {
843 panic( "pad: create bond underpaid" )
844 }
845 if name == "" || symbol == "" {
846 panic( "pad: name and symbol required" )
847 }
848 if len(symbol) > 12 {
849 panic( "pad: symbol too long" )
850 }
851 if bySymbol.Has(symbol) {
852 panic( "pad: symbol taken" )
853 }
854 extra := sent - bondNeed
855 if extra > 0 {
856 creditProtocol(extra)
857 }
858
859 creator := cur.Previous().Address()
860 id := nextID.Next().String()
861
862 // Real GRC20 bound to this pad realm (mint/burn only via pad ledger).
863 // Decimals=0: whole-token units (matches existing trade amounts).
864 token, ledger := grc20.NewToken(name, symbol, 0 , nextTokenID.Next(), cur)
865
866 // Adena (and Gnoswap registries) resolve tokens ONLY via grc20reg under key
867 // packagePath.SYMBOL - Token.ID() itself is packagePath.SYMBOL.seq and is
868 // rejected as "Invalid path" if pasted into Adena without registration.
869 // Skip in unit tests (testSkipBanker); production always registers.
870 regKey := ""
871 if !testSkipBanker {
872 regKey = grc20reg.Register(cross(cur), token, symbol)
873 }
874
875 ensureListVenuesSeeded()
876 pref := DefaultListVenue()
877 return finishCreate(cur, id, name, symbol, uri, creator, token, ledger, regKey, bondNeed, pref)
878 }
879
880 // CreateWithVenue is Create + PreferredVenue in one MsgCall (fee escrow follows venue).
881 // venueId: "zdex" → no GNS list escrow; "gnoswap" → ListFeeRequired GNS.
882 func CreateWithVenue(cur realm, name, symbol, uri, venueId string ) string {
883 requireInit()
884 sent := requireUserPayment(cur)
885 bondNeed := requiredCreateBond()
886 if bondNeed <= 0 {
887 panic( "pad: create bond misconfigured" )
888 }
889 if sent < bondNeed {
890 panic( "pad: create bond underpaid" )
891 }
892 name = strings.TrimSpace(name)
893 symbol = strings.TrimSpace(symbol)
894 uri = strings.TrimSpace(uri)
895 if name == "" || symbol == "" {
896 panic( "pad: name and symbol required" )
897 }
898 if len(symbol) > 12 {
899 panic( "pad: symbol too long" )
900 }
901 if bySymbol.Has(symbol) {
902 panic( "pad: symbol taken" )
903 }
904 extra := sent - bondNeed
905 if extra > 0 {
906 creditProtocol(extra)
907 }
908 creator := cur.Previous().Address()
909 id := nextID.Next().String()
910 token, ledger := grc20.NewToken(name, symbol, 0 , nextTokenID.Next(), cur)
911 regKey := ""
912 if !testSkipBanker {
913 regKey = grc20reg.Register(cross(cur), token, symbol)
914 }
915 ensureListVenuesSeeded()
916 vid := normalizeVenueID(venueId)
917 if !venueEnabled(vid) {
918 panic( "pad: venue disabled or unknown" )
919 }
920 return finishCreate(cur, id, name, symbol, uri, creator, token, ledger, regKey, bondNeed, vid)
921 }
922
923 func finishCreate(
924 cur realm,
925 id, name, symbol, uri string ,
926 creator address,
927 token *grc20.Token,
928 ledger *grc20.PrivateLedger,
929 regKey string ,
930 bondNeed int64 ,
931 pref string ,
932 ) string {
933 l := &Launch{
934 ID: id,
935 Name: name,
936 Symbol: symbol,
937 URI: uri,
938 Creator: creator,
939 Status: StatusCurve,
940 Created: runtime.ChainHeight(),
941 token: token,
942 ledger: ledger,
943 TokenID: token.ID(),
944 VirtualUgnot: VirtualUgnot0,
945 VirtualToken: VirtualToken0,
946 UniqueBuyers: avl.Tree{},
947 snipeBought: avl.Tree{},
948 BondUgnot: bondNeed,
949 Trades: avl.Tree{},
950 PreferredVenue: pref,
951 }
952 // Venue-aware list fee: zdex → 0 GNS; gnoswap → ListFeeRequired.
953 lockListFeeEscrow(l, listFeeForVenue(pref))
954 recordTrade(l, TradeSideOpen, 0 , 0 )
955 launches.Set(id, l)
956 bySymbol.Set(symbol, id)
957
958 chain.Emit( "Created" ,
959 "id" , id,
960 "symbol" , symbol,
961 "creator" , creator.String(),
962 "token" , l.TokenID,
963 "reg" , regKey,
964 "listFeeGns" , strconv.FormatInt(l.ListFeeGns, 10 ),
965 "preferredVenue" , pref,
966 )
967 notifyCreate(cur, creator, id)
968 return id
969 }
970
971 // AdenaPathOf returns the grc20reg / Adena token key: packagePath.SYMBOL
972 // (Token.ID is packagePath.SYMBOL.seq - Adena rejects that form).
973 func AdenaPathOf(id string ) string {
974 l := mustLaunch(id)
975 return adenaKeyFromTokenID(l.TokenID, l.Symbol)
976 }
977
978 // adenaKeyFromTokenID strips the trailing .seq from Token.ID when present.
979 func adenaKeyFromTokenID(tokenID, symbol string ) string {
980 if tokenID == "" {
981 return ""
982 }
983 // Token.ID = packagePath.symbol.seq -> registry key = packagePath.symbol
984 suffix := "." + symbol + "."
985 if i := strings.LastIndex(tokenID, suffix); i >= 0 {
986 // packagePath + "." + symbol
987 return tokenID[:i] + "." + symbol
988 }
989 // Already packagePath.symbol or unknown layout
990 if strings.HasSuffix(tokenID, "." +symbol) {
991 return tokenID
992 }
993 return tokenID
994 }
995
996 // maxGrossForNetIn finds largest gross ugnot <= sentMax whose fee-split netIn <= maxNet.
997 func maxGrossForNetIn(maxNet, sentMax int64 ) int64 {
998 if maxNet <= 0 || sentMax <= 0 {
999 return 0
1000 }
1001 lo, hi := int64 ( 0 ), sentMax
1002 for lo < hi {
1003 mid := (lo + hi + 1 ) / 2
1004 f := ammmath.ApplyFee(mid, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS)
1005 net := f.Net + f.Remainder
1006 if net <= maxNet {
1007 lo = mid
1008 } else {
1009 hi = mid - 1
1010 }
1011 }
1012 return lo
1013 }
1014
1015 // readyToGraduate is true when raise met the threshold, or the entire curve
1016 // float is sold (sold-out escape: threshold may be unreachable with bad virtuals).
1017 func readyToGraduate(l *Launch) bool {
1018 if l == nil || l.Status != StatusCurve {
1019 return false
1020 }
1021 if l.RaisedUgnot <= 0 {
1022 return false
1023 }
1024 if ammmath.CanGraduate(l.RaisedUgnot, graduationThreshold()) {
1025 return true
1026 }
1027 // Curve exhausted before threshold: still graduate with whatever was raised
1028 // so the market is never permanently stuck on Buy/Graduate.
1029 return l.RealSold >= CurveSupply
1030 }
1031
1032 // Buy spends WUGNOT on the bonding curve; credits tokens.
1033 // amountWugnot: max to spend (may overpay). UI: Deposit + Transfer(pad, amt) + Buy.
1034 // Overpay stays as claimable prepaid — ClaimWugnot anytime (incl. after list).
1035 // minTokensOut: slippage floor (0 = disabled). Auto-graduates at threshold or sold-out.
1036 //
1037 // Production: collateral is real WUGNOT on pad -> Graduate can auto-list Gnoswap.
1038 // Tests (testSkipBanker): amountWugnot ignored; OriginSend ugnot is used as units.
1039 //
1040 // Last-fill (no overshoot):
1041 // 1. Cap net so RaisedUgnot never exceeds graduationThreshold() (refund excess WUGNOT).
1042 // 2. Cap by remaining curve tokens (CurveSupply - RealSold).
1043 //
1044 // If the curve is already sold out (or raise already filled), Buy refunds the full
1045 // take and graduates when ready - no panic so users are not stuck mid-tx.
1046 func Buy(cur realm, id string , amountWugnot, minTokensOut int64 ) int64 {
1047 requireInit()
1048 sent := takeWugnotIn(cur, amountWugnot)
1049 l := mustLaunch(id)
1050 if l.Status != StatusCurve {
1051 panic( "pad: not on curve (use SwapBuy)" )
1052 }
1053 buyer := cur.Previous().Address()
1054
1055 remainingTok := CurveSupply - l.RealSold
1056 needRaise := graduationThreshold() - l.RaisedUgnot
1057 // Already complete: refund payment and graduate (sold-out or raise-filled).
1058 if remainingTok <= 0 || needRaise <= 0 {
1059 if !readyToGraduate(l) {
1060 // Edge: zero raise with empty float should not happen in production.
1061 if remainingTok <= 0 {
1062 panic( "pad: curve sold out with no raise" )
1063 }
1064 panic( "pad: raise filled - call Graduate" )
1065 }
1066 if sent > 0 {
1067 // Keep as claimable credit (ClaimWugnot) — safe after graduate/list too.
1068 creditRefund(buyer, sent)
1069 chain.Emit( "BuyRefund" ,
1070 "id" , id,
1071 "buyer" , buyer.String(),
1072 "refund" , strconv.FormatInt(sent, 10 ),
1073 "toCredit" , "1" ,
1074 )
1075 }
1076 graduate(cur, l)
1077 return 0
1078 }
1079
1080 usedGross := sent
1081 fee := ammmath.ApplyFee(usedGross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS)
1082 // Net enters curve; remainder boosts virtual ugnot (stays as collateral).
1083 netIn := fee.Net + fee.Remainder
1084
1085 // Max net allowed: min(user net, remaining raise, remaining tokens).
1086 maxNet := netIn
1087 if maxNet > needRaise {
1088 maxNet = needRaise
1089 }
1090 maxNetTok := ammmath.MaxNetInForTokenOut(l.VirtualUgnot, l.VirtualToken, remainingTok)
1091 if maxNetTok > 0 && maxNet > maxNetTok {
1092 maxNet = maxNetTok
1093 }
1094 if maxNet <= 0 {
1095 panic( "pad: no fill capacity remaining" )
1096 }
1097
1098 // Clamp gross + recompute fee when caps bind (last-fill refund path).
1099 if maxNet < netIn {
1100 usedGross = maxGrossForNetIn(maxNet, sent)
1101 if usedGross <= 0 {
1102 panic( "pad: buy too small for remaining fill" )
1103 }
1104 fee = ammmath.ApplyFee(usedGross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS)
1105 netIn = fee.Net + fee.Remainder
1106 if netIn > maxNet {
1107 netIn = maxNet
1108 }
1109 }
1110
1111 tokensOut, newVU, newVT := ammmath.BuyTokens(l.VirtualUgnot, l.VirtualToken, netIn)
1112 // Integer edge: step down net until tokens <= remaining curve supply.
1113 for tokensOut > remainingTok && netIn > 1 {
1114 netIn--
1115 tokensOut, newVU, newVT = ammmath.BuyTokens(l.VirtualUgnot, l.VirtualToken, netIn)
1116 }
1117 if tokensOut > remainingTok || tokensOut <= 0 {
1118 panic( "pad: cannot fill remaining curve supply" )
1119 }
1120 // If net was reduced further, shrink usedGross so refund is correct.
1121 if netIn < maxNet || usedGross < sent {
1122 // Re-derive gross that yields this netIn (<= sent).
1123 g2 := maxGrossForNetIn(netIn, sent)
1124 if g2 > 0 && g2 < usedGross {
1125 usedGross = g2
1126 fee = ammmath.ApplyFee(usedGross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS)
1127 // Keep curve netIn as simulated (may be slightly below fee.Net+Rem).
1128 }
1129 }
1130 // Hard safety: never overshoot graduation raise after this buy.
1131 if l.RaisedUgnot+netIn > graduationThreshold() {
1132 netIn = graduationThreshold() - l.RaisedUgnot
1133 if netIn <= 0 {
1134 panic( "pad: raise filled - call Graduate" )
1135 }
1136 tokensOut, newVU, newVT = ammmath.BuyTokens(l.VirtualUgnot, l.VirtualToken, netIn)
1137 for tokensOut > remainingTok && netIn > 1 {
1138 netIn--
1139 tokensOut, newVU, newVT = ammmath.BuyTokens(l.VirtualUgnot, l.VirtualToken, netIn)
1140 }
1141 if tokensOut <= 0 {
1142 panic( "pad: cannot fill remaining raise" )
1143 }
1144 usedGross = maxGrossForNetIn(netIn, sent)
1145 if usedGross <= 0 {
1146 panic( "pad: buy too small for remaining raise" )
1147 }
1148 fee = ammmath.ApplyFee(usedGross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS)
1149 }
1150
1151 refund := sent - usedGross
1152 if refund > 0 {
1153 // Overpay stays claimable — user ClaimWugnot anytime (incl. after list).
1154 creditRefund(buyer, refund)
1155 }
1156
1157 requireMinOut(tokensOut, minTokensOut, "tokens out" )
1158 checkAndAddSnipe(l, buyer, tokensOut)
1159
1160 // Mutate only after all checks pass.
1161 // Fees stay as WUGNOT on pad (liabilities); netIn is raised collateral for LP.
1162 l.CreatorFees += fee.Creator
1163 creditProtocol(fee.Protocol)
1164 l.VirtualUgnot = newVU
1165 l.VirtualToken = newVT
1166 l.RealSold += tokensOut
1167 l.RaisedUgnot += netIn
1168 // Invariant: raise never exceeds threshold after Buy.
1169 if l.RaisedUgnot > graduationThreshold() {
1170 panic( "pad: raise overshoot invariant" )
1171 }
1172
1173 addBal(l, buyer, tokensOut)
1174 noteBuyer(l, buyer)
1175 maybeRefundBond(cur, l)
1176 recordTrade(l, TradeSideBuy, usedGross, tokensOut)
1177
1178 chain.Emit( "Buy" ,
1179 "id" , id,
1180 "buyer" , buyer.String(),
1181 "ugnot" , strconv.FormatInt(usedGross, 10 ),
1182 "wugnot" , strconv.FormatInt(usedGross, 10 ),
1183 "tokens" , strconv.FormatInt(tokensOut, 10 ),
1184 )
1185 if refund > 0 {
1186 chain.Emit( "BuyRefund" ,
1187 "id" , id,
1188 "buyer" , buyer.String(),
1189 "refund" , strconv.FormatInt(refund, 10 ),
1190 )
1191 }
1192 notifyTrade(cur, buyer, id, 0 , usedGross)
1193
1194 if readyToGraduate(l) {
1195 graduate(cur, l)
1196 }
1197 return tokensOut
1198 }
1199
1200 // RemainingRaiseUgnot is net ugnot still needed to hit graduationThreshold() (0 if met/over).
1201 func RemainingRaiseUgnot(id string ) int64 {
1202 l := mustLaunch(id)
1203 if l.Status != StatusCurve {
1204 return 0
1205 }
1206 if l.RaisedUgnot >= graduationThreshold() {
1207 return 0
1208 }
1209 return graduationThreshold() - l.RaisedUgnot
1210 }
1211
1212 // Sell burns curve tokens and pays WUGNOT (fee on output).
1213 // minWugnotOut: slippage floor (0 = disabled). User may wugnot.Withdraw to ugnot.
1214 func Sell(cur realm, id string , tokensIn, minWugnotOut int64 ) int64 {
1215 requireInit()
1216 if !cur.Previous().IsUserCall() {
1217 panic( "pad: must be EOA MsgCall" )
1218 }
1219 if tokensIn <= 0 {
1220 panic( "pad: tokensIn must be positive" )
1221 }
1222 l := mustLaunch(id)
1223 if l.Status != StatusCurve {
1224 panic( "pad: not on curve (use SwapSell)" )
1225 }
1226 seller := cur.Previous().Address()
1227 if balOf(l, seller) < tokensIn {
1228 panic( "pad: insufficient token balance" )
1229 }
1230
1231 gross, newVU, newVT := ammmath.SellTokens(l.VirtualUgnot, l.VirtualToken, tokensIn)
1232 fee := ammmath.ApplyFeeOnOutput(gross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS)
1233 requireMinOut(fee.Net, minWugnotOut, "wugnot out" )
1234
1235 // Full gross left virtual reserves; retain fee in virtual ugnot (cash stays in realm).
1236 l.VirtualUgnot = newVU + fee.Fee
1237 l.VirtualToken = newVT
1238 l.RealSold -= tokensIn
1239 if l.RealSold < 0 {
1240 l.RealSold = 0
1241 }
1242
1243 // User receives net; creator+protocol become fee liabilities (leave Raised).
1244 payOut := fee.Net + fee.Creator + fee.Protocol
1245 if l.RaisedUgnot >= payOut {
1246 l.RaisedUgnot -= payOut
1247 } else {
1248 l.RaisedUgnot = 0
1249 }
1250 l.CreatorFees += fee.Creator
1251 creditProtocol(fee.Protocol)
1252
1253 addBal(l, seller, -tokensIn)
1254 payWugnotOut(cur, seller, fee.Net)
1255 recordTrade(l, TradeSideSell, fee.Net, tokensIn)
1256
1257 chain.Emit( "Sell" ,
1258 "id" , id,
1259 "seller" , seller.String(),
1260 "tokens" , strconv.FormatInt(tokensIn, 10 ),
1261 "ugnot" , strconv.FormatInt(fee.Net, 10 ),
1262 "wugnot" , strconv.FormatInt(fee.Net, 10 ),
1263 )
1264 notifyTrade(cur, seller, id, 1 , fee.Net)
1265 return fee.Net
1266 }
1267
1268 // Graduate permissionlessly moves a ready curve into a permanently locked CPMM.
1269 // Ready when RaisedUgnot >= graduationThreshold(), or when RealSold >= CurveSupply
1270 // with RaisedUgnot > 0 (sold-out before threshold - escape hatch for unreachable raise).
1271 func Graduate(cur realm, id string ) {
1272 requireInit()
1273 l := mustLaunch(id)
1274 if l.Status != StatusCurve {
1275 panic( "pad: already graduated" )
1276 }
1277 if !readyToGraduate(l) {
1278 panic( "pad: not ready to graduate (need raise threshold or curve sold out)" )
1279 }
1280 graduate(cur, l)
1281 }
1282
1283 func graduate(cur realm, l *Launch) {
1284 if l.Status != StatusCurve {
1285 return
1286 }
1287 // Liquidity capital = all raised GNOT. Token side is sized to the current
1288 // bonding-curve spot so internal CPMM / Gnoswap open ~ last curve trade
1289 // (seamless graduate). Dumping ALL unsold tokens made DEX spot << curve exit.
1290 poolU := l.RaisedUgnot
1291 if poolU <= 0 {
1292 panic( "pad: empty pool ugnot" )
1293 }
1294 remaining := TotalSupply - l.RealSold
1295 if remaining <= 0 {
1296 panic( "pad: no remaining tokens for liquidity" )
1297 }
1298 // tokensForLP = raised * VirtualToken / VirtualUgnot (same units as reserves)
1299 poolT := remaining
1300 if l.VirtualUgnot > 0 && l.VirtualToken > 0 {
1301 needed := poolU * l.VirtualToken / l.VirtualUgnot
1302 if needed > 0 && needed < remaining {
1303 poolT = needed
1304 }
1305 }
1306
1307 l.PoolUgnot = poolU
1308 l.PoolToken = poolT
1309 l.LeftoverTokens = remaining - poolT
1310 l.RaisedUgnot = 0
1311 l.VirtualUgnot = 0
1312 l.VirtualToken = 0
1313 l.Status = StatusGraduated
1314
1315 // Forfeit unrefunded bond to protocol at graduation if still locked.
1316 if !l.BondRefunded && l.BondUgnot > 0 {
1317 creditProtocol(l.BondUgnot)
1318 l.BondUgnot = 0
1319 l.BondRefunded = true
1320 }
1321
1322 // Mark graduation on chart at pool spot (matches curve exit when sized above).
1323 recordTrade(l, TradeSideOpen, poolU, poolT)
1324
1325 // NEVER auto-list inside Buy/Graduate.
1326 // Gnoswap CreatePool/Mint does WUGNOT Approve+TransferFrom; realm spender
1327 // frame often panics "insufficient allowance" and REVERTS the entire Buy
1328 // (including the curve fill that triggered graduation). List is a separate
1329 // EOA call: pre-fund pad WUGNOT/GNS then RetryListGnoswap.
1330 // Unit tests may still force list via testForceGnoswapList.
1331 l.GnoswapReady = true
1332 listed := false
1333 if testForceGnoswapList {
1334 listed = listOnGnoswapWithFunding(cur, l, poolU, poolT)
1335 }
1336 if !listed {
1337 // Internal CPMM: PoolToken is pad-accounting reserve (not minted GRC20).
1338 // Circulating = user balances; pool side is virtual reserve PoolToken.
1339 if l.GnoswapNote == "" {
1340 if l.PreferredVenue == VenueZdex {
1341 l.GnoswapNote = "internal CPMM; ReleaseListSeed → EOA zdex CreatePool → ConfirmZdexList"
1342 } else {
1343 l.GnoswapNote = "internal CPMM; Transfer WUGNOT+GNS to pad then RetryListGnoswap"
1344 }
1345 }
1346 chain.Emit( "Graduated" ,
1347 "id" , l.ID,
1348 "poolUgnot" , strconv.FormatInt(poolU, 10 ),
1349 "poolToken" , strconv.FormatInt(poolT, 10 ),
1350 "token" , l.TokenID,
1351 "gnoswap_listed" , "0" ,
1352 )
1353 return
1354 }
1355 // Listed on Gnoswap (test-only path): capital is in the CL position (NFT owned by pad).
1356 // Internal SwapBuy/Sell disabled (PoolUgnot/PoolToken kept as listing record).
1357 chain.Emit( "Graduated" ,
1358 "id" , l.ID,
1359 "poolUgnot" , strconv.FormatInt(poolU, 10 ),
1360 "poolToken" , strconv.FormatInt(poolT, 10 ),
1361 "token" , l.TokenID,
1362 "gnoswap_listed" , "1" ,
1363 "poolPath" , l.GnoswapPoolPath,
1364 "positionId" , strconv.FormatUint(l.GnoswapPositionID, 10 ),
1365 )
1366 }
1367
1368 // SetPreferredVenue sets the DEX target used when RetryList(id, "") is called.
1369 // Creator or protocol only; locked once listed. Syncs Create-time GNS escrow
1370 // (gnoswap locks ListFeeRequired; zdex unlocks/refunds escrow).
1371 func SetPreferredVenue(cur realm, id, venueId string ) {
1372 requireInit()
1373 if !cur.Previous().IsUserCall() {
1374 panic( "pad: must be EOA MsgCall" )
1375 }
1376 l := mustLaunch(id)
1377 caller := cur.Previous().Address()
1378 if caller != l.Creator && caller != protocolAddr {
1379 panic( "pad: creator or protocol only" )
1380 }
1381 if l.GnoswapListed || l.ListVenue != "" {
1382 panic( "pad: already listed" )
1383 }
1384 ensureListVenuesSeeded()
1385 vid := normalizeVenueID(venueId)
1386 if !venueEnabled(vid) {
1387 panic( "pad: venue disabled or unknown" )
1388 }
1389 l.PreferredVenue = vid
1390 syncListFeeForVenue(cur, l, vid)
1391 chain.Emit( "SetPreferredVenue" ,
1392 "id" , id,
1393 "venue" , vid,
1394 "listFeeGns" , strconv.FormatInt(l.ListFeeGns, 10 ),
1395 )
1396 }
1397
1398 // PreferredVenueOf returns the Create-time / SetPreferredVenue target.
1399 func PreferredVenueOf(id string ) string {
1400 return mustLaunch(id).PreferredVenue
1401 }
1402
1403 // ListSeedReleasedOf reports whether ReleaseListSeed already ran.
1404 func ListSeedReleasedOf(id string ) bool {
1405 return mustLaunch(id).ListSeedReleased
1406 }
1407
1408 // ReleaseListSeed sends graduated internal-CPMM seed to the creator for EOA zdex CreatePool:
1409 // - PoolUgnot as WUGNOT Transfer to creator (EOA may wugnot.Withdraw → ugnot OriginSend)
1410 // - PoolToken minted as GRC20 to creator (was pad-accounting only)
1411 // PoolUgnot/PoolToken kept as listing-size record; reservedWugnot skips via ListSeedReleased.
1412 // Disables internal SwapBuy/Sell. Creator or protocol may call; assets always go to creator.
1413 func ReleaseListSeed(cur realm, id string ) {
1414 requireInit()
1415 if !cur.Previous().IsUserCall() {
1416 panic( "pad: must be EOA MsgCall" )
1417 }
1418 l := mustLaunch(id)
1419 caller := cur.Previous().Address()
1420 if caller != l.Creator && caller != protocolAddr {
1421 panic( "pad: creator or protocol only" )
1422 }
1423 if l.Status != StatusGraduated {
1424 panic( "pad: not graduated" )
1425 }
1426 if l.GnoswapListed || l.ListVenue != "" {
1427 panic( "pad: already listed" )
1428 }
1429 if l.ListSeedReleased {
1430 panic( "pad: list seed already released" )
1431 }
1432 poolU := l.PoolUgnot
1433 poolT := l.PoolToken
1434 if poolU <= 0 || poolT <= 0 {
1435 panic( "pad: empty internal pool" )
1436 }
1437 // Fail-closed: must fully fund creator before killing internal CPMM.
1438 if !testSkipBanker {
1439 have := wugnot.BalanceOf(padAddr)
1440 if have < poolU {
1441 panic( "pad: underfunded WUGNOT for list seed release" )
1442 }
1443 }
1444 // Mark released before Transfer so reservedWugnot drops in same tx.
1445 l.ListSeedReleased = true
1446 l.PoolUgnot = 0
1447 l.PoolToken = 0
1448 l.LiqWugnotUsed = poolU
1449 l.GnoswapNote = "list seed released to creator for zdex CreatePool; then ConfirmZdexList"
1450 if !testSkipBanker {
1451 wugnot.Transfer(cross(cur), l.Creator, poolU)
1452 } else {
1453 payWugnotOut(cur, l.Creator, poolU)
1454 }
1455 addBal(l, l.Creator, poolT)
1456 chain.Emit( "ReleaseListSeed" ,
1457 "id" , id,
1458 "to" , l.Creator.String(),
1459 "wugnot" , strconv.FormatInt(poolU, 10 ),
1460 "tokens" , strconv.FormatInt(poolT, 10 ),
1461 )
1462 }
1463
1464 // ConfirmZdexList marks a launch listed on zdex after EOA CreatePool succeeded.
1465 // poolId e.g. "ugnot|SYMBOL" (must be non-empty; no on-chain pool verify).
1466 // Creator or protocol. Prefer ReleaseListSeed first when pad still holds LP seed.
1467 func ConfirmZdexList(cur realm, id, poolId string ) {
1468 requireInit()
1469 if !cur.Previous().IsUserCall() {
1470 panic( "pad: must be EOA MsgCall" )
1471 }
1472 l := mustLaunch(id)
1473 caller := cur.Previous().Address()
1474 if caller != l.Creator && caller != protocolAddr {
1475 panic( "pad: creator or protocol only" )
1476 }
1477 if l.Status != StatusGraduated {
1478 panic( "pad: not graduated" )
1479 }
1480 if l.GnoswapListed || l.ListVenue != "" {
1481 panic( "pad: already listed" )
1482 }
1483 // Avoid marking listed while pad still holds unreleased LP capital.
1484 if !l.ListSeedReleased && (l.PoolUgnot > 0 || l.PoolToken > 0 ) {
1485 panic( "pad: ReleaseListSeed first (or empty pool)" )
1486 }
1487 poolId = strings.TrimSpace(poolId)
1488 if poolId == "" {
1489 panic( "pad: empty poolId" )
1490 }
1491 l.ListVenue = VenueZdex
1492 l.GnoswapListed = true // UI/compat: "listed on DEX"
1493 l.GnoswapPoolPath = poolId
1494 l.GnoswapPositionID = 0
1495 if l.LiqWugnotUsed == 0 {
1496 l.LiqWugnotUsed = l.PoolUgnot
1497 }
1498 l.GnoswapNote = "confirmed zdex list; pool=" + poolId
1499 chain.Emit( "ConfirmZdexList" ,
1500 "id" , id,
1501 "poolPath" , poolId,
1502 "symbol" , l.Symbol,
1503 "venue" , VenueZdex,
1504 )
1505 chain.Emit( "ListedRetry" ,
1506 "id" , id,
1507 "venue" , VenueZdex,
1508 "poolPath" , poolId,
1509 "positionId" , "0" ,
1510 "poolUgnot" , strconv.FormatInt(l.PoolUgnot, 10 ),
1511 "poolToken" , strconv.FormatInt(l.PoolToken, 10 ),
1512 )
1513 }
1514
1515 // RetryList lists a graduated launch on the chosen venue.
1516 // Permissionless EOA MsgCall. Uses PoolUgnot / PoolToken (curve-spot sized).
1517 // venueId: empty → PreferredVenue → DefaultListVenue(); unknown/disabled → soft-fail note.
1518 func RetryList(cur realm, id, venueId string ) bool {
1519 requireInit()
1520 if !cur.Previous().IsUserCall() {
1521 panic( "pad: must be EOA MsgCall" )
1522 }
1523 l := mustLaunch(id)
1524 if l.Status != StatusGraduated {
1525 panic( "pad: not graduated" )
1526 }
1527 if l.GnoswapListed || l.ListVenue != "" {
1528 panic( "pad: already listed" )
1529 }
1530 if l.ListSeedReleased {
1531 panic( "pad: list seed released - use ConfirmZdexList after EOA CreatePool" )
1532 }
1533 poolU := l.PoolUgnot
1534 poolT := l.PoolToken
1535 if poolU <= 0 || poolT <= 0 {
1536 panic( "pad: empty internal pool" )
1537 }
1538 raw := strings.TrimSpace(venueId)
1539 if raw == "" && l.PreferredVenue != "" {
1540 raw = l.PreferredVenue
1541 }
1542 vid := normalizeVenueID(raw)
1543 ok := tryListVenue(cur, l, vid, poolU, poolT)
1544 if ok {
1545 chain.Emit( "ListedRetry" ,
1546 "id" , l.ID,
1547 "venue" , l.ListVenue,
1548 "poolPath" , l.GnoswapPoolPath,
1549 "positionId" , strconv.FormatUint(l.GnoswapPositionID, 10 ),
1550 "poolUgnot" , strconv.FormatInt(poolU, 10 ),
1551 "poolToken" , strconv.FormatInt(poolT, 10 ),
1552 )
1553 // Compat event name for indexers that listen for GnoswapListedRetry.
1554 if l.ListVenue == VenueGnoswap {
1555 chain.Emit( "GnoswapListedRetry" ,
1556 "id" , l.ID,
1557 "poolPath" , l.GnoswapPoolPath,
1558 "positionId" , strconv.FormatUint(l.GnoswapPositionID, 10 ),
1559 "poolUgnot" , strconv.FormatInt(poolU, 10 ),
1560 "poolToken" , strconv.FormatInt(poolT, 10 ),
1561 )
1562 }
1563 } else if l.GnoswapNote == "" {
1564 l.GnoswapNote = "retry list failed - check ListNeed / ListNeedFor; venue=" + vid
1565 }
1566 return ok
1567 }
1568
1569 // RetryListGnoswap is the Gnoswap-specific wrapper (compat for existing UI).
1570 func RetryListGnoswap(cur realm, id string ) bool {
1571 return RetryList(cur, id, VenueGnoswap)
1572 }
1573
1574 // TokenIDOf returns the GRC20 Token.ID() for a launch.
1575 func TokenIDOf(id string ) string {
1576 return mustLaunch(id).TokenID
1577 }
1578
1579 // GRC20Bank returns the underlying *grc20.Token for interop (metadata / external DEX).
1580 // Does not expose PrivateLedger - mint/burn stay pad-only.
1581 func GRC20Bank(id string ) *grc20.Token {
1582 l := mustLaunch(id)
1583 if l.token == nil {
1584 panic( "pad: no token" )
1585 }
1586 return l.token
1587 }
1588
1589 // SwapBuy buys tokens from the graduated internal pool with WUGNOT.
1590 // amountWugnot: max to spend (Approve pad). minTokensOut: slippage (0 = off).
1591 // Disabled when listed on Gnoswap (trade via router).
1592 func SwapBuy(cur realm, id string , amountWugnot, minTokensOut int64 ) int64 {
1593 requireInit()
1594 sent := takeWugnotIn(cur, amountWugnot)
1595 l := mustLaunch(id)
1596 if l.Status != StatusGraduated {
1597 panic( "pad: not graduated (use Buy)" )
1598 }
1599 if l.GnoswapListed || l.ListVenue != "" {
1600 panic( "pad: listed on DEX - trade via router, not pad SwapBuy" )
1601 }
1602 if l.ListSeedReleased {
1603 panic( "pad: list seed released - finish zdex CreatePool + ConfirmZdexList" )
1604 }
1605 buyer := cur.Previous().Address()
1606
1607 fee := ammmath.ApplyFee(sent, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS)
1608 tokensOut, newPU, newPT := ammmath.PoolSwapUgnotForToken(
1609 l.PoolUgnot, l.PoolToken, fee.Net, fee.Remainder,
1610 )
1611 requireMinOut(tokensOut, minTokensOut, "tokens out" )
1612
1613 // Refund unused gross if pool math used less (rare); fees from sent.
1614 // Pool swap uses fee.Net into pool; full sent stays as fee+pool contribution.
1615 l.CreatorFees += fee.Creator
1616 creditProtocol(fee.Protocol)
1617 l.PoolUgnot = newPU
1618 l.PoolToken = newPT
1619 addBal(l, buyer, tokensOut)
1620 noteBuyer(l, buyer)
1621 recordTrade(l, TradeSideBuy, sent, tokensOut)
1622
1623 chain.Emit( "SwapBuy" ,
1624 "id" , id,
1625 "buyer" , buyer.String(),
1626 "ugnot" , strconv.FormatInt(sent, 10 ),
1627 "wugnot" , strconv.FormatInt(sent, 10 ),
1628 "tokens" , strconv.FormatInt(tokensOut, 10 ),
1629 )
1630 notifyTrade(cur, buyer, id, 0 , sent)
1631 return tokensOut
1632 }
1633
1634 // SwapSell sells tokens into the graduated pool for WUGNOT.
1635 // minWugnotOut: slippage floor (0 = disabled).
1636 func SwapSell(cur realm, id string , tokensIn, minWugnotOut int64 ) int64 {
1637 requireInit()
1638 if !cur.Previous().IsUserCall() {
1639 panic( "pad: must be EOA MsgCall" )
1640 }
1641 if tokensIn <= 0 {
1642 panic( "pad: tokensIn must be positive" )
1643 }
1644 l := mustLaunch(id)
1645 if l.Status != StatusGraduated {
1646 panic( "pad: not graduated (use Sell)" )
1647 }
1648 if l.GnoswapListed || l.ListVenue != "" {
1649 panic( "pad: listed on DEX - trade via router, not pad SwapSell" )
1650 }
1651 if l.ListSeedReleased {
1652 panic( "pad: list seed released - finish zdex CreatePool + ConfirmZdexList" )
1653 }
1654 seller := cur.Previous().Address()
1655 if balOf(l, seller) < tokensIn {
1656 panic( "pad: insufficient token balance" )
1657 }
1658
1659 gross, newPU, newPT := ammmath.PoolSwapTokenForUgnot(l.PoolUgnot, l.PoolToken, tokensIn)
1660 fee := ammmath.ApplyFeeOnOutput(gross, FeeBPS, CreatorFeeShareBPS, ProtocolFeeShareBPS)
1661 requireMinOut(fee.Net, minWugnotOut, "wugnot out" )
1662
1663 // Retain fee in pool ugnot (cash stays); user gets net WUGNOT.
1664 l.PoolUgnot = newPU + fee.Fee
1665 l.PoolToken = newPT
1666 l.CreatorFees += fee.Creator
1667 creditProtocol(fee.Protocol)
1668
1669 addBal(l, seller, -tokensIn)
1670 payWugnotOut(cur, seller, fee.Net)
1671 recordTrade(l, TradeSideSell, fee.Net, tokensIn)
1672
1673 chain.Emit( "SwapSell" ,
1674 "id" , id,
1675 "seller" , seller.String(),
1676 "tokens" , strconv.FormatInt(tokensIn, 10 ),
1677 "ugnot" , strconv.FormatInt(fee.Net, 10 ),
1678 "wugnot" , strconv.FormatInt(fee.Net, 10 ),
1679 )
1680 notifyTrade(cur, seller, id, 1 , fee.Net)
1681 return fee.Net
1682 }
1683
1684 // Transfer moves GRC20 tokens between addresses (user-initiated).
1685 func Transfer(cur realm, id string , to address, amount int64 ) {
1686 requireInit()
1687 if !cur.Previous().IsUserCall() {
1688 panic( "pad: must be EOA MsgCall" )
1689 }
1690 if amount <= 0 {
1691 panic( "pad: amount must be positive" )
1692 }
1693 if !to.IsValid() {
1694 panic( "pad: invalid to" )
1695 }
1696 l := mustLaunch(id)
1697 from := cur.Previous().Address()
1698 if from == to {
1699 panic( "pad: self transfer" )
1700 }
1701 if l.ledger == nil {
1702 panic( "pad: no GRC20 ledger" )
1703 }
1704 if err := l.ledger.Transfer(from, to, amount); err != nil {
1705 panic( "pad: transfer: " + err.Error())
1706 }
1707 chain.Emit( "Transfer" , "id" , id, "from" , from.String(), "to" , to.String(),
1708 "amount" , strconv.FormatInt(amount, 10 ))
1709 }
1710
1711 // Approve sets GRC20 allowance so DEX/contracts can TransferFrom.
1712 func Approve(cur realm, id string , spender address, amount int64 ) {
1713 requireInit()
1714 if !cur.Previous().IsUserCall() {
1715 panic( "pad: must be EOA MsgCall" )
1716 }
1717 if !spender.IsValid() {
1718 panic( "pad: invalid spender" )
1719 }
1720 l := mustLaunch(id)
1721 if l.ledger == nil {
1722 panic( "pad: no GRC20 ledger" )
1723 }
1724 owner := cur.Previous().Address()
1725 if err := l.ledger.Approve(owner, spender, amount); err != nil {
1726 panic( "pad: approve: " + err.Error())
1727 }
1728 chain.Emit( "Approval" , "id" , id, "owner" , owner.String(), "spender" , spender.String(),
1729 "amount" , strconv.FormatInt(amount, 10 ))
1730 }
1731
1732 // TransferFrom spends allowance: spender = MsgCall EOA caller.
1733 // Enables DEX / routers that hold allowance from Approve.
1734 func TransferFrom(cur realm, id string , from, to address, amount int64 ) {
1735 requireInit()
1736 if !cur.Previous().IsUserCall() {
1737 panic( "pad: must be EOA MsgCall" )
1738 }
1739 if amount <= 0 {
1740 panic( "pad: amount must be positive" )
1741 }
1742 if !from.IsValid() || !to.IsValid() {
1743 panic( "pad: invalid address" )
1744 }
1745 if from == to {
1746 panic( "pad: self transfer" )
1747 }
1748 l := mustLaunch(id)
1749 if l.ledger == nil {
1750 panic( "pad: no GRC20 ledger" )
1751 }
1752 spender := cur.Previous().Address()
1753 if err := l.ledger.TransferFrom(from, spender, to, amount); err != nil {
1754 panic( "pad: transferFrom: " + err.Error())
1755 }
1756 chain.Emit( "TransferFrom" , "id" , id, "from" , from.String(), "to" , to.String(),
1757 "spender" , spender.String(), "amount" , strconv.FormatInt(amount, 10 ))
1758 }
1759
1760 // ClaimCreatorFees withdraws accrued creator fees for a launch.
1761 // Only the token creator may claim. Fees stay on pad until claimed.
1762 func ClaimCreatorFees(cur realm, id string ) int64 {
1763 requireInit()
1764 if !cur.Previous().IsUserCall() {
1765 panic( "pad: must be EOA MsgCall" )
1766 }
1767 l := mustLaunch(id)
1768 caller := cur.Previous().Address()
1769 if caller != l.Creator {
1770 panic( "pad: not creator" )
1771 }
1772 amt := l.CreatorFees
1773 if amt <= 0 {
1774 return 0
1775 }
1776 l.CreatorFees = 0
1777 // Production: fees accrue as WUGNOT on pad. Tests pay ugnot via payWugnotOut.
1778 payWugnotOut(cur, caller, amt)
1779 chain.Emit( "ClaimCreator" , "id" , id, "amount" , strconv.FormatInt(amt, 10 ))
1780 return amt
1781 }
1782
1783 // payoutProtocolFees sends all pending protocolFees to protocolAddr.
1784 // Shared by ClaimProtocolFees and PushProtocolFees.
1785 func payoutProtocolFees(cur realm) int64 {
1786 amt := protocolFees
1787 if amt <= 0 {
1788 return 0
1789 }
1790 if !protocolAddr.IsValid() {
1791 panic( "pad: protocol address unset" )
1792 }
1793 protocolFees = 0
1794 protocolFeesPaid += amt
1795 payWugnotOut(cur, protocolAddr, amt)
1796 chain.Emit( "ClaimProtocol" ,
1797 "to" , protocolAddr.String(),
1798 "amount" , strconv.FormatInt(amt, 10 ),
1799 )
1800 return amt
1801 }
1802
1803 // ClaimProtocolFees withdraws pending protocol fees to protocolAddr.
1804 // Only the current protocol treasury key may call (same wallet that Init'd,
1805 // unless TransferProtocol was used).
1806 func ClaimProtocolFees(cur realm) int64 {
1807 requireInit()
1808 if !cur.Previous().IsUserCall() {
1809 panic( "pad: must be EOA MsgCall" )
1810 }
1811 if cur.Previous().Address() != protocolAddr {
1812 panic( "pad: not protocol" )
1813 }
1814 return payoutProtocolFees(cur)
1815 }
1816
1817 // PushProtocolFees sends pending protocol fees to protocolAddr.
1818 // Permissionless: anyone may call so treasury can be paid without the protocol
1819 // key signing (still only pays the configured protocolAddr).
1820 func PushProtocolFees(cur realm) int64 {
1821 requireInit()
1822 if !cur.Previous().IsUserCall() {
1823 panic( "pad: must be EOA MsgCall" )
1824 }
1825 return payoutProtocolFees(cur)
1826 }
1827
1828 // TransferProtocol rotates the protocol fee recipient (current protocol only).
1829 // Pending protocolFees stay on pad until claimed/pushed to the *new* address.
1830 func TransferProtocol(cur realm, newAddr address) {
1831 requireInit()
1832 if !cur.Previous().IsUserCall() {
1833 panic( "pad: must be EOA MsgCall" )
1834 }
1835 if cur.Previous().Address() != protocolAddr {
1836 panic( "pad: not protocol" )
1837 }
1838 if !newAddr.IsValid() {
1839 panic( "pad: invalid new protocol address" )
1840 }
1841 if newAddr == protocolAddr {
1842 panic( "pad: same protocol address" )
1843 }
1844 old := protocolAddr
1845 protocolAddr = newAddr
1846 chain.Emit( "TransferProtocol" , "from" , old.String(), "to" , newAddr.String())
1847 }
1848
1849 // ProtocolAddress returns the current protocol treasury address (bech32).
1850 func ProtocolAddress() string {
1851 return protocolAddr.String()
1852 }
1853
1854 // ProtocolFeesPaid returns lifetime ugnot already paid out to the treasury.
1855 func ProtocolFeesPaid() int64 {
1856 return protocolFeesPaid
1857 }
1858
1859 // FeeInfo returns protocolAddr|pendingUgnot|paidUgnot for UIs.
1860 func FeeInfo() string {
1861 return protocolAddr.String() + "|" +
1862 strconv.FormatInt(protocolFees, 10 ) + "|" +
1863 strconv.FormatInt(protocolFeesPaid, 10 )
1864 }
1865
1866 // PadAddress returns this pad realm's bech32 package address (fund WUGNOT here).
1867 func PadAddress() string {
1868 return padAddr.String()
1869 }
1870
1871 // AdminInfo is a single-line dashboard snapshot for the ops UI:
1872 //
1873 // protocolAddr|pendingFees|paidFees|reservedUgnot|launchCount|pointsOn|inited|padAddr
1874 //
1875 // pointsOn/inited are 0|1.
1876 func AdminInfo() string {
1877 pts := "0"
1878 if pointsEnabled {
1879 pts = "1"
1880 }
1881 ini := "0"
1882 if inited {
1883 ini = "1"
1884 }
1885 return protocolAddr.String() + "|" +
1886 strconv.FormatInt(protocolFees, 10 ) + "|" +
1887 strconv.FormatInt(protocolFeesPaid, 10 ) + "|" +
1888 strconv.FormatInt(reservedUgnot(), 10 ) + "|" +
1889 strconv.Itoa(launches.Size()) + "|" +
1890 pts + "|" +
1891 ini + "|" +
1892 padAddr.String()
1893 }
1894
1895 // IsProtocol reports whether addr is the current treasury (for UI gating).
1896 func IsProtocol(addr string ) bool {
1897 if !inited || !protocolAddr.IsValid() {
1898 return false
1899 }
1900 return protocolAddr.String() == addr
1901 }
1902
1903 // reservedUgnot is ugnot the pad must keep to honor user/creator liabilities
1904 // and active markets (curve raised, internal CPMM, bonds, pending fees).
1905 func reservedUgnot() int64 {
1906 reserved := protocolFees
1907 launches.Iterate( "" , "" , func (_ string , value any ) bool {
1908 l := value.(*Launch)
1909 reserved += l.CreatorFees
1910 if !l.BondRefunded {
1911 reserved += l.BondUgnot
1912 }
1913 if l.Status == StatusCurve {
1914 reserved += l.RaisedUgnot
1915 }
1916 // Internal CPMM (fallback when not listed / seed not released).
1917 if l.Status == StatusGraduated && !l.GnoswapListed && !l.ListSeedReleased {
1918 reserved += l.PoolUgnot
1919 }
1920 return false
1921 })
1922 return reserved
1923 }
1924
1925 // ReservedUgnot is ugnot the pad must keep for markets + pending claims.
1926 func ReservedUgnot() int64 {
1927 return reservedUgnot()
1928 }
1929
1930 // freeUgnot reports bank ugnot above reserved liabilities (0 if short/test).
1931 func freeUgnot(cur realm) int64 {
1932 if testSkipBanker {
1933 return 0
1934 }
1935 bk := banker.NewBanker(banker.BankerTypeReadonly, cur)
1936 bal := bk.GetCoins(cur.Address()).AmountOf(DenomUgnot)
1937 free := bal - reservedUgnot()
1938 if free < 0 {
1939 return 0
1940 }
1941 return free
1942 }
1943
1944 // WithdrawProtocolUgnot lets the treasury pull free ugnot from the pad bank
1945 // (e.g. raised backlog after Gnoswap list, to re-wrap as WUGNOT inventory).
1946 // Capped by free balance; panics if amount > free.
1947 func WithdrawProtocolUgnot(cur realm, amount int64 ) int64 {
1948 requireInit()
1949 if !cur.Previous().IsUserCall() {
1950 panic( "pad: must be EOA MsgCall" )
1951 }
1952 if cur.Previous().Address() != protocolAddr {
1953 panic( "pad: not protocol" )
1954 }
1955 if amount <= 0 {
1956 panic( "pad: amount must be positive" )
1957 }
1958 free := freeUgnot(cur)
1959 if amount > free {
1960 panic( "pad: amount exceeds free ugnot (reserved for markets/fees)" )
1961 }
1962 sendUgnot(cur, protocolAddr, amount)
1963 chain.Emit( "WithdrawProtocolUgnot" ,
1964 "to" , protocolAddr.String(),
1965 "amount" , strconv.FormatInt(amount, 10 ),
1966 "freeLeft" , strconv.FormatInt(free-amount, 10 ),
1967 )
1968 return amount
1969 }
1970
1971 // --- read helpers (non-crossing) ---
1972
1973 func BalanceOf(id string , owner address) int64 {
1974 return balOf(mustLaunch(id), owner)
1975 }
1976
1977 // ListBuyers returns unique buyer addresses (one per line), capped for query size.
1978 // Only addresses that bought at least once on this pad (UniqueBuyers). Not full GRC20 holders
1979 // who received tokens via transfer.
1980 func ListBuyers(id string ) string {
1981 l := mustLaunch(id)
1982 const maxN = 100
1983 out := ""
1984 n := 0
1985 l.UniqueBuyers.Iterate( "" , "" , func (key string , _ any ) bool {
1986 if n >= maxN {
1987 return true
1988 }
1989 if out != "" {
1990 out += "\n"
1991 }
1992 out += key
1993 n++
1994 return false
1995 })
1996 return out
1997 }
1998
1999 func GetStatus(id string ) int {
2000 return mustLaunch(id).Status
2001 }
2002
2003 func GetRaised(id string ) int64 {
2004 return mustLaunch(id).RaisedUgnot
2005 }
2006
2007 func GetPool(id string ) (ugnot, token int64 ) {
2008 l := mustLaunch(id)
2009 return l.PoolUgnot, l.PoolToken
2010 }
2011
2012 func GetCreatorFees(id string ) int64 {
2013 return mustLaunch(id).CreatorFees
2014 }
2015
2016 func ProtocolFees() int64 {
2017 return protocolFees
2018 }
2019
2020 func LaunchCount() int {
2021 return launches.Size()
2022 }
2023
2024 func ResolveSymbol(symbol string ) string {
2025 s, ok := bySymbol.Get(symbol).( string )
2026 if !ok {
2027 return ""
2028 }
2029 return s
2030 }
2031
2032 // ListIDs returns newline-separated launch IDs (sorted by AVL key / creation order).
2033 func ListIDs() string {
2034 out := ""
2035 launches.Iterate( "" , "" , func (key string , _ any ) bool {
2036 if out != "" {
2037 out += "\n"
2038 }
2039 out += key
2040 return false
2041 })
2042 return out
2043 }
2044
2045 // LaunchInfo returns a single-line pipe-delimited summary for UIs/indexers:
2046 //
2047 // id|name|symbol|status|raised|sold|buyers|creatorFees|poolUgnot|poolToken|uri|creator|virtualUgnot|virtualToken|created|tokenID|gnoswapReady|gnoswapListed|gnoswapPoolPath|gnoswapNote|listVenue
2048 //
2049 // status: 0=curve 1=graduated; gnoswapReady/listed: 0|1
2050 // gnoswapNote: optional (padv12+); pipes stripped for delimiter safety.
2051 // listVenue: optional (padv23+); empty if unlisted.
2052 func LaunchInfo(id string ) string {
2053 l := mustLaunch(id)
2054 gs := "0"
2055 if l.GnoswapReady {
2056 gs = "1"
2057 }
2058 gl := "0"
2059 if l.GnoswapListed {
2060 gl = "1"
2061 }
2062 note := strings.ReplaceAll(l.GnoswapNote, "|" , "/" )
2063 venue := strings.ReplaceAll(l.ListVenue, "|" , "/" )
2064 return l.ID + "|" +
2065 l.Name + "|" +
2066 l.Symbol + "|" +
2067 strconv.Itoa(l.Status) + "|" +
2068 strconv.FormatInt(l.RaisedUgnot, 10 ) + "|" +
2069 strconv.FormatInt(l.RealSold, 10 ) + "|" +
2070 strconv.Itoa(l.BuyerCount) + "|" +
2071 strconv.FormatInt(l.CreatorFees, 10 ) + "|" +
2072 strconv.FormatInt(l.PoolUgnot, 10 ) + "|" +
2073 strconv.FormatInt(l.PoolToken, 10 ) + "|" +
2074 l.URI + "|" +
2075 l.Creator.String() + "|" +
2076 strconv.FormatInt(l.VirtualUgnot, 10 ) + "|" +
2077 strconv.FormatInt(l.VirtualToken, 10 ) + "|" +
2078 strconv.FormatInt(l.Created, 10 ) + "|" +
2079 l.TokenID + "|" +
2080 gs + "|" +
2081 gl + "|" +
2082 l.GnoswapPoolPath + "|" +
2083 note + "|" +
2084 venue
2085 }
2086
2087 // ParamsInfo returns parameters for UI display.
2088 // total|curve|poolSeed|gradThreshold|feeBps|createBond|listFeeGns
2089 // createBond is live from bond realm when not in unit-test mode.
2090 // listFeeGns: Create-time GNS escrow required (padv20+).
2091 func ParamsInfo() string {
2092 return strconv.FormatInt(TotalSupply, 10 ) + "|" +
2093 strconv.FormatInt(CurveSupply, 10 ) + "|" +
2094 strconv.FormatInt(PoolSeed, 10 ) + "|" +
2095 strconv.FormatInt(graduationThreshold(), 10 ) + "|" +
2096 strconv.FormatInt(FeeBPS, 10 ) + "|" +
2097 strconv.FormatInt(requiredCreateBond(), 10 ) + "|" +
2098 strconv.FormatInt(requiredListFeeGns(), 10 )
2099 }
2100
2101 // TradeHistory returns newline-separated chart points:
2102 //
2103 // height|side|ugnot|tokens|priceScaled
2104 //
2105 // side: 0=buy 1=sell 2=open/graduate. Ordered oldest -> newest.
2106 func TradeHistory(id string ) string {
2107 l := mustLaunch(id)
2108 out := ""
2109 l.Trades.Iterate( "" , "" , func (_ string , value any ) bool {
2110 t := value.(*Trade)
2111 line := strconv.FormatInt(t.Height, 10 ) + "|" +
2112 strconv.Itoa(t.Side) + "|" +
2113 strconv.FormatInt(t.Ugnot, 10 ) + "|" +
2114 strconv.FormatInt(t.Tokens, 10 ) + "|" +
2115 strconv.FormatInt(t.Price, 10 )
2116 if out != "" {
2117 out += "\n"
2118 }
2119 out += line
2120 return false
2121 })
2122 return out
2123 }
2124
2125 // TradeCount returns number of stored chart samples for a launch.
2126 func TradeCount(id string ) int {
2127 return mustLaunch(id).Trades.Size()
2128 }
2129
2130 // resetForTest clears package state between unit tests.
2131 func resetForTest() {
2132 launches = avl.Tree{}
2133 bySymbol = avl.Tree{}
2134 nextID = 0
2135 nextTokenID = 0
2136 var zero address
2137 protocolAddr = zero
2138 // padAddr is set in package init - do not clear (realm address is fixed).
2139 protocolFees = 0
2140 protocolFeesPaid = 0
2141 inited = false
2142 pointsEnabled = false
2143 graduationUgnot = 0
2144 listFeeGnsLive = 0
2145 testSkipBanker = true // unit tests skip banker; integration/chain tests leave false
2146 testForceGnoswapList = false
2147 testForceZdexList = false
2148 totalListFeeGns = 0
2149 totalWugnotCredit = 0
2150 wugnotCredit = avl.Tree{}
2151 resetListVenuesForTest()
2152 }
2153