Skip to content

Commit

Permalink
feat: Provide export command to export transaction to csv
Browse files Browse the repository at this point in the history
```bash
sid export --start-height 864790 --end-height 864791
```

```csv
Transaction Hash,Staking Output Index,Inclusion Height,Staker Public Key,Staking Time,Finality Provider Public Key,Is Overflow,Staking Value
1b42ce46130a1d4b3bdd56b5cb325976851af1ab76951565aa4858c7d16dad00,0,864790,139f4e3ec192e83b9c6789ff644261b8fa5d7b716d1813bee744e3472f264d99,64000,fa7496f63a857d894aa393767325bf6f84560e9141f4ec54496c50f546f48bfb,false,1905000
3ddfb76b9971b786fc798a98f8fc5edc42a074c47ef28df812a389c16536b401,0,864790,b18ac73a57e6d3413284d1c91c14744464d71f19397c8ab053bc99c1ed96cafe,64000,bb0bceda25d82f10a69feca9c076d85f61d750c9a481b8105d8389325538fdd1,false,500000
967939f50dfbef960a40422843f5e3d749ceb0fba7a94542028f0dfb3e95e501,0,864790,d7c9ab7d619ada29535baec2ff77308b9014ef1c285a09fb175aa1c3e6d73f90,64000,68db13ba45241bcc1db1e63d998d78d9fce4b3f42772ef355e007a1105100476,false,500000000
27f7e4995c85ce1dc634bc6d838a7f126232a7d2459dc9f572abae56fbb0f502,0,864790,d68f068a7f1889bea28c2706bba437459eba45e9d44c49665585850be4b32e55,64000,609b4b8e27e214fd830e69a83a8270a03f7af356f64dde433a7e4b81b2399806,false,999994180
d3cb7879444d85543f73f1c51b16bbb5837383ff99d30af04d4f79a6e4a52703,0,864790,7c2e13aed3377c7781c2bd501deb638ba5ed02ad1bf757348004cf71f306f7ed,64000,db9160428e401753dc1a9952ffd4fa3386c7609cf8411d2b6d79c42323ca9923,false,26500000
```

Table

| Transaction Hash | Staking Output Index | Inclusion Height | Staker
Public Key | Staking Time | Finality Provider Public Key | Is Overflow |
Staking Value |

|------------------|----------------------|------------------|-------------------|--------------|------------------------------|-------------|---------------|
| 1b42ce46130a1d4b3bdd56b5cb325976851af1ab76951565aa4858c7d16dad00 | 0 |
864790 |
139f4e3ec192e83b9c6789ff644261b8fa5d7b716d1813bee744e3472f264d99 | 64000
| fa7496f63a857d894aa393767325bf6f84560e9141f4ec54496c50f546f48bfb |
false | 1905000 |
| 3ddfb76b9971b786fc798a98f8fc5edc42a074c47ef28df812a389c16536b401 | 0 |
864790 |
b18ac73a57e6d3413284d1c91c14744464d71f19397c8ab053bc99c1ed96cafe | 64000
| bb0bceda25d82f10a69feca9c076d85f61d750c9a481b8105d8389325538fdd1 |
false | 500000 |
| 967939f50dfbef960a40422843f5e3d749ceb0fba7a94542028f0dfb3e95e501 | 0 |
864790 |
d7c9ab7d619ada29535baec2ff77308b9014ef1c285a09fb175aa1c3e6d73f90 | 64000
| 68db13ba45241bcc1db1e63d998d78d9fce4b3f42772ef355e007a1105100476 |
false | 500000000 |

Screenshot

<img width="1875" alt="Screenshot 2024-10-15 at 22 16 26"
src="https://github.com/user-attachments/assets/4c8aae75-a687-4e9d-be98-07709868800b">

---------

Co-authored-by: Rafael Tenfen <[email protected]>
Co-authored-by: Cirrus Gai <[email protected]>
  • Loading branch information
3 people authored Oct 17, 2024
1 parent 2d087b7 commit bad6003
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
gosec-args: "-no-fail ./..."

