offer.gno
3.66 Kb · 123 lines
1package gnogle_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 recordSale(collID, tokenID, owner, buyer, o.amount, "offer")
110
111 chain.Emit("OfferAccepted", "collection", collID, "tokenId", tokenID,
112 "buyer", buyer.String(), "seller", owner.String(),
113 "amount", strconv.FormatInt(o.amount, 10))
114}
115
116// GetOffer returns a buyer's offer amount on a token (read-only).
117func GetOffer(collID, tokenID string, buyer address) (amount int64, exists bool) {
118 v, ok := offers.Get(offerKey(collID, tokenID, buyer))
119 if !ok {
120 return 0, false
121 }
122 return v.(*Offer).amount, true
123}