bubblerumble3_test.gno
20.84 Kb · 585 lines
1package bubblerumble3
2
3import (
4 "chain"
5 "chain/banker"
6 "chain/runtime"
7 "strings"
8 "testing"
9)
10
11const realmPath = "gno.land/r/g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr/bubblerumble3"
12
13var (
14 alice = address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")
15 bob = address("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0")
16 carol = address("g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c")
17 dave = address("g1leu8d2vsplhehcfkjg50mwgdpxdkt8tztu95wr") // also the admin
18)
19
20func aborts(t *testing.T, want string, f func()) {
21 t.Helper()
22 r := revive(f)
23 if r == nil {
24 t.Fatalf("expected an abort containing %q, got none", want)
25 }
26 msg := ""
27 switch e := r.(type) {
28 case error:
29 msg = e.Error()
30 case string:
31 msg = e
32 }
33 if !strings.Contains(msg, want) {
34 t.Fatalf("abort %q does not contain %q", msg, want)
35 }
36}
37
38func held(who address) int64 {
39 return banker.NewReadonlyBanker().GetCoins(who).AmountOf("ugnot")
40}
41
42// fund attaches ugnot to the next call the way a transaction would. The caller
43// is set with testing.SetRealm in the test body: it binds to the calling frame.
44func fund(ugnot int64) {
45 if ugnot > 0 {
46 testing.IssueCoins(chain.PackageAddress(realmPath), chain.Coins{{"ugnot", ugnot}})
47 testing.SetOriginSend(chain.Coins{{"ugnot", ugnot}})
48 } else {
49 testing.SetOriginSend(nil)
50 }
51}
52
53// heldAtReset is what the realm held when the scenario began: coins from
54// earlier scenarios stay in the test realm after reset() forgets their pools.
55var heldAtReset int64
56
57func reset() {
58 pools = nil
59 players = map[address]*Player{}
60 teamPaid = map[string]int64{}
61 teamWon = map[string]int64{}
62 teamSize = map[string]int64{}
63 shotsOn = true
64 heldAtReset = held(chain.PackageAddress(realmPath))
65}
66
67// The conservation check: every ugnot the realm took in during the scenario is
68// either in an open pot or credited to somebody.
69func realmHeld() int64 { return held(chain.PackageAddress(realmPath)) - heldAtReset }
70
71func owedAll() int64 {
72 var sum int64
73 for _, p := range pools {
74 if !p.Paid {
75 sum += p.Pot
76 }
77 }
78 for _, pl := range players {
79 sum += pl.Owed
80 }
81 return sum
82}
83
84func conserved(t *testing.T) {
85 t.Helper()
86 if realmHeld() != owedAll() {
87 t.Fatalf("realm holds %d, pots and credits account for %d", realmHeld(), owedAll())
88 }
89}
90
91func owed(a address) int64 { return player(a).Owed }
92
93func TestSplitSelfRebidFlagLockAndWithdraw(cur realm, t *testing.T) {
94 reset()
95 fund(0)
96 testing.SetRealm(testing.NewUserRealm(dave))
97 id := CreatePool(cross(cur), "split", 3600, 1_000_000, 0)
98 p := pool(id)
99 if p.Life != 10*3600 {
100 t.Fatalf("default life is ten windows: %d", p.Life)
101 }
102
103 // first bid: dividends and the inviter's cut fall back into the pot, the creator is paid 5% at once
104 d0 := held(dave)
105 fund(1_000_000)
106 testing.SetRealm(testing.NewUserRealm(alice))
107 Bid(cross(cur), id, "", "kr", 0)
108 if p.Pot != 950_000 || held(dave)-d0 != 50_000 || p.DivPaid != 0 || p.FirstBidAt.IsZero() {
109 t.Fatalf("first bid: pot %d creator +%d div %d", p.Pot, held(dave)-d0, p.DivPaid)
110 }
111
112 // bob, invited by alice, bids 2 GNOT: alice is credited all the dividends and paid 10% as inviter at once
113 a0 := held(alice)
114 fund(2_000_000)
115 testing.SetRealm(testing.NewUserRealm(bob))
116 Bid(cross(cur), id, alice.String(), "us", 0)
117 if owed(alice) != 600_000 || held(alice)-a0 != 200_000 || p.Pot != 950_000+2_000_000-600_000-200_000-100_000 {
118 t.Fatalf("second bid: alice owed %d, +%d in hand, pot %d", owed(alice), held(alice)-a0, p.Pot)
119 }
120 if player(bob).Referrer != alice || player(alice).Recruits != 1 || p.find(alice).Earned != 600_000 {
121 t.Fatalf("inviter: %+v", *player(bob))
122 }
123
124 // bob rebids on himself, switching his flag to KR: allowed; his dividends go
125 // to alice, not himself; his flag in THIS pool stays US (locked at his first bid)
126 b1 := held(bob)
127 fund(2_000_000)
128 testing.SetRealm(testing.NewUserRealm(bob))
129 Bid(cross(cur), id, "", "kr", 0)
130 if p.LastBidder != bob || p.Bids != 3 || owed(alice) != 1_200_000 || owed(bob) != 0 || b1-held(bob) != 0 {
131 t.Fatalf("self rebid: last %s bids %d alice owed %d bob owed %d", p.LastBidder, p.Bids, owed(alice), owed(bob))
132 }
133 if player(bob).Flag != "KR" || p.find(bob).Flag != "US" || p.History[2].Flag != "US" {
134 t.Fatalf("flag lock: player %s member %s history %s", player(bob).Flag, p.find(bob).Flag, p.History[2].Flag)
135 }
136 conserved(t)
137
138 // alice withdraws what she is owed; a second withdraw has nothing
139 a1 := held(alice)
140 fund(0)
141 testing.SetRealm(testing.NewUserRealm(alice))
142 if got := Withdraw(cross(cur)); got != 1_200_000 || held(alice)-a1 != 1_200_000 || owed(alice) != 0 || player(alice).Withdrawn != 1_200_000 {
143 t.Fatalf("withdraw: %d, +%d, owed %d", got, held(alice)-a1, owed(alice))
144 }
145 aborts(t, "nothing to withdraw", func() { Withdraw(cross(cur)) })
146 conserved(t)
147
148 // in a new pool, bob's locked flag is his current one
149 fund(0)
150 testing.SetRealm(testing.NewUserRealm(dave))
151 id2 := CreatePool(cross(cur), "next", 3600, 1_000_000, 0)
152 fund(1_000_000)
153 testing.SetRealm(testing.NewUserRealm(bob))
154 Bid(cross(cur), id2, "", "", 0)
155 if pool(id2).find(bob).Flag != "KR" {
156 t.Fatalf("new pool flag %s", pool(id2).find(bob).Flag)
157 }
158}
159
160func TestBoostFloor(t *testing.T) {
161 if c := ClockFor(3600, 100, 1000); c != 360 {
162 t.Fatalf("ten times the price on an hour: %d", c)
163 }
164 if c := ClockFor(600, 100, 1000); c != MinClock {
165 t.Fatalf("ten times the price on ten minutes floors at five: %d", c)
166 }
167 if c := ClockFor(60, 100, 100); c != 60 {
168 t.Fatalf("a plain bid on a one-minute window keeps the window: %d", c)
169 }
170 if c := ClockFor(60, 100, 1000); c != 60 {
171 t.Fatalf("no boost can shorten a window under the floor: %d", c)
172 }
173 if mulDiv(80_000_000_000, 3_000_000_000, 7_000_000_000) != 34_285_714_285 {
174 t.Fatal("mulDiv on numbers whose product passes int64")
175 }
176}
177
178func TestLastDaySlidesToProRata(cur realm, t *testing.T) {
179 reset()
180 fund(0)
181 testing.SetRealm(testing.NewUserRealm(dave))
182 // a one-hour window and a 90-minute life: the life ends before the second clock would
183 id := CreatePool(cross(cur), "slide", 3600, 1_000_000, 5400)
184 p := pool(id)
185 fund(1_000_000)
186 testing.SetRealm(testing.NewUserRealm(alice))
187 Bid(cross(cur), id, "", "FR", 0)
188 if p.LiveBps() != 10000 {
189 t.Fatalf("fresh pool live %d", p.LiveBps())
190 }
191 // 50 minutes pass (5s a block), bob bids exactly the price (more would shorten
192 // his clock): 2400 of 5400 seconds of life are left
193 testing.SkipHeights(600)
194 fund(1_414_214)
195 testing.SetRealm(testing.NewUserRealm(bob))
196 Bid(cross(cur), id, "", "US", 0)
197 if p.LiveBps() != 10000*2400/5400 {
198 t.Fatalf("live %d", p.LiveBps())
199 }
200 if !p.Deadline().Equal(p.LastDay()) {
201 t.Fatalf("deadline %v should be the last day %v, not the clock", p.Deadline(), p.LastDay())
202 }
203 // the last day comes with time still on bob's clock: the pool is over anyway
204 testing.SkipHeights(600)
205 if !p.Over() || p.LiveBps() != 0 {
206 t.Fatalf("over %v live %d", p.Over(), p.LiveBps())
207 }
208 pot := p.Pot
209 a0, b0 := owed(alice), owed(bob) // alice already holds dividends from bob's bid
210 fund(0)
211 testing.SetRealm(testing.NewUserRealm(carol))
212 Claim(cross(cur), id)
213 // bob (US) won alone under his flag, so the team share goes to the other bidder,
214 // alice; nothing is live, so the rest is split by paid-in
215 cut := pot * TeamShare / 100
216 rest := pot - cut
217 total := p.find(alice).Paid + p.find(bob).Paid
218 wantA := cut + rest*p.find(alice).Paid/total
219 if owed(alice)-a0 != wantA || owed(bob)-b0 != pot-wantA {
220 t.Fatalf("alice +%d (want %d) bob +%d (want %d)", owed(alice)-a0, wantA, owed(bob)-b0, pot-wantA)
221 }
222 if p.TeamPaid != cut || p.SharedPaid != rest*p.find(alice).Paid/total || p.WinnerPaid != pot-wantA {
223 t.Fatalf("bookkeeping: team %d shared %d winner %d", p.TeamPaid, p.SharedPaid, p.WinnerPaid)
224 }
225 conserved(t)
226
227 // a pool that closes early with a single bidder credits them everything
228 reset()
229 fund(0)
230 testing.SetRealm(testing.NewUserRealm(dave))
231 id = CreatePool(cross(cur), "quick", 60, 1_000_000, 7200)
232 p = pool(id)
233 fund(1_000_000)
234 testing.SetRealm(testing.NewUserRealm(alice))
235 Bid(cross(cur), id, "", "FR", 0)
236 testing.SkipHeights(13) // 65s: the clock ran out, the life barely started
237 fund(0)
238 testing.SetRealm(testing.NewUserRealm(bob))
239 Claim(cross(cur), id)
240 if owed(alice) != p.Pot || p.SharedPaid != 0 || p.TeamPaid != 0 {
241 t.Fatalf("solo: alice owed %d of %d, shared %d team %d", owed(alice), p.Pot, p.SharedPaid, p.TeamPaid)
242 }
243 conserved(t)
244}
245
246func TestFlagWar(cur realm, t *testing.T) {
247 reset()
248 fund(0)
249 testing.SetRealm(testing.NewUserRealm(dave))
250 id := CreatePool(cross(cur), "team", 3600, 1_000_000, 0)
251 p := pool(id)
252 fund(20_000_000)
253 testing.SetRealm(testing.NewUserRealm(dave))
254 Bid(cross(cur), id, "", "FR", 0)
255 fund(1_414_214)
256 testing.SetRealm(testing.NewUserRealm(alice))
257 Bid(cross(cur), id, "", "FR", 0)
258 fund(1_732_051)
259 testing.SetRealm(testing.NewUserRealm(carol))
260 Bid(cross(cur), id, "", "US", 0)
261 fund(2_000_000)
262 testing.SetRealm(testing.NewUserRealm(alice))
263 Bid(cross(cur), id, "", "", 0) // alice on top, FR
264 testing.SkipHeights(721)
265 pot := p.Pot
266 live := p.LiveBps()
267 if live == 0 || live == 10000 {
268 t.Fatalf("live %d should be partway", live)
269 }
270 a0, d0, c0 := owed(alice), owed(dave), owed(carol)
271 fund(0)
272 testing.SetRealm(testing.NewUserRealm(bob))
273 Claim(cross(cur), id)
274 // the team share goes to dave, the only OTHER FR bidder; the winner takes none of it
275 cut := pot * TeamShare / 100
276 rest := pot - cut
277 shared := rest - rest*live/10000
278 total := p.find(alice).Paid + p.find(dave).Paid + p.find(carol).Paid
279 daveShare, carolShare := shared*p.find(dave).Paid/total, shared*p.find(carol).Paid/total
280 if owed(dave)-d0 != cut+daveShare {
281 t.Fatalf("dave: +%d want team %d + share %d", owed(dave)-d0, cut, daveShare)
282 }
283 if owed(carol)-c0 != carolShare {
284 t.Fatalf("carol (US) gets only her pro-rata share: +%d want %d", owed(carol)-c0, carolShare)
285 }
286 if owed(alice)-a0 != pot-cut-daveShare-carolShare {
287 t.Fatalf("alice: +%d want %d", owed(alice)-a0, pot-cut-daveShare-carolShare)
288 }
289 if p.WinnerFlag != "FR" || teamWon["FR"] != pot || p.TeamPaid != cut || p.SharedPaid != daveShare+carolShare {
290 t.Fatalf("bookkeeping: %+v", *p)
291 }
292 conserved(t)
293 aborts(t, "already paid out", func() { Claim(cross(cur), id) })
294
295 // a private flag buys nothing: a winner nobody else flew with hands the team
296 // share to all the other bidders, pro-rata
297 reset()
298 fund(0)
299 testing.SetRealm(testing.NewUserRealm(dave))
300 id = CreatePool(cross(cur), "lone", 60, 1_000_000, 0)
301 p = pool(id)
302 fund(1_000_000)
303 testing.SetRealm(testing.NewUserRealm(alice))
304 Bid(cross(cur), id, "", "FR", 0)
305 fund(1_414_214)
306 testing.SetRealm(testing.NewUserRealm(carol))
307 Bid(cross(cur), id, "", "US", 0)
308 fund(1_732_051)
309 testing.SetRealm(testing.NewUserRealm(bob))
310 Bid(cross(cur), id, "", "AQ", 0) // bob, alone under AQ, on top
311 testing.SkipHeights(13)
312 pot = p.Pot
313 a0, c0, b0 := owed(alice), owed(carol), owed(bob)
314 fund(0)
315 testing.SetRealm(testing.NewUserRealm(dave))
316 Claim(cross(cur), id)
317 cut = pot * TeamShare / 100
318 others := p.find(alice).Paid + p.find(carol).Paid
319 // the life (ten minutes) had 65 seconds on it: a sliver of the rest is shared too
320 rest = pot - cut
321 shared = rest - rest*p.LiveBps()/10000
322 total = others + p.find(bob).Paid
323 wantA := cut*p.find(alice).Paid/others + shared*p.find(alice).Paid/total
324 wantC := cut*p.find(carol).Paid/others + shared*p.find(carol).Paid/total
325 if owed(alice)-a0 != wantA || owed(carol)-c0 != wantC || owed(bob)-b0 != pot-wantA-wantC {
326 t.Fatalf("lone flag: alice +%d (want %d) carol +%d (want %d) bob +%d", owed(alice)-a0, wantA, owed(carol)-c0, wantC, owed(bob)-b0)
327 }
328 conserved(t)
329}
330
331func TestShots(cur realm, t *testing.T) {
332 reset()
333 fund(0)
334 testing.SetRealm(testing.NewUserRealm(dave))
335 id := CreatePool(cross(cur), "shots", 3600, 1_000_000, 0)
336 p := pool(id)
337 fund(5_000_000)
338 testing.SetRealm(testing.NewUserRealm(dave))
339 Bid(cross(cur), id, "", "", 0)
340
341 // the shot must leave the price covered, and must buy some chance
342 fund(1_000_000)
343 testing.SetRealm(testing.NewUserRealm(alice))
344 aborts(t, "for the bid", func() { Bid(cross(cur), id, "", "", 500_000) })
345 fund(1_414_214 + 1)
346 testing.SetRealm(testing.NewUserRealm(alice))
347 aborts(t, "too small for any chance", func() { Bid(cross(cur), id, "", "", 1) })
348 heldAtReset += 1_000_000 + 1_414_215 // coins attached to the two aborted calls never reach a real realm
349
350 // a 0.1 GNOT shot at a ~6 GNOT pot: 0.8 × 0.1/6.7 ≈ 1.2%, capped at 1%; the shot goes into the pot whole
351 pot0 := p.Pot
352 fund(1_414_214 + 100_000)
353 testing.SetRealm(testing.NewUserRealm(alice))
354 Bid(cross(cur), id, "", "", 100_000)
355 if len(p.Shots) != 1 || p.Shots[0].Bps != ShotMaxBps || p.Shots[0].Resolved {
356 t.Fatalf("shot: %+v", *p.Shots[0])
357 }
358 // dividends (30%) credited to dave, the inviter's 10% back into the pot, creator 5% out, the shot in whole
359 if toPot := int64(1_414_214 - 424_264 - 141_421 - 70_710 + 141_421); p.Pot != pot0+toPot+100_000 || owed(dave) != 424_264 {
360 t.Fatalf("pot %d, dave owed %d", p.Pot, owed(dave))
361 }
362 sh := p.Shots[0]
363 if sh.Prize != p.Pot || sh.Prize > ShotCap || p.find(alice).Paid != 1_414_214+100_000 {
364 t.Fatalf("prize %d pot %d paid %d", sh.Prize, p.Pot, p.find(alice).Paid)
365 }
366 // the odds of a small shot: 0.02 GNOT at 5 GNOT is 0.8 × 0.02/5 = 0.32%
367 if bps := ShotBps(20_000, 5_000_000); bps != 32 {
368 t.Fatalf("bps %d", bps)
369 }
370 // not drawn before the delay
371 Draw(cross(cur), id)
372 if sh.Resolved {
373 t.Fatal("drawn too early")
374 }
375 testing.SkipHeights(ShotDelay)
376 h := runtime.ChainHeight()
377 fund(0)
378 testing.SetRealm(testing.NewUserRealm(bob))
379 want := shotHits(seedFor(id, sh, h), sh.Bps) // the same inputs the realm hashes
380 Draw(cross(cur), id)
381 if !sh.Resolved || sh.DrawH != h || sh.Won != want {
382 t.Fatalf("drawn: %+v want hit=%v", *sh, want)
383 }
384 if want {
385 if !p.Popped || p.LastBidder != alice || !p.Over() || p.Misses != 0 {
386 t.Fatalf("a hit pops: %+v", *p)
387 }
388 } else if p.Popped || p.LastBidder != alice || p.Misses != 1 {
389 t.Fatalf("a miss is counted and changes nothing else: %+v", *p)
390 }
391 Draw(cross(cur), id) // a second draw is a no-op
392 if p.Misses > 1 {
393 t.Fatal("drawn twice")
394 }
395 conserved(t)
396
397 // the draw itself: over every seed in a cycle, the chance is exactly bps in 10000
398 hits := 0
399 for s := uint64(0); s < 10000; s++ {
400 if shotHits(s, 32) {
401 hits++
402 }
403 }
404 if hits != 32 {
405 t.Fatalf("32 bps should hit 32 of 10000 seeds, got %d", hits)
406 }
407
408 // a shot nobody draws in its window is void: counted as a miss, never a hit
409 reset()
410 fund(0)
411 testing.SetRealm(testing.NewUserRealm(dave))
412 id = CreatePool(cross(cur), "late", 3600, 1_000_000, 0)
413 p = pool(id)
414 fund(5_000_000)
415 testing.SetRealm(testing.NewUserRealm(dave))
416 Bid(cross(cur), id, "", "", 0)
417 fund(1_414_214 + 1_000_000)
418 testing.SetRealm(testing.NewUserRealm(alice))
419 Bid(cross(cur), id, "", "", 1_000_000) // 1% anyway
420 testing.SkipHeights(ShotDelay + ShotWindow + 1)
421 Draw(cross(cur), id)
422 if !p.Shots[0].Resolved || p.Shots[0].Won || p.Misses != 1 || p.Popped {
423 t.Fatalf("void: %+v misses %d", *p.Shots[0], p.Misses)
424 }
425 // the last block of the window still draws
426 fund(1_732_051 + 1_000_000)
427 testing.SetRealm(testing.NewUserRealm(alice))
428 Bid(cross(cur), id, "", "", 1_000_000)
429 testing.SkipHeights(ShotDelay + ShotWindow)
430 Draw(cross(cur), id)
431 if !p.Shots[1].Resolved || p.Shots[1].DrawH != runtime.ChainHeight() {
432 t.Fatalf("window end: %+v", *p.Shots[1])
433 }
434 conserved(t)
435
436 // the switch: only the admin, and a shot is refused while it is off
437 fund(0)
438 testing.SetRealm(testing.NewUserRealm(alice))
439 aborts(t, "only the deployer", func() { SetShots(cross(cur), false) })
440 fund(0)
441 testing.SetRealm(testing.NewUserRealm(dave))
442 SetShots(cross(cur), false)
443 fund(2_000_000 + 1_000_000)
444 testing.SetRealm(testing.NewUserRealm(alice))
445 aborts(t, "shots are off", func() { Bid(cross(cur), id, "", "", 1_000_000) })
446 if !strings.Contains(Render("json"), "\"shotsOn\":false") {
447 t.Error("json should say shots are off")
448 }
449}
450
451func TestSliceOnABigPot(cur realm, t *testing.T) {
452 reset()
453 fund(0)
454 testing.SetRealm(testing.NewUserRealm(dave))
455 id := CreatePool(cross(cur), "big", 3600, 1_000_000, 0)
456 p := pool(id)
457 fund(50 * ShotCap) // 500 GNOT
458 testing.SetRealm(testing.NewUserRealm(dave))
459 Bid(cross(cur), id, "", "", 0)
460 fund(1_414_214 + 1_000_000) // a 1 GNOT shot: 0.8 × 1/10 = 8%, capped at 1% of a 10 GNOT slice
461 testing.SetRealm(testing.NewUserRealm(alice))
462 Bid(cross(cur), id, "", "", 1_000_000)
463 sh := p.Shots[0]
464 if sh.Prize != ShotCap || sh.Bps != ShotMaxBps {
465 t.Fatalf("shot at a big pot: %+v", *sh)
466 }
467 testing.SkipHeights(ShotDelay)
468 h := runtime.ChainHeight()
469 fund(0)
470 testing.SetRealm(testing.NewUserRealm(bob))
471 pot0, a0 := p.Pot, owed(alice)
472 hit := shotHits(seedFor(id, sh, h), sh.Bps)
473 Draw(cross(cur), id)
474 if hit {
475 if p.Popped || p.Pot != pot0-ShotCap || owed(alice)-a0 != ShotCap || p.ShotPaid != ShotCap {
476 t.Fatalf("slice: %+v alice +%d", *p, owed(alice)-a0)
477 }
478 } else if p.Pot != pot0 || owed(alice) != a0 || p.Misses != 1 {
479 t.Fatalf("a miss at a big pot changes nothing but the tally: misses %d", p.Misses)
480 }
481 conserved(t)
482}
483
484func TestBidAfterPopRefunds(cur realm, t *testing.T) {
485 reset()
486 fund(0)
487 testing.SetRealm(testing.NewUserRealm(dave))
488 id := CreatePool(cross(cur), "pop", 3600, 1_000_000, 0)
489 p := pool(id)
490 fund(1_000_000)
491 testing.SetRealm(testing.NewUserRealm(dave))
492 Bid(cross(cur), id, "", "", 0)
493 // a shot that cannot miss is due: the next bid draws it, finds the pool popped, and refunds
494 p.Shots = append(p.Shots, &Shot{N: 1, Bidder: alice, Paid: 1, Prize: p.Pot, Bps: 10000, CommitH: runtime.ChainHeight() - ShotDelay})
495 b0 := held(bob)
496 fund(2_000_000)
497 testing.SetRealm(testing.NewUserRealm(bob))
498 Bid(cross(cur), id, "", "", 0)
499 if !p.Popped || p.LastBidder != alice || p.Bids != 1 || held(bob)-b0 != 2_000_000 { // refunded in full
500 t.Fatalf("after pop: %+v bob %d→%d", *p, b0, held(bob))
501 }
502 conserved(t)
503 // and the pop credits alice as the winner; dave, the only other bidder, is the team
504 fund(0)
505 testing.SetRealm(testing.NewUserRealm(carol))
506 Claim(cross(cur), id)
507 cut := p.Pot * TeamShare / 100
508 if owed(alice) != p.Pot-cut || p.TeamPaid != cut || owed(dave) != cut {
509 t.Fatalf("popped payout: alice owed %d of %d, team %d, dave owed %d", owed(alice), p.Pot, p.TeamPaid, owed(dave))
510 }
511 conserved(t)
512
513 // a closed, unpopped pool refuses a bid outright
514 reset()
515 fund(0)
516 testing.SetRealm(testing.NewUserRealm(dave))
517 id = CreatePool(cross(cur), "closed", 60, 1_000_000, 0)
518 fund(1_000_000)
519 testing.SetRealm(testing.NewUserRealm(dave))
520 Bid(cross(cur), id, "", "", 0)
521 testing.SkipHeights(13)
522 fund(2_000_000)
523 testing.SetRealm(testing.NewUserRealm(bob))
524 aborts(t, "pool is closed", func() { Bid(cross(cur), id, "", "", 0) })
525}
526
527func TestRosterCapAndCreateBounds(cur realm, t *testing.T) {
528 reset()
529 fund(0)
530 testing.SetRealm(testing.NewUserRealm(dave))
531 id := CreatePool(cross(cur), "cap", 3600, 1_000, 0)
532 p := pool(id)
533 p.Roster = nil
534 for i := 0; i < RosterMax; i++ {
535 p.Roster = append(p.Roster, &Member{Addr: address("g1x" + itoa(int64(i))), Paid: 1})
536 }
537 fund(1_000)
538 testing.SetRealm(testing.NewUserRealm(alice))
539 Bid(cross(cur), id, "", "", 0)
540 if len(p.Roster) != RosterMax || p.Roster[len(p.Roster)-1].Addr != alice || p.Roster[0].Addr != address("g1x1") {
541 t.Fatalf("roster cap: len %d first %s last %s", len(p.Roster), p.Roster[0].Addr, p.Roster[len(p.Roster)-1].Addr)
542 }
543 fund(0)
544 testing.SetRealm(testing.NewUserRealm(dave))
545 aborts(t, "life must be at least the window", func() { CreatePool(cross(cur), "x", 7200, 1_000_000, 3600) })
546 aborts(t, "life must be between", func() { CreatePool(cross(cur), "x", 3600, 1_000_000, 31*24*3600) })
547 id = CreatePool(cross(cur), "x", 20*24*3600, 1_000_000, 0)
548 if pool(id).Life != MaxLife {
549 t.Fatalf("ten windows past the maximum life is the maximum: %d", pool(id).Life)
550 }
551 id = CreatePool(cross(cur), "y", 10, 1_000_000, 0)
552 if pool(id).Life != MinLife {
553 t.Fatalf("ten windows under the minimum life is the minimum: %d", pool(id).Life)
554 }
555}
556
557func TestRender(cur realm, t *testing.T) {
558 reset()
559 fund(0)
560 testing.SetRealm(testing.NewUserRealm(dave))
561 id := CreatePool(cross(cur), "r", 3600, 1_000_000, 0)
562 fund(1_000_000)
563 testing.SetRealm(testing.NewUserRealm(alice))
564 Bid(cross(cur), id, "", "FR", 0)
565 fund(1_414_214 + 200_000)
566 testing.SetRealm(testing.NewUserRealm(bob))
567 Bid(cross(cur), id, alice.String(), "US", 200_000)
568 all := Render("json")
569 for _, want := range []string{"\"split\":{\"pot\":55,\"div\":30,\"ref\":10,\"creator\":5,\"team\":35,\"roster\":100,\"shotCap\":10000000,\"shotMaxBps\":100,\"shotFair\":8000,\"shotDelay\":3,\"shotWindow\":2,\"minClock\":300}", "\"shotsOn\":true", "\"bidders\":2", "\"flag\":\"US\"", "\"liveBps\":", "\"lastDay\":", "\"shots\":1,\"misses\":0,\"pending\":1", "\"height\":"} {
570 if !strings.Contains(all, want) {
571 t.Errorf("json missing %s in %s", want, all)
572 }
573 }
574 one := Render("json/1")
575 if !strings.Contains(one, "\"shot\":200000") || !strings.Contains(one, "\"shotlog\":[{\"n\":1") || !strings.Contains(one, "\"hit\":false") {
576 t.Errorf("detail: %s", one)
577 }
578 me := Render("me/" + alice.String())
579 if !strings.Contains(me, "\"owed\":424264") || !strings.Contains(me, "\"withdrawn\":0") {
580 t.Errorf("me: %s", me)
581 }
582 if !strings.Contains(Render(""), "last day") || !strings.Contains(Render("1"), "a 0.2 GNOT shot") {
583 t.Errorf("markdown: %s", Render("1"))
584 }
585}