Buy
Ask AI
Buy sells `tickets` tickets in the CURRENT window to the caller.
Cost is tickets * TicketPrice, pulled from the caller with the GRC20 allowance path, so the caller wraps native ugnot with wugnot.Deposit and approves this realm first. houseCutBps of the cost goes to the ops pot; the remainder — and all integer-division dust, since the cut is floored — joins this window's prize pot and is recorded in the caller's spend for the window.
EVERY CHECK RUNS BEFORE THE PULL, and the window record is created only after it, so a failed Buy moves nothing and creates nothing.
Each purchase APPENDS ITS OWN ticket index rather than growing an existing one. That is not a style choice: it is the property Commit depends on, and the reason is spelled out there.
THE WINDOW'S FIRST BUY PINS THE WINDOW'S BEACON ROUND, as a side effect of creating the record (getOrCreateDraw). That is deliberate and is the only moment the round is ever chosen. Two consequences a caller can see. First, a sale needs a beacon that has published at least once AND been relayed within speedMaxStaleBlocks, or it is refused with the beacon's own message — a refused sale rather than money accepted into a window that could never be drawn fairly. Second, the buyer who opens a window does not get to pick anything useful: the round is fixed two hours ahead of the moment the ticket set closes, so at the instant it is pinned there is no ticket set for it to be advantageous against.
Panics if tickets is not positive, if the cost overflows int64, if the allowance is short, or if THIS WINDOW IS ALREADY COMMITTED — the last of which is the frozen-total rule, not a convenience check. Once a window's total is frozen, no further ticket may join it.
THAT LAST REFUSAL IS UNREACHABLE THROUGH THE ENTRY POINTS, and is kept deliberately. Commit only accepts a window that is already over, and Buy only ever sells into currentWindow(), so the live window cannot be in a committed state — which is exactly why a commit can no longer shut a window's sales down early (AUDIT-2026-09-28.md finding 2). The check stays as a second lock on the frozen-total rule: if the window's identity ever stops being "the one the clock is on", this is the line that still refuses to grow a frozen ticket set.
ONLY A USER MAY BUY, AND THE REASON IS THE PAYOUT, NOT THE PURCHASE (AUDIT-2026-09-28.md finding 8). A ticket is a claim on two transfers this realm will later push OUT: Resolve's whole pot to the winner's address, and Refund's spend back to the buyer's. Both go to cur.Previous().Address(), which for a realm caller is that realm's PACKAGE-DERIVED address — an address whose only "key" is its package path, so nothing can move wugnot out of it unless that realm happens to contain code that does. A realm with no such code that won a draw would have the entire window's pot transferred into an address it can never spend from: money that has left this realm's books (heldPots drops, the invariant stays green) and is unreachable by anybody, for ever. This realm cannot tell which realms those are and must not try, so it refuses them all at the door instead of stranding a pot at settlement time.
cur.Previous().IsUserCall() IS THE CHECK — AND IsUser() IS NOT. IsUser() also accepts a `maketx run` ephemeral realm, which is code executing under a user's signature and NOT an EOA: its address is still package-derived, so it can win a pot and then cease to exist with the money in it. r/splash/exchange states the same distinction about the same word for a different failure mode (a shared send envelope); the property being asked for — "the thing in front of me is a wallet somebody holds a private key for" — is identical.
THERE IS NO SECOND, UNGUARDED ENTRY POINT HERE, and that is a difference from the raffle worth naming: the raffle must leave FundPot realm-callable for the duel's rake, so its asymmetry has to be documented at both ends to stop someone "making it consistent". THIS REALM HAS NO FundPot (see the package comment), so THE GUARD ABOVE IS THE ONLY DOOR MONEY COMES IN BY, and every ugnot in a pot is therefore attributable to a user address that can receive it back.
Command
# WARNING: This command is running in an INSECURE mode.
# It is strongly recommended to use a hardware device for signing
# and avoid trusting any computer connected to the internet,
# as your private keys could be exposed.
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed" -func "Buy" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -chainid "onyx-1" -remote "https://rpc.onyx.testnets.gno.land" ADDRESSgnokey query -remote "https://rpc.onyx.testnets.gno.land" auth/accounts/ADDRESS
gnokey maketx call -pkgpath "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/arcade/speed" -func "Buy" -args $'' -gas-fee 1000000ugnot -gas-wanted 1_000_000_000 -send "" -broadcast=false ADDRESS > call.tx
gnokey sign -tx-path call.tx -chainid "onyx-1" -account-number ACCOUNTNUMBER -account-sequence SEQUENCENUMBER ADDRESS
gnokey broadcast -remote "https://rpc.onyx.testnets.gno.land" call.tx