Skip to content

Commit

Permalink
Notify for utxos spent with and without confirmation (#80)
Browse files Browse the repository at this point in the history
* Update core & utxo repo impls to handle utxos spent and utxo spent with confirmation

* Make electrum client notifiy for "spent" and "spent with confirmation" utxos

* Update interface
  • Loading branch information
altafan authored Feb 1, 2024
1 parent efddeef commit 8f3f6d2
Show file tree
Hide file tree
Showing 20 changed files with 514 additions and 175 deletions.
71 changes: 38 additions & 33 deletions api-spec/protobuf/gen/go/ocean/v1/types.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api-spec/protobuf/ocean/v1/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ enum UtxoEventType {
UTXO_EVENT_TYPE_LOCKED = 3;
UTXO_EVENT_TYPE_UNLOCKED = 4;
UTXO_EVENT_TYPE_SPENT = 5;
UTXO_EVENT_TYPE_CONFIRMED_SPENT = 6;
}

enum WebhookEventType {
Expand Down
19 changes: 18 additions & 1 deletion internal/core/application/account_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,26 @@ func (as *AccountService) listenToUtxoChannel(
for _, u := range utxos {
utxoKeys = append(utxoKeys, u.Key())
}

if utxos[0].IsConfirmedSpent() {
count, err := as.repoManager.UtxoRepository().ConfirmSpendUtxos(
context.Background(), utxoKeys, utxos[0].SpentStatus,
)
if err != nil {
as.warn(
err, "error while updating utxos status to confirmed spend for account %s",
accountName,
)
}
if count > 0 {
as.log("confirmed spend of %d utxos for account %s", count, accountName)
}
continue
}

if utxos[0].IsSpent() {
count, err := as.repoManager.UtxoRepository().SpendUtxos(
context.Background(), utxoKeys, utxos[0].SpentStatus,
context.Background(), utxoKeys, utxos[0].SpentStatus.Txid,
)
if err != nil {
as.warn(
Expand Down
4 changes: 2 additions & 2 deletions internal/core/application/account_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ func TestAccountService(t *testing.T) {

// Simulate withdrawing all funds by spending every spendable utxo coming
// from ListUtxosForAccount.
status := domain.UtxoStatus{hex.EncodeToString(make([]byte, 32)), 1, 0, ""}
_, err = repoManager.UtxoRepository().SpendUtxos(ctx, utxos.Spendable.Keys(), status)
txid := hex.EncodeToString(make([]byte, 32))
_, err = repoManager.UtxoRepository().SpendUtxos(ctx, utxos.Spendable.Keys(), txid)
require.NoError(t, err)

// Now deleting the account should work without errors.
Expand Down
12 changes: 10 additions & 2 deletions internal/core/application/notification_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,18 @@ func (ns *NotificationService) listenToUtxoChannel(
ns.log("start listening to utxo channel for script %s", scriptHash)

for utxos := range chUtxos {
eventType := domain.UtxoAdded
if utxos[0].IsSpent() {
var eventType domain.UtxoEventType
switch {
case utxos[0].IsConfirmedSpent():
eventType = domain.UtxoConfirmedSpend
case utxos[0].IsSpent():
eventType = domain.UtxoSpent
case utxos[0].IsConfirmed():
eventType = domain.UtxoConfirmed
default:
eventType = domain.UtxoAdded
}

utxoInfo := make([]domain.UtxoInfo, 0, len(utxos))
for _, u := range utxos {
utxoInfo = append(utxoInfo, u.Info())
Expand Down
4 changes: 2 additions & 2 deletions internal/core/application/notification_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ func testGetUtxoChannel(t *testing.T) {

time.Sleep(time.Second)

status := domain.UtxoStatus{hex.EncodeToString(make([]byte, 32)), 1, 0, ""}
repoManager.UtxoRepository().SpendUtxos(ctx, keys, status)
txid := hex.EncodeToString(make([]byte, 32))
repoManager.UtxoRepository().SpendUtxos(ctx, keys, txid)

time.Sleep(time.Second)

Expand Down
29 changes: 25 additions & 4 deletions internal/core/domain/utxo.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,16 @@ type Utxo struct {
ConfirmedStatus UtxoStatus
}

// IsSpent returns whether the utxo have been spent.
// IsSpent returns whether the utxo is spent by a tx in mempool.
func (u *Utxo) IsSpent() bool {
return u.SpentStatus != UtxoStatus{}
}

// IsConfirmedSpent returns whether the utxo is spent by a tx in blockchain.
func (u *Utxo) IsConfirmedSpent() bool {
return u.IsSpent() && len(u.SpentStatus.BlockHash) > 0
}

// IsConfirmed returns whether the utxo is confirmed.
func (u *Utxo) IsConfirmed() bool {
return u.ConfirmedStatus != UtxoStatus{}
Expand Down Expand Up @@ -133,12 +138,28 @@ func (u *Utxo) Info() UtxoInfo {
}
}

// Spend marks the utxos as spent.
func (u *Utxo) Spend(status UtxoStatus) error {
// Spend marks the utxos as spent and resets the lock timestamp.
func (u *Utxo) Spend(txid string) error {
if u.IsSpent() {
return nil
}

if len(txid) <= 0 {
return fmt.Errorf("missing txid")
}
u.SpentStatus = UtxoStatus{
Txid: txid,
}
u.LockTimestamp = 0
return nil
}

// ConfirmSpend adds confirmation (block) info to a spent utxo.
func (u *Utxo) ConfirmSpend(status UtxoStatus) error {
if u.IsConfirmedSpent() {
return nil
}

emptyStatus := UtxoStatus{}
if status == emptyStatus {
return fmt.Errorf("status must not be empty")
Expand All @@ -149,8 +170,8 @@ func (u *Utxo) Spend(status UtxoStatus) error {
if status.BlockHeight == 0 && status.BlockTime == 0 && status.BlockHash == "" {
return fmt.Errorf("missing block info")
}

u.SpentStatus = status
u.LockTimestamp = 0
return nil
}

Expand Down
19 changes: 12 additions & 7 deletions internal/core/domain/utxo_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ const (
UtxoLocked
UtxoUnlocked
UtxoSpent
UtxoConfirmedSpend
)

var (
utxoTypeString = map[UtxoEventType]string{
UtxoAdded: "UtxoAdded",
UtxoConfirmed: "UtxoConfirmed",
UtxoLocked: "UtxoLocked",
UtxoUnlocked: "UtxoUnlocked",
UtxoSpent: "UtxoSpent",
UtxoAdded: "UtxoAdded",
UtxoConfirmed: "UtxoConfirmed",
UtxoLocked: "UtxoLocked",
UtxoUnlocked: "UtxoUnlocked",
UtxoSpent: "UtxoSpent",
UtxoConfirmedSpend: "UtxoConfirmedSpend",
}
)

Expand Down Expand Up @@ -60,9 +62,12 @@ type UtxoRepository interface {
// 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)
// SpendUtxos updates the status of the given list of utxos to "spent".
// SpendUtxos updates the status of the given list of utxos to "spent" by the given txid.
// Generates a UtxoSpent event if successfull.
SpendUtxos(ctx context.Context, utxoKeys []UtxoKey, status UtxoStatus) (int, error)
SpendUtxos(ctx context.Context, utxoKeys []UtxoKey, txid string) (int, error)
// ConfirmSpendUtxos updates the status of the given list of utxos to "confirmed spend".
// Generates a UtxoConfirmedSpend event if successfull.
ConfirmSpendUtxos(ctx context.Context, utxoKeys []UtxoKey, status UtxoStatus) (int, error)
// ConfirmUtxos updates the status of the given list of utxos to "confirmed".
// Generates a UtxoConfirmed event if successfull.
ConfirmUtxos(ctx context.Context, utxoKeys []UtxoKey, status UtxoStatus) (int, error)
Expand Down
24 changes: 23 additions & 1 deletion internal/core/domain/utxo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,33 @@ func TestSpendUtxo(t *testing.T) {
u := domain.Utxo{}
require.False(t, u.IsSpent())

err := u.Spend(domain.UtxoStatus{hex.EncodeToString(make([]byte, 32)), 1, 0, ""})
err := u.Spend(hex.EncodeToString(make([]byte, 32)))
require.NoError(t, err)
require.True(t, u.IsSpent())
}

func TestConfirmSpendUtxo(t *testing.T) {
t.Parallel()

u := domain.Utxo{}
require.False(t, u.IsConfirmedSpent())

err := u.Spend(hex.EncodeToString(make([]byte, 32)))
require.NoError(t, err)
require.True(t, u.IsSpent())
require.False(t, u.IsConfirmedSpent())

err = u.ConfirmSpend(domain.UtxoStatus{
Txid: hex.EncodeToString(make([]byte, 32)),
BlockHeight: 1,
BlockTime: time.Now().Unix(),
BlockHash: hex.EncodeToString(make([]byte, 32)),
})
require.NoError(t, err)
require.True(t, u.IsSpent())
require.True(t, u.IsConfirmedSpent())
}

func TestConfirmUtxo(t *testing.T) {
t.Parallel()

Expand Down
Loading

0 comments on commit 8f3f6d2

Please sign in to comment.