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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

connect4_test.gno

23.86 Kb · 662 lines
  1package connect4_v4
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"crypto/bech32"
  7	"crypto/sha256"
  8	"strconv"
  9	"strings"
 10	"testing"
 11
 12	"gno.land/p/nt/avl/v0"
 13	"gno.land/p/nt/ownable/v0"
 14	"gno.land/p/nt/testutils/v0"
 15	"gno.land/p/nt/uassert/v0"
 16)
 17
 18const stake = int64(10_000_000)
 19
 20var (
 21	alice   = testutils.TestAddress("alice")
 22	bob     = testutils.TestAddress("bob")
 23	carol   = testutils.TestAddress("carol")
 24	admin   = address("g1cvr48r7l7lkmvp77cr6zg2zhu26jgfwr0y8pew")
 25	pkgAddr = chain.PackageAddress("gno.land/r/samcrew/connect4")
 26)
 27
 28func reset() {
 29	games, active, stats = avl.Tree{}, avl.Tree{}, avl.Tree{}
 30	nextID, fee, feesCollected = 0, 100_000, 0
 31	topWins, topGnot = nil, nil
 32	used, pendingOwner = avl.Tree{}, ""
 33	owner = ownable.NewWithAddress(admin)
 34	testSession = false
 35}
 36
 37// pay sets the ugnot sent with the next call and credits the realm with it,
 38// mirroring what the chain does. testing.SetRealm must be called in the same
 39// frame that makes the cross call, so tests call it inline rather than here.
 40func pay(amount int64) {
 41	if amount == 0 {
 42		testing.SetOriginSend(nil)
 43		return
 44	}
 45	coins := chain.Coins{{"ugnot", amount}}
 46	testing.SetOriginSend(coins)
 47	testing.IssueCoins(pkgAddr, coins)
 48}
 49
 50func balance(addr address) int64 {
 51	return banker.NewReadonlyBanker().GetCoin(addr, "ugnot")
 52}
 53
 54// phrase returns a passphrase unique to the next game id.
 55func phrase() string { return "four random words " + strconv.Itoa(nextID+1) }
 56
 57// commit returns the commitment for phrase().
 58func commit() string { return sha256Hex(phrase()) }
 59
 60// seed returns the acceptor's seed for game id; seedCommit its commitment.
 61func seed(id int) string       { return "random seed " + strconv.Itoa(id) }
 62func seedCommit(id int) string { return sha256Hex(seed(id)) }
 63
 64// accepted returns a game between alice (creator) and bob awaiting Reveal,
 65// and its passphrase.
 66func accepted(cur realm) (*Game, string) {
 67	p := phrase()
 68	testing.SetRealm(testing.NewUserRealm(alice))
 69	pay(stake)
 70	id := Offer(cross(cur), "", 10, sha256Hex(p), fee)
 71	testing.SetRealm(testing.NewUserRealm(bob))
 72	pay(stake)
 73	Accept(cross(cur), id, seedCommit(id))
 74	return mustGet(id), p
 75}
 76
 77// started returns a revealed Playing game between alice (creator) and bob.
 78func started(cur realm) *Game {
 79	g, p := accepted(cur)
 80	revealBoth(cur, g, p)
 81	return g
 82}
 83
 84// revealBoth has the creator reveal p and the acceptor its seed.
 85func revealBoth(cur realm, g *Game, p string) {
 86	testing.SetRealm(testing.NewUserRealm(g.Creator))
 87	pay(0)
 88	Reveal(cross(cur), g.ID, p)
 89	testing.SetRealm(testing.NewUserRealm(g.Acceptor))
 90	pay(0)
 91	RevealSeed(cross(cur), g.ID, seed(g.ID))
 92}
 93
 94// move plays column for whoever is on turn.
 95func move(cur realm, g *Game, column int) {
 96	testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
 97	pay(0)
 98	Play(cross(cur), g.ID, column, g.Moves)
 99}
