diff --git a/internal/core/application/account_service.go b/internal/core/application/account_service.go index 9465663..f3ba89d 100644 --- a/internal/core/application/account_service.go +++ b/internal/core/application/account_service.go @@ -7,6 +7,7 @@ import ( "time" log "github.com/sirupsen/logrus" + "github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/ocean/internal/core/domain" "github.com/vulpemventures/ocean/internal/core/ports" ) @@ -160,8 +161,18 @@ func (as *AccountService) GetBalanceForAccount( } func (as *AccountService) ListUtxosForAccount( - ctx context.Context, accountName string, + ctx context.Context, accountName string, addresses []string, ) (*UtxoInfo, error) { + scripts := make([][]byte, 0, len(addresses)) + if len(addresses) > 0 { + for _, addr := range addresses { + script, err := address.ToOutputScript(addr) + if err != nil { + return nil, fmt.Errorf("invalid address %s", addr) + } + scripts = append(scripts, script) + } + } w, err := as.repoManager.WalletRepository().GetWallet(ctx) if err != nil { return nil, err @@ -173,14 +184,14 @@ func (as *AccountService) ListUtxosForAccount( } spendableUtxos, err := as.repoManager.UtxoRepository().GetSpendableUtxosForAccount( - ctx, account.Namespace, + ctx, account.Namespace, scripts..., ) if err != nil { return nil, err } lockedUtxos, err := as.repoManager.UtxoRepository().GetLockedUtxosForAccount( - ctx, account.Namespace, + ctx, account.Namespace, scripts..., ) if err != nil { return nil, err diff --git a/internal/core/application/account_service_test.go b/internal/core/application/account_service_test.go index f8c615d..cd039a6 100644 --- a/internal/core/application/account_service_test.go +++ b/internal/core/application/account_service_test.go @@ -49,7 +49,7 @@ func TestAccountService(t *testing.T) { require.NoError(t, err) require.GreaterOrEqual(t, len(addresses), 2) - utxos, err := svc.ListUtxosForAccount(ctx, accountName) + utxos, err := svc.ListUtxosForAccount(ctx, accountName, nil) require.NoError(t, err) require.NotNil(t, utxos) require.NotEmpty(t, utxos.Spendable) diff --git a/internal/core/domain/utxo_repository.go b/internal/core/domain/utxo_repository.go index a0cd48b..c57d3f4 100644 --- a/internal/core/domain/utxo_repository.go +++ b/internal/core/domain/utxo_repository.go @@ -55,10 +55,14 @@ type UtxoRepository interface { GetAllUtxosForAccount(ctx context.Context, account string) ([]*Utxo, error) // GetSpendableUtxosForAccount returns the list of spendable utxos for the // given account. The list incldues only confirmed and unlocked utxos. - GetSpendableUtxosForAccount(ctx context.Context, account string) ([]*Utxo, error) + GetSpendableUtxosForAccount( + ctx context.Context, account string, addresses ...[]byte, + ) ([]*Utxo, error) // GetLockedUtxosForAccount returns the list of all currently locked utxos // for the given account. - GetLockedUtxosForAccount(ctx context.Context, account string) ([]*Utxo, error) + GetLockedUtxosForAccount( + ctx context.Context, account string, scripts ...[]byte, + ) ([]*Utxo, error) // GetBalanceForAccount returns the confirmed, unconfirmed and locked // balances per each asset for the given account. GetBalanceForAccount(ctx context.Context, account string) (map[string]*Balance, error) diff --git a/internal/infrastructure/storage/db/badger/utxo_repository.go b/internal/infrastructure/storage/db/badger/utxo_repository.go index 403e6a1..b6cc850 100644 --- a/internal/infrastructure/storage/db/badger/utxo_repository.go +++ b/internal/infrastructure/storage/db/badger/utxo_repository.go @@ -2,6 +2,7 @@ package dbbadger import ( "context" + "encoding/hex" "fmt" "sync" @@ -83,22 +84,66 @@ func (r *utxoRepository) GetAllUtxosForAccount( } func (r *utxoRepository) GetSpendableUtxosForAccount( - ctx context.Context, accountName string, + ctx context.Context, accountName string, scripts ...[]byte, ) ([]*domain.Utxo, error) { query := badgerhold.Where("SpentStatus").Eq(domain.UtxoStatus{}). And("ConfirmedStatus").Ne(domain.UtxoStatus{}). And("LockTimestamp").Eq(int64(0)).And("AccountName").Eq(accountName) - return r.findUtxos(ctx, query) + utxos, err := r.findUtxos(ctx, query) + if err != nil { + return nil, err + } + + if len(scripts) <= 0 { + return utxos, nil + } + + indexedScripts := make(map[string]struct{}) + for _, script := range scripts { + indexedScripts[hex.EncodeToString(script)] = struct{}{} + } + + filteredUtxos := make([]*domain.Utxo, 0, len(utxos)) + for _, u := range utxos { + script := hex.EncodeToString(u.Script) + if _, ok := indexedScripts[script]; ok { + filteredUtxos = append(filteredUtxos, u) + } + } + + return filteredUtxos, nil } func (r *utxoRepository) GetLockedUtxosForAccount( - ctx context.Context, accountName string, + ctx context.Context, accountName string, scripts ...[]byte, ) ([]*domain.Utxo, error) { query := badgerhold.Where("SpentStatus").Eq(domain.UtxoStatus{}). And("LockTimestamp").Gt(int64(0)).And("AccountName").Eq(accountName) - return r.findUtxos(ctx, query) + utxos, err := r.findUtxos(ctx, query) + if err != nil { + return nil, err + } + + if len(scripts) <= 0 { + return utxos, nil + } + + indexedScripts := make(map[string]struct{}) + for _, script := range scripts { + indexedScripts[hex.EncodeToString(script)] = struct{}{} + } + + filteredUtxos := make([]*domain.Utxo, 0, len(utxos)) + for _, u := range utxos { + script := hex.EncodeToString(u.Script) + if _, ok := indexedScripts[script]; ok { + filteredUtxos = append(filteredUtxos, u) + } + } + + return filteredUtxos, nil } func (r *utxoRepository) GetBalanceForAccount( diff --git a/internal/infrastructure/storage/db/inmemory/utxo_repository.go b/internal/infrastructure/storage/db/inmemory/utxo_repository.go index fbfdb54..e84d73b 100644 --- a/internal/infrastructure/storage/db/inmemory/utxo_repository.go +++ b/internal/infrastructure/storage/db/inmemory/utxo_repository.go @@ -2,6 +2,7 @@ package inmemory import ( "context" + "encoding/hex" "sync" "github.com/vulpemventures/ocean/internal/core/domain" @@ -88,21 +89,65 @@ func (r *utxoRepository) GetAllUtxosForAccount( } func (r *utxoRepository) GetSpendableUtxosForAccount( - _ context.Context, account string, + _ context.Context, account string, scripts ...[]byte, ) ([]*domain.Utxo, error) { r.store.lock.RLock() defer r.store.lock.RUnlock() - return r.getUtxosForAccount(account, true, false) + utxos, err := r.getUtxosForAccount(account, true, false) + if err != nil { + return nil, err + } + + if len(scripts) <= 0 { + return utxos, nil + } + + indexedScripts := make(map[string]struct{}) + for _, script := range scripts { + indexedScripts[hex.EncodeToString(script)] = struct{}{} + } + + filteredUtxos := make([]*domain.Utxo, 0, len(utxos)) + for _, u := range utxos { + script := hex.EncodeToString(u.Script) + if _, ok := indexedScripts[script]; ok { + filteredUtxos = append(filteredUtxos, u) + } + } + + return filteredUtxos, nil } func (r *utxoRepository) GetLockedUtxosForAccount( - _ context.Context, account string, + _ context.Context, account string, scripts ...[]byte, ) ([]*domain.Utxo, error) { r.store.lock.RLock() defer r.store.lock.RUnlock() - return r.getUtxosForAccount(account, false, true) + utxos, err := r.getUtxosForAccount(account, true, false) + if err != nil { + return nil, err + } + + if len(scripts) <= 0 { + return utxos, nil + } + + indexedScripts := make(map[string]struct{}) + for _, script := range scripts { + indexedScripts[hex.EncodeToString(script)] = struct{}{} + } + + filteredUtxos := make([]*domain.Utxo, 0, len(utxos)) + for _, u := range utxos { + script := hex.EncodeToString(u.Script) + if _, ok := indexedScripts[script]; ok { + filteredUtxos = append(filteredUtxos, u) + } + } + + return filteredUtxos, nil } func (r *utxoRepository) GetBalanceForAccount( diff --git a/internal/infrastructure/storage/db/postgres/utxo_repository.go b/internal/infrastructure/storage/db/postgres/utxo_repository.go index ad9eb65..2255100 100644 --- a/internal/infrastructure/storage/db/postgres/utxo_repository.go +++ b/internal/infrastructure/storage/db/postgres/utxo_repository.go @@ -3,6 +3,7 @@ package postgresdb import ( "context" "database/sql" + "encoding/hex" "sync" "github.com/jackc/pgconn" @@ -268,67 +269,87 @@ func (u *utxoRepositoryPg) GetAllUtxosForAccount( } func (u *utxoRepositoryPg) GetSpendableUtxosForAccount( - ctx context.Context, account string, + ctx context.Context, account string, scripts ...[]byte, ) ([]*domain.Utxo, error) { - resp := make([]*domain.Utxo, 0) - utxos, err := u.querier.GetUtxosForAccount(ctx, account) + utxos := make([]*domain.Utxo, 0) + rows, err := u.querier.GetUtxosForAccount(ctx, account) if err != nil { return nil, nil } - req := make([]queries.GetAllUtxosRow, 0, len(utxos)) - for _, v := range utxos { - req = append( - req, - toGetAllUtxosRow(v), - ) - + query := make([]queries.GetAllUtxosRow, 0, len(rows)) + for _, row := range rows { + query = append(query, toGetAllUtxosRow(row)) } - utxosByKey, err := u.convertToUtxos(req) + utxosByKey, err := u.convertToUtxos(query) if err != nil { return nil, nil } - for _, v := range utxosByKey { - if !v.IsLocked() && v.IsConfirmed() && !v.IsSpent() { - resp = append(resp, v) + indexedScripts := make(map[string]struct{}) + if len(scripts) > 0 { + for _, script := range scripts { + indexedScripts[hex.EncodeToString(script)] = struct{}{} } } - return resp, nil + for _, utxo := range utxosByKey { + if !utxo.IsLocked() && utxo.IsConfirmed() && !utxo.IsSpent() { + if len(indexedScripts) <= 0 { + utxos = append(utxos, utxo) + continue + } + + if _, ok := indexedScripts[hex.EncodeToString(utxo.Script)]; ok { + utxos = append(utxos, utxo) + } + } + } + + return utxos, nil } func (u *utxoRepositoryPg) GetLockedUtxosForAccount( - ctx context.Context, account string, + ctx context.Context, account string, scripts ...[]byte, ) ([]*domain.Utxo, error) { - resp := make([]*domain.Utxo, 0) - utxos, err := u.querier.GetUtxosForAccount(ctx, account) + utxos := make([]*domain.Utxo, 0) + rows, err := u.querier.GetUtxosForAccount(ctx, account) if err != nil { return nil, nil } - req := make([]queries.GetAllUtxosRow, 0, len(utxos)) - for _, v := range utxos { - req = append( - req, - toGetAllUtxosRow(v), - ) - + query := make([]queries.GetAllUtxosRow, 0, len(rows)) + for _, v := range rows { + query = append(query, toGetAllUtxosRow(v)) } - utxosByKey, err := u.convertToUtxos(req) + utxosByKey, err := u.convertToUtxos(query) if err != nil { return nil, nil } - for _, v := range utxosByKey { - if v.IsLocked() { - resp = append(resp, v) + indexedScripts := make(map[string]struct{}) + if len(scripts) > 0 { + for _, script := range scripts { + indexedScripts[hex.EncodeToString(script)] = struct{}{} } } - return resp, nil + for _, utxo := range utxosByKey { + if utxo.IsLocked() { + if len(indexedScripts) <= 0 { + utxos = append(utxos, utxo) + continue + } + + if _, ok := indexedScripts[hex.EncodeToString(utxo.Script)]; ok { + utxos = append(utxos, utxo) + } + } + } + + return utxos, nil } func (u *utxoRepositoryPg) GetBalanceForAccount( diff --git a/internal/interfaces/grpc/handler/account.go b/internal/interfaces/grpc/handler/account.go index 421c6fb..6b1681b 100644 --- a/internal/interfaces/grpc/handler/account.go +++ b/internal/interfaces/grpc/handler/account.go @@ -173,8 +173,9 @@ func (a *account) ListUtxos( if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } + addresses := req.GetAddresses() - utxosInfo, err := a.appSvc.ListUtxosForAccount(ctx, name) + utxosInfo, err := a.appSvc.ListUtxosForAccount(ctx, name, addresses) if err != nil { return nil, err }