Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

v2 source realm

Package nsskin holds purchasable card art — the skins — as tokens of their own, and binds them to names.

Overview

Package nsskin holds purchasable card art — the skins — as tokens of their own, and binds them to names.

WHY THIS IS ITS OWN REALM AND NOT PART OF nsdata. SKINS.md §8 listed it as undecided; it is decided here, because the reasoning turned out to be one-sided rather than balanced.

Example
 1nsdata IS PERMANENT. It cannot be replaced, ever. A second kind of
 2token in it means every mistake in the skin model is permanent too,
 3and the skin model is the newest and least settled thing here.
 4
 5nsdata.OwnerOf DELIBERATELY PANICS FOR EXPIRED TOKENS so a lapsed
 6name cannot be listed anywhere. Skins do not expire. That machinery
 7is exactly wrong for them, and a conditional on token kind inside
 8the one realm that can never be fixed is the worst place for it.
 9
10A NEW REALM CAN BE REPLACED while it holds a handful of tokens. The
11vault cannot, at any point, for any reason.

So being wrong here costs a migration nobody notices. Being wrong inside nsdata costs forever.

A SKIN IS TWO THINGS

Example
1A DEFINITION is the artwork: a name, a sprite, an optional extension
2lock, and the artist to pay. One per design, stored once — a
3thousand holders of one design cost one sprite, not a thousand.
4
5A TOKEN is somebody's copy. Editions are unlimited, because SKINS.md
6is explicit that a cap is a floor price with extra steps and resale
7value is never part of the pitch.

A SKIN BINDS TO ONE NAME, AND TRAVELS WITH IT

This reverses an earlier recommendation of mine, and the new answer is better. I had argued for keeping them separate — a pointer from the name, reverting on sale — because names expire and skins should not. The objection to bundling was that a buyer sees a skinned card on a marketplace and receives an unskinned name.

Binding solves that rather than working around it. The skin genuinely comes with the name, so the card is not misleading, and the small disclosure line the old model needed inside the artwork disappears. And a skin still does not die with a name: when the name goes, the skin comes back.

WHY ATTACHING ESCROWS THE TOKEN, WHICH IS THE WHOLE CUSTODY ANSWER

The worry: a name listed on a marketplace that takes CUSTODY. If the skin lived in the seller's wallet, a custodial listing would hold the name while the skin sat somewhere else, and untangling that on expiry would need cooperation we cannot compel.

So attaching moves the token to THIS REALM'S OWN ADDRESS. While attached it is in nobody's wallet: it cannot be listed, cannot be sold out from under the name, and a marketplace escrowing the NAME never touches it. The question of freeing a skin from somebody else's custody does not arise, because it was never in it.

Taking it out again is one rule with one fallback:

Example
1WHILE THE NAME EXISTS, its CURRENT OWNER may claim the skin. That is
2what makes it travel: buy the name, claim the skin.
3
4IF THE NAME RECORD IS GONE — lapsed and reclaimed — whoever attached
5it may take it back, which is the "it returns to your wallet" case.

One limitation, stated rather than hidden: buy a name with a skin, never claim it, and let that name lapse completely, and the skin returns to whoever attached it. Claiming on purchase avoids it entirely, and the site prompts for exactly that.

Functions 42

func AllPrices

Action
1func AllPrices() string
source

AllPrices is every id and its price, for a shop that wants the lot in one read. AllSkins carries the price too; this exists for a panel that wants prices without the rest and for checking the two agree.

func AllSkins

Action
1func AllSkins() string
source

AllSkins packs the whole catalogue into ONE read.

A shop page needs every definition's name, lock and availability, and asking for them one at a time is sixty round trips before anything appears on screen. The ART is deliberately NOT here: it is the bulk of the data, it is committed in the repo as the canonical source, and a page that already has it does not need to fetch it again.

Six fields: id, name, lock, withdrawn, minted, price in ugnot.

Tab-separated fields, newline-separated records — not JSON, because building JSON by string concatenation in a realm is how quoting bugs get shipped, and a shop is not worth that risk. No field can contain a tab or a newline: ids and locks are [a-z0-9-] by rule, and a name is checked here rather than assumed.

func ArtForName

Action
1func ArtForName(label, domain string) string
source

ArtForName is the one question the renderer asks, and it is answered here so the rule lives with the data it depends on.

It returns the sprite a name should be drawn with, or "" for the ordinary card. Everything the renderer would otherwise have to remember to check is inside it, because a renderer that has to remember three things before drawing is one that eventually forgets one.

The checks, and what each of them makes self-cleaning:

Example
1the name wears a token at all
2that token's definition has not been WITHDRAWN — so a takedown
3  reverts every name wearing it, everywhere, at once, with no
4  migration and no per-name cleanup
5the binding is FRESH, pinned to this registration — so a name that
6  lapsed and was taken by somebody else inherits nothing
7the LOCK still matches — belt and braces, since Attach checks it too

Note what is NOT checked: who holds the token in their wallet. Nobody does. It is in this realm's custody for as long as it is attached, which is what makes the skin travel with the name instead of with a wallet.

func Attach

crossing Action
1func Attach(cur realm, tid, label, domain string)
source

Attach binds a skin to a name and puts the token into this realm's custody. See the long note in types.gno for why custody rather than a lock in place: it is what makes a marketplace's escrow of the NAME irrelevant to the skin.

ONE SKIN, ONE NAME, both ways. A token may be attached to one name, and a name may wear one skin. Letting one copy dress every name a person owns would make the second copy pointless, and the object stops meaning anything.

func Buy

crossing Action
1func Buy(cur realm, id, label, domain string) string
source

BUYING ONE, AND WEARING IT, IN A SINGLE TRANSACTION.

Splitting these would mean a buyer signs twice and can stop between — leaving a skin they paid for sitting unworn in a wallet, which is the state nobody wants and the one a two-step flow produces most often. So the name is an argument: pass it and the skin is minted and attached together; pass "" and it is minted loose, which is what somebody buying ahead of registering a name needs.

NOTHING IS EVER HELD. Payment, treasury and refund all settle inside the buyer's own transaction, exactly as nslogic does it — and for the same reason, which is worth restating because it is a legal position rather than a style: a realm you can have a BALANCE in is a different kind of thing to operate, subject to different rules, run by people with licences. Every ugnot that arrives leaves again in the same call.

THE 70/30 SPLIT IS NOT HERE, and that is deliberate rather than forgotten. Every skin in this collection is ours, so the artist and the treasury are the same address and a split would move money from a pocket to itself while costing gas to do it. The artist is recorded on every definition so the day it stops being us, this is where the split goes — see SKINS.md.

func Claim

crossing Action
1func Claim(cur realm, tid string)
source

Claim takes a skin out of custody and into the caller's wallet.

WHO MAY, and this is the whole travels-with-the-name rule:

Example
 1the CURRENT OWNER of the name, while the name record still exists.
 2Buy the name, claim the skin — including a name bought from someone
 3else, which is the point.
 4
 5the LAST KNOWN NAME OWNER, once the record is gone entirely. That is
 6the "it comes back to your wallet when the name expires" case — and
 7"last known" rather than "whoever attached it", because after a sale
 8those are different people and only one of them paid for it. See
 9Refresh above for how that is kept current without a hook nsdata
10does not have.

The binding is pinned to one REGISTRATION. If a name lapsed and somebody else registered it, its timestamp has changed, so the new registrant is not the owner of anything here and cannot claim.

func ClearSkinPrice

crossing Action
1func ClearSkinPrice(cur realm, id string)
source

ClearSkinPrice drops an override, putting the design back under the rule and the base price.

func DefineSkin

crossing Action
1func DefineSkin(cur realm, id, name, art, lock string, artist address)
source

DefineSkin registers a design. It does not mint anything.

Definitions and copies are separate calls on purpose: defining is the expensive, careful, one-off act that puts an artist's work and a sprite on chain, while minting is the cheap repeated one. Rolling them together would mean re-sending the artwork with every sale.

A definition may be REDEFINED only while nothing has been minted from it. After the first copy exists, the art is what its holders bought and it stops being ours to edit — the only lever left is withdrawal, which is honest about what it is doing.

func HasPriceOverride

Action
1func HasPriceOverride(id string) bool
source

HasPriceOverride reports whether this id has been priced by hand. A panel needs it to tell "set to zero" from "free by the rule", which look identical in the price alone and are different decisions.

func ID

Action
1func ID() string
source

ID is the collection id wallets and indexers key on: "<this realm's path>.MNSKIN.0000000".

func IsFreeByRule

Action
1func IsFreeByRule(id string) bool
source

IsFreeByRule reports what the rule alone says, ignoring any override. Exposed so an admin panel can show what it would revert to.

func ListFreeSkins

Action
1func ListFreeSkins() string
source

ListFreeSkins names every definition that costs nothing right now, counting overrides — so it answers "what can I have for free", which is the question, rather than "what does the rule say", which is trivia.

func ListSkins

Action
1func ListSkins() []string
source

ListSkins returns every definition id, in tree order. Small by construction — this is a curated shop, not a submission firehose — and if that ever stops being true it wants paging rather than a bigger answer.