100
101// expireClock advances block time past the 90s move clock (SkipHeights is 5s/block).
102func expireClock() { testing.SkipHeights(19) }
103
104func TestWinPaysPotMinusFee(cur realm, t *testing.T) {
105	reset()
106	g := started(cur)
107	first := g.player(g.Turn)
108	before := balance(first)
109	for _, c := range []int{1, 2, 1, 2, 1, 2, 1} { // first mover stacks column 1
110		move(cur, g, c)
111	}
112	uassert.Equal(t, "won", g.Status.String())
113	uassert.Equal(t, first, g.Winner)
114	uassert.Equal(t, 2*stake-100_000, balance(first)-before)
115	uassert.Equal(t, int64(100_000), feesCollected)
116	uassert.Equal(t, 1, statsOf(first).Wins)
117	uassert.Equal(t, stake-100_000, statsOf(first).GnotWon)
118	uassert.Equal(t, 1, statsOf(g.player(3-g.Turn)).Games)
119	uassert.Equal(t, 0, active.Size())
120}
121
122func TestOfferRejects(cur realm, t *testing.T) {
123	reset()
124	testing.SetRealm(testing.NewUserRealm(alice))
125	pay(minStake - 1)
126	uassert.AbortsContains(t, cur, "stake below minimum", func() { Offer(cross(cur), "", 10, commit(), fee) })
127	testing.SetRealm(testing.NewUserRealm(alice))
128	pay(stake)
129	uassert.AbortsContains(t, cur, "validFor", func() { Offer(cross(cur), "", 0, commit(), fee) })
130	testing.SetRealm(testing.NewUserRealm(alice))
131	pay(stake)
132	uassert.AbortsContains(t, cur, "validFor", func() { Offer(cross(cur), "", 61, commit(), fee) })
133	testing.SetRealm(testing.NewUserRealm(alice))
134	pay(stake)
135	uassert.AbortsContains(t, cur, "yourself", func() { Offer(cross(cur), alice, 10, commit(), fee) })
136	testing.SetRealm(testing.NewUserRealm(alice))
137	pay(0)
138	uassert.AbortsContains(t, cur, "send exactly one", func() { Offer(cross(cur), "", 10, commit(), fee) })
139	testing.SetRealm(testing.NewUserRealm(alice))
140	testing.SetOriginSend(chain.Coins{{"ugnot", stake}, {"foo", 1}})
141	uassert.AbortsContains(t, cur, "send exactly one", func() { Offer(cross(cur), "", 10, commit(), fee) })
142}
143
144func TestAcceptRejects(cur realm, t *testing.T) {
145	reset()
146	testing.SetRealm(testing.NewUserRealm(alice))
147	pay(stake)
148	id := Offer(cross(cur), bob, 1, commit(), fee)
149	testing.SetRealm(testing.NewUserRealm(alice))
150	pay(stake)
151	uassert.AbortsContains(t, cur, "own offer", func() { Accept(cross(cur), id, seedCommit(id)) })
152	testing.SetRealm(testing.NewUserRealm(carol))
153	pay(stake)
154	uassert.AbortsContains(t, cur, "reserved", func() { Accept(cross(cur), id, seedCommit(id)) })
155	testing.SetRealm(testing.NewUserRealm(bob))
156	pay(stake - 1)
157	uassert.AbortsContains(t, cur, "must send exactly", func() { Accept(cross(cur), id, seedCommit(id)) })
158	testing.SetRealm(testing.NewUserRealm(bob))
159	pay(stake + 1)
160	uassert.AbortsContains(t, cur, "must send exactly", func() { Accept(cross(cur), id, seedCommit(id)) })
161	testing.SkipHeights(13) // 65s > 1 minute
162	testing.SetRealm(testing.NewUserRealm(bob))
163	pay(stake)
164	uassert.AbortsContains(t, cur, "expired", func() { Accept(cross(cur), id, seedCommit(id)) })
165	testing.SetRealm(testing.NewUserRealm(bob))
166	pay(stake)
167	uassert.AbortsContains(t, cur, "not found", func() { Accept(cross(cur), 99, seedCommit(99)) })
168}
169
170func TestPlayRejects(cur realm, t *testing.T) {
171	reset()
172	g := started(cur)
173	testing.SetRealm(testing.NewUserRealm(g.player(3 - g.Turn)))
174	pay(0)
175	uassert.AbortsContains(t, cur, "not your turn", func() { Play(cross(cur), g.ID, 1, g.Moves) })
176	testing.SetRealm(testing.NewUserRealm(carol))
177	pay(0)
178	uassert.AbortsContains(t, cur, "not your turn", func() { Play(cross(cur), g.ID, 1, g.Moves) })
179	testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
180	pay(0)
181	uassert.AbortsContains(t, cur, "column must be", func() { Play(cross(cur), g.ID, 0, g.Moves) })
182	testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
183	pay(0)
184	uassert.AbortsContains(t, cur, "column must be", func() { Play(cross(cur), g.ID, 8, g.Moves) })
185	expireClock()
186	testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
187	pay(0)
188	uassert.AbortsContains(t, cur, "clock ran out", func() { Play(cross(cur), g.ID, 1, g.Moves) })
189}
190
191func TestFullColumnRejected(cur realm, t *testing.T) {
192	reset()
193	g := started(cur)
194	for i := 0; i < rows; i++ {
195		move(cur, g, 4) // alternating players never make 4 in a column
196	}
197	testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
198	pay(0)
199	uassert.AbortsContains(t, cur, "column is full", func() { Play(cross(cur), g.ID, 4, g.Moves) })
200}
201
202// The creator revealed, then the first mover never moved.
203func TestTimeoutBeforeFirstMoveIsVoid(cur realm, t *testing.T) {
204	reset()
205	g := started(cur)
206	testing.SetRealm(testing.NewUserRealm(carol))
207	pay(0)
208	uassert.AbortsContains(t, cur, "has not run out", func() { ClaimTimeout(cross(cur), g.ID) })
209	a, b := balance(alice), balance(bob)
210	expireClock()
211	testing.SetRealm(testing.NewUserRealm(carol))
212	pay(0) // anyone may settle
213	ClaimTimeout(cross(cur), g.ID)
214	uassert.Equal(t, "void", g.Status.String())
215	uassert.Equal(t, stake, balance(alice)-a)
216	uassert.Equal(t, stake, balance(bob)-b)
217	uassert.Equal(t, int64(0), feesCollected)
218	uassert.Equal(t, 0, stats.Size())
219}
220
221func TestTimeoutAfterMoveForfeits(cur realm, t *testing.T) {
222	reset()
223	g := started(cur)
224	mover := g.player(g.Turn)
225	loser := g.player(3 - g.Turn)
226	move(cur, g, 1)
227	expireClock()
228	before := balance(mover)
229	testing.SetRealm(testing.NewUserRealm(mover))
230	pay(0)
231	ClaimTimeout(cross(cur), g.ID)
232	uassert.Equal(t, "won", g.Status.String())
233	uassert.Equal(t, mover, g.Winner)
234	uassert.Equal(t, 2*stake-100_000, balance(mover)-before)
235	uassert.Equal(t, int64(100_000), feesCollected)
236	uassert.Equal(t, 1, statsOf(loser).Games)
237}
238
239func TestResign(cur realm, t *testing.T) {
240	reset()
241	g := started(cur)
242	testing.SetRealm(testing.NewUserRealm(carol))
243	pay(0)
244	uassert.AbortsContains(t, cur, "not a player", func() { Resign(cross(cur), g.ID) })
245	before := balance(bob)
246	testing.SetRealm(testing.NewUserRealm(alice))
247	pay(0)
248	Resign(cross(cur), g.ID)
249	uassert.Equal(t, "won", g.Status.String())
250	uassert.Equal(t, bob, g.Winner)
251	uassert.Equal(t, 2*stake-100_000, balance(bob)-before)
252	uassert.Equal(t, int64(100_000), feesCollected)
253	uassert.Equal(t, 1, statsOf(alice).Games)
254}
255
256func TestCancel(cur realm, t *testing.T) {
257	reset()
258	testing.SetRealm(testing.NewUserRealm(alice))
259	pay(stake)
260	id := Offer(cross(cur), "", 1, commit(), fee)
261	testing.SetRealm(testing.NewUserRealm(bob))
262	pay(0)
263	uassert.AbortsContains(t, cur, "only the creator", func() { Cancel(cross(cur), id) })
264	before := balance(alice)
265	testing.SetRealm(testing.NewUserRealm(alice))
266	pay(0)
267	Cancel(cross(cur), id) // creator, before expiry
268	uassert.Equal(t, "cancelled", mustGet(id).Status.String())
269	uassert.Equal(t, stake, balance(alice)-before)
270
271	// Once expired, anyone may cancel; the refund still goes to the creator.
272	testing.SetRealm(testing.NewUserRealm(alice))
273	pay(stake)
274	id = Offer(cross(cur), "", 1, commit(), fee)
275	testing.SkipHeights(13) // 65s > 1 minute
276	before = balance(alice)
277	testing.SetRealm(testing.NewUserRealm(carol))
278	pay(0)
279	Cancel(cross(cur), id)
280	uassert.Equal(t, "cancelled", mustGet(id).Status.String())
281	uassert.Equal(t, stake, balance(alice)-before)
282
283	g := started(cur)
284	testing.SetRealm(testing.NewUserRealm(alice))
285	pay(0)
286	uassert.AbortsContains(t, cur, "not open", func() { Cancel(cross(cur), g.ID) })
287}
288
289func TestDrawRefundsBoth(cur realm, t *testing.T) {
290	reset()
291	g := started(cur)
292	// Fill every cell but the top of column 7 with a pattern that has no
293	// four in a row; player 1 owns the last cell.
294	for c := 0; c < cols; c++ {
295		for r := 0; r < rows; r++ {
296			g.Board[c][r] = byte(1 + (c/2+r)%2)
297		}
298	}
299	g.Board[6][5] = 0
300	g.Moves, g.Turn = cols*rows-1, 1
301	a, b := balance(alice), balance(bob)
302	move(cur, g, 7)
303	uassert.Equal(t, "draw", g.Status.String())
304	uassert.Equal(t, stake, balance(alice)-a)
305	uassert.Equal(t, stake, balance(bob)-b)
306	uassert.Equal(t, 1, statsOf(alice).Draws)
307}
308
309func TestFeeSnapshotAndWithdraw(cur realm, t *testing.T) {
310	reset()
311	testing.SetRealm(testing.NewUserRealm(carol))
312	pay(0)
313	uassert.AbortsContains(t, cur, "not owner", func() { SetFee(cross(cur), 0) })
314	testing.SetRealm(testing.NewUserRealm(carol))
315	pay(0)
316	uassert.AbortsContains(t, cur, "not owner", func() { WithdrawFees(cross(cur), carol) })
317	g := started(cur) // offered at the default 0.1 GNOT fee
318	testing.SetRealm(testing.NewUserRealm(admin))
319	pay(0)
320	uassert.AbortsContains(t, cur, "fee must be between", func() { SetFee(cross(cur), -1) })
321	uassert.AbortsContains(t, cur, "fee must be between 0 and 0.5 GNOT", func() { SetFee(cross(cur), feeCap+1) })
322	SetFee(cross(cur), feeCap)
323	testing.SetRealm(testing.NewUserRealm(alice))
324	pay(0)
325	Resign(cross(cur), g.ID)
326	uassert.Equal(t, int64(100_000), feesCollected)
327
328	before := balance(carol)
329	testing.SetRealm(testing.NewUserRealm(admin))
330	pay(0)
331	WithdrawFees(cross(cur), carol)
332	uassert.Equal(t, int64(100_000), balance(carol)-before)
333	uassert.Equal(t, int64(0), feesCollected)
334
335	// A raised fee applies to new offers only, and stays below the minimum stake.
336	testing.SetRealm(testing.NewUserRealm(alice))
337	pay(minStake)
338	id := Offer(cross(cur), "", 10, commit(), fee)
339	uassert.Equal(t, int64(feeCap), mustGet(id).Fee)
340}
341
342func TestFinishedGameRejects(cur realm, t *testing.T) {
343	reset()
344	g := started(cur)
345	testing.SetRealm(testing.NewUserRealm(alice))
346	pay(0)
347	Resign(cross(cur), g.ID)
348	testing.SetRealm(testing.NewUserRealm(bob))
349	pay(0)
350	uassert.AbortsContains(t, cur, "not in progress", func() { Play(cross(cur), g.ID, 1, g.Moves) })
351	uassert.AbortsContains(t, cur, "not in progress", func() { Resign(cross(cur), g.ID) })
352	expireClock()
353	testing.SetRealm(testing.NewUserRealm(bob))
354	pay(0)
355	uassert.AbortsContains(t, cur, "not in progress", func() { ClaimTimeout(cross(cur), g.ID) })
356	testing.SetRealm(testing.NewUserRealm(carol))
357	pay(stake)
358	uassert.AbortsContains(t, cur, "not open", func() { Accept(cross(cur), g.ID, seedCommit(g.ID)) })
359	testing.SetRealm(testing.NewUserRealm(carol))
360	pay(stake)
361	uassert.AbortsContains(t, cur, "invalid opponent", func() { Offer(cross(cur), "notanaddress", 10, commit(), fee) })
362}
363
364func TestRevealPhase(cur realm, t *testing.T) {
365	reset()
366	g, p := accepted(cur)
367	uassert.Equal(t, byte(0), g.Turn)
368	for _, who := range []address{alice, bob} {
369		testing.SetRealm(testing.NewUserRealm(who))
370		pay(0)
371		uassert.AbortsContains(t, cur, "waiting for the creator", func() { Play(cross(cur), g.ID, 1, g.Moves) })
372	}
373	testing.SetRealm(testing.NewUserRealm(bob))
374	pay(0)
375	uassert.AbortsContains(t, cur, "only the creator", func() { Reveal(cross(cur), g.ID, p) })
376	testing.SetRealm(testing.NewUserRealm(alice))
377	pay(0)
378	uassert.AbortsContains(t, cur, "does not match", func() { Reveal(cross(cur), g.ID, p+"x") })
379	testing.SetRealm(testing.NewUserRealm(bob))
380	pay(0)
381	uassert.AbortsContains(t, cur, "not awaiting the acceptor's seed", func() { RevealSeed(cross(cur), g.ID, seed(g.ID)) })
382	testing.SetRealm(testing.NewUserRealm(alice))
383	pay(0)
384	Reveal(cross(cur), g.ID, p)
385	uassert.Equal(t, byte(0), g.Turn) // the draw waits for the seed
386	uassert.True(t, g.Revealed)
387	testing.SetRealm(testing.NewUserRealm(alice))
388	pay(0)
389	uassert.AbortsContains(t, cur, "not awaiting the creator's reveal", func() { Reveal(cross(cur), g.ID, p) })
390	uassert.AbortsContains(t, cur, "only the acceptor", func() { RevealSeed(cross(cur), g.ID, seed(g.ID)) })
391	testing.SetRealm(testing.NewUserRealm(bob))
392	pay(0)
393	uassert.AbortsContains(t, cur, "seed does not match", func() { RevealSeed(cross(cur), g.ID, "other") })
394	testing.SetRealm(testing.NewUserRealm(bob))
395	pay(0)
396	RevealSeed(cross(cur), g.ID, seed(g.ID))
397	uassert.True(t, g.Turn == 1 || g.Turn == 2)
398	testing.SetRealm(testing.NewUserRealm(bob))
399	pay(0)
400	uassert.AbortsContains(t, cur, "not awaiting the acceptor's seed", func() { RevealSeed(cross(cur), g.ID, seed(g.ID)) })
401	move(cur, g, 1) // the picked first mover can play
402	uassert.Equal(t, 1, g.Moves)
403}
404
405func TestLateRevealForfeits(cur realm, t *testing.T) {
406	reset()
407	g, p := accepted(cur)
408	expireClock()
409	testing.SetRealm(testing.NewUserRealm(alice))
410	pay(0)
411	uassert.AbortsContains(t, cur, "reveal clock ran out", func() { Reveal(cross(cur), g.ID, p) })
412	before := balance(bob)
413	testing.SetRealm(testing.NewUserRealm(carol))
414	pay(0)
415	ClaimTimeout(cross(cur), g.ID)
416	uassert.Equal(t, "won", g.Status.String())
417	uassert.Equal(t, bob, g.Winner)
418	uassert.Equal(t, 2*stake-100_000, balance(bob)-before)
419	uassert.Equal(t, int64(100_000), feesCollected)
420	uassert.Equal(t, 1, statsOf(bob).Wins)
421	uassert.Equal(t, 1, statsOf(alice).Games)
422}
423
424func TestLateSeedForfeits(cur realm, t *testing.T) {
425	reset()
426	g, p := accepted(cur)
427	testing.SetRealm(testing.NewUserRealm(alice))
428	pay(0)
429	Reveal(cross(cur), g.ID, p)
430	expireClock()
431	testing.SetRealm(testing.NewUserRealm(bob))
432	pay(0)
433	uassert.AbortsContains(t, cur, "reveal clock ran out", func() { RevealSeed(cross(cur), g.ID, seed(g.ID)) })
434	before := balance(alice)
435	testing.SetRealm(testing.NewUserRealm(carol))
436	pay(0)
437	ClaimTimeout(cross(cur), g.ID)
438	uassert.Equal(t, alice, g.Winner)
439	uassert.Equal(t, 2*stake-100_000, balance(alice)-before)
440}
441
442func TestCommitmentRejects(cur realm, t *testing.T) {
443	reset()
444	good := commit()
445	for _, bad := range []string{"", good[:63], good + "0", "G" + good[1:], "A" + good[1:]} {
446		testing.SetRealm(testing.NewUserRealm(alice))
447		pay(stake)
448		uassert.AbortsContains(t, cur, "64 lowercase hex", func() { Offer(cross(cur), "", 10, bad, fee) })
449	}
450	testing.SetRealm(testing.NewUserRealm(alice))
451	pay(stake)
452	Offer(cross(cur), "", 10, good, fee)
453	testing.SetRealm(testing.NewUserRealm(carol))
454	pay(stake)
455	Offer(cross(cur), "", 10, good, fee) // another creator may use it: nobody can block anyone's offer
456	testing.SetRealm(testing.NewUserRealm(alice))
457	pay(stake)
458	uassert.AbortsContains(t, cur, "commitment already used", func() { Offer(cross(cur), "", 10, good, fee) })
459
460	// An acceptor can't reuse a seed commitment either, even one used as a passphrase commitment.
461	testing.SetRealm(testing.NewUserRealm(bob))
462	pay(stake)
463	Accept(cross(cur), 1, seedCommit(1))
464	testing.SetRealm(testing.NewUserRealm(bob))
465	pay(stake)
466	uassert.AbortsContains(t, cur, "commitment already used", func() { Accept(cross(cur), 2, seedCommit(1)) })
467	testing.SetRealm(testing.NewUserRealm(bob))
468	pay(stake)
469	uassert.AbortsContains(t, cur, "seedCommitment must be 64 lowercase hex", func() { Accept(cross(cur), 2, "x") })
470}
471
472// bech32m gives the other checksum of the same address, which IsValid accepts.
473func bech32m(a address) address {
474	_, data, err := bech32.DecodeNoLimit(a.String())
475	if err != nil {
476		panic(err)
477	}
478	m, err := bech32.EncodeM("g", data)
479	if err != nil {
480		panic(err)
481	}
482	return address(m)
483}
484
485func TestOpponentMustBeCanonical(cur realm, t *testing.T) {
486	reset()
487	for _, other := range []address{
488		address(strings.ToUpper(bob.String())),
489		bech32m(bob),
490		address(strings.ToUpper(bech32m(bob).String())),
491	} {
492		uassert.True(t, other.IsValid())
493		testing.SetRealm(testing.NewUserRealm(alice))
494		pay(stake)
495		uassert.AbortsContains(t, cur, "lower-case", func() { Offer(cross(cur), other, 10, commit(), fee) })
496	}
497	testing.SetRealm(testing.NewUserRealm(alice))
498	pay(stake)
499	Offer(cross(cur), bob, 10, commit(), fee)
500}
501
502func TestOfferRefusesAFeeAboveItsMaximum(cur realm, t *testing.T) {
503	reset()
504	testing.SetRealm(testing.NewUserRealm(admin))
505	pay(0)
506	SetFee(cross(cur), 300_000) // raised while the creator's offer is in flight
507	testing.SetRealm(testing.NewUserRealm(alice))
508	pay(stake)
509	uassert.AbortsContains(t, cur, "the house fee is 0.3 GNOT, above your maximum of 0.1 GNOT", func() {
510		Offer(cross(cur), "", 10, commit(), 100_000)
511	})
512	testing.SetRealm(testing.NewUserRealm(alice))
513	pay(stake)
514	id := Offer(cross(cur), "", 10, commit(), 300_000)
515	uassert.Equal(t, int64(300_000), mustGet(id).Fee)
516	testing.SetRealm(testing.NewUserRealm(alice))
517	pay(stake)
518	id = Offer(cross(cur), "", 10, commit(), 1_000_000) // a higher maximum still pays only the current fee
519	uassert.Equal(t, int64(300_000), mustGet(id).Fee)
520}
521
522func TestOwnershipTransferTakesTwoSteps(cur realm, t *testing.T) {
523	reset()
524	testing.SetRealm(testing.NewUserRealm(carol))
525	pay(0)
526	uassert.AbortsContains(t, cur, "not owner", func() { TransferOwnership(cross(cur), carol) })
527	// A non-canonical spelling, which could never accept, is refused up front.
528	for _, other := range []address{address(strings.ToUpper(carol.String())), bech32m(carol)} {
529		testing.SetRealm(testing.NewUserRealm(admin))
530		pay(0)
531		uassert.AbortsContains(t, cur, "invalid address", func() { TransferOwnership(cross(cur), other) })
532	}
533	testing.SetRealm(testing.NewUserRealm(carol))
534	pay(0)
535	uassert.AbortsContains(t, cur, "no ownership transfer pending", func() { AcceptOwnership(cross(cur)) })
536	testing.SetRealm(testing.NewUserRealm(admin))
537	pay(0)
538	SetFee(cross(cur), 0) // still the owner
539	TransferOwnership(cross(cur), carol)
540	testing.SetRealm(testing.NewUserRealm(bob))
541	pay(0)
542	uassert.AbortsContains(t, cur, "no ownership transfer pending", func() { AcceptOwnership(cross(cur)) })
543	testing.SetRealm(testing.NewUserRealm(carol))
544	pay(0)
545	AcceptOwnership(cross(cur))
546	SetFee(cross(cur), 1)
547	testing.SetRealm(testing.NewUserRealm(admin))
548	pay(0)
549	uassert.AbortsContains(t, cur, "not owner", func() { SetFee(cross(cur), 0) })
550}
551
552func TestPaymentsRequireUserCall(cur realm, t *testing.T) {
553	reset()
554	testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/somerealm"))
555	pay(stake)
556	uassert.AbortsContains(t, cur, "only user-call", func() { Offer(cross(cur), "", 10, commit(), fee) })
557	testing.SetRealm(testing.NewUserRealm(alice))
558	pay(stake)
559	id := Offer(cross(cur), "", 10, commit(), fee)
560	testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/somerealm"))
561	pay(stake)
562	uassert.AbortsContains(t, cur, "only user-call", func() { Accept(cross(cur), id, seedCommit(id)) })
563}
564
565func TestNonPayableRejectsCoins(cur realm, t *testing.T) {
566	reset()
567	g, p := accepted(cur)
568	calls := map[string]func(){
569		"Reveal":            func() { Reveal(cross(cur), g.ID, p) },
570		"RevealSeed":        func() { RevealSeed(cross(cur), g.ID, seed(g.ID)) },
571		"AcceptOwnership":   func() { AcceptOwnership(cross(cur)) },
572		"Play":              func() { Play(cross(cur), g.ID, 1, g.Moves) },
573		"ClaimTimeout":      func() { ClaimTimeout(cross(cur), g.ID) },
574		"Resign":            func() { Resign(cross(cur), g.ID) },
575		"Cancel":            func() { Cancel(cross(cur), g.ID) },
576		"SetFee":            func() { SetFee(cross(cur), 0) },
577		"WithdrawFees":      func() { WithdrawFees(cross(cur), admin) },
578		"TransferOwnership": func() { TransferOwnership(cross(cur), carol) },
579	}
580	for name, call := range calls {
581		testing.SetRealm(testing.NewUserRealm(admin))
582		pay(1)
583		uassert.AbortsContains(t, cur, "does not accept coins", call, name)
584	}
585}
586
587func TestDrawUsesBothSecrets(cur realm, t *testing.T) {
588	// The draw is fixed by the passphrase and the seed, whenever the reveals land.
589	reset()
590	g, p := accepted(cur)
591	testing.SkipHeights(5) // 25s, inside the reveal clocks
592	revealBoth(cur, g, p)
593	d := sha256.Sum256([]byte(strings.Join([]string{p, seed(g.ID), strconv.Itoa(g.ID), alice.String(), bob.String()}, "|")))
594	uassert.Equal(t, 1+d[0]&1, g.Turn)
595	// The creator can't fix the outcome alone: the same passphrase gives both
596	// first movers depending on the acceptor's seed.
597	firsts := map[byte]bool{}
598	for i := 0; i < 32 && len(firsts) < 2; i++ {
599		firsts[firstMover(p, "seed "+strconv.Itoa(i), g)] = true
600	}
601	uassert.Equal(t, 2, len(firsts))
602}
603
604func TestPlayRefusesStaleMove(cur realm, t *testing.T) {
605	reset()
606	g := started(cur)
607	stale := g.Moves
608	move(cur, g, 1)
609	testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
610	pay(0)
611	uassert.AbortsContains(t, cur, "stale move: the game is at move 1", func() { Play(cross(cur), g.ID, 2, stale) })
612	uassert.AbortsContains(t, cur, "stale move", func() { Play(cross(cur), g.ID, 2, g.Moves+1) })
613	Play(cross(cur), g.ID, 2, g.Moves)
614	uassert.Equal(t, 2, g.Moves)
615}
616
617func TestSessionKeysCannotResignStakeOrOwn(cur realm, t *testing.T) {
618	reset()
619	g := started(cur)
620	testSession = true
621	testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
622	pay(0)
623	uassert.AbortsContains(t, cur, "Resign needs the account's own key", func() { Resign(cross(cur), g.ID) })
624	testing.SetRealm(testing.NewUserRealm(alice))
625	pay(stake)
626	uassert.AbortsContains(t, cur, "Offer needs the account's own key", func() { Offer(cross(cur), "", 10, commit(), fee) })
627	testing.SetRealm(testing.NewUserRealm(admin))
628	pay(0)
629	uassert.AbortsContains(t, cur, "SetFee needs", func() { SetFee(cross(cur), 0) })
630	uassert.AbortsContains(t, cur, "WithdrawFees needs", func() { WithdrawFees(cross(cur), admin) })
631	uassert.AbortsContains(t, cur, "TransferOwnership needs", func() { TransferOwnership(cross(cur), carol) })
632	// Moves stay open to sessions: that is what Quick play is for.
633	move(cur, g, 1)
634	uassert.Equal(t, 1, g.Moves)
635}
636
637func TestSessionKeysCannotAccept(cur realm, t *testing.T) {
638	reset()
639	testing.SetRealm(testing.NewUserRealm(alice))
640	pay(stake)
641	id := Offer(cross(cur), "", 10, commit(), fee)
642	testSession = true
643	testing.SetRealm(testing.NewUserRealm(bob))
644	pay(stake)
645	uassert.AbortsContains(t, cur, "Accept needs the account's own key", func() { Accept(cross(cur), id, seedCommit(id)) })
646}
647
648func TestBump(t *testing.T) {
649	var b []leader
650	for i := 0; i < 12; i++ {
651		b = bump(b, testutils.TestAddress(strconv.Itoa(i)), int64(i))
652	}
653	uassert.Equal(t, topN, len(b))
654	uassert.Equal(t, int64(11), b[0].Score)
655	uassert.Equal(t, int64(2), b[9].Score)
656	b = bump(b, testutils.TestAddress("5"), 20) // already listed: moves up, not duplicated
657	uassert.Equal(t, topN, len(b))
658	uassert.Equal(t, testutils.TestAddress("5"), b[0].Addr)
659	b = bump(b, testutils.TestAddress("late"), 11) // ties keep the earlier holder first
660	uassert.Equal(t, testutils.TestAddress("11"), b[1].Addr)
661	uassert.Equal(t, testutils.TestAddress("late"), b[2].Addr)
662}