1package feeledger23import "testing"45const (6 maxInt64 = int64(1<<63 - 1)7 minInt64 = int64(-1 << 63)8)910// mustPanicWith asserts fn panics with an error whose message is want.11// Non-crossing panics in the same package are recoverable.12func mustPanicWith(t *testing.T, want string, fn func()) {13 t.Helper()14 defer func() {15 r := recover()16 if r == nil {17 t.Fatalf("expected panic %q, got none", want)18 }19 err, ok := r.(error)20 if !ok || err.Error() != want {21 t.Fatalf("panic = %v, want %q", r, want)22 }23 }()24 fn()25}2627func TestNewBounds(t *testing.T) {28 for _, bad := range []int64{-1, BpsDenominator + 1, minInt64, maxInt64} {29 if _, err := New(bad); err != ErrInvalidBps {30 t.Fatalf("New(%d) err = %v, want ErrInvalidBps", bad, err)31 }32 }33 for _, ok := range []int64{0, 1, BpsDenominator} {34 if _, err := New(ok); err != nil {35 t.Fatalf("New(%d) err = %v", ok, err)36 }37 }38 mustPanicWith(t, ErrInvalidBps.Error(), func() { MustNew(-1) })39}4041func TestFeeForRounding(t *testing.T) {42 // floor(max * 0.9999) computed the same overflow-free way feeFor does.43 wantMaxAt9999 := maxInt64/10000*9999 + maxInt64%10000*9999/1000044 cases := []struct {45 amount, bps, want int6446 }{47 {999, 100, 9}, // 9.99 floors to 948 {10000, 1, 1}, // exact49 {9999, 1, 0}, // 0.9999 floors to 0 — no minimum fee50 {1, 9999, 0}, // 0.9999 floors to 051 {10, 9999, 9}, // 9.999 floors to 952 {100, 10000, 100}, // full-fee bps: fee == amount53 {0, 500, 0}, // zero amount previews zero fee54 {1000, 0, 0}, // zero-fee config55 {1000, 250, 25}, // exact 2.5%56 {999, 250, 24}, // 24.975 floors to 2457 {maxInt64, 10000, maxInt64}, // no intermediate overflow58 {maxInt64, 9999, wantMaxAt9999},59 }60 for _, c := range cases {61 got, err := FeeFor(c.amount, c.bps)62 if err != nil {63 t.Fatalf("FeeFor(%d,%d) err = %v", c.amount, c.bps, err)64 }65 if got != c.want {66 t.Fatalf("FeeFor(%d,%d) = %d, want %d", c.amount, c.bps, got, c.want)67 }68 if got < 0 || got > c.amount {69 t.Fatalf("FeeFor(%d,%d) = %d out of [0,amount]", c.amount, c.bps, got)70 }71 }72 if _, err := FeeFor(-1, 100); err != ErrInvalidAmount {73 t.Fatalf("FeeFor(-1,100) err = %v, want ErrInvalidAmount", err)74 }75 if _, err := FeeFor(100, -1); err != ErrInvalidBps {76 t.Fatalf("FeeFor(100,-1) err = %v, want ErrInvalidBps", err)77 }78 if _, err := FeeFor(100, BpsDenominator+1); err != ErrInvalidBps {79 t.Fatalf("FeeFor over-bps err = %v, want ErrInvalidBps", err)80 }81}8283func TestDepositValidation(t *testing.T) {84 l := MustNew(500)85 type bad struct {86 account string87 amount, bps int6488 want error89 }90 for _, c := range []bad{91 {"", 100, 0, ErrEmptyAccount},92 {"alice", 0, 0, ErrInvalidAmount},93 {"alice", -7, 0, ErrInvalidAmount},94 {"alice", minInt64, 0, ErrInvalidAmount},95 {"alice", 100, -1, ErrInvalidBps},96 {"alice", 100, 501, ErrInvalidBps}, // above the ledger cap97 } {98 if _, _, err := l.Deposit(c.account, c.amount, c.bps); err != c.want {99 t.Fatalf("Deposit(%q,%d,%d) err = %v, want %v", c.account, c.amount, c.bps, err, c.want)100 }101 }102 if l.UsersTotal() != 0 || l.FeesAccrued() != 0 || l.Accounts() != 0 {103 t.Fatal("failed deposits mutated state")104 }105}106107func TestDepositAndAccumulate(t *testing.T) {108 l := MustNew(1000)109 credited, fee, err := l.Deposit("alice", 1000, 250)110 if err != nil || credited != 975 || fee != 25 {111 t.Fatalf("Deposit = (%d,%d,%v), want (975,25,nil)", credited, fee, err)112 }113 credited, fee, err = l.Deposit("alice", 999, 250)114 if err != nil || credited != 975 || fee != 24 { // fee floors, credit rounds up115 t.Fatalf("Deposit#2 = (%d,%d,%v), want (975,24,nil)", credited, fee, err)116 }117 if got := l.BalanceOf("alice"); got != 1950 {118 t.Fatalf("BalanceOf = %d, want 1950", got)119 }120 if l.UsersTotal() != 1950 || l.FeesAccrued() != 49 || l.Liabilities() != 1999 {121 t.Fatalf("totals = %d/%d/%d", l.UsersTotal(), l.FeesAccrued(), l.Liabilities())122 }123 // credited + fee always reassembles both deposits exactly.124 if l.Liabilities() != 1000+999 {125 t.Fatal("conservation broken: credited+fee != deposited")126 }127}128129func TestFullFeeBpsCreditsZero(t *testing.T) {130 l := MustNew(BpsDenominator)131 credited, fee, err := l.Deposit("alice", 777, BpsDenominator)132 if err != nil || credited != 0 || fee != 777 {133 t.Fatalf("Deposit = (%d,%d,%v), want (0,777,nil)", credited, fee, err)134 }135 if l.Accounts() != 0 || l.BalanceOf("alice") != 0 {136 t.Fatal("zero-credit deposit must not create an account entry")137 }138 if l.FeesAccrued() != 777 || l.UsersTotal() != 0 {139 t.Fatal("fee pot mismatch on full-fee deposit")140 }141}142143func TestDepositOverflow(t *testing.T) {144 l := MustNew(BpsDenominator)145 if _, _, err := l.Deposit("alice", maxInt64, 0); err != nil {146 t.Fatalf("seed deposit err = %v", err)147 }148 // Account balance would overflow.149 if _, _, err := l.Deposit("alice", 1, 0); err != ErrOverflow {150 t.Fatalf("balance-overflow err = %v, want ErrOverflow", err)151 }152 // Joint liabilities (users + fees) would overflow even though the153 // fee pot alone would not.154 if _, _, err := l.Deposit("bob", 100, BpsDenominator); err != ErrOverflow {155 t.Fatalf("liabilities-overflow err = %v, want ErrOverflow", err)156 }157 if l.BalanceOf("bob") != 0 || l.FeesAccrued() != 0 || l.UsersTotal() != maxInt64 {158 t.Fatal("failed overflow deposits mutated state")159 }160}161162func TestWithdraw(t *testing.T) {163 l := MustNew(0)164 l.MustDeposit("alice", 1000, 0)165166 if err := l.Withdraw("", 10); err != ErrEmptyAccount {167 t.Fatalf("empty account err = %v", err)168 }169 if err := l.Withdraw("alice", 0); err != ErrInvalidAmount {170 t.Fatalf("zero amount err = %v", err)171 }172 if err := l.Withdraw("alice", -3); err != ErrInvalidAmount {173 t.Fatalf("negative amount err = %v", err)174 }175 if err := l.Withdraw("alice", 1001); err != ErrInsufficient {176 t.Fatalf("over-balance err = %v", err)177 }178 if err := l.Withdraw("ghost", 1); err != ErrInsufficient {179 t.Fatalf("absent account err = %v", err)180 }181 if l.BalanceOf("alice") != 1000 || l.UsersTotal() != 1000 {182 t.Fatal("failed withdrawals mutated state")183 }184185 if err := l.Withdraw("alice", 400); err != nil {186 t.Fatalf("partial withdraw err = %v", err)187 }188 if l.BalanceOf("alice") != 600 || l.UsersTotal() != 600 || l.Accounts() != 1 {189 t.Fatal("partial withdraw wrong state")190 }191 if err := l.Withdraw("alice", 600); err != nil {192 t.Fatalf("exact withdraw err = %v", err)193 }194 if l.BalanceOf("alice") != 0 || l.UsersTotal() != 0 || l.Accounts() != 0 {195 t.Fatal("exact withdraw must remove the account entry")196 }197 if err := l.Withdraw("alice", 1); err != ErrInsufficient {198 t.Fatalf("repeat withdraw err = %v, want ErrInsufficient", err)199 }200 mustPanicWith(t, ErrInsufficient.Error(), func() { l.MustWithdraw("alice", 1) })201}202203func TestWithdrawAll(t *testing.T) {204 l := MustNew(0)205 if got, err := l.WithdrawAll("nobody"); err != nil || got != 0 {206 t.Fatalf("WithdrawAll(nobody) = (%d,%v), want (0,nil)", got, err)207 }208 if _, err := l.WithdrawAll(""); err != ErrEmptyAccount {209 t.Fatalf("WithdrawAll(\"\") err = %v", err)210 }211 l.MustDeposit("alice", 555, 0)212 if got, err := l.WithdrawAll("alice"); err != nil || got != 555 {213 t.Fatalf("WithdrawAll = (%d,%v), want (555,nil)", got, err)214 }215 if got, err := l.WithdrawAll("alice"); err != nil || got != 0 {216 t.Fatalf("repeat WithdrawAll = (%d,%v), want (0,nil)", got, err)217 }218 if l.UsersTotal() != 0 || l.Accounts() != 0 {219 t.Fatal("WithdrawAll left state behind")220 }221}222223func TestWithdrawFees(t *testing.T) {224 l := MustNew(1000)225 l.MustDeposit("alice", 10000, 1000) // fee 1000226 l.MustDeposit("bob", 10000, 1000) // fee 1000227 if got := l.WithdrawFees(); got != 2000 {228 t.Fatalf("WithdrawFees = %d, want 2000", got)229 }230 if got := l.WithdrawFees(); got != 0 {231 t.Fatalf("repeat WithdrawFees = %d, want 0", got)232 }233 if l.FeesAccrued() != 0 || l.UsersTotal() != 18000 {234 t.Fatal("WithdrawFees corrupted totals")235 }236}237238func TestConservationAcrossSequence(t *testing.T) {239 l := MustNew(1000)240 deposited := int64(0)241 withdrawn := int64(0)242 feesOut := int64(0)243244 step := func() {245 t.Helper()246 // Internal consistency: usersTotal == sum of balances.247 sum := int64(0)248 l.Iterate(func(_ string, b int64) bool {249 if b <= 0 {250 t.Fatal("zero/negative balance stored")251 }252 sum += b253 return false254 })255 if sum != l.UsersTotal() {256 t.Fatalf("usersTotal %d != Σ balances %d", l.UsersTotal(), sum)257 }258 // Conservation: everything in == everything owed + everything out.259 if deposited != l.Liabilities()+withdrawn+feesOut {260 t.Fatalf("conservation: in=%d owed=%d out=%d+%d",261 deposited, l.Liabilities(), withdrawn, feesOut)262 }263 }264265 deposit := func(who string, amt, bps int64) {266 l.MustDeposit(who, amt, bps)267 deposited += amt268 step()269 }270 claim := func(who string, amt int64) {271 l.MustWithdraw(who, amt)272 withdrawn += amt273 step()274 }275276 deposit("alice", 1000, 0)277 deposit("bob", 999, 250)278 deposit("alice", 12345, 1000)279 claim("alice", 1000)280 deposit("carol", 1, 1000) // fee floors to 0281 feesOut += l.WithdrawFees()282 step()283 claim("bob", 974)284 all, _ := l.WithdrawAll("alice")285 withdrawn += all286 step()287 deposit("dave", 7, 999)288 feesOut += l.WithdrawFees()289 step()290}291292func TestCheckedAddBranches(t *testing.T) {293 cases := []struct {294 a, b, want int64295 ok bool296 }{297 {5, 3, 8, true},298 {5, -3, 2, true}, // negative-b branch, no overflow299 {-4, -6, -10, true},300 {maxInt64, 1, 0, false}, // positive overflow301 {minInt64, -1, 0, false}, // negative overflow302 {maxInt64, -maxInt64, 0, true},303 }304 for _, c := range cases {305 got, ok := checkedAdd(c.a, c.b)306 if ok != c.ok || (ok && got != c.want) {307 t.Fatalf("checkedAdd(%d,%d) = (%d,%v), want (%d,%v)", c.a, c.b, got, ok, c.want, c.ok)308 }309 }310}311312func TestIterateSortedAndEarlyStop(t *testing.T) {313 l := MustNew(0)314 l.MustDeposit("carol", 3, 0)315 l.MustDeposit("alice", 1, 0)316 l.MustDeposit("bob", 2, 0)317 got := ""318 l.Iterate(func(a string, _ int64) bool {319 got += a + ","320 return false321 })322 if got != "alice,bob,carol," {323 t.Fatalf("Iterate order = %q", got)324 }325 n := 0326 l.Iterate(func(string, int64) bool { n++; return n == 2 })327 if n != 2 {328 t.Fatalf("early stop visited %d", n)329 }330}331Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.