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.

joinprice_test.gno

4.50 Kb · 119 lines
  1package crews
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"testing"
  7
  8	"chain"
  9
 10	"gno.land/p/nym-moultest123/x/social/crew/v0"
 11	"gno.land/p/nt/uassert/v0"
 12	"gno.land/p/nt/urequire/v0"
 13)
 14
 15// The price a crew page shows a joiner, and the floor Join's envelope guard
 16// enforces, must both be exactly the least amount that mints a whole share.
 17//
 18// Join mints amount * totalShares / treasury, rounded down, so that amount is
 19// treasury / totalShares rounded UP. Rounded down instead, it is one ugnot
 20// short whenever the treasury is not an exact multiple of the share count,
 21// which is the ordinary state after any Fund: the page quotes a price, the
 22// guard accepts it, and the join reverts on "buys no whole share".
 23//
 24// The table is over that rule, not over one input: awkward remainders where
 25// floor and ceiling differ, exact multiples where they must not, and a crew
 26// everybody left, where the opening price applies.
 27func TestTheShownPriceBuysAShareAndOneLessIsRefusedByTheGuard(cur realm, t *testing.T) {
 28	cases := []struct {
 29		name   string
 30		create int64 // what the founder pays in, at the opening price
 31		fund   int64 // then funded with this, which mints nothing
 32		leave  bool  // then the founder ragequits, leaving an empty crew
 33		want   int64 // the least amount that mints one share
 34	}{
 35		{"treasury 3001 over 2 shares", 2000, 1001, false, 1501},
 36		{"treasury 3001 over 3 shares", 3000, 1, false, 1001},
 37		{"ten GNOT and seven ugnot over 10000 shares", tenGNOT, 7, false, 1001},
 38		{"ten GNOT and 999 ugnot over 10000 shares", tenGNOT, 999, false, 1001},
 39		{"exact multiple at the opening price", tenGNOT, 0, false, 1000},
 40		{"exact multiple after doubling", tenGNOT, tenGNOT, false, 2000},
 41		{"a single share", 1000, 0, false, 1000},
 42		{"everybody left", 1000, 0, true, crew.InitialPricePerShare},
 43	}
 44	for _, tc := range cases {
 45		reset()
 46		id := create(cur, alice, tc.create, "crew")
 47		if tc.fund > 0 {
 48			fund(cur, carol, tc.fund, id)
 49		}
 50		if tc.leave {
 51			testing.SetRealm(testing.NewUserRealm(alice))
 52			Ragequit(cross(cur), id)
 53		}
 54
 55		// The oracle, independent of the code under test: the least a with
 56		// a * shares >= treasury, or the opening price when nobody holds one.
 57		_, _, _, shares, treasury := Get(id)
 58		oracle := crew.InitialPricePerShare
 59		if shares > 0 && treasury > 0 {
 60			oracle = treasury / shares
 61			if treasury%shares != 0 {
 62				oracle++
 63			}
 64		}
 65		urequire.Equal(t, tc.want, oracle, tc.name+": the table agrees with the rule")
 66
 67		page := Render("crew/" + strconv.FormatInt(id, 10))
 68		uassert.Equal(t, tc.want, shownPrice(t, page),
 69			tc.name+": the page quotes what buys a share: "+page)
 70
 71		// One ugnot under it is refused by the envelope guard, which names the
 72		// real price, rather than by Join after the guard let it through.
 73		testing.IssueCoins(realmAddr, chain.Coins{{denom, tc.want - 1}})
 74		testing.SetOriginSend(chain.Coins{{denom, tc.want - 1}})
 75		testing.SetRealm(testing.NewUserRealm(bob))
 76		uassert.AbortsContains(t, cur, "costs at least "+strconv.FormatInt(tc.want, 10)+denom,
 77			func() { Join(cross(cur), id) }, tc.name+": the guard refuses one ugnot short")
 78
 79		// Exactly the quoted price buys exactly one share.
 80		got := join(cur, bob, tc.want, id)
 81		uassert.Equal(t, int64(1), got, tc.name+": the quoted price mints one share")
 82	}
 83}
 84
 85// shownPrice reads the price cell of a crew page's summary table, the first
 86// row after the header that starts with "| members", in ugnot. It is keyed on
 87// the row's position, not the column's title, so it reads the same cell
 88// whatever that column is called.
 89func shownPrice(t *testing.T, page string) int64 {
 90	lines := strings.Split(page, "\n")
 91	for i, l := range lines {
 92		if !strings.HasPrefix(l, "| members") {
 93			continue
 94		}
 95		urequire.True(t, i+2 < len(lines), "the summary table has a row: "+page)
 96		cells := strings.Split(lines[i+2], "|")
 97		urequire.Equal(t, 6, len(cells), "four cells: "+lines[i+2])
 98		return parseGNOT(t, strings.TrimSpace(cells[4]))
 99	}
100	t.Fatalf("no summary table on the crew page: %s", page)
101	return 0
102}
103
104// parseGNOT turns num.GNOTf's "1.0015 GNOT" back into ugnot.
105func parseGNOT(t *testing.T, s string) int64 {
106	urequire.True(t, strings.HasSuffix(s, " GNOT"), "a GNOT amount: "+s)
107	s = strings.TrimSuffix(s, " GNOT")
108	whole, frac := s, ""
109	if i := strings.Index(s, "."); i >= 0 {
110		whole, frac = s[:i], s[i+1:]
111	}
112	urequire.True(t, len(frac) <= 6, "at most six decimals: "+s)
113	frac += strings.Repeat("0", 6-len(frac))
114	w, err := strconv.ParseInt(whole, 10, 64)
115	urequire.NoError(t, err, s)
116	f, err := strconv.ParseInt(frac, 10, 64)
117	urequire.NoError(t, err, s)
118	return w*1000000 + f
119}