Skip to content

Commit 05baf2c

Browse files
Feat(#591) : add monitor task unfail failed txs (#593)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent da04ff3 commit 05baf2c

21 files changed

+700
-2
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
sdk "github.com/bsv-blockchain/go-sdk/wallet"
8+
"github.com/bsv-blockchain/go-wallet-toolbox/examples/internal/example_setup"
9+
"github.com/bsv-blockchain/go-wallet-toolbox/examples/internal/show"
10+
)
11+
12+
var (
13+
// DefaultLimit is the default number of actions to retrieve
14+
DefaultLimit = uint32(100)
15+
16+
// DefaultOffset is the default starting position for pagination
17+
DefaultOffset = uint32(0)
18+
19+
// DefaultOriginator specifies the originator domain or FQDN used to identify the source of the request.
20+
DefaultOriginator = "example.com"
21+
22+
// DefaultIncludeLabels determines whether to include labels in the response
23+
DefaultIncludeLabels = true
24+
25+
// DefaultUnfail determines whether to request unfail processing for returned failed actions
26+
DefaultUnfail = false
27+
)
28+
29+
// defaultListFailedActionsArgs creates default arguments for listing failed wallet actions
30+
func defaultListFailedActionsArgs() sdk.ListActionsArgs {
31+
return sdk.ListActionsArgs{
32+
Limit: &DefaultLimit,
33+
Offset: &DefaultOffset,
34+
IncludeLabels: &DefaultIncludeLabels,
35+
}
36+
}
37+
38+
// This example demonstrates how to list failed actions for the Alice wallet
39+
func main() {
40+
show.ProcessStart("List Failed Actions")
41+
42+
ctx := context.Background()
43+
44+
// Create Alice's wallet instance
45+
alice := example_setup.CreateAlice()
46+
47+
// Create the wallet interface and establish database connection
48+
aliceWallet, cleanup := alice.CreateWallet(ctx)
49+
defer cleanup()
50+
51+
show.Step("Alice", "Listing failed actions")
52+
53+
// Configure pagination and filtering parameters
54+
args := defaultListFailedActionsArgs()
55+
show.Info("ListFailedActionsArgs", args)
56+
show.Info("Unfail", DefaultUnfail)
57+
show.Separator()
58+
59+
// Retrieve paginated list of failed wallet actions
60+
actions, err := aliceWallet.ListFailedActions(ctx, args, DefaultUnfail, DefaultOriginator)
61+
if err != nil {
62+
panic(fmt.Errorf("failed to list failed actions: %w", err))
63+
}
64+
65+
show.Info("FailedActions", actions)
66+
show.ProcessComplete("List Failed Actions")
67+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# List Failed Actions Example
2+
3+
This example demonstrates how to retrieve a paginated list of failed wallet actions and optionally request recovery ("unfail") in the Go Wallet Toolbox SDK. Failed actions are transactions or actions that did not complete successfully.
4+
5+
## Overview
6+
7+
The process involves several steps:
8+
1. Creating a wallet instance and establishing database connection.
9+
2. Configuring pagination and display parameters.
10+
3. Calling the wallet's `ListFailedActions` method with the configured arguments and an `unfail` flag.
11+
4. Processing and displaying the returned failed actions, including total count and details.
12+
13+
When `unfail` is set to true, the call requests recovery for the returned failed actions, promoting them to be reprocessed by the unfail workflow.
14+
15+
## Code Walkthrough
16+
17+
### Configuration Parameters
18+
19+
The example uses the following configurable constants:
20+
21+
- **`DefaultLimit`**: Maximum number of actions to return per request (default: `100`)
22+
- **`DefaultOffset`**: Starting position for pagination (default: `0`)
23+
- **`DefaultOriginator`**: The originator domain or FQDN allowed to use this permission (default: `"example.com"`)
24+
- **`DefaultIncludeLabels`**: Whether to include labels in the response (default: `true`)
25+
- **`DefaultUnfail`**: Whether to request promotion of the listed failed actions to "unfail" (default: `false`)
26+
27+
### Request Parameters
28+
29+
`ListFailedActions` reuses `ListActionsArgs` for pagination and inclusion flags:
30+
31+
- **`Limit`**: Controls how many actions to retrieve in a single request
32+
- **`Offset`**: Specifies the starting position for pagination
33+
- **`IncludeLabels`**: Include action labels in the response
34+
35+
### Behavior Notes
36+
37+
- The wallet injects a reserved spec-op label for "failed actions" so storage filters by status = failed.
38+
- If `unfail = true`, a control label is also included; after fetching results, storage promotes those TXIDs for the unfail processing flow (best-effort).
39+
40+
## Running the Example
41+
42+
To run this example:
43+
44+
```bash
45+
go run ./examples/wallet_examples/list_failed_actions/list_failed_actions.go
46+
```
47+
48+
## Expected Output
49+
50+
```text
51+
🚀 STARTING: List Failed Actions
52+
============================================================
53+
Using remote storage: http://localhost:8100
54+
CreateWallet: 02a675b6767bf17f8d37755d4afb8dcea49f79c0ae696f0f59c0b38154e482520f
55+
56+
=== STEP ===
57+
Alice is performing: Listing failed actions
58+
--------------------------------------------------
59+
ListFailedActionsArgs: {Labels:[] LabelQueryMode: IncludeLabels:0x7ff7033e34e0 IncludeInputs:<nil> IncludeInputSourceLockingScripts:<nil> IncludeInputUnlockingScripts:<nil> IncludeOutputs:<nil> IncludeOutputLockingScripts:<nil> Limit:0x7ff7033e3548 Offset:0x7ff7042fee4c SeekPermission:<nil>}
60+
Unfail: true
61+
============================================================
62+
FailedActions: &{TotalActions:2 Actions:[{Txid:225cc7b2be25be0c3cc032d09fd8035128c9d8e2f0d32db6ab7d875648263bb4 Satoshis:-38 Status:failed IsOutgoing:true Description:mintPushDropToken Labels:[mintPushDropToken] Version:1 LockTime:0 Inputs:[] Outputs:[]} {Txid:568f51aa28dca612ee18351695c6a7e3f22a4a468909a5493442ed01e68e922f Satoshis:-38 Status:failed IsOutgoing:true Description:mintPushDropToken Labels:[mintPushDropToken] Version:1 LockTime:0 Inputs:[] Outputs:[]}]}
63+
============================================================
64+
🎉 COMPLETED: List Failed Actions
65+
```
66+
67+
## Integration Steps
68+
69+
To integrate failed-action listing into your application:
70+
71+
1. **Configure pagination parameters** based on your UI needs (page size, starting offset).
72+
2. **Set the originator identifier** to your application's domain or identifier.
73+
3. **Choose label inclusion** if you need metadata for display or filtering.
74+
4. **Decide on `unfail` behavior**: set to `true` to request recovery of the listed failed actions.
75+
5. **Call `ListFailedActions`** on your wallet instance with the arguments and `unfail` flag.
76+
6. **Handle the response** and show failed actions to the user.
77+
7. **Monitor recovery**: when `unfail` is requested, the background unfail flow will attempt to move those actions forward.
78+
79+
## Additional Resources
80+
81+
- [List Failed Actions Code](./list_failed_actions.go) - Complete code example for listing failed actions
82+
- [List Actions Documentation](../list_actions/list_actions.md) - General action listing doc
83+
84+

infra-config.example.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ monitor:
5252
enabled: true
5353
interval_seconds: 300
5454
start_immediately: true
55+
un_fail:
56+
enabled: true
57+
interval_seconds: 600
58+
start_immediately: false
5559
name: go-storage-server
5660
server_private_key: ""
5761
synchronize_tx_statuses:

pkg/defs/monitor.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ const (
2222

2323
// FailAbandonedMonitorTask marks transactions as failed if they have been abandoned or not processed within a set period.
2424
FailAbandonedMonitorTask MonitorTask = "fail_abandoned"
25+
26+
// UnFailMonitorTask is a monitoring task that checks for failed transactions and reverifies failed tx statuses.
27+
UnFailMonitorTask MonitorTask = "un_fail"
2528
)
2629

2730
// ParseMonitorTaskStr parses a string to a MonitorTask or returns an error
2831
func ParseMonitorTaskStr(task string) (MonitorTask, error) {
29-
return parseEnumCaseInsensitive(task, CheckForProofsMonitorTask, SendWaitingMonitorTask, FailAbandonedMonitorTask)
32+
return parseEnumCaseInsensitive(task, CheckForProofsMonitorTask, SendWaitingMonitorTask, FailAbandonedMonitorTask, UnFailMonitorTask)
3033
}
3134

3235
// TaskConfig defines configuration parameters for a monitoring task
@@ -48,6 +51,7 @@ type TasksConfig struct {
4851
CheckForProofs TaskConfig `mapstructure:"check_for_proofs"`
4952
SendWaiting TaskConfig `mapstructure:"send_waiting"`
5053
FailAbandoned TaskConfig `mapstructure:"fail_abandoned"`
54+
UnFail TaskConfig `mapstructure:"un_fail"`
5155
}
5256

5357
func (t *TasksConfig) all() iter.Seq2[MonitorTask, TaskConfig] {
@@ -135,6 +139,10 @@ func DefaultMonitorConfig() Monitor {
135139
Enabled: true,
136140
IntervalSeconds: must.ConvertToUInt((5 * time.Minute).Seconds()),
137141
},
142+
UnFail: TaskConfig{
143+
Enabled: true,
144+
IntervalSeconds: must.ConvertToUInt((10 * time.Minute).Seconds()),
145+
},
138146
},
139147
}
140148
}

