diff --git a/backend/go.mod b/backend/go.mod index 5871c2b..b70f3d5 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,10 +3,12 @@ module github.com/emochka2007/block-accounting go 1.22.2 require ( + github.com/Masterminds/squirrel v1.5.4 github.com/fatih/color v1.16.0 github.com/go-chi/chi/v5 v5.0.12 github.com/go-chi/render v1.0.3 github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.6.0 github.com/google/wire v0.6.0 github.com/urfave/cli/v2 v2.27.2 golang.org/x/crypto v0.18.0 @@ -15,6 +17,8 @@ require ( require ( github.com/ajg/form v1.5.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 2280286..8cbf579 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,7 +1,11 @@ +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= @@ -12,16 +16,25 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= diff --git a/backend/internal/interface/rest/controllers/auth.go b/backend/internal/interface/rest/controllers/auth.go index 3af1da5..919e7dd 100644 --- a/backend/internal/interface/rest/controllers/auth.go +++ b/backend/internal/interface/rest/controllers/auth.go @@ -1,6 +1,7 @@ package controllers import ( + "errors" "fmt" "log/slog" "net/http" @@ -9,6 +10,10 @@ import ( "github.com/emochka2007/block-accounting/internal/pkg/bip32" ) +var ( + ErrorAuthInvalidMnemonic = errors.New("Invalid Mnemonic") +) + type AuthController interface { Join(w http.ResponseWriter, req *http.Request) error Login(w http.ResponseWriter, req *http.Request) error @@ -34,19 +39,20 @@ func NewAuthController( const mnemonicEntropyBitSize int = 256 func (c *authController) Join(w http.ResponseWriter, req *http.Request) error { - entropy, err := bip32.NewEntropy(mnemonicEntropyBitSize) + request, err := c.presenter.CreateJoinRequest(req) if err != nil { - return fmt.Errorf("error generate new entropy. %w", err) + return fmt.Errorf("error create join request. %w", err) } - mnemonic, err := bip32.NewMnemonic(entropy) - if err != nil { - return fmt.Errorf("error generate mnemonic from entropy. %w", err) + c.log.Debug("join request", slog.String("mnemonic", request.Mnemonic)) + + if !bip32.IsMnemonicValid(request.Mnemonic) { + return fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic) } // todo create user - return c.presenter.ResponseJoin(w, mnemonic) + return nil } func (c *authController) Login(w http.ResponseWriter, req *http.Request) error { diff --git a/backend/internal/interface/rest/domain/dto.go b/backend/internal/interface/rest/domain/dto.go index a6bb79b..ef99894 100644 --- a/backend/internal/interface/rest/domain/dto.go +++ b/backend/internal/interface/rest/domain/dto.go @@ -5,7 +5,7 @@ import ( "fmt" ) -type JoinResponse struct { +type JoinRequest struct { Mnemonic string `json:"mnemonic"` } diff --git a/backend/internal/interface/rest/presenters/auth.go b/backend/internal/interface/rest/presenters/auth.go index 2d62eb0..597997b 100644 --- a/backend/internal/interface/rest/presenters/auth.go +++ b/backend/internal/interface/rest/presenters/auth.go @@ -3,13 +3,15 @@ package presenters import ( "encoding/json" "fmt" + "io" "net/http" "github.com/emochka2007/block-accounting/internal/interface/rest/domain" ) type AuthPresenter interface { - ResponseJoin(w http.ResponseWriter, mnemonic string) error + CreateJoinRequest(r *http.Request) (*domain.JoinRequest, error) + // ResponseJoin(w http.ResponseWriter, mnemonic string) error } type authPresenter struct{} @@ -18,17 +20,34 @@ func NewAuthPresenter() AuthPresenter { return &authPresenter{} } -func (p *authPresenter) ResponseJoin(w http.ResponseWriter, mnemonic string) error { - out, err := json.Marshal(domain.JoinResponse{ - Mnemonic: mnemonic, - }) +func (p *authPresenter) CreateJoinRequest(r *http.Request) (*domain.JoinRequest, error) { + defer r.Body.Close() + + data, err := io.ReadAll(r.Body) if err != nil { - return fmt.Errorf("error marshal join response. %w", err) + return nil, fmt.Errorf("error read request body. %w", err) } - if _, err = w.Write(out); err != nil { - return fmt.Errorf("error write response. %w", err) + var request domain.JoinRequest + + if err := json.Unmarshal(data, &request); err != nil { + return nil, fmt.Errorf("error unmarshal join request. %w", err) } - return nil + return &request, nil } + +// func (p *authPresenter) ResponseJoin(w http.ResponseWriter, mnemonic string) error { +// out, err := json.Marshal(domain.JoinResponse{ +// Mnemonic: mnemonic, +// }) +// if err != nil { +// return fmt.Errorf("error marshal join response. %w", err) +// } + +// if _, err = w.Write(out); err != nil { +// return fmt.Errorf("error write response. %w", err) +// } + +// return nil +// } diff --git a/backend/internal/interface/rest/server.go b/backend/internal/interface/rest/server.go index a26092f..f0e8711 100644 --- a/backend/internal/interface/rest/server.go +++ b/backend/internal/interface/rest/server.go @@ -92,7 +92,7 @@ func (s *Server) buildRouter() { r.Delete("/{tx_id}", nil) // remove }) - s.Post("/invite", nil) // create a new invite link + s.Post("/invite/{hash}", nil) // create a new invite link s.Route("/employees", func(r chi.Router) { r.Get("/", nil) // list diff --git a/backend/internal/pkg/models/tx.go b/backend/internal/pkg/models/tx.go new file mode 100644 index 0000000..52054be --- /dev/null +++ b/backend/internal/pkg/models/tx.go @@ -0,0 +1,38 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type Transaction struct { + Id uuid.UUID + + Description string + OrganizationId uuid.UUID + CreatedBy *User + Amount int64 + + ToAddr []byte + + MaxFeeAllowed int64 + Deadline time.Time + + CreatedAt time.Time + UpdatedAt time.Time + + ConfirmedAt time.Time + CancelledAt time.Time + + CommitedAt time.Time +} + +type TransactionConfirmation struct { + TxId uuid.UUID + User *User + OrganizationId uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Confirmed bool +} diff --git a/backend/internal/pkg/models/user.go b/backend/internal/pkg/models/user.go index e869522..bb8300f 100644 --- a/backend/internal/pkg/models/user.go +++ b/backend/internal/pkg/models/user.go @@ -1,34 +1,39 @@ package models +import ( + "github.com/google/uuid" +) + type UserIdentity interface { - Id() string - Mnemonic() string + Id() uuid.UUID + Seed() []byte IsAdmin() bool } type User struct { - id string - mnemonic string - isAdmin bool + ID uuid.UUID + Bip32Seed []byte + Admin bool + Activated bool } -func NewUser(id string, mnemonic string) *User { +func NewUser(id uuid.UUID, seed []byte) *User { return &User{ - id: id, - mnemonic: mnemonic, + ID: id, + Bip32Seed: seed, } } -func (u *User) Id() string { - return u.id +func (u *User) Id() uuid.UUID { + return u.ID } -func (u *User) Mnemonic() string { - return u.mnemonic +func (u *User) Seed() []byte { + return u.Bip32Seed } func (u *User) IsAdmin() bool { - return u.isAdmin + return u.Admin } type OrganizationUser struct { diff --git a/backend/internal/pkg/sqlutils/tx.go b/backend/internal/pkg/sqlutils/tx.go index ad0f9c6..84aa9d6 100644 --- a/backend/internal/pkg/sqlutils/tx.go +++ b/backend/internal/pkg/sqlutils/tx.go @@ -21,7 +21,7 @@ type TransactionalStorage interface { Conn(ctx context.Context) DBTX } -type txCtxKey struct{} +type TxCtxKey struct{} func Transaction(ctx context.Context, db *sql.DB, fn func(context.Context) error) error { var err error @@ -70,14 +70,14 @@ func Transaction(ctx context.Context, db *sql.DB, fn func(context.Context) error return fmt.Errorf("error begin transaction. %w", err) } - ctx = context.WithValue(ctx, txCtxKey{}, tx) + ctx = context.WithValue(ctx, TxCtxKey{}, tx) } return fn(ctx) } func hasExternalTransaction(ctx context.Context) bool { - if _, ok := ctx.Value(txCtxKey{}).(*sql.Tx); ok { + if _, ok := ctx.Value(TxCtxKey{}).(*sql.Tx); ok { return true } diff --git a/backend/internal/usecase/interactors/smartcontract/contract.go b/backend/internal/usecase/interactors/smartcontract/contract.go new file mode 100644 index 0000000..5f4b84b --- /dev/null +++ b/backend/internal/usecase/interactors/smartcontract/contract.go @@ -0,0 +1,4 @@ +package smartcontract + +type SmartContractInteractor interface { +} diff --git a/backend/internal/usecase/repository/transactions/repository.go b/backend/internal/usecase/repository/transactions/repository.go new file mode 100644 index 0000000..1bfea7c --- /dev/null +++ b/backend/internal/usecase/repository/transactions/repository.go @@ -0,0 +1,316 @@ +package transactions + +import ( + "context" + "database/sql" + "errors" + "fmt" + "slices" + "time" + + sq "github.com/Masterminds/squirrel" + "github.com/emochka2007/block-accounting/internal/pkg/models" + sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils" + "github.com/google/uuid" +) + +type GetTransactionsFilter int + +const ( + GetFilterExpired GetTransactionsFilter = iota + GetFilterNonExpired + GetFilterCancelled + GetFilterConfirmed + GetFilterCommited +) + +type GetTransactionsParams struct { + Ids uuid.UUIDs + OrganizationId uuid.UUID + CreatedById uuid.UUID + To []byte + Filters []GetTransactionsFilter +} + +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 +} + +type repositorySQL struct { + db *sql.DB +} + +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 int64 + toAddr []byte + maxFeeAllowed int64 + 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, + &amount, + &toAddr, + &maxFeeAllowed, + &deadline, + &createdAt, + &updatedAt, + &confirmedAt, + &cancelledAt, + &commitedAt, + + &createdById, + &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.User{ + ID: createdById, + Bip32Seed: 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, fmt.Errorf("error execute transactional operation. %w", 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 { + query := sq.Insert("transactions").Columns( + "t.id", + "t.description", + "t.organization_id", + "t.created_by", + "t.amount", + "t.to_addr", + "t.max_fee_allowed", + + // todo insert later + // "t.deadline", + // "t.created_at", + // "t.updated_at", + // "t.confirmed_at", + // "t.cancelled_at", + // "t.commited_at", + ).Values( + tx.Id, + tx.Description, + tx.OrganizationId, + tx.CreatedBy.ID, + tx.Amount, + tx.ToAddr, + tx.MaxFeeAllowed, + ) + + // 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 fmt.Errorf("error execute transactional operation. %w", 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 { + return nil +} + +func (r *repositorySQL) CancelTransaction(ctx context.Context, params CancelTransactionParams) error { + 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.created_at, + t.updated_at, + t.confirmed_at, + t.cancelled_at, + t.commited_at, + + u.id, + u.seed, + u.created_at, + u.activated_at, + u.is_admin`, + ).From("transactions as t"). + InnerJoin("users as u on u.id = t.created_by"). + Where(sq.Eq{ + "t.organization_id": params.OrganizationId, + }) + + if len(params.Ids) > 0 { + query = query.Where(sq.Eq{ + "t.id": params.Ids, + }) + } + + if params.CreatedById != uuid.Nil { + query = query.Where(sq.Eq{ + "t.created_by": params.CreatedById, + }) + } + + if params.OrganizationId != uuid.Nil { + query = query.Where(sq.Eq{ + "t.organization_id": params.OrganizationId, + }) + } + + if params.To != nil { + query = query.Where(sq.Eq{ + "t.to_addr": params.To, + }) + } + + if slices.Contains(params.Filters, GetFilterExpired) { + query = query.Where(sq.LtOrEq{ + "t.deadline": time.Now(), + }) + } else if slices.Contains(params.Filters, GetFilterNonExpired) { + query = query.Where(sq.GtOrEq{ + "t.deadline": time.Now(), + }) + } + + if slices.Contains(params.Filters, GetFilterCancelled) { + query = query.Where(sq.NotEq{ + "t.cancelled_at": nil, + }) + } else if slices.Contains(params.Filters, GetFilterConfirmed) { + query = query.Where(sq.NotEq{ + "t.confirmed_at": nil, + }) + } + + if slices.Contains(params.Filters, GetFilterCommited) { + query = query.Where(sq.NotEq{ + "t.commited_at": nil, + }) + } + + return query +} diff --git a/backend/migrations/blockd.sql b/backend/migrations/blockd.sql new file mode 100644 index 0000000..5733637 --- /dev/null +++ b/backend/migrations/blockd.sql @@ -0,0 +1,74 @@ +create table if not exists users ( + id uuid not null, + seed bytea not null unique, + created_at timestamp default current_timestamp, + activated_at timestamp default null, + primary key (id, seed) +); + +create table if not exists organizations ( + id uuid primary key, + name varchar(300) default 'My Organization' not null, + created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp +); + +create index if not exists index_organizations_id + on organizations (id); + +create table organizations_users ( + organization_id uuid not null, + user_id uuid not null, + added_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp, + is_admin bool default false, + primary key(organization_id, user_id) +); + +create index if not exists index_organizations_users_organization_id_user_id_is_admin + on organizations_users (organization_id, user_id, is_admin); + +create index if not exists index_organizations_users_organization_id_user_id + on organizations_users (organization_id, user_id); + +create table if not exists transactions ( + id uuid primary key, + description text default 'New Transaction', + organization_id uuid not null, + created_by uuid not null, + amount bigint default 0, + + to_addr bytea not null, + + max_fee_allowed bigint default 0, + deadline timestamp default null, + + created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp, + + confirmed_at timestamp default null, + cancelled_at timestamp default null, + + commited_at timestamp default null +); + +create index if not exists index_transactions_id_organization_id + on transactions (organization_id); + +create index if not exists index_transactions_id_organization_id_created_by + on transactions (organization_id, created_by); + +create index if not exists index_transactions_organization_id_deadline + on transactions (organization_id, deadline); + +create table transactions_confirmations ( + tx_id uuid not null, + user_id uuid not null, + organization_id uuid not null, + created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp, + confirmed bool +); + +create index if not exists index_transactions_confirmations_tx_id_user_id_organization_id + on transactions_confirmations (tx_id, user_id, organization_id); \ No newline at end of file