// nsmarket — names for sale, without anybody holding anything. // // HOW A SALE WORKS, and why it is built this way. // // The seller keeps their name. Listing does not move it here: it records // an asking price and relies on the GRC-721 approval the seller has // already granted this realm. Ownership does not change until somebody // buys, and the seller can walk away at any moment by revoking the // approval — at which point the listing simply stops working. // // The buyer's money is never held either. Buy is ONE call: it checks the // listing still stands, moves the name, pays the seller, takes the fee, // refunds any overpayment. All of it inside the transaction the buyer // signed. If any part fails the whole thing reverts and nothing moved. // // Which means: at no point does this realm hold somebody else's name or // somebody else's money. That is not a nice-to-have. An escrow that // holds either one is a different kind of business, with a different set // of rules, run by people with licences. // // THIS REALM HAS NO VAULT PRIVILEGES. It is not in nsdata's trusted set // and does not ask to be. Every transfer goes through nslogic's ordinary // GRC-721 path, which means it can only move a name the owner has // explicitly approved — the same permission any other marketplace would // need. However wrong the code below turns out to be, the worst it can // do is fail. package nsmarket import ( "gno.land/p/nt/avl/v0" ) // A listing. Kept small on purpose: the seller pays to store it, and // everything derivable is derived rather than copied. The seller's // address is here because the name can change hands outside this realm // and a stale seller must be caught, not trusted. type Listing struct { seller address price int64 // ugnot listed int64 // unix seconds /* PINNED TO ONE REGISTRATION, the same way nsskin pins a binding. Without it a listing outlives the registration it was made against: list at 50 GNOT having granted SetApprovalForAll, let the name lapse, re-register it yourself — the burn clears the per-token approval but NOT the operator grant, so owner matches, IsNameValid is true, and any passer-by executes the year-old listing against the name you just paid to get back. */ registered int64 /* PINNED TO ONE OWNERSHIP EPOCH as well as one registration. The registration pin does not catch a name that was SOLD AND BOUGHT BACK: owner matches, the name is valid, the registration date never changed, and an operator grant (SetApprovalForAll) survives transfers — so a seller who listed, sold elsewhere and later bought the name back found their old listing executable by anyone at the old price. The ledger's epoch moves on every transfer, so it catches it. Zero means a listing written before this field, treated as unpinned. */ epoch int64 } var ( // Keyed by token id, "label*domain". listings avl.Tree sold int64 // completed sales, for the honest version of a stat /* This realm's own address, remembered by List. The reads need it to ask whether the seller's approval still names this realm, and a read has no `cur` to learn it from. Nothing can be listed before List runs, so it is always set when a listing exists to be read. */ marketAddr address ) /* WHO PAYS FOR A LISTING'S STORAGE, AND WHO GETS IT BACK. Measured on Pearl, 2026-09-10. The seller's List transaction pays the storage deposit for the listing. The chain refunds a deposit to whoever's transaction FREES the storage, so when Buy removes the listing the refund goes to the BUYER: the buyer's balance fell by 0.31 GNOT less than price plus gas. Unlist refunds the seller, and Prune refunds whoever prunes. This is gno's rule, not ours, and it cannot be routed elsewhere. The amount is small, but the site's quote should not promise a seller "price minus fee" as if listing were free: listing costs the deposit, recovered only by unlisting. */ const ( maxPage = 200 minPrice = int64(1) // zero would be a giveaway, not a sale maxPrice = int64(1) << 52 // guards the fee arithmetic from overflow ugnot = "ugnot" )