offer.gno
3.59 Kb · 122 lines
1package nftmarket
2
3import (
4 "chain"
5 "strconv"
6
7 "gno.land/p/g18wk4a80cr7dqa25vfka2yug5n3pd50udled6y3/grc721"
8 "gno.land/p/nt/avl/v0"
9)
10
11// Offer is a standing bid on a specific token (whether or not it is listed).
12// The offered GNOT is escrowed in the realm until the offer is accepted by the
13// token owner or withdrawn by the buyer. A buyer may hold at most one offer per
14// token.
15type Offer struct {
16 collID string
17 tokenID string
18 buyer address
19 amount int64 // escrowed ugnot
20 madeAt int64 // block height
21}
22
23var offers avl.Tree // "collID/tokenID/buyer" -> *Offer
24
25func offerKey(collID, tokenID string, buyer address) string {
26 return collID + "/" + tokenID + "/" + buyer.String()
27}
28
29// MakeOffer escrows GNOT as an offer on a token. Re-offering replaces (and
30// refunds) the caller's previous offer on the same token.
31func MakeOffer(cur realm, collID, tokenID string) {
32 assertUserCall(cur)
33 coll := mustGetCollection(collID)
34 buyer := cur.Previous().Address()
35
36 amount := receivedUgnot()
37 if amount <= 0 {
38 panic("an offer must include a positive ugnot amount")
39 }
40
41 // Disallow offering on a token you already own.
42 if owner, err := coll.nft.OwnerOf(grc721.TokenID(tokenID)); err == nil && owner == buyer {
43 panic("you already own this token")
44 }
45
46 key := offerKey(collID, tokenID, buyer)
47 if v, exists := offers.Get(key); exists {
48 payout(cur, buyer, v.(*Offer).amount) // refund the replaced offer
49 }
50 offers.Set(key, &Offer{collID: collID, tokenID: tokenID, buyer: buyer, amount: amount, madeAt: now()})
51
52 chain.Emit("OfferMade", "collection", collID, "tokenId", tokenID,
53 "buyer", buyer.String(), "amount", strconv.FormatInt(amount, 10))
54}
55
56// CancelOffer withdraws the caller's offer and refunds the escrowed amount.
57func CancelOffer(cur realm, collID, tokenID string) {
58 assertUserCall(cur)
59 buyer := cur.Previous().Address()
60 key := offerKey(collID, tokenID, buyer)
61
62 v, ok := offers.Get(key)
63 if !ok {
64 panic("you have no offer on this token")
65 }
66 o := v.(*Offer)
67 offers.Remove(key)
68 payout(cur, buyer, o.amount)
69
70 chain.Emit("OfferCancelled", "collection", collID, "tokenId", tokenID, "buyer", buyer.String())
71}
72
73// AcceptOffer lets the current owner of a token accept a specific buyer's
74// offer: the NFT goes to the buyer and the escrowed amount is split
75// seller / royalty / fee. The token must be held by the caller (not escrowed in
76// a listing or auction).
77func AcceptOffer(cur realm, collID, tokenID string, buyer address) {
78 assertUserCall(cur)
79 coll := mustGetCollection(collID)
80 owner := cur.Previous().Address()
81 tid := grc721.TokenID(tokenID)
82
83 curOwner, err := coll.nft.OwnerOf(tid)
84 if err != nil {
85 panic(err)
86 }
87 if curOwner != owner {
88 panic("only the current token owner can accept an offer (delist/cancel any auction first)")
89 }
90
91 key := offerKey(collID, tokenID, buyer)
92 v, ok := offers.Get(key)
93 if !ok {
94 panic("no such offer")
95 }
96 o := v.(*Offer)
97 offers.Remove(key)
98
99 if err := coll.nft.TransferFrom(owner, owner, buyer, tid); err != nil {
100 panic(err)
101 }
102 royalty := o.amount * coll.royaltyBps / bpsDenominator
103 fee := o.amount * feeBps / bpsDenominator
104 sellerAmt := o.amount - royalty - fee
105
106 feePot += fee
107 payout(cur, coll.creator, royalty)
108 payout(cur, owner, sellerAmt)
109
110 chain.Emit("OfferAccepted", "collection", collID, "tokenId", tokenID,
111 "buyer", buyer.String(), "seller", owner.String(),
112 "amount", strconv.FormatInt(o.amount, 10))
113}
114
115// GetOffer returns a buyer's offer amount on a token (read-only).
116func GetOffer(collID, tokenID string, buyer address) (amount int64, exists bool) {
117 v, ok := offers.Get(offerKey(collID, tokenID, buyer))
118 if !ok {
119 return 0, false
120 }
121 return v.(*Offer).amount, true
122}