block-accounting/backend/internal/infrastructure/repository/transactions/repository.go
2024-06-04 00:54:17 +03:00

713 lines
16 KiB
Go

package transactions
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
sq "github.com/Masterminds/squirrel"
"github.com/emochka2007/block-accounting/internal/infrastructure/repository/organizations"
"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
Pending bool
ReadyToConfirm bool
}
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
AddMultisig(ctx context.Context, multisig models.Multisig) error
ListMultisig(ctx context.Context, params ListMultisigsParams) ([]models.Multisig, error)
ConfirmMultisig(ctx context.Context, params ConfirmMultisigParams) error
AddPayrollContract(ctx context.Context, params AddPayrollContract) error
ListPayrolls(ctx context.Context, params ListPayrollsParams) ([]models.Payroll, error)
}
type repositorySQL struct {
db *sql.DB
orgRepo organizations.Repository
}
func NewRepository(db *sql.DB, orgRepo organizations.Repository) Repository {
return &repositorySQL{
db: db,
orgRepo: orgRepo,
}
}
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,
t.confirmations_required,
t.created_at,
t.updated_at,
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,
})
}
if params.Pending {
query = query.InnerJoin("multisig_confirmations as mc on mc.multisig_id = t.multisig_id").Where(
sq.Lt{},
)
}
query = query.Limit(uint64(params.Limit))
return query
}
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",
"address",
"confirmations",
"created_at",
"updated_at",
).Values(
multisig.ID,
multisig.OrganizationID,
multisig.Title,
multisig.Address,
multisig.ConfirmationsRequired,
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)
if _, err := addOwnerQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
return fmt.Errorf("error insert multisig owner data. %w", err)
}
}
return nil
})
}
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)
if len(params.IDs) > 0 {
query = query.Where(sq.Eq{
"id": params.IDs,
})
}
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
}
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
})
}
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
}
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
}