func Mint

crossing Action
1func Mint(cur realm, id string, to address) string
source

Mint issues one copy of a definition to somebody.

The serial is the definition's own running count, so token ids read as "ember#1", "ember#2". Never reused: the counter only goes up, even across burns, because a reissued id would let a new token inherit whatever an old one was pointed at.

func PriceOf

Action
1func PriceOf(id string) int64
source

PriceOf is what this design costs right now, in ugnot.

It does NOT check that the id exists: a price for a definition that is not there is meaningless rather than wrong, and Buy is the thing that must refuse an unknown id — which it already does, by name, before it ever asks what the price is.

func Refresh

crossing Action
1func Refresh(cur realm, tid string)
source

Refresh points a binding's `binder` at the name's CURRENT owner.

WHY THIS EXISTS, and it is the fix for a real hole. `binder` decides who gets the skin back once the name record is gone. Set once at Attach and never updated, it names whoever ATTACHED the skin — so: Alice attaches, sells the name to Bob, the skin travels with it as intended, Bob lets the name lapse, and the skin goes back to ALICE. Bob paid for it. That is not a limitation, it is taking something from the person who bought it.

nsdata cannot tell us a name changed hands — it is permanent, it has no hook, and it never will. So `binder` cannot be maintained; it can only be CAUGHT UP. This is that, and it is deliberately open to anyone:

Example
1it only ever writes the name's real current owner, so calling it can
2never move the skin anywhere it should not go, and there is nothing
3to gain by calling it on somebody else's binding
4
5which means the site can call it on a purchase, a keeper can call it
6on a schedule, the buyer can call it themselves, or nobody calls it
7at all and the next Attach or Claim does it — all with the same
8result and none of them needing to be trusted

Nothing on chain is automatic here. gno has no scheduler and nsdata has no callback, so every version of "the skin follows the sale" is one extra call by someone. Making that call safe for ANYONE to make is as close to automatic as this can get, and it is close enough.

func SetPrice

crossing Action
1func SetPrice(cur realm, p int64)
source

SetPrice sets the BASE price, in ugnot — what a design costs when the rule has not made it free and no admin has priced it by hand. ugnot because that is what the chain moves: what it is worth in anything else is a question for the page that quotes it, and pinning a dollar figure into a contract that cannot see an exchange rate would be a lie with a number in it.

func SetSkinPrice

crossing Action
1func SetSkinPrice(cur realm, id string, p int64)
source

SetSkinPrice prices one design by hand, overriding the rule.

Zero is a legitimate value and is NOT the same as removing the override: it says "this one is free because I said so", which survives a change to the base price and to the rule. ClearSkinPrice is how you go back to being unopinionated.

func SetSkinPrices

crossing Action
1func SetSkinPrices(cur realm, packed string)
source

SetSkinPrices prices many designs in one call.

A panel that lets somebody retune a collection produces a batch of changes, and sixty separate transactions is sixty flat fees for what is one decision. Same encoding as AllSkins reads back — tab between the id and the price, newline between records — so the two halves of the panel speak one format.

An empty price field CLEARS the override rather than setting zero. The panel shows an empty box for "no opinion" and a 0 for "free on purpose", and those must not collapse into each other on the way here.

func SkinOfToken

Action
1func SkinOfToken(tid string) string
source

SkinOfToken is skinOfToken for callers outside the realm.

func SkinOnName

Action
1func SkinOnName(label, domain string) string
source

SkinOnName returns the token currently worn by a name, or "".

func TokenMetadata

Action
1func TokenMetadata(tid grc721.TokenID) (string, error)
source

TokenMetadata is the skin's metadata as a camelCase JSON string: name, description, externalUrl and attributes of {traitType, value}. No image in it; that is TokenURI.

func TokenURI

Action
1func TokenURI(tid grc721.TokenID) (string, error)
source

TokenURI is the skin's image, as a data URI, for a token that exists.

func Withdraw

crossing Action
1func Withdraw(cur realm, id string, withdrawn bool)
source

Withdraw is the takedown control, and SKINS.md §6 makes it non-negotiable: it has to exist before a single outside submission is accepted, which means it has to exist now, while there is nothing to take down and it is cheap to build.

Somebody will submit a character somebody else owns, or a bank's logo, or something vile, and it will render inside other people's wallets with our name beside it. When that happens the fix has to be one call, not a migration.

It does NOT burn anything. The holder keeps the token; it simply stops rendering, and every name pointing at it falls back to the ordinary card at once. Burning somebody's property to solve our problem is a different act with different consequences, and it is not this one.

Dependencies

Source Files 9