Skip to content

Commit 96210e9

Browse files
authored
Add balance checkpoint store (#46)
* Add balance checkpoint store * Add comments to balance store
1 parent 9b66248 commit 96210e9

File tree

9 files changed

+527
-0
lines changed

9 files changed

+527
-0
lines changed

pkg/code/data/balance/checkpoint.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package balance
2+
3+
import (
4+
"errors"
5+
"time"
6+
)
7+
8+
// Note: Only supports external balances
9+
type Record struct {
10+
Id uint64
11+
12+
TokenAccount string
13+
Quarks uint64
14+
SlotCheckpoint uint64
15+
16+
LastUpdatedAt time.Time
17+
}
18+
19+
func (r *Record) Validate() error {
20+
if len(r.TokenAccount) == 0 {
21+
return errors.New("token account is required")
22+
}
23+
24+
return nil
25+
}
26+
27+
func (r *Record) Clone() Record {
28+
return Record{
29+
Id: r.Id,
30+
31+
TokenAccount: r.TokenAccount,
32+
Quarks: r.Quarks,
33+
SlotCheckpoint: r.SlotCheckpoint,
34+
35+
LastUpdatedAt: r.LastUpdatedAt,
36+
}
37+
}
38+
39+
func (r *Record) CopyTo(dst *Record) {
40+
dst.Id = r.Id
41+
42+
dst.TokenAccount = r.TokenAccount
43+
dst.Quarks = r.Quarks
44+
dst.SlotCheckpoint = r.SlotCheckpoint
45+
46+
dst.LastUpdatedAt = r.LastUpdatedAt
47+
}

pkg/code/data/balance/memory/store.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package memory
2+
3+
import (
4+
"context"
5+
"sync"
6+
"time"
7+
8+
"github.com/code-payments/code-server/pkg/code/data/balance"
9+
)
10+
11+
type store struct {
12+
mu sync.Mutex
13+
records []*balance.Record
14+
last uint64
15+
}
16+
17+
// New returns a new in memory balance.Store
18+
func New() balance.Store {
19+
return &store{}
20+
}
21+
22+
// SaveCheckpoint implements balance.Store.SaveCheckpoint
23+
func (s *store) SaveCheckpoint(_ context.Context, data *balance.Record) error {
24+
if err := data.Validate(); err != nil {
25+
return err
26+
}
27+
28+
s.mu.Lock()
29+
defer s.mu.Unlock()
30+
31+
s.last++
32+
if item := s.find(data); item != nil {
33+
if data.SlotCheckpoint <= item.SlotCheckpoint {
34+
return balance.ErrStaleCheckpoint
35+
}
36+
37+
item.SlotCheckpoint = data.SlotCheckpoint
38+
item.Quarks = data.Quarks
39+
item.LastUpdatedAt = time.Now()
40+
item.CopyTo(data)
41+
} else {
42+
if data.Id == 0 {
43+
data.Id = s.last
44+
}
45+
data.LastUpdatedAt = time.Now()
46+
c := data.Clone()
47+
s.records = append(s.records, &c)
48+
}
49+
50+
return nil
51+
}
52+
53+
// GetCheckpoint implements balance.Store.GetCheckpoint
54+
func (s *store) GetCheckpoint(_ context.Context, account string) (*balance.Record, error) {
55+
s.mu.Lock()
56+
defer s.mu.Unlock()
57+
58+
if item := s.findByTokenAccount(account); item != nil {
59+
cloned := item.Clone()
60+
return &cloned, nil
61+
}
62+
return nil, balance.ErrCheckpointNotFound
63+
}
64+
65+
func (s *store) find(data *balance.Record) *balance.Record {
66+
for _, item := range s.records {
67+
if item.Id == data.Id {
68+
return item
69+
}
70+
if data.TokenAccount == item.TokenAccount {
71+
return item
72+
}
73+
}
74+
return nil
75+
}
76+
77+
func (s *store) findByTokenAccount(account string) *balance.Record {
78+
for _, item := range s.records {
79+
if account == item.TokenAccount {
80+
return item
81+
}
82+
}
83+
return nil
84+
}
85+
86+
func (s *store) reset() {
87+
s.mu.Lock()
88+
defer s.mu.Unlock()
89+
90+
s.records = nil
91+
s.last = 0
92+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package memory
2+
3+
import (
4+
"testing"
5+
6+
"github.com/code-payments/code-server/pkg/code/data/balance/tests"
7+
)
8+
9+
func TestBalanceMemoryStore(t *testing.T) {
10+
testStore := New()
11+
teardown := func() {
12+
testStore.(*store).reset()
13+
}
14+
tests.RunTests(t, testStore, teardown)
15+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package postgres
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"time"
7+
8+
"github.com/jmoiron/sqlx"
9+
10+
"github.com/code-payments/code-server/pkg/code/data/balance"
11+
pgutil "github.com/code-payments/code-server/pkg/database/postgres"
12+
)
13+
14+
const (
15+
tableName = "codewallet__core_balancecheckpoint"
16+
)
17+
18+
type model struct {
19+
Id sql.NullInt64 `db:"id"`
20+
21+
TokenAccount string `db:"token_account"`
22+
Quarks uint64 `db:"quarks"`
23+
SlotCheckpoint uint64 `db:"slot_checkpoint"`
24+
25+
LastUpdatedAt time.Time `db:"last_updated_at"`
26+
}
27+
28+
func toModel(obj *balance.Record) (*model, error) {
29+
if err := obj.Validate(); err != nil {
30+
return nil, err
31+
}
32+
33+
return &model{
34+
TokenAccount: obj.TokenAccount,
35+
Quarks: obj.Quarks,
36+
SlotCheckpoint: obj.SlotCheckpoint,
37+
LastUpdatedAt: obj.LastUpdatedAt,
38+
}, nil
39+
}
40+
41+
func fromModel(obj *model) *balance.Record {
42+
return &balance.Record{
43+
Id: uint64(obj.Id.Int64),
44+
TokenAccount: obj.TokenAccount,
45+
Quarks: obj.Quarks,
46+
SlotCheckpoint: obj.SlotCheckpoint,
47+
LastUpdatedAt: obj.LastUpdatedAt,
48+
}
49+
}
50+
51+
func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error {
52+
return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error {
53+
query := `INSERT INTO ` + tableName + `
54+
(token_account, quarks, slot_checkpoint, last_updated_at)
55+
VALUES ($1, $2, $3, $4)
56+
57+
ON CONFLICT (token_account)
58+
DO UPDATE
59+
SET quarks = $2, slot_checkpoint = $3, last_updated_at = $4
60+
WHERE ` + tableName + `.token_account = $1 AND ` + tableName + `.slot_checkpoint < $3
61+
62+
RETURNING
63+
id, token_account, quarks, slot_checkpoint, last_updated_at`
64+
65+
m.LastUpdatedAt = time.Now()
66+
67+
err := tx.QueryRowxContext(
68+
ctx,
69+
query,
70+
m.TokenAccount,
71+
m.Quarks,
72+
m.SlotCheckpoint,
73+
m.LastUpdatedAt.UTC(),
74+
).StructScan(m)
75+
76+
return pgutil.CheckNoRows(err, balance.ErrStaleCheckpoint)
77+
})
78+
}
79+
80+
func dbGetCheckpoint(ctx context.Context, db *sqlx.DB, account string) (*model, error) {
81+
res := &model{}
82+
83+
query := `SELECT
84+
id, token_account, quarks, slot_checkpoint, last_updated_at
85+
FROM ` + tableName + `
86+
WHERE token_account = $1
87+
LIMIT 1`
88+
89+
err := db.GetContext(ctx, res, query, account)
90+
if err != nil {
91+
return nil, pgutil.CheckNoRows(err, balance.ErrCheckpointNotFound)
92+
}
93+
return res, nil
94+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package postgres
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
7+
"github.com/jmoiron/sqlx"
8+
9+
"github.com/code-payments/code-server/pkg/code/data/balance"
10+
)
11+
12+
type store struct {
13+
db *sqlx.DB
14+
}
15+
16+
// New returns a new postgres balance.Store
17+
func New(db *sql.DB) balance.Store {
18+
return &store{
19+
db: sqlx.NewDb(db, "pgx"),
20+
}
21+
}
22+
23+
// SaveCheckpoint implements balance.Store.SaveCheckpoint
24+
func (s *store) SaveCheckpoint(ctx context.Context, record *balance.Record) error {
25+
model, err := toModel(record)
26+
if err != nil {
27+
return err
28+
}
29+
30+
if err := model.dbSave(ctx, s.db); err != nil {
31+
return err
32+
}
33+
34+
res := fromModel(model)
35+
res.CopyTo(record)
36+
37+
return nil
38+
}
39+
40+
// GetCheckpoint implements balance.Store.GetCheckpoint
41+
func (s *store) GetCheckpoint(ctx context.Context, account string) (*balance.Record, error) {
42+
model, err := dbGetCheckpoint(ctx, s.db, account)
43+
if err != nil {
44+
return nil, err
45+
}
46+
return fromModel(model), nil
47+
}

0 commit comments

Comments
 (0)