Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}