mirror of
https://github.com/emo2007/block-accounting.git
synced 2024-11-14 14:16:27 +00:00
713 lines
16 KiB
Go
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
|
|
}
|