docker_pipeline:
uses: babylonlabs-io/.github/.github/workflows/reusable_docker_pipeline.yml@v0.6.0
uses: babylonlabs-io/.github/.github/workflows/reusable_docker_pipeline.yml@v0.8.0
secrets: inherit
with:
publish: false
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Staking protocol and serves as the ground truth for the Bitcoin Staking system.
Our [API service](https://github.com/babylonlabs-io/staking-api-service)
exhibits how these events are utilized and presented.
6. Monitoring the status of the service through [Prometheus metrics](./doc/metrics.md).
7. Exporting staking transactions from the indexer store to a CSV file.

## Usage

Expand Down Expand Up @@ -100,6 +101,16 @@ earliest `activation_height`. If the database is not empty, the user can specify
a height that is not higher than `last_processed_height + 1` via `--start-height`.
This is to ensure that no staking data will be missed.

### 5. Exporting staking transactions

We can export the indexed staking transactions via the command:

```bash
sid export <start-height> <end-height> --output transactions.csv
```

![export](./doc/staking_export.png)

### Tests

Run unit tests:
Expand Down
139 changes: 139 additions & 0 deletions cmd/sid/cli/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package cli

import (
"encoding/csv"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strconv"

"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/urfave/cli"

"github.com/babylonlabs-io/staking-indexer/config"
"github.com/babylonlabs-io/staking-indexer/indexerstore"
"github.com/babylonlabs-io/staking-indexer/utils"
)

const (
defaultTxExportOutputFileName = "transactions.csv"
)

var ExportCommand = cli.Command{
Name: "export",
Usage: "Export transactions from the indexer store to a CSV file based on block height.",
UsageText: fmt.Sprintf("export [start-height] [end-height] [--%s=path/to/%s]", defaultTxExportOutputFileName, outputFileFlag),
Flags: []cli.Flag{
cli.StringFlag{
Name: homeFlag,
Usage: "The path to the staking indexer home directory",
Value: config.DefaultHomeDir,
},
cli.StringFlag{
Name: outputFileFlag,
Usage: "Path to the export file",
Value: filepath.Join(config.DefaultHomeDir, defaultTxExportOutputFileName),
},
},
Action: exportTransactions,
}

func exportTransactions(c *cli.Context) error {
args := c.Args()
if len(args) != 2 {
return fmt.Errorf("not enough params, please specify [start-height] and [end-height]")
}

startHeightStr, endHeightStr := args[0], args[1]
startHeight, err := strconv.ParseUint(startHeightStr, 10, 64)
if err != nil {
return fmt.Errorf("unable to parse %s: %w", startHeightStr, err)
}

endHeight, err := strconv.ParseUint(endHeightStr, 10, 64)
if err != nil {
return fmt.Errorf("unable to parse %s: %w", endHeightStr, err)
}

if startHeight > endHeight {
return fmt.Errorf("the [start-height] %d should not be greater than the [end-height] %d", startHeight, endHeight)
}

homePath, err := filepath.Abs(c.String(homeFlag))
if err != nil {
return err
}
homePath = utils.CleanAndExpandPath(homePath)

outputPath := c.String("output")
outputPath = utils.CleanAndExpandPath(outputPath)

// Load configuration
cfg, err := config.LoadConfig(homePath)
if err != nil {
return fmt.Errorf("failed to load configuration: %w", err)
}

// Initialize IndexerStore
dbBackend, err := cfg.DatabaseConfig.GetDbBackend()
if err != nil {
return fmt.Errorf("failed to create db backend: %w", err)
}
defer dbBackend.Close()

indexerStore, err := indexerstore.NewIndexerStore(dbBackend)
if err != nil {
return fmt.Errorf("failed to initialize IndexerStore: %w", err)
}

// Open output file
file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer file.Close()

writer := csv.NewWriter(file)
defer writer.Flush()

// Write CSV header
err = writer.Write([]string{"Transaction Hash", "Staking Output Index", "Inclusion Height", "Staker Public Key", "Staking Time", "Finality Provider Public Key", "Is Overflow", "Staking Value"})
if err != nil {
return fmt.Errorf("failed to write CSV header: %w", err)
}

// Get start height and end height parameters

fmt.Printf("Exporting transactions from height %d to %d\n", startHeight, endHeight)

// Export data using ScanStoredStakingTransactions method
err = indexerStore.ScanStoredStakingTransactions(func(tx *indexerstore.StoredStakingTransaction) error {

// Filter based on height parameters
if tx.InclusionHeight >= startHeight && tx.InclusionHeight < endHeight {
fmt.Printf("Exporting transaction %s, InclusionHeight %d\n", tx.Tx.TxHash().String(), tx.InclusionHeight)
record := []string{
tx.Tx.TxHash().String(),
fmt.Sprintf("%d", tx.StakingOutputIdx),
fmt.Sprintf("%d", tx.InclusionHeight),
hex.EncodeToString(schnorr.SerializePubKey(tx.StakerPk)),
fmt.Sprintf("%d", tx.StakingTime),
hex.EncodeToString(schnorr.SerializePubKey(tx.FinalityProviderPk)),
fmt.Sprintf("%t", tx.IsOverflow),
fmt.Sprintf("%d", tx.StakingValue),
}
return writer.Write(record)
}
return nil

})

if err != nil {
return fmt.Errorf("failed to export transactions: %w", err)
}

fmt.Printf("Exporting transactions from %s to %s\n", homePath, outputPath)

return nil
}
2 changes: 1 addition & 1 deletion cmd/sid/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func main() {
app := cli.NewApp()
app.Name = "sid"
app.Usage = "Staking Indexer Daemon (sid)."
app.Commands = append(app.Commands, sidcli.StartCommand, sidcli.InitCommand, sidcli.BtcHeaderCommand)
app.Commands = append(app.Commands, sidcli.StartCommand, sidcli.InitCommand, sidcli.BtcHeaderCommand, sidcli.ExportCommand)

if err := app.Run(os.Args); err != nil {
fatal(err)
Expand Down
Binary file added doc/staking_export.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions indexerstore/indexer_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,30 @@ func (is *IndexerStore) GetStakingTransaction(txHash *chainhash.Hash) (*StoredSt
return storedTx, nil
}

// ScanStoredStakingTransactions iterates through and exports all stored staking transactions
func (is *IndexerStore) ScanStoredStakingTransactions(callback func(*StoredStakingTransaction) error) error {
return is.db.View(func(tx kvdb.RTx) error {
txBucket := tx.ReadBucket(stakingTxBucketName)
if txBucket == nil {
return ErrCorruptedTransactionsDb
}

return txBucket.ForEach(func(k, v []byte) error {
var storedTxProto proto.StakingTransaction
if err := pm.Unmarshal(v, &storedTxProto); err != nil {
return fmt.Errorf("failed to parse staking transaction: %w", err)
}

storedTx, err := protoStakingTxToStoredStakingTx(&storedTxProto)
if err != nil {
return fmt.Errorf("failed to convert staking transaction: %w", err)
}

return callback(storedTx)
})
}, func() {})
}

func protoStakingTxToStoredStakingTx(protoTx *proto.StakingTransaction) (*StoredStakingTransaction, error) {
var stakingTx wire.MsgTx
err := stakingTx.Deserialize(bytes.NewReader(protoTx.TransactionBytes))
Expand Down

0 comments on commit bad6003

Please sign in to comment.