package utxo import ( "errors" "sync" "testing" "git.smesh.lol/tinywallet/pkg/store" ) // memPersistence keeps the store's durable side in memory. type memPersistence struct { mu sync.Mutex items map[string][]byte } func (m *memPersistence) Load() (map[string][]byte, error) { m.mu.Lock() defer m.mu.Unlock() out := make(map[string][]byte, len(m.items)) for key, value := range m.items { out[key] = append([]byte{}, value...) } return out, nil } func (m *memPersistence) Write(batch []store.Op) error { m.mu.Lock() defer m.mu.Unlock() for _, op := range batch { if op.Delete { delete(m.items, op.Key) continue } m.items[op.Key] = append([]byte{}, op.Value...) } return nil } // open builds a table over a persisted cache. func open(t *testing.T, persist store.Persistence) (*Table, *store.Cache) { t.Helper() cache, err := store.OpenManual(persist) if err != nil { t.Fatal(err) } table, err := Open(cache) if err != nil { t.Fatal(err) } return table, cache } // coin builds a coin with a recognisable id. func coin(id byte, vout uint32, value uint64, height uint32) Coin { var txid [32]byte txid[0] = id return Coin{TxID: txid, Vout: vout, Value: value, Height: height} } // TestAddAndList: a coin survives, with its value and height. func TestAddAndList(t *testing.T) { table, _ := open(t, &memPersistence{items: map[string][]byte{}}) if err := table.Add(coin(1, 0, 50000, 10)); err != nil { t.Fatal(err) } if err := table.Add(coin(2, 3, 70000, 12)); err != nil { t.Fatal(err) } coins, err := table.Unspent() if err != nil { t.Fatal(err) } if len(coins) != 2 { t.Fatalf("%d coins", len(coins)) } // Newest height first. if coins[0].Height != 12 || coins[1].Height != 10 { t.Fatalf("the order is %d then %d", coins[0].Height, coins[1].Height) } if coins[0].Value != 70000 || coins[0].Vout != 3 { t.Fatalf("the coin is %+v", coins[0]) } } // TestSpend: spending removes exactly one coin, and spending it twice is an error. func TestSpend(t *testing.T) { table, _ := open(t, &memPersistence{items: map[string][]byte{}}) first := coin(1, 0, 50000, 10) second := coin(2, 0, 50000, 11) for _, entry := range []Coin{first, second} { if err := table.Add(entry); err != nil { t.Fatal(err) } } if err := table.Spend(first.TxID, first.Vout); err != nil { t.Fatal(err) } coins, err := table.Unspent() if err != nil { t.Fatal(err) } if len(coins) != 1 || coins[0].TxID != second.TxID { t.Fatalf("the remaining coins are %+v", coins) } if err := table.Spend(first.TxID, first.Vout); !errors.Is(err, ErrSpent) { t.Fatalf("spending a spent coin: %v", err) } } // TestReopenSeesTheCoins: the table is the store, and the store is what survives a reload. func TestReopenSeesTheCoins(t *testing.T) { persist := &memPersistence{items: map[string][]byte{}} table, cache := open(t, persist) if err := table.Add(coin(7, 1, 12345, 99)); err != nil { t.Fatal(err) } if err := cache.Close(); err != nil { t.Fatal(err) } reopened, _ := open(t, persist) coins, err := reopened.Unspent() if err != nil { t.Fatal(err) } if len(coins) != 1 || coins[0].Value != 12345 || coins[0].Height != 99 { t.Fatalf("the reopened table holds %+v", coins) } } // TestAddingTwiceReplaces: a reorg can move a confirmation, and the newer answer stands. func TestAddingTwiceReplaces(t *testing.T) { table, _ := open(t, &memPersistence{items: map[string][]byte{}}) if err := table.Add(coin(1, 0, 50000, 10)); err != nil { t.Fatal(err) } if err := table.Add(coin(1, 0, 50000, 20)); err != nil { t.Fatal(err) } coins, err := table.Unspent() if err != nil { t.Fatal(err) } if len(coins) != 1 || coins[0].Height != 20 { t.Fatalf("the table holds %+v", coins) } } // TestOpenNeedsAStore: nothing to persist into is not a table. func TestOpenNeedsAStore(t *testing.T) { if _, err := Open(nil); err == nil { t.Fatal("a table opened over no store") } }