/
keeper.go
297 lines (255 loc) · 10 KB
/
keeper.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
package auction
import (
"bytes"
"fmt"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
)
type Keeper struct {
bankKeeper bankKeeper
storeKey sdk.StoreKey
cdc *codec.Codec
// TODO codespace
}
// NewKeeper returns a new auction keeper.
func NewKeeper(cdc *codec.Codec, bankKeeper bankKeeper, storeKey sdk.StoreKey) Keeper {
return Keeper{
bankKeeper: bankKeeper,
storeKey: storeKey,
cdc: cdc,
}
}
// TODO these 3 start functions be combined or abstracted away?
// StartForwardAuction starts a normal auction. Known as flap in maker.
func (k Keeper) StartForwardAuction(ctx sdk.Context, seller sdk.AccAddress, lot sdk.Coin, initialBid sdk.Coin) (ID, sdk.Error) {
// create auction
auction, initiatorOutput := NewForwardAuction(seller, lot, initialBid, endTime(ctx.BlockHeight())+MaxAuctionDuration)
// start the auction
auctionID, err := k.startAuction(ctx, &auction, initiatorOutput)
if err != nil {
return 0, err
}
return auctionID, nil
}
// StartReverseAuction starts an auction where sellers compete by offering decreasing prices. Known as flop in maker.
func (k Keeper) StartReverseAuction(ctx sdk.Context, buyer sdk.AccAddress, bid sdk.Coin, initialLot sdk.Coin) (ID, sdk.Error) {
// create auction
auction, initiatorOutput := NewReverseAuction(buyer, bid, initialLot, endTime(ctx.BlockHeight())+MaxAuctionDuration)
// start the auction
auctionID, err := k.startAuction(ctx, &auction, initiatorOutput)
if err != nil {
return 0, err
}
return auctionID, nil
}
// StartForwardReverseAuction starts an auction where bidders bid up to a maxBid, then switch to bidding down on price. Known as flip in maker.
func (k Keeper) StartForwardReverseAuction(ctx sdk.Context, seller sdk.AccAddress, lot sdk.Coin, maxBid sdk.Coin, otherPerson sdk.AccAddress) (ID, sdk.Error) {
// create auction
initialBid := sdk.NewInt64Coin(maxBid.Denom, 0) // set the bidding coin denomination from the specified max bid
auction, initiatorOutput := NewForwardReverseAuction(seller, lot, initialBid, endTime(ctx.BlockHeight())+MaxAuctionDuration, maxBid, otherPerson)
// start the auction
auctionID, err := k.startAuction(ctx, &auction, initiatorOutput)
if err != nil {
return 0, err
}
return auctionID, nil
}
func (k Keeper) startAuction(ctx sdk.Context, auction Auction, initiatorOutput bankOutput) (ID, sdk.Error) {
// get ID
newAuctionID, err := k.getNextAuctionID(ctx)
if err != nil {
return 0, err
}
// set ID
auction.SetID(newAuctionID)
// subtract coins from initiator
_, err = k.bankKeeper.SubtractCoins(ctx, initiatorOutput.Address, sdk.NewCoins(initiatorOutput.Coin))
if err != nil {
return 0, err
}
// store auction
k.setAuction(ctx, auction)
k.incrementNextAuctionID(ctx)
return newAuctionID, nil
}
// PlaceBid places a bid on any auction.
func (k Keeper) PlaceBid(ctx sdk.Context, auctionID ID, bidder sdk.AccAddress, bid sdk.Coin, lot sdk.Coin) sdk.Error {
// get auction from store
auction, found := k.GetAuction(ctx, auctionID)
if !found {
return sdk.ErrInternal("auction doesn't exist")
}
// place bid
coinOutputs, coinInputs, err := auction.PlaceBid(endTime(ctx.BlockHeight()), bidder, lot, bid) // update auction according to what type of auction it is // TODO should this return updated Auction to be more immutable?
if err != nil {
return err
}
// TODO this will fail if someone tries to update their bid without the full bid amount sitting in their account
// sub outputs
for _, output := range coinOutputs {
_, err = k.bankKeeper.SubtractCoins(ctx, output.Address, sdk.NewCoins(output.Coin)) // TODO handle errors properly here. All coin transfers should be atomic. InputOutputCoins may work
if err != nil {
panic(err)
}
}
// add inputs
for _, input := range coinInputs {
_, err = k.bankKeeper.AddCoins(ctx, input.Address, sdk.NewCoins(input.Coin)) // TODO errors
if err != nil {
panic(err)
}
}
// store updated auction
k.setAuction(ctx, auction)
return nil
}
// CloseAuction closes an auction and distributes funds to the seller and highest bidder.
// TODO because this is called by the end blocker, it has to be valid for the duration of the EndTime block. Should maybe move this to a begin blocker?
func (k Keeper) CloseAuction(ctx sdk.Context, auctionID ID) sdk.Error {
// get the auction from the store
auction, found := k.GetAuction(ctx, auctionID)
if !found {
return sdk.ErrInternal("auction doesn't exist")
}
// error if auction has not reached the end time
if ctx.BlockHeight() < int64(auction.GetEndTime()) { // auctions close at the end of the block with blockheight == EndTime
return sdk.ErrInternal(fmt.Sprintf("auction can't be closed as curent block height (%v) is under auction end time (%v)", ctx.BlockHeight(), auction.GetEndTime()))
}
// payout to the last bidder
coinInput := auction.GetPayout()
_, err := k.bankKeeper.AddCoins(ctx, coinInput.Address, sdk.NewCoins(coinInput.Coin))
if err != nil {
return err
}
// delete auction from store (and queue)
k.deleteAuction(ctx, auctionID)
return nil
}
// ---------- Store methods ----------
// Use these to add and remove auction from the store.
// getNextAuctionID gets the next available global AuctionID
func (k Keeper) getNextAuctionID(ctx sdk.Context) (ID, sdk.Error) { // TODO don't need error return here
// get next ID from store
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getNextAuctionIDKey())
if bz == nil {
// if not found, set the id at 0
bz = k.cdc.MustMarshalBinaryLengthPrefixed(ID(0))
store.Set(k.getNextAuctionIDKey(), bz)
// TODO Why does the gov module set the id in genesis? :
//return 0, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set")
}
var auctionID ID
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auctionID)
return auctionID, nil
}
// incrementNextAuctionID increments the global ID in the store by 1
func (k Keeper) incrementNextAuctionID(ctx sdk.Context) sdk.Error {
// get next ID from store
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getNextAuctionIDKey())
if bz == nil {
panic("initial auctionID never set in genesis")
//return 0, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set") // TODO is this needed? Why not just set it zero here?
}
var auctionID ID
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auctionID)
// increment the stored next ID
bz = k.cdc.MustMarshalBinaryLengthPrefixed(auctionID + 1)
store.Set(k.getNextAuctionIDKey(), bz)
return nil
}
// setAuction puts the auction into the database and adds it to the queue
// it overwrites any pre-existing auction with same ID
func (k Keeper) setAuction(ctx sdk.Context, auction Auction) {
// remove the auction from the queue if it is already in there
existingAuction, found := k.GetAuction(ctx, auction.GetID())
if found {
k.removeFromQueue(ctx, existingAuction.GetEndTime(), existingAuction.GetID())
}
// store auction
store := ctx.KVStore(k.storeKey)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(auction)
store.Set(k.getAuctionKey(auction.GetID()), bz)
// add to the queue
k.insertIntoQueue(ctx, auction.GetEndTime(), auction.GetID())
}
// getAuction gets an auction from the store by auctionID
func (k Keeper) GetAuction(ctx sdk.Context, auctionID ID) (Auction, bool) {
var auction Auction
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getAuctionKey(auctionID))
if bz == nil {
return auction, false // TODO what is the correct behavior when an auction is not found? gov module follows this pattern of returning a bool
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &auction)
return auction, true
}
// deleteAuction removes an auction from the store without any validation
func (k Keeper) deleteAuction(ctx sdk.Context, auctionID ID) {
// remove from queue
auction, found := k.GetAuction(ctx, auctionID)
if found {
k.removeFromQueue(ctx, auction.GetEndTime(), auctionID)
}
// delete auction
store := ctx.KVStore(k.storeKey)
store.Delete(k.getAuctionKey(auctionID))
}
// ---------- Queue and key methods ----------
// These are lower level function used by the store methods above.
func (k Keeper) getNextAuctionIDKey() []byte {
return []byte("nextAuctionID")
}
func (k Keeper) getAuctionKey(auctionID ID) []byte {
return []byte(fmt.Sprintf("auctions:%d", auctionID))
}
// Inserts a AuctionID into the queue at endTime
func (k Keeper) insertIntoQueue(ctx sdk.Context, endTime endTime, auctionID ID) {
// get the store
store := ctx.KVStore(k.storeKey)
// marshal thing to be inserted
bz := k.cdc.MustMarshalBinaryLengthPrefixed(auctionID)
// store it
store.Set(
getQueueElementKey(endTime, auctionID),
bz,
)
}
// removes an auctionID from the queue
func (k Keeper) removeFromQueue(ctx sdk.Context, endTime endTime, auctionID ID) {
store := ctx.KVStore(k.storeKey)
store.Delete(getQueueElementKey(endTime, auctionID))
}
// Returns an iterator for all the auctions in the queue that expire by endTime
func (k Keeper) getQueueIterator(ctx sdk.Context, endTime endTime) sdk.Iterator { // TODO rename to "getAuctionsByExpiry" ?
// get store
store := ctx.KVStore(k.storeKey)
// get an interator
return store.Iterator(
queueKeyPrefix, // start key
sdk.PrefixEndBytes(getQueueElementKeyPrefix(endTime)), // end key (apparently exclusive but tests suggested otherwise)
)
}
// GetAuctionIterator returns an iterator over all auctions in the store
func (k Keeper) GetAuctionIterator(ctx sdk.Context) sdk.Iterator {
store := ctx.KVStore(k.storeKey)
return sdk.KVStorePrefixIterator(store, nil)
}
var queueKeyPrefix = []byte("queue")
var keyDelimiter = []byte(":")
// Returns half a key for an auctionID in the queue, it missed the id off the end
func getQueueElementKeyPrefix(endTime endTime) []byte {
return bytes.Join([][]byte{
queueKeyPrefix,
sdk.Uint64ToBigEndian(uint64(endTime)), // TODO check this gives correct ordering
}, keyDelimiter)
}
// Returns the key for an auctionID in the queue
func getQueueElementKey(endTime endTime, auctionID ID) []byte {
return bytes.Join([][]byte{
queueKeyPrefix,
sdk.Uint64ToBigEndian(uint64(endTime)), // TODO check this gives correct ordering
sdk.Uint64ToBigEndian(uint64(auctionID)),
}, keyDelimiter)
}