Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

offers.gno

7.89 Kb · 216 lines
  1package market
  2
  3import (
  4	"chain"
  5	"strconv"
  6	"strings"
  7	"time"
  8
  9	"gno.land/p/nt/avl/v0"
 10	"gno.land/p/nt/seqid/v0"
 11	"gno.land/p/samcrew/launchpad/merkle/v1"
 12	sm "gno.land/p/samcrew/launchpad/safemath/v1"
 13	"gno.land/r/samcrew/launchpad/nft/v1"
 14)
 15
 16const (
 17	OfferToken      = "token"      // one named token
 18	OfferCollection = "collection" // any token of the collection
 19	OfferTrait      = "trait"      // any token proven to carry a trait
 20	StatusAccepted  = "accepted"
 21	// The token's own listing, closed because the token was sold to an offer.
 22	StatusSuperseded = "superseded"
 23	maxTraitBytes    = 100
 24)
 25
 26// Offer is a bid whose full price sits in this realm until it is accepted,
 27// cancelled by its buyer or returned after expiry. An offer exists while it
 28// is open and no longer.
 29type Offer struct {
 30	ID         string // "O<n>"
 31	Kind       string
 32	Collection string
 33	Number     int64  // token offers only
 34	Trait      string // trait offers only: "<type>=<value>"
 35	Buyer      address
 36	Price      int64
 37	Currency   string
 38	CreatedAt  int64
 39	ExpiresAt  int64
 40	FeeBPS     int64 // protocol fee, pinned when the buyer funded the offer
 41}
 42
 43var (
 44	lastOffer seqid.ID
 45	offers    avl.Tree // offer key -> *Offer, in creation order
 46	open      avl.Tree // "<collection>/<offer key>" -> *Offer
 47	byBuyer   avl.Tree // "<buyer>/<offer key>" -> *Offer
 48)
 49
 50// findOffer returns an open offer and its key, or nil: a closed one is gone.
 51func findOffer(id string) (*Offer, string) {
 52	if key, ok := orderKey('O', id); ok {
 53		if o := offers.Get(key); o != nil {
 54			return o.(*Offer), key
 55		}
 56	}
 57	return nil, ""
 58}
 59
 60func mustOffer(id string) (*Offer, string) {
 61	o, key := findOffer(id)
 62	if o == nil {
 63		panic("launchpad/market: unknown offer")
 64	}
 65	return o, key
 66}
 67
 68// TraitLeaf is the Merkle leaf that proves a token carries a trait, as
 69// committed by the collection's creator in the NFT ledger.
 70func TraitLeaf(collection string, number int64, trait string) merkle.Hash {
 71	return merkle.Leaf([]byte("launchpad-trait-v1|" + collection + "|" +
 72		strconv.FormatInt(number, 10) + "|" + trait))
 73}
 74
 75// MakeOffer escrows price and returns the offer ID. kind selects what the
 76// offer is for: number is set for a token offer, trait for a trait offer, and
 77// both are zero and empty otherwise. A trait offer needs a collection whose
 78// creator committed its traits. The protocol fee in force is pinned on the
 79// offer; it comes out of what the seller receives, never on top of the price,
 80// and maxFeeBPS is the highest the buyer lets a seller be charged.
 81func MakeOffer(cur realm, kind, collection string, number int64, trait string,
 82	price, expiresAt int64, currencyKey string, maxFeeBPS int64) string {
 83	buyer := directCaller(cur)
 84	fee := pinned(collection, price, expiresAt, currencyKey, maxFeeBPS)
 85	ok := false
 86	switch kind {
 87	case OfferToken:
 88		ok = trait == "" && nft.StatusOf(collection, number) == nft.StatusActive &&
 89			nft.OwnerOf(collection, number) != buyer
 90	case OfferCollection:
 91		ok = trait == "" && number == 0
 92	case OfferTrait:
 93		ok = number == 0 && len(trait) <= maxTraitBytes && strings.Count(trait, "=") == 1 &&
 94			!strings.HasPrefix(trait, "=") && !strings.HasSuffix(trait, "=") &&
 95			!strings.Contains(trait, "|") && nft.TraitsRoot(collection) != ""
 96	}
 97	if !ok {
 98		panic("launchpad/market: invalid offer")
 99	}
100	before := take(cur, currencyKey, price)
101	escrow[currencyKey] = sm.Add(escrow[currencyKey], price)
102	id, key := nextOrder(&lastOffer, "O")
103	o := &Offer{ID: id, Kind: kind, Collection: collection, Number: number,
104		Trait: trait, Buyer: buyer, Price: price, Currency: currencyKey,
105		CreatedAt: time.Now().Unix(), ExpiresAt: expiresAt, FeeBPS: fee}
106	offers.Set(key, o)
107	open.Set(collection+"/"+key, o)
108	byBuyer.Set(buyer.String()+"/"+key, o)
109	settled(cur, currencyKey, before)
110	chain.Emit("OfferMade", "offer", id, "kind", kind, "collection", collection,
111		"number", strconv.FormatInt(number, 10), "trait", trait,
112		"buyer", buyer.String(), "currency", currencyKey,
113		"price", strconv.FormatInt(price, 10), "fee_bps", strconv.FormatInt(fee, 10),
114		"expires_at", strconv.FormatInt(expiresAt, 10))
115	return id
116}
117
118// release deletes an offer, takes its price out of escrow and says why in its
119// closing event; the caller then pays the price out. Every exit goes through
120// here, and an offer leaves escrow once, by its own key: with `settled`, an
121// exit pays out exactly the price of the one offer it closed, so a fault in
122// one offer's exit can never spend another offer's escrow.
123func release(o *Offer, key, status string) {
124	if v := offers.Get(key); v == nil || v.(*Offer) != o {
125		panic("launchpad/market: offer already closed")
126	}
127	offers.Remove(key)
128	open.Remove(o.Collection + "/" + key)
129	byBuyer.Remove(o.Buyer.String() + "/" + key)
130	escrow[o.Currency] = sm.Sub(escrow[o.Currency], o.Price)
131	chain.Emit("OfferClosed", "offer", o.ID, "status", status)
132}
133
134// refund returns an open offer's escrow to its buyer. Refunds never consult
135// the config realm: no pause or policy change can hold a buyer's funds.
136func refund(cur realm, o *Offer, key, status string) {
137	before := untouched(cur, o.Currency)
138	release(o, key, status)
139	pay(cur, o.Currency, o.Buyer, o.Price)
140	settled(cur, o.Currency, before)
141}
142
143// CancelOffer takes back the caller's own open offer, expired or not.
144func CancelOffer(cur realm, id string) {
145	who := directCaller(cur)
146	o, key := mustOffer(id)
147	if who != o.Buyer {
148		panic("launchpad/market: buyer only")
149	}
150	refund(cur, o, key, StatusCancelled)
151}
152
153// ExpireOffer returns to its buyer the escrow of an offer that expired more
154// than graceToClose ago. Anyone may call it: the price goes to the buyer, and
155// the chain pays the caller the storage deposit of the bytes the offer
156// occupied, so for a week after expiry only the buyer, with CancelOffer,
157// takes its own offer back.
158func ExpireOffer(cur realm, id string) {
159	directCaller(cur)
160	o, key := mustOffer(id)
161	if time.Now().Unix() < o.ExpiresAt+graceToClose {
162		panic("launchpad/market: only its buyer closes this offer yet")
163	}
164	refund(cur, o, key, StatusExpired)
165}
166
167// AcceptOffer sells the caller's token number to an open offer. proof is the
168// comma-separated Merkle path of the token's trait for a trait offer, and
169// empty otherwise. currencyKey and price are the terms the seller read: an
170// offer on other terms, such as an ID the seller never saw, buys nothing. The
171// caller must first approve this realm in the NFT ledger.
172func AcceptOffer(cur realm, id string, number int64, proof, currencyKey string, price int64) {
173	seller := directCaller(cur)
174	o, key := mustOffer(id)
175	if o.Currency != currencyKey || o.Price != price {
176		panic("launchpad/market: order terms differ from those signed")
177	}
178	before := untouched(cur, o.Currency)
179	p := policy(o.Currency)
180	ok := time.Now().Unix() < o.ExpiresAt && seller != o.Buyer &&
181		sellable(o.Collection, number, seller)
182	switch {
183	case !ok:
184	case o.Kind == OfferToken:
185		ok = number == o.Number && proof == ""
186	case o.Kind == OfferCollection:
187		ok = proof == ""
188	default:
189		root, _ := merkle.ParseHash(nft.TraitsRoot(o.Collection))
190		path, err := merkle.ParseProof(proof)
191		ok = err == nil && merkle.Verify(root, TraitLeaf(o.Collection, number, o.Trait), path)
192	}
193	if !ok {
194		panic("launchpad/market: offer unavailable for this token")
195	}
196	release(o, key, StatusAccepted)
197	if l := listingOf(o.Collection, number); l != nil {
198		closeListing(l, StatusSuperseded)
199	}
200	nft.TransferFrom(cross(cur), o.Collection, seller, o.Buyer, number)
201	settle(cur, o.Currency, o.Collection, o.Price, o.FeeBPS, seller, p.treasury)
202	settled(cur, o.Currency, before)
203	chain.Emit("OfferAccepted", "offer", id, "collection", o.Collection,
204		"number", strconv.FormatInt(number, 10), "buyer", o.Buyer.String(),
205		"seller", seller.String(), "currency", o.Currency,
206		"price", strconv.FormatInt(o.Price, 10))
207}
208
209// GetOffer returns an open offer; a closed or unknown ID aborts.
210func GetOffer(id string) Offer {
211	o, _ := mustOffer(id)
212	return *o
213}
214
215// EscrowOf is the total of open offers in a currency.
216func EscrowOf(currencyKey string) int64 { return escrow[currencyKey] }