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