Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction List #281

Merged
merged 6 commits into from
Mar 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion pkg/store/mock/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (s *Store) Close() error {
return nil
}

func (s *Store) ListTransactions(context.Context, *models.PageInfo) (*models.TransactionPage, error) {
func (s *Store) ListTransactions(context.Context, *models.TransactionPageInfo) (*models.TransactionPage, error) {
return nil, nil
}

Expand All @@ -47,6 +47,10 @@ func (s *Store) ArchiveTransaction(context.Context, uuid.UUID) error {
return nil
}

func (s *Store) CountTransactions(context.Context) (*models.TransactionCounts, error) {
return nil, nil
}

func (s *Store) PrepareTransaction(context.Context, uuid.UUID) (models.PreparedTransaction, error) {
return nil, nil
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/store/models/pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ type PageInfo struct {
}

type TransactionPage struct {
Transactions []*Transaction `json:"transactions"`
Page *PageInfo `json:"page"`
Transactions []*Transaction `json:"transactions"`
Page *TransactionPageInfo `json:"page"`
}

type SecureEnvelopePage struct {
Expand Down
39 changes: 39 additions & 0 deletions pkg/store/models/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ type Transaction struct {
envelopes []*SecureEnvelope // Associated secure envelopes
}

type TransactionCounts struct {
Active map[string]int // Active transaction counts by status
Archived map[string]int // Archived transaction counts by status
}

type TransactionPageInfo struct {
PageInfo
Status []string `json:"status,omitempty"`
VirtualAsset []string `json:"asset,omitempty"`
Archives bool `json:"archives,omitempty"`
}

type SecureEnvelope struct {
Model
EnvelopeID uuid.UUID // Also a foreign key reference to the Transaction
Expand Down Expand Up @@ -322,3 +334,30 @@ func (e *SecureEnvelope) Transaction() (*Transaction, error) {
func (e *SecureEnvelope) SetTransaction(tx *Transaction) {
e.transaction = tx
}

func (c *TransactionCounts) TotalActive() int {
var total int
for _, count := range c.Active {
total += count
}
return total
}

func (c *TransactionCounts) TotalArchived() int {
var total int
for _, count := range c.Archived {
total += count
}
return total
}

func (c *TransactionCounts) Total() int {
var total int
for _, count := range c.Active {
total += count
}
for _, count := range c.Archived {
total += count
}
return total
}
88 changes: 83 additions & 5 deletions pkg/store/sqlite/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"database/sql"
"errors"
"fmt"
"strings"
"time"

dberr "github.com/trisacrypto/envoy/pkg/store/errors"
Expand All @@ -18,23 +19,50 @@ import (
// Transaction CRUD interface
//==========================================================================

const listTransactionsSQL = "SELECT t.id, t.source, t.status, t.counterparty, t.counterparty_id, t.originator, t.originator_address, t.beneficiary, t.beneficiary_address, t.virtual_asset, t.amount, t.archived, t.archived_on, t.last_update, t.modified, t.created, count(e.id) AS numEnvelopes FROM transactions t LEFT JOIN secure_envelopes e ON t.id=e.envelope_id WHERE t.archived=0 GROUP BY t.id ORDER BY t.created DESC"
const listTransactionsSQL = "SELECT t.id, t.source, t.status, t.counterparty, t.counterparty_id, t.originator, t.originator_address, t.beneficiary, t.beneficiary_address, t.virtual_asset, t.amount, t.archived, t.archived_on, t.last_update, t.modified, t.created, count(e.id) AS numEnvelopes FROM transactions t LEFT JOIN secure_envelopes e ON t.id=e.envelope_id WHERE t.archived=:archives GROUP BY t.id ORDER BY t.created DESC"

func (s *Store) ListTransactions(ctx context.Context, page *models.PageInfo) (out *models.TransactionPage, err error) {
func (s *Store) ListTransactions(ctx context.Context, page *models.TransactionPageInfo) (out *models.TransactionPage, err error) {
var tx *sql.Tx
if tx, err = s.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}); err != nil {
return nil, err
}
defer tx.Rollback()

// TODO: handle pagination
out = &models.TransactionPage{
Transactions: make([]*models.Transaction, 0),
Page: models.PageInfoFrom(page),
Page: &models.TransactionPageInfo{
PageInfo: *models.PageInfoFrom(&page.PageInfo),
Status: page.Status,
VirtualAsset: page.VirtualAsset,
Archives: page.Archives,
},
}

// Create the base query and the query parameters list.
query := listTransactionsSQL
params := []interface{}{sql.Named("archives", page.Archives)}

// If there are filters in the page query, then modify the SQL query with them.
if len(page.Status) > 0 || len(page.VirtualAsset) > 0 {
filters := make([]string, 0, 2)
if len(page.Status) > 0 {
inquery, inparams := listParametrize(page.Status, "s")
filters = append(filters, "status IN "+inquery)
params = append(params, inparams...)
}

if len(page.VirtualAsset) > 0 {
inquery, inparams := listParametrize(page.VirtualAsset, "a")
filters = append(filters, "virtual_asset IN "+inquery)
params = append(params, inparams...)
}

query = "WITH txns AS (" + listTransactionsSQL + ") SELECT * FROM txns WHERE "
query += strings.Join(filters, " AND ")
}

var rows *sql.Rows
if rows, err = tx.Query(listTransactionsSQL); err != nil {
if rows, err = tx.Query(query, params...); err != nil {
// TODO: handle database specific errors
return nil, err
}
Expand Down Expand Up @@ -204,6 +232,56 @@ func (s *Store) archiveTransaction(tx *sql.Tx, transactionID uuid.UUID) (err err
return nil
}

const countTransactionsSQL = "SELECT count(id), status FROM transactions WHERE archived=:archived GROUP BY status"

func (s *Store) CountTransactions(ctx context.Context) (counts *models.TransactionCounts, err error) {
var tx *sql.Tx
if tx, err = s.BeginTx(ctx, nil); err != nil {
return nil, err
}
defer tx.Rollback()

counts = &models.TransactionCounts{
Active: make(map[string]int),
Archived: make(map[string]int),
}

if err = s.countTransactions(tx, counts, false); err != nil {
return nil, err
}

if err = s.countTransactions(tx, counts, true); err != nil {
return nil, err
}

tx.Commit()
return counts, nil
}

func (s *Store) countTransactions(tx *sql.Tx, counts *models.TransactionCounts, archived bool) (err error) {
var rows *sql.Rows
if rows, err = tx.Query(countTransactionsSQL, sql.Named("archived", archived)); err != nil {
return err
}
defer rows.Close()

for rows.Next() {
var count int
var status string
if err = rows.Scan(&count, &status); err != nil {
return err
}

if archived {
counts.Archived[status] = count
} else {
counts.Active[status] = count
}
}

return rows.Err()
}

//===========================================================================
// Secure Envelopes CRUD Interface
//===========================================================================
Expand Down
3 changes: 2 additions & 1 deletion pkg/store/sqlite/transactions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import (
func (s *storeTestSuite) TestListTransactions() {
require := s.Require()
ctx := context.Background()
query := &models.TransactionPageInfo{}

page, err := s.store.ListTransactions(ctx, nil)
page, err := s.store.ListTransactions(ctx, query)
require.NoError(err, "could not list transactions from database")
require.NotNil(page, "a nil page was returned without transactions")
require.Len(page.Transactions, 4, "expected transactions to be returned in list")
Expand Down
24 changes: 24 additions & 0 deletions pkg/store/sqlite/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package sqlite

import (
"database/sql"
"fmt"
"strings"
)

// Used to parameterize a list of values for an IN clause in a SQLite query.
// Given a list of values, ["apple", "berry", "cherry"], and a prefix, "f",
// the function will return the query string "(f0, f1, f2)" and the params slice
// []interface{}{sql.Named("f0", "apple"), sql.Named("f1", "berry"), sql.Named("f2", "cherry")}.
// The placeholders are used to prevent SQL injection attacks and the query string can
// be appended to a query such as "SELECT * FROM fruits WHERE name IN ".
func listParametrize(values []string, prefix string) (query string, params []interface{}) {
placeholders := make([]string, 0, len(values))
params = make([]interface{}, 0, len(values))
for i, param := range values {
placeholder := fmt.Sprintf("%s%d", prefix, i)
placeholders = append(placeholders, ":"+placeholder)
params = append(params, sql.Named(placeholder, param))
}
return "(" + strings.Join(placeholders, ", ") + ")", params
}
3 changes: 2 additions & 1 deletion pkg/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,14 @@ type Stats interface {
// part of completing a travel rule exchange for the transaction.
type TransactionStore interface {
SecureEnvelopeStore
ListTransactions(context.Context, *models.PageInfo) (*models.TransactionPage, error)
ListTransactions(context.Context, *models.TransactionPageInfo) (*models.TransactionPage, error)
CreateTransaction(context.Context, *models.Transaction) error
RetrieveTransaction(context.Context, uuid.UUID) (*models.Transaction, error)
UpdateTransaction(context.Context, *models.Transaction) error
DeleteTransaction(context.Context, uuid.UUID) error
ArchiveTransaction(context.Context, uuid.UUID) error
PrepareTransaction(context.Context, uuid.UUID) (models.PreparedTransaction, error)
CountTransactions(context.Context) (*models.TransactionCounts, error)
}

// SecureEnvelopes are associated with individual transactions.
Expand Down
Loading
Loading