diff --git a/internal/core/application/account_service.go b/internal/core/application/account_service.go index 9465663..f063a72 100644 --- a/internal/core/application/account_service.go +++ b/internal/core/application/account_service.go @@ -160,7 +160,7 @@ func (as *AccountService) GetBalanceForAccount( } func (as *AccountService) ListUtxosForAccount( - ctx context.Context, accountName string, + ctx context.Context, accountName string, scripts [][]byte, ) (*UtxoInfo, error) { w, err := as.repoManager.WalletRepository().GetWallet(ctx) if err != nil { @@ -173,14 +173,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/application/transaction_service.go b/internal/core/application/transaction_service.go index 12ce586..9359241 100644 --- a/internal/core/application/transaction_service.go +++ b/internal/core/application/transaction_service.go @@ -105,7 +105,7 @@ func (ts *TransactionService) SelectUtxos( } utxos, err := ts.repoManager.UtxoRepository().GetSpendableUtxosForAccount( - ctx, account.Namespace, + ctx, account.Namespace, nil, ) if err != nil { return nil, 0, -1, err @@ -396,7 +396,7 @@ func (ts *TransactionService) Transfer( } utxos, err := utxoRepo.GetSpendableUtxosForAccount( - ctx, account.Namespace, + ctx, account.Namespace, nil, ) if err != nil { return "", err @@ -778,7 +778,7 @@ func (ts *TransactionService) scheduleUtxoUnlocker() { for accountName := range w.Accounts { utxos, _ := utxoRepo.GetLockedUtxosForAccount( - ctx, accountName, + ctx, accountName, nil, ) if len(utxos) > 0 { utxosToUnlock := make([]domain.UtxoKey, 0, len(utxos)) diff --git a/internal/core/domain/utxo_repository.go b/internal/core/domain/utxo_repository.go index a0cd48b..bb43a28 100644 --- a/internal/core/domain/utxo_repository.go +++ b/internal/core/domain/utxo_repository.go @@ -55,10 +55,11 @@ 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) + // Can be filtered by output scripts. + GetSpendableUtxosForAccount(ctx context.Context, account string, scripts [][]byte) ([]*Utxo, error) // GetLockedUtxosForAccount returns the list of all currently locked utxos - // for the given account. - GetLockedUtxosForAccount(ctx context.Context, account string) ([]*Utxo, error) + // for the given account. Can be filtered by output scripts. + 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..0995d52 100644 --- a/internal/infrastructure/storage/db/badger/utxo_repository.go +++ b/internal/infrastructure/storage/db/badger/utxo_repository.go @@ -1,6 +1,7 @@ package dbbadger import ( + "bytes" "context" "fmt" "sync" @@ -69,7 +70,7 @@ func (r *utxoRepository) GetSpendableUtxos( ctx context.Context, ) ([]*domain.Utxo, error) { query := badgerhold.Where("SpentStatus").Eq(domain.UtxoStatus{}). - And("ConfirmedStatus").Ne(domain.UtxoStatus{}).And("LockTimestamp").Eq(int64(0)) + And("LockTimestamp").Eq(int64(0)) return r.findUtxos(ctx, query) } @@ -83,22 +84,57 @@ 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 + } + + filteredUtxos := make([]*domain.Utxo, 0, len(utxos)) + for _, u := range utxos { + for _, script := range scripts { + if bytes.Equal(u.Script, script) { + filteredUtxos = append(filteredUtxos, u) + break + } + } + } + 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 + } + + filteredUtxos := make([]*domain.Utxo, 0, len(utxos)) + for _, u := range utxos { + for _, script := range scripts { + if bytes.Equal(u.Script, script) { + filteredUtxos = append(filteredUtxos, u) + break + } + } + } + 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..0f5b593 100644 --- a/internal/infrastructure/storage/db/inmemory/utxo_repository.go +++ b/internal/infrastructure/storage/db/inmemory/utxo_repository.go @@ -1,6 +1,7 @@ package inmemory import ( + "bytes" "context" "sync" @@ -84,25 +85,25 @@ func (r *utxoRepository) GetAllUtxosForAccount( r.store.lock.RLock() defer r.store.lock.RUnlock() - return r.getUtxosForAccount(account, false, false) + return r.getUtxosForAccount(account, false, false, nil) } 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) + return r.getUtxosForAccount(account, true, false, scripts) } 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) + return r.getUtxosForAccount(account, false, true, scripts) } func (r *utxoRepository) GetBalanceForAccount( @@ -111,7 +112,7 @@ func (r *utxoRepository) GetBalanceForAccount( r.store.lock.RLock() defer r.store.lock.RUnlock() - utxos, _ := r.getUtxosForAccount(account, false, false) + utxos, _ := r.getUtxosForAccount(account, false, false, nil) balance := make(map[string]*domain.Balance) for _, u := range utxos { if u.IsSpent() { @@ -228,7 +229,7 @@ func (r *utxoRepository) getUtxos(spendableOnly bool) []*domain.Utxo { utxos := make([]*domain.Utxo, 0, len(r.store.utxos)) for _, u := range r.store.utxos { if spendableOnly { - if !u.IsLocked() && u.IsConfirmed() && !u.IsSpent() { + if !u.IsLocked() && !u.IsSpent() { utxos = append(utxos, u) } continue @@ -239,7 +240,7 @@ func (r *utxoRepository) getUtxos(spendableOnly bool) []*domain.Utxo { } func (r *utxoRepository) getUtxosForAccount( - account string, spendableOnly, lockedOnly bool, + account string, spendableOnly, lockedOnly bool, scripts [][]byte, ) ([]*domain.Utxo, error) { keys := r.store.utxosByAccount[account] if len(keys) == 0 { @@ -251,14 +252,14 @@ func (r *utxoRepository) getUtxosForAccount( u := r.store.utxos[k.Hash()] if spendableOnly { - if !u.IsLocked() && u.IsConfirmed() && !u.IsSpent() { + if !u.IsLocked() && !u.IsSpent() { utxos = append(utxos, u) } continue } if lockedOnly { - if u.IsLocked() { + if u.IsLocked() && !u.IsSpent() { utxos = append(utxos, u) } continue @@ -266,7 +267,20 @@ func (r *utxoRepository) getUtxosForAccount( utxos = append(utxos, u) } - return utxos, nil + if len(scripts) <= 0 { + return utxos, nil + } + + filteredUtxos := make([]*domain.Utxo, 0, len(utxos)) + for _, u := range utxos { + for _, script := range scripts { + if bytes.Equal(u.Script, script) { + filteredUtxos = append(filteredUtxos, u) + break + } + } + } + return filteredUtxos, nil } func (r *utxoRepository) spendUtxos( diff --git a/internal/infrastructure/storage/db/postgres/utxo_repository.go b/internal/infrastructure/storage/db/postgres/utxo_repository.go index ad9eb65..97f0c1f 100644 --- a/internal/infrastructure/storage/db/postgres/utxo_repository.go +++ b/internal/infrastructure/storage/db/postgres/utxo_repository.go @@ -1,6 +1,7 @@ package postgresdb import ( + "bytes" "context" "database/sql" "sync" @@ -229,7 +230,7 @@ func (u *utxoRepositoryPg) GetSpendableUtxos( } for _, v := range utxosByKey { - if !v.IsLocked() && v.IsConfirmed() && !v.IsSpent() { + if !v.IsLocked() && !v.IsSpent() { resp = append(resp, v) } } @@ -268,7 +269,7 @@ 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) @@ -291,8 +292,17 @@ func (u *utxoRepositoryPg) GetSpendableUtxosForAccount( } for _, v := range utxosByKey { - if !v.IsLocked() && v.IsConfirmed() && !v.IsSpent() { - resp = append(resp, v) + if !v.IsLocked() && !v.IsSpent() { + found := len(scripts) <= 0 + for _, script := range scripts { + if bytes.Equal(v.Script, script) { + found = true + break + } + } + if found { + resp = append(resp, v) + } } } @@ -300,7 +310,7 @@ func (u *utxoRepositoryPg) GetSpendableUtxosForAccount( } 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) @@ -323,8 +333,17 @@ func (u *utxoRepositoryPg) GetLockedUtxosForAccount( } for _, v := range utxosByKey { - if v.IsLocked() { - resp = append(resp, v) + if v.IsLocked() && !v.IsSpent() { + found := len(scripts) <= 0 + for _, script := range scripts { + if bytes.Equal(v.Script, script) { + found = true + break + } + } + if found { + resp = append(resp, v) + } } } diff --git a/internal/infrastructure/storage/db/test/utxo_repository_test.go b/internal/infrastructure/storage/db/test/utxo_repository_test.go index deb6531..b066a42 100644 --- a/internal/infrastructure/storage/db/test/utxo_repository_test.go +++ b/internal/infrastructure/storage/db/test/utxo_repository_test.go @@ -80,13 +80,31 @@ func testAddAndGetUtxos(t *testing.T, repo domain.UtxoRepository) { utxos, err = repo.GetSpendableUtxos(ctx) require.NoError(t, err) - require.Empty(t, utxos) + confirmedUtxos, unconfirmedUtxos := make([]*domain.Utxo, 0), make([]*domain.Utxo, 0) + for _, u := range utxos { + if u.IsConfirmed() { + confirmedUtxos = append(confirmedUtxos, u) + continue + } + unconfirmedUtxos = append(unconfirmedUtxos, u) + } + require.Empty(t, confirmedUtxos) + require.Len(t, unconfirmedUtxos, len(newUtxos)) - utxos, err = repo.GetSpendableUtxosForAccount(ctx, accountName) + utxos, err = repo.GetSpendableUtxosForAccount(ctx, accountName, nil) require.NoError(t, err) - require.Empty(t, utxos) + confirmedUtxos, unconfirmedUtxos = make([]*domain.Utxo, 0), make([]*domain.Utxo, 0) + for _, u := range utxos { + if u.IsConfirmed() { + confirmedUtxos = append(confirmedUtxos, u) + continue + } + unconfirmedUtxos = append(unconfirmedUtxos, u) + } + require.Empty(t, confirmedUtxos) + require.Len(t, unconfirmedUtxos, len(newUtxos)) - utxos, err = repo.GetLockedUtxosForAccount(ctx, accountName) + utxos, err = repo.GetLockedUtxosForAccount(ctx, accountName, nil) require.NoError(t, err) require.Empty(t, utxos) @@ -138,11 +156,29 @@ func testConfirmUtxos(t *testing.T, repo domain.UtxoRepository) { utxos, err := repo.GetSpendableUtxos(ctx) require.NoError(t, err) - require.Len(t, utxos, len(newUtxos)) + confirmedUtxos, unconfirmedUtxos := make([]*domain.Utxo, 0), make([]*domain.Utxo, 0) + for _, u := range utxos { + if u.IsConfirmed() { + confirmedUtxos = append(confirmedUtxos, u) + continue + } + unconfirmedUtxos = append(unconfirmedUtxos, u) + } + require.Empty(t, unconfirmedUtxos) + require.Len(t, confirmedUtxos, len(newUtxos)) - utxos, err = repo.GetSpendableUtxosForAccount(ctx, accountName) + utxos, err = repo.GetSpendableUtxosForAccount(ctx, accountName, nil) require.NoError(t, err) - require.Len(t, utxos, len(newUtxos)) + confirmedUtxos, unconfirmedUtxos = make([]*domain.Utxo, 0), make([]*domain.Utxo, 0) + for _, u := range utxos { + if u.IsConfirmed() { + confirmedUtxos = append(confirmedUtxos, u) + continue + } + unconfirmedUtxos = append(unconfirmedUtxos, u) + } + require.Empty(t, unconfirmedUtxos) + require.Len(t, confirmedUtxos, len(newUtxos)) utxoBalance, err := repo.GetBalanceForAccount(ctx, accountName) require.NoError(t, err) @@ -166,7 +202,7 @@ func testLockUtxos(t *testing.T, repo domain.UtxoRepository) { require.NoError(t, err) require.Zero(t, count) - utxos, err := repo.GetLockedUtxosForAccount(ctx, accountName) + utxos, err := repo.GetLockedUtxosForAccount(ctx, accountName, nil) require.NoError(t, err) require.Len(t, utxos, len(newUtxos)) @@ -174,7 +210,7 @@ func testLockUtxos(t *testing.T, repo domain.UtxoRepository) { require.NoError(t, err) require.Empty(t, utxos) - utxos, err = repo.GetSpendableUtxosForAccount(ctx, accountName) + utxos, err = repo.GetSpendableUtxosForAccount(ctx, accountName, nil) require.NoError(t, err) require.Empty(t, utxos) @@ -200,7 +236,7 @@ func testUnlockUtxos(t *testing.T, repo domain.UtxoRepository) { require.NoError(t, err) require.Zero(t, count) - utxos, err := repo.GetLockedUtxosForAccount(ctx, accountName) + utxos, err := repo.GetLockedUtxosForAccount(ctx, accountName, nil) require.NoError(t, err) require.Empty(t, utxos) @@ -208,7 +244,7 @@ func testUnlockUtxos(t *testing.T, repo domain.UtxoRepository) { require.NoError(t, err) require.Len(t, utxos, len(newUtxos)) - utxos, err = repo.GetSpendableUtxosForAccount(ctx, accountName) + utxos, err = repo.GetSpendableUtxosForAccount(ctx, accountName, nil) require.NoError(t, err) require.Len(t, utxos, len(newUtxos)) @@ -238,7 +274,7 @@ func testSpendUtxos(t *testing.T, repo domain.UtxoRepository) { require.NoError(t, err) require.Empty(t, utxos) - utxos, err = repo.GetSpendableUtxosForAccount(ctx, accountName) + utxos, err = repo.GetSpendableUtxosForAccount(ctx, accountName, nil) require.NoError(t, err) require.Empty(t, utxos) @@ -268,7 +304,7 @@ func testConfirmSpentUtxos(t *testing.T, repo domain.UtxoRepository) { require.NoError(t, err) require.Empty(t, utxos) - utxos, err = repo.GetSpendableUtxosForAccount(ctx, accountName) + utxos, err = repo.GetSpendableUtxosForAccount(ctx, accountName, nil) require.NoError(t, err) require.Empty(t, utxos) diff --git a/internal/interfaces/grpc/handler/account.go b/internal/interfaces/grpc/handler/account.go index 421c6fb..a26f94d 100644 --- a/internal/interfaces/grpc/handler/account.go +++ b/internal/interfaces/grpc/handler/account.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/vulpemventures/go-elements/address" pb "github.com/vulpemventures/ocean/api-spec/protobuf/gen/go/ocean/v1" "github.com/vulpemventures/ocean/internal/core/application" "google.golang.org/grpc/codes" @@ -173,8 +174,17 @@ func (a *account) ListUtxos( if err != nil { return nil, status.Error(codes.InvalidArgument, err.Error()) } + addresses := req.GetAddresses() + scripts := make([][]byte, 0, len(addresses)) + for _, addr := range addresses { + script, err := address.ToOutputScript(addr) + if err != nil { + return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid address %s", addr)) + } + scripts = append(scripts, script) + } - utxosInfo, err := a.appSvc.ListUtxosForAccount(ctx, name) + utxosInfo, err := a.appSvc.ListUtxosForAccount(ctx, name, scripts) if err != nil { return nil, err }