pkg/internal/specops/spec_ops.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package specops
2+
3+
// ListActionsSpecOpFailedActionsLabel indicates that listActions should return only actions with status 'failed'.
4+
const ListActionsSpecOpFailedActionsLabel = "97d4eb1e49215e3374cc2c1939a7c43a55e95c7427bf2d45ed63e3b4e0c88153"
5+
6+
// IsListActionsSpecOp returns true if the provided label is a reserved listActions spec-op.
7+
func IsListActionsSpecOp(label string) bool {
8+
return label == ListActionsSpecOpFailedActionsLabel
9+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package validate
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/wdk"
7+
)
8+
9+
func ListFailedActionsArgs(args *wdk.ListFailedActionsArgs) error {
10+
if args == nil {
11+
return fmt.Errorf("args cannot be nil")
12+
}
13+
14+
if args.Limit > MaxPaginationLimit {
15+
return fmt.Errorf("limit must be less than or equal to %d", MaxPaginationLimit)
16+
}
17+
if args.Offset > MaxPaginationOffset {
18+
return fmt.Errorf("offset must be less than or equal to %d", MaxPaginationOffset)
19+
}
20+
21+
if !args.SeekPermissions.Value() {
22+
return fmt.Errorf("operation not allowed without permission (seekPermissions=false)")
23+
}
24+
25+
if !args.IncludeInputs.Value() {
26+
if args.IncludeInputUnlockingScripts.Value() {
27+
return fmt.Errorf("includeInputUnlockingScripts cannot be true when includeInputs is false")
28+
}
29+
if args.IncludeInputSourceLockingScripts.Value() {
30+
return fmt.Errorf("includeInputSourceLockingScripts cannot be true when includeInputs is false")
31+
}
32+
}
33+
34+
if !args.IncludeOutputs.Value() && args.IncludeOutputLockingScripts.Value() {
35+
return fmt.Errorf("includeOutputLockingScripts cannot be true when includeOutputs is false")
36+
}
37+
38+
return nil
39+
}

