Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}