block-accounting/backend/internal/infrastructure/repository/transactions/repository.go

713 lines
16 KiB
Go
Raw Normal View History

2024-05-24 17:44:24 +00:00
package transactions
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
sq "github.com/Masterminds/squirrel"
2024-06-03 21:54:17 +00:00
"github.com/emochka2007/block-accounting/internal/infrastructure/repository/organizations"
2024-05-24 17:44:24 +00:00
"github.com/emochka2007/block-accounting/internal/pkg/models"
sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils"
"github.com/google/uuid"
)
type GetTransactionsParams struct {
Ids uuid.UUIDs
OrganizationId uuid.UUID
Limit int64
CursorId uuid.UUID
2024-05-28 22:46:30 +00:00
Pending bool
ReadyToConfirm bool
2024-05-24 17:44:24 +00:00
}
type ConfirmTransactionParams struct {
TxId uuid.UUID
UserId uuid.UUID
OrganizationId uuid.UUID
}
type CancelTransactionParams struct {
TxId uuid.UUID
UserId uuid.UUID
OrganizationId uuid.UUID
}
type Repository interface {
GetTransactions(ctx context.Context, params GetTransactionsParams) ([]*models.Transaction, error)
CreateTransaction(ctx context.Context, tx models.Transaction) error
UpdateTransaction(ctx context.Context, tx models.Transaction) error
DeleteTransaction(ctx context.Context, tx models.Transaction) error
ConfirmTransaction(ctx context.Context, params ConfirmTransactionParams) error
CancelTransaction(ctx context.Context, params CancelTransactionParams) error
2024-05-26 00:30:35 +00:00
AddMultisig(ctx context.Context, multisig models.Multisig) error
2024-05-26 18:20:14 +00:00
ListMultisig(ctx context.Context, params ListMultisigsParams) ([]models.Multisig, error)
2024-05-27 21:29:05 +00:00
ConfirmMultisig(ctx context.Context, params ConfirmMultisigParams) error
AddPayrollContract(ctx context.Context, params AddPayrollContract) error
2024-05-28 20:14:15 +00:00
ListPayrolls(ctx context.Context, params ListPayrollsParams) ([]models.Payroll, error)
2024-05-24 17:44:24 +00:00
}
type repositorySQL struct {
2024-05-26 18:20:14 +00:00
db *sql.DB
orgRepo organizations.Repository
2024-05-24 17:44:24 +00:00
}
2024-05-26 18:20:14 +00:00
func NewRepository(db *sql.DB, orgRepo organizations.Repository) Repository {
2024-05-24 17:44:24 +00:00
return &repositorySQL{
2024-05-26 18:20:14 +00:00
db: db,
orgRepo: orgRepo,
2024-05-24 17:44:24 +00:00
}
}
func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX {
if tx, ok := ctx.Value(sqltools.TxCtxKey).(*sql.Tx); ok {
return tx
}
return s.db
}
func (r *repositorySQL) GetTransactions(
ctx context.Context,
params GetTransactionsParams,
) ([]*models.Transaction, error) {
var txs []*models.Transaction = make([]*models.Transaction, 0, len(params.Ids))
err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
query := buildGetTransactionsQuery(params)
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch transactions data from database. %w", err)
}
defer func() {
if cErr := rows.Close(); cErr != nil {
err = errors.Join(fmt.Errorf("error close database rows. %w", cErr), err)
}
}()
for rows.Next() {
var (
id uuid.UUID
description string
organizationId uuid.UUID
amount float64
toAddr []byte
maxFeeAllowed float64
deadline sql.NullTime
createdAt time.Time
updatedAt time.Time
confirmedAt sql.NullTime
cancelledAt sql.NullTime
commitedAt sql.NullTime
createdById uuid.UUID
createdBySeed []byte
createdByCreatedAt time.Time
createdByActivatedAt sql.NullTime
createdByIsAdmin bool
)
if err = rows.Scan(
&id,
&description,
&organizationId,
&createdById,
&amount,
&toAddr,
&maxFeeAllowed,
&deadline,
&createdAt,
&updatedAt,
&confirmedAt,
&cancelledAt,
&commitedAt,
&createdBySeed,
&createdByCreatedAt,
&createdByActivatedAt,
&createdByIsAdmin,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
tx := &models.Transaction{
Id: id,
Description: description,
OrganizationId: organizationId,
Amount: amount,
ToAddr: toAddr,
MaxFeeAllowed: maxFeeAllowed,
CreatedBy: &models.OrganizationUser{
User: models.User{
ID: createdById,
Bip39Seed: createdBySeed,
},
},
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
if deadline.Valid {
tx.Deadline = deadline.Time
}
if confirmedAt.Valid {
tx.ConfirmedAt = confirmedAt.Time
}
if commitedAt.Valid {
tx.CommitedAt = commitedAt.Time
}
if cancelledAt.Valid {
tx.CancelledAt = cancelledAt.Time
}
if createdByActivatedAt.Valid {
tx.CreatedBy.Activated = true
}
txs = append(txs, tx)
}
return nil
})
if err != nil {
return nil, err
}
return txs, nil
}
func (r *repositorySQL) CreateTransaction(ctx context.Context, tx models.Transaction) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
columns := []string{
"id",
"description",
"organization_id",
"created_by",
"amount",
"to_addr",
"max_fee_allowed",
"created_at",
"updated_at",
}
values := []any{
tx.Id,
tx.Description,
tx.OrganizationId,
tx.CreatedBy.ID,
tx.Amount,
tx.ToAddr,
tx.MaxFeeAllowed,
tx.CreatedAt,
tx.CreatedAt,
}
if !tx.Deadline.IsZero() {
columns = append(columns, "deadline")
values = append(values, tx.Deadline)
}
query := sq.Insert("transactions").
Columns(columns...).
Values(values...).
PlaceholderFormat(sq.Dollar)
// todo add optional insertions
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert new transaction. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) UpdateTransaction(ctx context.Context, tx models.Transaction) error {
return nil
}
func (r *repositorySQL) DeleteTransaction(ctx context.Context, tx models.Transaction) error {
return nil
}
func (r *repositorySQL) ConfirmTransaction(ctx context.Context, params ConfirmTransactionParams) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Update("transactions").
SetMap(sq.Eq{
"confirmed_at": time.Now(),
"cancelled_at": nil,
}).
Where(sq.Eq{
"id": params.TxId,
"organization_id": params.OrganizationId,
}).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error update confirmed at. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func (r *repositorySQL) CancelTransaction(ctx context.Context, params CancelTransactionParams) error {
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Update("transactions").
SetMap(sq.Eq{
"cancelled_at": time.Now(),
"confirmed_at": nil,
}).
Where(sq.Eq{
"id": params.TxId,
"organization_id": params.OrganizationId,
}).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error update confirmed at. %w", err)
}
return nil
}); err != nil {
return err
}
return nil
}
func buildGetTransactionsQuery(params GetTransactionsParams) sq.SelectBuilder {
query := sq.Select(
`t.id,
t.description,
t.organization_id,
t.created_by,
t.amount,
t.to_addr,
t.max_fee_allowed,
t.deadline,
2024-05-28 22:46:30 +00:00
t.confirmations_required,
2024-05-24 17:44:24 +00:00
t.created_at,
t.updated_at,
2024-05-28 22:46:30 +00:00
2024-05-24 17:44:24 +00:00
t.confirmed_at,
t.cancelled_at,
t.commited_at,
u.seed,
u.created_at,
u.activated_at,
ou.is_admin`,
).From("transactions as t").
InnerJoin("users as u on u.id = t.created_by").
InnerJoin(
`organizations_users as ou on
u.id = ou.user_id and ou.organization_id = t.organization_id`,
).
Where(sq.Eq{
"t.organization_id": params.OrganizationId,
}).PlaceholderFormat(sq.Dollar)
if len(params.Ids) > 0 {
query = query.Where(sq.Eq{
"t.id": params.Ids,
})
}
if params.OrganizationId != uuid.Nil {
query = query.Where(sq.Eq{
"t.organization_id": params.OrganizationId,
})
}
2024-05-28 22:46:30 +00:00
if params.Pending {
query = query.InnerJoin("multisig_confirmations as mc on mc.multisig_id = t.multisig_id").Where(
sq.Lt{},
)
2024-05-24 17:44:24 +00:00
}
query = query.Limit(uint64(params.Limit))
return query
}
2024-05-26 00:30:35 +00:00
func (r *repositorySQL) AddMultisig(
ctx context.Context,
multisig models.Multisig,
) error {
return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Insert("multisigs").Columns(
"id",
"organization_id",
"title",
2024-05-26 18:20:14 +00:00
"address",
"confirmations",
2024-05-26 00:30:35 +00:00
"created_at",
"updated_at",
).Values(
multisig.ID,
multisig.OrganizationID,
multisig.Title,
2024-05-26 18:20:14 +00:00
multisig.Address,
multisig.ConfirmationsRequired,
2024-05-26 00:30:35 +00:00
multisig.CreatedAt,
multisig.UpdatedAt,
).PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert multisig data. %w", err)
}
for _, owner := range multisig.Owners {
addOwnerQuery := sq.Insert("multisig_owners").Columns(
"multisig_id",
"owner_id",
"created_at",
"updated_at",
).Values(
multisig.ID,
owner.Id(),
multisig.CreatedAt,
multisig.UpdatedAt,
).PlaceholderFormat(sq.Dollar)
2024-05-26 00:30:35 +00:00
if _, err := addOwnerQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert multisig owner data. %w", err)
}
}
return nil
})
}
2024-05-26 18:20:14 +00:00
type ListMultisigsParams struct {
IDs uuid.UUIDs
OrganizationID uuid.UUID
}
func (r *repositorySQL) ListMultisig(
ctx context.Context,
params ListMultisigsParams,
) ([]models.Multisig, error) {
msgs := make([]models.Multisig, 0)
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Select(
"id",
"organization_id",
"title",
"address",
"confirmations",
"created_at",
"updated_at",
).From("multisigs").Where(sq.Eq{
"organization_id": params.OrganizationID,
}).PlaceholderFormat(sq.Dollar)
2024-05-27 21:29:05 +00:00
if len(params.IDs) > 0 {
query = query.Where(sq.Eq{
"id": params.IDs,
})
}
2024-05-26 18:20:14 +00:00
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch multisigs from database. %w", err)
}
defer rows.Close()
msgsTmp := make([]*models.Multisig, 0)
for rows.Next() {
var (
id uuid.UUID
organizationID uuid.UUID
address []byte
title string
confirmations int
createdAt time.Time
updatedAt time.Time
)
if err = rows.Scan(
&id,
&organizationID,
&title,
&address,
&confirmations,
&createdAt,
&updatedAt,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
msgsTmp = append(msgsTmp, &models.Multisig{
ID: id,
Title: title,
Address: address,
OrganizationID: organizationID,
ConfirmationsRequired: confirmations,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
for _, m := range msgsTmp {
owners, err := r.fetchOwners(ctx, fetchOwnersParams{
OrganizationID: params.OrganizationID,
MultisigID: m.ID,
})
if err != nil {
return err
}
m.Owners = owners
msgs = append(msgs, *m)
}
return nil
}); err != nil {
return nil, err
}
return msgs, nil
}
2024-05-27 21:29:05 +00:00
type ConfirmMultisigParams struct {
MultisigID uuid.UUID
OrganizationsID uuid.UUID
CinfirmedBy *models.OrganizationUser
ConfirmedAt time.Time
}
func (r *repositorySQL) ConfirmMultisig(ctx context.Context, params ConfirmMultisigParams) error {
return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
deleteOldQuery := sq.Delete("multisig_confirmations").
Where(sq.Eq{
"multisig_id": params.MultisigID,
"owner_id": params.CinfirmedBy.Id(),
}).
PlaceholderFormat(sq.Dollar)
if _, err := deleteOldQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error delete old multisig confirmation. %w", err)
}
query := sq.Insert("multisig_confirmations").
Columns(
"multisig_id",
"owner_id",
"created_at",
).
Values(
params.MultisigID,
params.CinfirmedBy.Id(),
params.ConfirmedAt,
).
PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error add multisig confirmation. %w", err)
}
return nil
})
}
type AddPayrollContract struct {
ID uuid.UUID
Title string
Description string
Address []byte
Payload []byte
OrganizationID uuid.UUID
MultisigID uuid.UUID
CreatedAt time.Time
}
func (r *repositorySQL) AddPayrollContract(
ctx context.Context,
params AddPayrollContract,
) error {
return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Insert("payrolls").
Columns(
"id",
"title",
"description",
"address",
"payload",
"organization_id",
"multisig_id",
"created_at",
).
Values(
params.ID,
params.Title,
params.Description,
params.Address,
params.Payload,
params.OrganizationID,
params.MultisigID,
params.CreatedAt,
).
PlaceholderFormat(sq.Dollar)
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error add new payroll contract. %w", err)
}
return nil
})
}
2024-05-26 18:20:14 +00:00
type fetchOwnersParams struct {
MultisigID uuid.UUID
OrganizationID uuid.UUID
}
func (r *repositorySQL) fetchOwners(ctx context.Context, params fetchOwnersParams) ([]models.OrganizationParticipant, error) {
owners := make([]models.OrganizationParticipant, 0)
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Select("owner_id").From("multisig_owners").Where(sq.Eq{
"multisig_id": params.MultisigID,
}).PlaceholderFormat(sq.Dollar)
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch multisigs owners from database. %w", err)
}
defer rows.Close()
ids := make(uuid.UUIDs, 0)
for rows.Next() {
var ownerId uuid.UUID
if err = rows.Scan(&ownerId); err != nil {
return err
}
ids = append(ids, ownerId)
}
owners, err = r.orgRepo.Participants(ctx, organizations.ParticipantsParams{
OrganizationId: params.OrganizationID,
Ids: ids,
UsersOnly: true,
})
if err != nil {
return fmt.Errorf("error fetch owners as participants. %w", err)
}
return nil
}); err != nil {
return nil, err
}
return owners, nil
}
2024-05-28 20:14:15 +00:00
type ListPayrollsParams struct {
IDs []uuid.UUID
Limit int64
OrganizationID uuid.UUID
}
func (r *repositorySQL) ListPayrolls(ctx context.Context, params ListPayrollsParams) ([]models.Payroll, error) {
payrolls := make([]models.Payroll, 0, len(params.IDs))
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) error {
query := sq.Select(
"id",
"title",
"address",
"organization_id",
"multisig_id",
"created_at",
"updated_at",
).Where(sq.Eq{
"organization_id": params.OrganizationID,
}).PlaceholderFormat(sq.Dollar)
if params.Limit <= 0 {
params.Limit = 100
}
if len(params.IDs) > 0 {
query = query.Where(sq.Eq{
"id": params.IDs,
})
}
query = query.Limit(uint64(params.Limit))
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
if err != nil {
return fmt.Errorf("error fetch payrolls from database. %w", err)
}
defer rows.Close() // todo check error
for rows.Next() {
var (
id uuid.UUID
title string
address []byte
organizationId uuid.UUID
multisigId uuid.UUID
createdAt time.Time
updatedAt time.Time
)
if err = rows.Scan(
&id,
&title,
&address,
&organizationId,
&multisigId,
&createdAt,
&updatedAt,
); err != nil {
return fmt.Errorf("error scan row. %w", err)
}
payrolls = append(payrolls, models.Payroll{
ID: id,
Title: title,
Address: address,
OrganizationID: organizationId,
MultisigID: multisigId,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
return nil
}); err != nil {
return nil, err
}
return payrolls, nil
}