pkg/monitor/all_tasks.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,8 @@ func (d *Daemon) allTasksFactories() map[defs.MonitorTask]taskFactoryFunc {
1818
defs.FailAbandonedMonitorTask: func() tasks.TaskInterface {
1919
return tasks.NewFailAbandonedTask(d.storage)
2020
},
21+
defs.UnFailMonitorTask: func() tasks.TaskInterface {
22+
return tasks.NewUnFailTask(d.storage)
23+
},
2124
}
2225
}

pkg/monitor/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ type MonitoredStorage interface {
1010
SynchronizeTransactionStatuses(ctx context.Context) error
1111
SendWaitingTransactions(ctx context.Context, minTransactionAge time.Duration) error
1212
AbortAbandoned(ctx context.Context) error
13+
UnFail(ctx context.Context) error
1314
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package tasks
2+
3+
import (
4+
"context"
5+
"fmt"
6+
)
7+
8+
// UnFailTask iterates failed transactions and re-checks their on-chain status.
9+
type UnFailTask struct {
10+
storage UnFailChecker
11+
}
12+
13+
func NewUnFailTask(storage UnFailChecker) TaskInterface {
14+
return &UnFailTask{storage: storage}
15+
}
16+
17+
func (t *UnFailTask) Run(ctx context.Context) error {
18+
if err := t.storage.UnFail(ctx); err != nil {
19+
return fmt.Errorf("check failed transactions failed: %w", err)
20+
}
21+
return nil
22+
}

pkg/monitor/internal/tasks/check_for_proofs.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ type TransactionStatusesSynchronizer interface {
99
SynchronizeTransactionStatuses(ctx context.Context) error
1010
}
1111

12+
// UnFailChecker checks failed transactions against the chain and updates their status if they are found.
13+
type UnFailChecker interface {
14+
UnFail(ctx context.Context) error
15+
}
16+
1217
type CheckForProofsTask struct {
1318
storage TransactionStatusesSynchronizer
1419
}

0 commit comments

Comments
 (0)