buy.gno
4.43 Kb · 127 lines
1package nsskin
2
3import (
4 "chain"
5 "strings"
6 "chain/banker"
7 "chain/runtime/unsafe"
8
9 "gno.land/p/nt/ufmt/v0"
10 "gno.land/r/nym-mnsrehearsal001/nsdata/v1"
11)
12
13const ugnot = "ugnot"
14
15/*
16BUYING ONE, AND WEARING IT, IN A SINGLE TRANSACTION.
17
18Splitting these would mean a buyer signs twice and can stop between —
19leaving a skin they paid for sitting unworn in a wallet, which is the
20state nobody wants and the one a two-step flow produces most often. So
21the name is an argument: pass it and the skin is minted and attached
22together; pass "" and it is minted loose, which is what somebody buying
23ahead of registering a name needs.
24
25NOTHING IS EVER HELD. Payment, treasury and refund all settle inside the
26buyer's own transaction, exactly as nslogic does it — and for the same
27reason, which is worth restating because it is a legal position rather
28than a style: a realm you can have a BALANCE in is a different kind of
29thing to operate, subject to different rules, run by people with
30licences. Every ugnot that arrives leaves again in the same call.
31
32THE 70/30 SPLIT IS NOT HERE, and that is deliberate rather than
33forgotten. Every skin in this collection is ours, so the artist and the
34treasury are the same address and a split would move money from a
35pocket to itself while costing gas to do it. The artist is recorded on
36every definition so the day it stops being us, this is where the split
37goes — see SKINS.md.
38*/
39func Buy(cur realm, id, label, domain string) string {
40 if !cur.Previous().IsUserCall() {
41 panic("nsskin: only direct wallet calls may attach payment")
42 }
43 buyer := cur.Previous().Address()
44 // Same normalization as Mint, for the same reason: the token id is
45 // built from this string, and an un-lowercased one mints a token
46 // that can never render. See the note in write.gno.
47 id = strings.ToLower(strings.TrimSpace(id))
48 s := mustSkin(id)
49 if s.withdrawn {
50 panic("nsskin: " + id + " has been withdrawn and is not for sale")
51 }
52 // Refuse the sale rather than the attach. Minting a locked skin to
53 // somebody who wanted it for a name it will never go on is a refund
54 // request wearing the clothes of a successful purchase.
55 if label != "" && s.lock != "" && s.lock != domain {
56 panic(ufmt.Sprintf("nsskin: %s only goes on *%s names, not *%s", id, s.lock, domain))
57 }
58
59 sent := unsafe.OriginSend()
60 for _, c := range sent {
61 if c.Denom != ugnot {
62 panic("nsskin: only ugnot is accepted, got: " + c.Denom)
63 }
64 }
65 // PER DESIGN, not one figure for the collection. A third of the
66 // definitions are free by the rule in price.gno, and an admin may
67 // have priced any of them by hand — see PriceOf, which is the only
68 // place that decides.
69 due := PriceOf(id)
70 paid := sent.AmountOf(ugnot)
71 if paid < due {
72 panic(ufmt.Sprintf("nsskin: insufficient payment: need %d ugnot, got %d", due, paid))
73 }
74
75 // A FREE SKIN TAKES NO PAYMENT AND MOVES NO COINS, and the refund
76 // below is what makes that safe: somebody whose page quoted the old
77 // price, or who simply attached coins to a free mint, gets every
78 // ugnot back in the same transaction rather than donating it.
79 b := banker.NewBanker(banker.BankerTypeRealmSend, cur)
80 me := cur.Address()
81 if due > 0 {
82 b.SendCoins(me, nsdata.GetTreasury(), chain.Coins{chain.NewCoin(ugnot, due)})
83 }
84 if excess := paid - due; excess > 0 {
85 b.SendCoins(me, buyer, chain.Coins{chain.NewCoin(ugnot, excess)})
86 }
87
88 s.minted++
89 tid := ufmt.Sprintf("%s#%d", id, s.minted)
90 if err := ledger.Mint(buyer, grcID(tid)); err != nil {
91 panic("nsskin: mint failed: " + err.Error())
92 }
93 tokenSkin.Set(tid, id)
94
95 if label != "" {
96 wear(cur, tid, label, domain, buyer)
97 }
98 return tid
99}
100
101// SetPrice sets the BASE price, in ugnot — what a design costs when the
102// rule has not made it free and no admin has priced it by hand. ugnot
103// because that is what the chain moves: what it is worth in anything
104// else is a question for the page that quotes it, and pinning a dollar
105// figure into a contract that cannot see an exchange rate would be a lie
106// with a number in it.
107func SetPrice(cur realm, p int64) {
108 assertIsAdmin(cur)
109 if p < 0 {
110 panic("nsskin: a negative price is not a discount")
111 }
112 price = p
113}
114
115func GetPrice() int64 { return price }
116
117// Free calls must not be able to strand coins here. This realm has no
118// withdrawal path, so anything attached to one would sit in its address
119// permanently.
120func assertNoSend(cur realm) {
121 if !cur.Previous().IsUserCall() {
122 return
123 }
124 if len(unsafe.OriginSend()) > 0 {
125 panic("nsskin: this call takes no payment")
126 }
127}