Initial commit
Signed-off-by: Alex Vanin <a.vanin@yadro.com>
This commit is contained in:
commit
db7464d2d3
12 changed files with 1823 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
bin/*
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 TrueCloudLab
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
4
Makefile
Normal file
4
Makefile
Normal file
|
@ -0,0 +1,4 @@
|
|||
.PHONY: build
|
||||
|
||||
build:
|
||||
CGO_ENABLED=0 go build -o ./bin/monza ./
|
167
README.md
Normal file
167
README.md
Normal file
|
@ -0,0 +1,167 @@
|
|||
# Monza
|
||||
|
||||
Find notifications in [Neo3](https://neo.org/) compatible chains and more.
|
||||
|
||||
## Features
|
||||
|
||||
- Stores chain blocks in the filesystem, so it will not fetch it again
|
||||
at restart
|
||||
- Works with multiple chains by storing chain blocks based on magic number
|
||||
- Detailed notification output for NEP and FrostFS notifications
|
||||
- Use relative numbers for search interval
|
||||
- Use nice names for native contracts
|
||||
- Search multiple notifications at once
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
$ monza run -r http://localhost:30333 --from 22 --to 24 -n NewEpoch:* -n Transfer:gas
|
||||
syncing 100% [##################################################] (2/2, 3686 blocks/s)
|
||||
block:22 at:2023-09-27T13:50:03+03:00 name:Transfer from:56c989e76f9a2ca05bb5caa6c96f524d905accd8 to:nil amount:69915670
|
||||
block:22 at:2023-09-27T13:50:03+03:00 name:Transfer from:nil to:b248508f4ef7088e10c48f14d04be3272ca29eee amount:1219580
|
||||
block:22 at:2023-09-27T13:50:03+03:00 name:Transfer from:nil to:b248508f4ef7088e10c48f14d04be3272ca29eee amount:50000000
|
||||
block:22 at:2023-09-27T13:50:03+03:00 name:NewEpoch epoch:1
|
||||
block:23 at:2023-09-27T13:50:04+03:00 name:Transfer from:05195d17c8f013e258eb8dde1236c19d9a61b608 to:nil amount:33917200
|
||||
block:23 at:2023-09-27T13:50:04+03:00 name:Transfer from:6a131b2be19b7618dc22dbc8147015d947af67ce to:nil amount:12321080
|
||||
block:23 at:2023-09-27T13:50:04+03:00 name:Transfer from:b248508f4ef7088e10c48f14d04be3272ca29eee to:nil amount:12325080
|
||||
block:23 at:2023-09-27T13:50:04+03:00 name:Transfer from:nil to:b248508f4ef7088e10c48f14d04be3272ca29eee amount:8917170
|
||||
block:23 at:2023-09-27T13:50:04+03:00 name:Transfer from:nil to:b248508f4ef7088e10c48f14d04be3272ca29eee amount:20000000
|
||||
block:23 at:2023-09-27T13:50:04+03:00 name:Transfer from:nil to:b248508f4ef7088e10c48f14d04be3272ca29eee amount:50000000
|
||||
block:23 at:2023-09-27T13:50:04+03:00 name:Transfer from:6a131b2be19b7618dc22dbc8147015d947af67ce to:c1e14f19c3e60d0b9244d06dd7ba9b113135ec3b amount:243839460
|
||||
block:23 at:2023-09-27T13:50:04+03:00 name:Transfer from:b248508f4ef7088e10c48f14d04be3272ca29eee to:c1e14f19c3e60d0b9244d06dd7ba9b113135ec3b amount:250146763165
|
||||
```
|
||||
|
||||
### Detailed output
|
||||
|
||||
- NEP-17 `Transfer`
|
||||
|
||||
```
|
||||
block:23 at:2023-09-27T13:50:04+03:00 name:Transfer from:6a131b2be19b7618dc22dbc8147015d947af67ce to:c1e14f19c3e60d0b9244d06dd7ba9b113135ec3b amount:243839460
|
||||
```
|
||||
|
||||
- FrostFS `NewEpoch`
|
||||
|
||||
```
|
||||
block:22 at:2023-09-27T13:50:03+03:00 name:NewEpoch epoch:1
|
||||
```
|
||||
|
||||
- FrostFS `AddPeerSuccess`
|
||||
|
||||
```
|
||||
block:21 at:2023-09-27T13:50:02+03:00 name:AddPeerSuccess pubkey:[..6a8131]
|
||||
```
|
||||
|
||||
### Notifications
|
||||
|
||||
Search notifications based on notification name and contract address.
|
||||
|
||||
```
|
||||
monza run -r [endpoint] --from 110000 --to 110100 -n NewEpoch:ab8a83432af3cd32ce6ba3797f62b1ba330d7c3d
|
||||
```
|
||||
|
||||
Use wildcard to search notifications from any contract.
|
||||
|
||||
```
|
||||
monza run -r [endpoint] --from 110000 --to 110100 -n NewEpoch:*
|
||||
```
|
||||
|
||||
Use native contract names such as `gas` and `neo`.
|
||||
|
||||
```
|
||||
monza run -r [endpoint] --from 110000 --to 110100 -n Transfer:gas
|
||||
```
|
||||
|
||||
Search for multiple notifications.
|
||||
|
||||
```
|
||||
monza run -r [endpoint] --from 110000 --to 110100 -n Transfer:gas -n NewEpoch:*
|
||||
```
|
||||
|
||||
### Intervals
|
||||
|
||||
Define start and stop blocks.
|
||||
|
||||
```
|
||||
monza run -r [endpoint] --from 110000 --to 110100 -n NewEpoch:*
|
||||
```
|
||||
|
||||
Omit `--to` flag to search up to the latest block.
|
||||
|
||||
```
|
||||
monza run -r [endpoint] --from 110000 -n NewEpoch:*
|
||||
```
|
||||
|
||||
To look for `100` blocks before the latest block use prefix `m` (minus)
|
||||
|
||||
```
|
||||
monza run -r [endpoint] --from m100 -n NewEpoch:*
|
||||
```
|
||||
|
||||
To look for `100` blocks after specified `from` block, use prefix `p` (plus)
|
||||
|
||||
```
|
||||
monza run -r [endpoint] --from 101230 --to p100 -n NewEpoch:*
|
||||
```
|
||||
|
||||
### Other
|
||||
|
||||
Blocks are stored in bolt databases. Specify database dir with `-c` flag
|
||||
(default path is `$HOME/.config/monza`)
|
||||
|
||||
```
|
||||
monza run -r [endpoint] -c ./cache --from 110000 --to 110100 -n NewEpoch:*
|
||||
```
|
||||
|
||||
To speed up block fetching from the RPC node, use more parallel workers with
|
||||
`-w` flag.
|
||||
|
||||
```
|
||||
monza run -r [endpoint] -w 100 --from 110000 --to 110100 -n NewEpoch:*
|
||||
```
|
||||
|
||||
To disable progress bar use `--disable-progress-bar` flag.
|
||||
|
||||
### Stutter
|
||||
|
||||
Stutter command searches for blocks that produced with threshold a delay
|
||||
or slower.
|
||||
|
||||
```
|
||||
$ monza stutter -r [endpoint] --from 1159200 --to p30 --threshold 20s
|
||||
syncing 100% [##################################################] (30/30, 10 blocks/s)
|
||||
block:1159201 at:2022-03-29T18:20:42+03:00
|
||||
block:1159202 at:2022-03-29T18:22:12+03:00 [<- stutter for 1m30s]
|
||||
-- skipped 5 blocks --
|
||||
block:1159208 at:2022-03-29T18:23:43+03:00
|
||||
block:1159209 at:2022-03-29T18:25:13+03:00 [<- stutter for 1m30s]
|
||||
-- skipped 5 blocks --
|
||||
block:1159215 at:2022-03-29T18:26:43+03:00
|
||||
block:1159216 at:2022-03-29T18:28:13+03:00 [<- stutter for 1m30s]
|
||||
-- skipped 5 blocks --
|
||||
block:1159222 at:2022-03-29T18:29:43+03:00
|
||||
block:1159223 at:2022-03-29T18:31:13+03:00 [<- stutter for 1m30s]
|
||||
```
|
||||
|
||||
### Explorer
|
||||
|
||||
Run monza in interactive mode to navigate through blocks, transactions and
|
||||
notifications with `explore` command.
|
||||
|
||||
```
|
||||
$ monza explore -r [endpoint]
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Use `make build` command. Binary will be stored in `./bin/monza`.
|
||||
|
||||
## To Do
|
||||
|
||||
- [ ] `monza cache` command to manage bbolt instances: provide size and option to delete
|
||||
- [ ] Add verbose flag with for detailed view of notification body
|
||||
- [ ] Add more native contract hashes aliases
|
||||
- [ ] More NEP support (NEP-11?)
|
||||
|
||||
## License
|
||||
|
||||
Source code is available under the [MIT License](/LICENSE).
|
194
encoding.go
Normal file
194
encoding.go
Normal file
|
@ -0,0 +1,194 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
)
|
||||
|
||||
const nonCompatibleMsg = "not compatible with FrostFS"
|
||||
|
||||
func PrintEvent(b *block.Block, n state.NotificationEvent, extra string) {
|
||||
d := time.Unix(int64(b.Timestamp/1e3), 0)
|
||||
s := fmt.Sprintf("block:%d at:%s name:%s",
|
||||
b.Index, d.Format(time.RFC3339), n.Name,
|
||||
)
|
||||
|
||||
if len(extra) != 0 {
|
||||
s += fmt.Sprintf(" [%s]", extra)
|
||||
}
|
||||
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
func PrintTransfer(b *block.Block, n state.NotificationEvent) {
|
||||
const nonCompatibleMsg = "not NEP-17 compatible"
|
||||
|
||||
items, ok := n.Item.Value().([]stackitem.Item)
|
||||
if !ok {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
if len(items) != 3 {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
snd, err := items[0].TryBytes()
|
||||
if err != nil {
|
||||
snd = nil
|
||||
}
|
||||
|
||||
rcv, err := items[1].TryBytes()
|
||||
if err != nil {
|
||||
rcv = nil
|
||||
}
|
||||
|
||||
bigAmount, err := items[2].TryInteger()
|
||||
if err != nil {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
var sndStr, rcvStr = "nil", "nil"
|
||||
if snd != nil {
|
||||
sndStr = hex.EncodeToString(revertBytes(snd))
|
||||
}
|
||||
|
||||
if rcv != nil {
|
||||
rcvStr = hex.EncodeToString(revertBytes(rcv))
|
||||
}
|
||||
|
||||
d := time.Unix(int64(b.Timestamp/1e3), 0)
|
||||
|
||||
s := fmt.Sprintf("block:%d at:%s name:%s from:%s to:%s amount:%d",
|
||||
b.Index, d.Format(time.RFC3339), n.Name,
|
||||
sndStr, rcvStr, bigAmount.Int64(),
|
||||
)
|
||||
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
func PrintNewEpoch(b *block.Block, n state.NotificationEvent) {
|
||||
items, ok := n.Item.Value().([]stackitem.Item)
|
||||
if !ok {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
if len(items) != 1 {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
epoch, err := items[0].TryInteger()
|
||||
if err != nil {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
d := time.Unix(int64(b.Timestamp/1e3), 0)
|
||||
|
||||
s := fmt.Sprintf("block:%d at:%s name:%s epoch:%d",
|
||||
b.Index, d.Format(time.RFC3339), n.Name, epoch,
|
||||
)
|
||||
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
func PrintAddPeerSuccess(b *block.Block, n state.NotificationEvent) {
|
||||
items, ok := n.Item.Value().([]stackitem.Item)
|
||||
if !ok {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
if len(items) != 1 {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := items[0].TryBytes()
|
||||
if err != nil {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
d := time.Unix(int64(b.Timestamp/1e3), 0)
|
||||
|
||||
s := fmt.Sprintf("block:%d at:%s name:%s pubkey:[..%s]",
|
||||
b.Index, d.Format(time.RFC3339), n.Name,
|
||||
hex.EncodeToString(data[len(data)-3:]),
|
||||
)
|
||||
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
func PrintUpdateState(b *block.Block, n state.NotificationEvent) {
|
||||
items, ok := n.Item.Value().([]stackitem.Item)
|
||||
if !ok {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
if len(items) != 2 {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
st, err := items[0].TryInteger()
|
||||
if err != nil {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
var stateStr string
|
||||
switch v := st.Uint64(); v {
|
||||
case 1:
|
||||
stateStr = "online"
|
||||
case 2:
|
||||
stateStr = "offline"
|
||||
default:
|
||||
stateStr = fmt.Sprintf("%d(unknown)", v)
|
||||
}
|
||||
|
||||
pubkey, err := items[1].TryBytes()
|
||||
if err != nil {
|
||||
PrintEvent(b, n, nonCompatibleMsg)
|
||||
return
|
||||
}
|
||||
|
||||
d := time.Unix(int64(b.Timestamp/1e3), 0)
|
||||
|
||||
s := fmt.Sprintf("block:%d at:%s name:%s pubkey:[..%s] state:%s",
|
||||
b.Index, d.Format(time.RFC3339), n.Name,
|
||||
hex.EncodeToString(pubkey[len(pubkey)-3:]),
|
||||
stateStr,
|
||||
)
|
||||
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
func PrintBlock(b *block.Block, extra string) {
|
||||
d := time.Unix(int64(b.Timestamp/1e3), 0)
|
||||
s := fmt.Sprintf("block:%d at:%s", b.Index, d.Format(time.RFC3339))
|
||||
|
||||
if len(extra) != 0 {
|
||||
s += fmt.Sprintf(" [%s]", extra)
|
||||
}
|
||||
|
||||
fmt.Println(s)
|
||||
}
|
||||
|
||||
func revertBytes(data []byte) []byte {
|
||||
ln := len(data)
|
||||
for i := 0; i < ln/2; i++ {
|
||||
data[i], data[ln-1-i] = data[ln-1-i], data[i]
|
||||
}
|
||||
return data
|
||||
}
|
508
explorer.go
Normal file
508
explorer.go
Normal file
|
@ -0,0 +1,508 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/monza/internal/chain"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type (
|
||||
Explorer struct {
|
||||
ctx context.Context
|
||||
chain *chain.Chain
|
||||
endpoint string
|
||||
app *tview.Application
|
||||
|
||||
jobCh chan fetchTask
|
||||
errCh chan error
|
||||
wg sync.WaitGroup
|
||||
|
||||
searchErrFlag bool
|
||||
}
|
||||
|
||||
fetchTask struct {
|
||||
txHash *util.Uint256
|
||||
index uint32
|
||||
}
|
||||
)
|
||||
|
||||
const defaultExploreWorkers = 100
|
||||
|
||||
func explorer(c *cli.Context) (err error) {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
|
||||
// parse blockchain info
|
||||
cacheDir := c.String(cacheFlagKey)
|
||||
if len(cacheDir) == 0 {
|
||||
cacheDir, err = defaultConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpoint := c.String(endpointFlagKey)
|
||||
blockchain, err := chain.Open(ctx, cacheDir, endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot initialize remote blockchain client: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
blockchain.Close()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
e := Explorer{
|
||||
ctx: ctx,
|
||||
chain: blockchain,
|
||||
endpoint: endpoint,
|
||||
app: tview.NewApplication(),
|
||||
jobCh: make(chan fetchTask),
|
||||
errCh: make(chan error),
|
||||
}
|
||||
e.startWorkers(defaultExploreWorkers)
|
||||
return e.Run()
|
||||
}
|
||||
|
||||
func (e *Explorer) Run() error {
|
||||
// UI basic elements
|
||||
blockList := tview.NewList()
|
||||
searchInput := tview.NewInputField()
|
||||
blockInfo := tview.NewTextView()
|
||||
txCounter := tview.NewTextView()
|
||||
txList := tview.NewList()
|
||||
notifications := tview.NewTextView()
|
||||
|
||||
// UI containers
|
||||
blockFlex := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(blockInfo, 0, 1, false).
|
||||
AddItem(txCounter, 2, 2, false).
|
||||
AddItem(txList, 5, 3, false)
|
||||
appGrid := tview.NewGrid().
|
||||
SetRows(15, -1, 1).
|
||||
SetColumns(-1, -2).
|
||||
AddItem(blockList, 0, 0, 2, 1, 0, 0, false).
|
||||
AddItem(searchInput, 2, 0, 1, 2, 0, 0, false).
|
||||
AddItem(blockFlex, 0, 1, 1, 1, 0, 0, false).
|
||||
AddItem(notifications, 1, 1, 1, 1, 0, 0, false)
|
||||
|
||||
// Initialize element style
|
||||
blockList.ShowSecondaryText(false).SetWrapAround(false)
|
||||
blockList.SetBorder(true).SetTitle("Blocks")
|
||||
searchInput.SetFieldBackgroundColor(tcell.ColorBlack)
|
||||
txList.ShowSecondaryText(false)
|
||||
setFocusColorStyle(blockList.Box, blockList.Box)
|
||||
setFocusColorStyle(blockFlex.Box, txList.Box)
|
||||
setFocusColorStyle(notifications.Box, notifications.Box)
|
||||
|
||||
// Handle redrawing events
|
||||
blockList.SetDrawFunc(func(_ tcell.Screen, _, _, _, _ int) (int, int, int, int) {
|
||||
posX, posY, width, height := blockList.GetInnerRect()
|
||||
e.redrawBlockList(blockList, height)
|
||||
return posX, posY, width, height
|
||||
})
|
||||
|
||||
// Handle non-default keyboard events
|
||||
appGrid.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Rune() == '/' {
|
||||
e.searchStatusBar(searchInput)
|
||||
e.app.SetFocus(searchInput)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
blockList.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Rune() {
|
||||
case 'q':
|
||||
e.app.Stop()
|
||||
return nil
|
||||
case 'r':
|
||||
e.refillBlockList(blockList)
|
||||
return nil
|
||||
default:
|
||||
return event
|
||||
}
|
||||
})
|
||||
searchInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if !e.searchErrFlag {
|
||||
return event
|
||||
}
|
||||
e.searchErrFlag = false
|
||||
e.defaultStatusBar(searchInput, blockList.GetItemCount())
|
||||
e.app.SetFocus(blockList)
|
||||
return nil
|
||||
})
|
||||
txList.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyTAB || event.Key() == tcell.KeyEnter {
|
||||
e.app.SetFocus(notifications)
|
||||
return nil
|
||||
}
|
||||
if event.Rune() == 'q' {
|
||||
e.hideBlockFlex(blockFlex, blockInfo, txCounter, notifications, txList)
|
||||
e.app.SetFocus(blockList)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
notifications.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyTAB || event.Rune() == 'q' {
|
||||
e.app.SetFocus(txList)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
// Handle search input logic
|
||||
searchInput.SetDoneFunc(func(key tcell.Key) {
|
||||
e.hideBlockFlex(blockFlex, blockInfo, txCounter, notifications, txList)
|
||||
e.refillBlockList(blockList)
|
||||
|
||||
input := searchInput.GetText()
|
||||
blockCount := blockList.GetItemCount()
|
||||
|
||||
// Try parse as lookingBlock index
|
||||
lookingBlock, err := strconv.Atoi(input)
|
||||
if err == nil {
|
||||
if lookingBlock < 0 || lookingBlock >= blockCount {
|
||||
e.errorStatusBar(searchInput, "invalid block number")
|
||||
return
|
||||
}
|
||||
from, to := blockIndexRange(lookingBlock, blockCount, 50)
|
||||
e.cacheBlocks(from, to)
|
||||
e.defaultStatusBar(searchInput, blockCount)
|
||||
blockList.SetCurrentItem(-lookingBlock - 1)
|
||||
e.app.SetFocus(blockList)
|
||||
return
|
||||
}
|
||||
// Try parse as transaction hash
|
||||
h, err := util.Uint256DecodeStringLE(input)
|
||||
if err == nil {
|
||||
appLog, err := e.chain.Client.GetApplicationLog(h, nil)
|
||||
if err != nil {
|
||||
e.errorStatusBar(searchInput, "tx hash not found")
|
||||
return
|
||||
}
|
||||
chainBlock, err := e.chain.BlockByHash(appLog.Container)
|
||||
if err != nil {
|
||||
e.errorStatusBar(searchInput, "can't get block of specified tx")
|
||||
return
|
||||
}
|
||||
from, to := blockIndexRange(int(chainBlock.Index), blockCount, 50)
|
||||
e.cacheBlocks(from, to)
|
||||
e.defaultStatusBar(searchInput, blockCount)
|
||||
blockList.SetCurrentItem(-int(chainBlock.Index) - 1)
|
||||
e.app.SetFocus(blockList)
|
||||
return
|
||||
}
|
||||
e.errorStatusBar(searchInput, "invalid input, expect valid block number or tx hash")
|
||||
})
|
||||
|
||||
// Handle selecting block in block list
|
||||
blockList.SetSelectedFunc(func(i int, s1, s2 string, r rune) {
|
||||
chainBlock, err := e.chain.Block(uint32(blockList.GetItemCount() - i - 1))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
e.cacheNotifications(chainBlock)
|
||||
e.displayBlockFlex(blockFlex, blockInfo, txCounter, notifications, txList, chainBlock)
|
||||
e.app.SetFocus(txList)
|
||||
})
|
||||
|
||||
// Handle select transaction in block flex
|
||||
txList.SetChangedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
|
||||
txCounter.SetText(fmt.Sprintf("Transaction %d of %d\n---", index+1, txList.GetItemCount()))
|
||||
txHash, err := util.Uint256DecodeStringLE(mainText)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
appLog, err := e.chain.ApplicationLog(txHash)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
events := make([]state.NotificationEvent, 0)
|
||||
for _, execution := range appLog.Executions {
|
||||
events = append(events, execution.Events...)
|
||||
}
|
||||
|
||||
var res string
|
||||
for _, event := range events {
|
||||
v, err := formatNotification(event)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
res += v
|
||||
}
|
||||
notifications.SetText(res)
|
||||
notifications.ScrollToBeginning()
|
||||
})
|
||||
|
||||
// Initialize element data
|
||||
e.fillBlockList(blockList)
|
||||
e.defaultStatusBar(searchInput, blockList.GetItemCount())
|
||||
|
||||
// Start UI
|
||||
e.app.SetRoot(appGrid, true).SetFocus(blockList)
|
||||
return e.app.Run()
|
||||
}
|
||||
|
||||
func (e *Explorer) fillBlockList(list *tview.List) {
|
||||
to, err := e.chain.Client.GetBlockCount()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// adding is much faster than inserting
|
||||
// so we reverse the order to use that
|
||||
for i := int64(to) - 1; i >= 0; i-- {
|
||||
line := fmt.Sprintf("#%d", i)
|
||||
list.AddItem(line, "", 0, nil)
|
||||
}
|
||||
|
||||
// cache some blocks upfront for smoother scrolling
|
||||
var from uint32
|
||||
if to > 100 {
|
||||
from = to - 100
|
||||
}
|
||||
e.cacheBlocks(from, to-1)
|
||||
}
|
||||
|
||||
func (e *Explorer) refillBlockList(list *tview.List) {
|
||||
from := uint32(list.GetItemCount())
|
||||
|
||||
to, err := e.chain.Client.GetBlockCount()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i := from; i < to; i++ {
|
||||
line := fmt.Sprintf("#%d", i)
|
||||
list.InsertItem(0, line, "", 0, nil)
|
||||
}
|
||||
|
||||
e.cacheBlocks(from, to-1)
|
||||
}
|
||||
|
||||
func (e *Explorer) redrawBlockList(list *tview.List, height int) {
|
||||
currentItemIndex := list.GetCurrentItem()
|
||||
itemCount := list.GetItemCount()
|
||||
|
||||
// range of blocks for detailed info in listbox
|
||||
// to avoid whole blockchain fetching, app works
|
||||
// with small ranges of visible blocks on the screen
|
||||
fromIndex := currentItemIndex - height
|
||||
if fromIndex < 0 {
|
||||
fromIndex = 0
|
||||
}
|
||||
|
||||
toIndex := currentItemIndex + height
|
||||
if toIndex > itemCount {
|
||||
toIndex = itemCount
|
||||
}
|
||||
|
||||
e.cacheBlocks(toBlockIndex(toIndex, itemCount), toBlockIndex(fromIndex, itemCount))
|
||||
for i := fromIndex; i < toIndex; i++ {
|
||||
elem, _ := list.GetItemText(i)
|
||||
// ignore blocks that has been parsed
|
||||
if strings.Contains(elem, "tx") {
|
||||
continue
|
||||
}
|
||||
blockIndex := toBlockIndex(i, itemCount)
|
||||
chainBlock, err := e.chain.Block(blockIndex)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ts := time.Unix(int64(chainBlock.Timestamp/1e3), 0)
|
||||
richText := fmt.Sprintf("#%d [%s] txs:%d", blockIndex, ts.Format(time.RFC3339), len(chainBlock.Transactions))
|
||||
list.SetItemText(i, richText, "")
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Explorer) startWorkers(amount int) {
|
||||
worker := func(ctx context.Context, ch <-chan fetchTask, out chan<- error) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case task, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
if task.txHash != nil {
|
||||
_, err = e.chain.ApplicationLog(*task.txHash)
|
||||
} else {
|
||||
_, err = e.chain.Block(task.index)
|
||||
}
|
||||
if err != nil {
|
||||
out <- err
|
||||
return
|
||||
}
|
||||
e.wg.Done()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < amount; i++ {
|
||||
go worker(e.ctx, e.jobCh, e.errCh)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Explorer) cacheBlocks(from, to uint32) {
|
||||
for i := from; i <= to; i++ {
|
||||
e.wg.Add(1)
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
return
|
||||
case err := <-e.errCh:
|
||||
panic(err)
|
||||
case e.jobCh <- fetchTask{index: i}:
|
||||
}
|
||||
}
|
||||
|
||||
wgCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
e.wg.Wait()
|
||||
close(wgCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
return
|
||||
case err := <-e.errCh:
|
||||
panic(err)
|
||||
case <-wgCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Explorer) cacheNotifications(block *block.Block) {
|
||||
for _, tx := range block.Transactions {
|
||||
h := tx.Hash()
|
||||
e.wg.Add(1)
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
return
|
||||
case err := <-e.errCh:
|
||||
panic(err)
|
||||
case e.jobCh <- fetchTask{txHash: &h}:
|
||||
}
|
||||
}
|
||||
|
||||
wgCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
e.wg.Wait()
|
||||
close(wgCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-e.ctx.Done():
|
||||
return
|
||||
case err := <-e.errCh:
|
||||
panic(err)
|
||||
case <-wgCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Explorer) defaultStatusBar(input *tview.InputField, blocks int) {
|
||||
message := fmt.Sprintf("Endpoint: %s Blocks: %d | Press q to back, / to search, r to resync.",
|
||||
e.endpoint,
|
||||
blocks)
|
||||
input.SetText("").
|
||||
SetLabelColor(tcell.ColorWhite).
|
||||
SetLabel(message)
|
||||
}
|
||||
|
||||
func (e *Explorer) searchStatusBar(input *tview.InputField) {
|
||||
input.SetText("").
|
||||
SetLabelColor(tcell.ColorGreen).
|
||||
SetLabel("Search block or transaction: ")
|
||||
}
|
||||
|
||||
func (e *Explorer) errorStatusBar(input *tview.InputField, message string) {
|
||||
e.searchErrFlag = true
|
||||
input.SetText("").
|
||||
SetLabelColor(tcell.ColorRed).
|
||||
SetLabel(message)
|
||||
}
|
||||
|
||||
func (e *Explorer) displayBlockFlex(f *tview.Flex, info, counter, notif *tview.TextView, list *tview.List, b *block.Block) {
|
||||
ln := min(len(b.Transactions), 7)
|
||||
for _, tx := range b.Transactions {
|
||||
list.AddItem(tx.Hash().StringLE(), "", 0, nil)
|
||||
}
|
||||
info.SetText(fmt.Sprintf("Exploring block #%d", b.Index))
|
||||
counter.SetText(fmt.Sprintf("Transaction %d of %d\n---", min(1, ln), list.GetItemCount()))
|
||||
notif.SetBorder(true).SetTitle("Notifications")
|
||||
f.Clear()
|
||||
f.SetBorder(true)
|
||||
f.AddItem(info, 0, 1, false)
|
||||
f.AddItem(counter, 2, 2, false)
|
||||
f.AddItem(list, ln, 3, false)
|
||||
}
|
||||
|
||||
func (e *Explorer) hideBlockFlex(flex *tview.Flex, info, counter, notif *tview.TextView, list *tview.List) {
|
||||
info.SetText("")
|
||||
counter.SetText("")
|
||||
list.Clear()
|
||||
flex.SetBorder(false)
|
||||
notif.SetText("").SetBorder(false).SetTitle("")
|
||||
}
|
||||
|
||||
func toBlockIndex(index, length int) uint32 {
|
||||
return uint32(length - index - 1)
|
||||
}
|
||||
|
||||
func blockIndexRange(index, length, delta int) (uint32, uint32) {
|
||||
from := index - delta
|
||||
to := index + delta
|
||||
if from < 0 {
|
||||
from = 0
|
||||
}
|
||||
if to >= length {
|
||||
to = length - 1
|
||||
}
|
||||
return uint32(from), uint32(to)
|
||||
}
|
||||
|
||||
func formatNotification(event state.NotificationEvent) (string, error) {
|
||||
data, err := stackitem.ToJSONWithTypes(event.Item)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var formatted bytes.Buffer
|
||||
err = json.Indent(&formatted, data, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s\n---\n%s\n\n", event.Name, formatted.String()), nil
|
||||
}
|
||||
|
||||
func setFocusColorStyle(target, focus *tview.Box) {
|
||||
focus.SetFocusFunc(func() {
|
||||
target.SetBorderColor(tcell.ColorGreen)
|
||||
target.SetBorderAttributes(tcell.AttrBold)
|
||||
})
|
||||
focus.SetBlurFunc(func() {
|
||||
target.SetBorderColor(tcell.ColorDefault)
|
||||
target.SetBorderAttributes(tcell.AttrNone)
|
||||
})
|
||||
}
|
176
flags.go
Normal file
176
flags.go
Normal file
|
@ -0,0 +1,176 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
endpointFlagKey = "rpc-endpoint"
|
||||
fromFlagKey = "from"
|
||||
toFlagKey = "to"
|
||||
notificationFlagKey = "notification"
|
||||
cacheFlagKey = "cache"
|
||||
workersFlagKey = "workers"
|
||||
disableProgressBarFlagKey = "disable-progress-bar"
|
||||
stutterThresholdFlagKey = "threshold"
|
||||
)
|
||||
|
||||
var (
|
||||
endpointFlag = &cli.StringFlag{
|
||||
Name: endpointFlagKey,
|
||||
Aliases: []string{"r"},
|
||||
Usage: "N3 RPC endpoint",
|
||||
Required: true,
|
||||
}
|
||||
|
||||
fromFlag = &cli.StringFlag{
|
||||
Name: fromFlagKey,
|
||||
Usage: "starting block (can be relative value with minus prefix, e.g. 'm100')",
|
||||
Required: true,
|
||||
Value: "",
|
||||
}
|
||||
|
||||
toFlag = &cli.StringFlag{
|
||||
Name: toFlagKey,
|
||||
Usage: "ending block (can be relative value with plus prefix, e.g. 'p100' or omitted for latest block in chain)",
|
||||
Required: false,
|
||||
Value: "",
|
||||
}
|
||||
|
||||
notificationFlag = &cli.StringSliceFlag{
|
||||
Name: notificationFlagKey,
|
||||
Aliases: []string{"n"},
|
||||
Usage: "'notification:contract' pair (specify LE script hash, '*' for any contract or 'gas' and 'neo' strings)",
|
||||
Required: true,
|
||||
Value: nil,
|
||||
}
|
||||
|
||||
cacheFlag = &cli.StringFlag{
|
||||
Name: cacheFlagKey,
|
||||
Aliases: []string{"c"},
|
||||
Usage: "path to the blockchain cache (default: $HOME/.config/monza)",
|
||||
Value: "",
|
||||
}
|
||||
|
||||
workersFlag = &cli.Uint64Flag{
|
||||
Name: workersFlagKey,
|
||||
Aliases: []string{"w"},
|
||||
Usage: "amount of workers for parallel block fetch",
|
||||
Value: 3,
|
||||
}
|
||||
|
||||
disableProgressBarFlag = &cli.BoolFlag{
|
||||
Name: disableProgressBarFlagKey,
|
||||
Usage: "disable progress bar output",
|
||||
}
|
||||
|
||||
stutterThresholdFlag = &cli.DurationFlag{
|
||||
Name: stutterThresholdFlagKey,
|
||||
Aliases: []string{"t"},
|
||||
Usage: "duration limit between block timestamps",
|
||||
Value: 20 * time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
func parseNotifications(notifications []string, cli *rpcclient.Client) (map[string]*util.Uint160, error) {
|
||||
res := make(map[string]*util.Uint160, len(notifications))
|
||||
|
||||
for _, n := range notifications {
|
||||
pair := strings.Split(n, ":")
|
||||
if len(pair) != 2 {
|
||||
return nil, fmt.Errorf("invalid notification %s", n)
|
||||
}
|
||||
|
||||
name := pair[0]
|
||||
|
||||
switch contractName := strings.ToLower(pair[1]); contractName {
|
||||
case "*":
|
||||
res[name] = nil
|
||||
case "gas":
|
||||
state, err := cli.GetContractStateByAddressOrName(nativenames.Gas)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid contract name %s", contractName)
|
||||
}
|
||||
res[name] = &state.Hash
|
||||
case "neo":
|
||||
state, err := cli.GetContractStateByAddressOrName(nativenames.Neo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid contract name %s", contractName)
|
||||
}
|
||||
res[name] = &state.Hash
|
||||
default:
|
||||
u160, err := util.Uint160DecodeStringLE(contractName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid contract name %s", contractName)
|
||||
}
|
||||
res[name] = &u160
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func parseInterval(fromStr, toStr string, cli *rpcclient.Client) (from, to uint32, err error) {
|
||||
switch { // parse from value and return result if it is relative
|
||||
case len(fromStr) == 0:
|
||||
return 0, 0, ErrInvalidInterval(fromStr, toStr)
|
||||
case fromStr[0] == 'm':
|
||||
v, err := strconv.Atoi(fromStr[1:])
|
||||
if err != nil || v <= 0 {
|
||||
return 0, 0, ErrInvalidInterval(fromStr, toStr)
|
||||
}
|
||||
h, err := cli.GetBlockCount()
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("latest block index unavailable: %w", err)
|
||||
}
|
||||
if uint32(v) >= h {
|
||||
return 0, 0, fmt.Errorf("latest block is less than from value, from:%s, to:%d", fromStr, h)
|
||||
}
|
||||
return h - uint32(v), h, nil
|
||||
default:
|
||||
v, err := strconv.Atoi(fromStr)
|
||||
if err != nil || v <= 0 {
|
||||
return 0, 0, ErrInvalidInterval(fromStr, toStr)
|
||||
}
|
||||
from = uint32(v)
|
||||
}
|
||||
|
||||
switch { // parse to value
|
||||
case len(toStr) == 0:
|
||||
h, err := cli.GetBlockCount()
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("latest block index unavailable: %w", err)
|
||||
}
|
||||
if h <= from {
|
||||
return 0, 0, fmt.Errorf("latest block is less than from value, from:%d, to:%d", from, h)
|
||||
}
|
||||
return from, h, nil
|
||||
case toStr[0] == 'p':
|
||||
v, err := strconv.Atoi(toStr[1:])
|
||||
if err != nil || v <= 0 {
|
||||
return 0, 0, ErrInvalidInterval(fromStr, toStr)
|
||||
}
|
||||
return from, from + uint32(v), nil
|
||||
default:
|
||||
v, err := strconv.Atoi(toStr)
|
||||
if err != nil || v <= 0 {
|
||||
return 0, 0, ErrInvalidInterval(fromStr, toStr)
|
||||
}
|
||||
if uint32(v) <= from {
|
||||
return 0, 0, ErrInvalidInterval(fromStr, toStr)
|
||||
}
|
||||
return from, uint32(v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func ErrInvalidInterval(from, to string) error {
|
||||
return fmt.Errorf("invalid block interval from:%s to:%s", from, to)
|
||||
}
|
39
go.mod
Normal file
39
go.mod
Normal file
|
@ -0,0 +1,39 @@
|
|||
module git.frostfs.info/TrueCloudLab/monza
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell/v2 v2.5.1
|
||||
github.com/nspcc-dev/neo-go v0.102.0
|
||||
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8
|
||||
github.com/schollz/progressbar/v3 v3.8.3
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
go.etcd.io/bbolt v1.3.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/hashicorp/golang-lru v0.6.0 // indirect
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect
|
||||
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.4.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/term v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
103
go.sum
Normal file
103
go.sum
Normal file
|
@ -0,0 +1,103 @@
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
|
||||
github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I=
|
||||
github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
|
||||
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 h1:n4ZaFCKt1pQJd7PXoMJabZWK9ejjbLOVrkl/lOUmshg=
|
||||
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22/go.mod h1:79bEUDEviBHJMFV6Iq6in57FEOCMcRhfQnfaf0ETA5U=
|
||||
github.com/nspcc-dev/neo-go v0.102.0 h1:O2Gt4JPOWmp0c+PnPWwd2wPI74BKSwkaNCEyvyQTWJw=
|
||||
github.com/nspcc-dev/neo-go v0.102.0/go.mod h1:QXxpZxJT2KedwM0Nlj8UO0/fZN2WIe4h/i03uBHKbnc=
|
||||
github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE=
|
||||
github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8 h1:xe+mmCnDN82KhC010l3NfYlA8ZbOuzbXAzSYBa6wbMc=
|
||||
github.com/rivo/tview v0.0.0-20220307222120-9994674d60a8/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/schollz/progressbar/v3 v3.8.3 h1:FnLGl3ewlDUP+YdSwveXBaXs053Mem/du+wr7XSYKl8=
|
||||
github.com/schollz/progressbar/v3 v3.8.3/go.mod h1:pWnVCjSBZsT2X3nx9HfRdnCDrpbevliMeoEVhStwHko=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo=
|
||||
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c=
|
||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
239
internal/chain/chain.go
Normal file
239
internal/chain/chain.go
Normal file
|
@ -0,0 +1,239 @@
|
|||
package chain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type Chain struct {
|
||||
db *bbolt.DB
|
||||
stateRoot bool
|
||||
Client *rpcclient.Client
|
||||
}
|
||||
|
||||
var (
|
||||
blocksBucket = []byte("blocks")
|
||||
logsBucket = []byte("logs")
|
||||
)
|
||||
|
||||
func Open(ctx context.Context, dir, endpoint string) (*Chain, error) {
|
||||
cli, err := rpcclient.New(ctx, endpoint, rpcclient.Options{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rpc connection: %w", err)
|
||||
}
|
||||
|
||||
err = cli.Init()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rpc client initialization: %w", err)
|
||||
}
|
||||
|
||||
v, err := cli.GetVersion()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rpc get version: %w", err)
|
||||
}
|
||||
|
||||
dbPath := path.Join(dir, strconv.Itoa(int(v.Protocol.Network))+".db")
|
||||
|
||||
db, err := bbolt.Open(dbPath, 0600, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("database [%s] init: %w", dbPath, err)
|
||||
}
|
||||
|
||||
return &Chain{db, v.Protocol.StateRootInHeader, cli}, nil
|
||||
}
|
||||
|
||||
func (d *Chain) Block(i uint32) (res *block.Block, err error) {
|
||||
cached, err := d.block(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cached != nil {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
metaBlock, err := d.Client.GetBlockByIndexVerbose(i)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block %d fetch: %w", i, err)
|
||||
}
|
||||
|
||||
return &metaBlock.Block, d.addBlock(&metaBlock.Block)
|
||||
}
|
||||
|
||||
func (d *Chain) BlockByHash(h util.Uint256) (res *block.Block, err error) {
|
||||
rev := h.Reverse()
|
||||
metaBlock, err := d.Client.GetBlockByHashVerbose(rev)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block %s fetch: %w", h.StringLE(), err)
|
||||
}
|
||||
|
||||
return &metaBlock.Block, d.addBlock(&metaBlock.Block)
|
||||
}
|
||||
|
||||
func (d *Chain) block(i uint32) (res *block.Block, err error) {
|
||||
err = d.db.View(func(tx *bbolt.Tx) error {
|
||||
key := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(key, i)
|
||||
|
||||
bkt := tx.Bucket(blocksBucket)
|
||||
if bkt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data := bkt.Get(key)
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
res = block.New(d.stateRoot)
|
||||
r := io.NewBinReaderFromBuf(data)
|
||||
res.DecodeBinary(r)
|
||||
|
||||
return r.Err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read block %d from cache: %w", i, err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *Chain) addBlock(block *block.Block) error {
|
||||
err := d.db.Batch(func(tx *bbolt.Tx) error {
|
||||
key := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(key, block.Index)
|
||||
|
||||
w := io.NewBufBinWriter()
|
||||
block.EncodeBinary(w.BinWriter)
|
||||
if w.Err != nil {
|
||||
return w.Err
|
||||
}
|
||||
|
||||
bkt, err := tx.CreateBucketIfNotExists(blocksBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bkt.Put(key, w.Bytes())
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot add block %d to cache: %w", block.Index, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Chain) ApplicationLog(txHash util.Uint256) (*result.ApplicationLog, error) {
|
||||
cached, err := d.applicationLog(txHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cached != nil {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
appLog, err := d.Client.GetApplicationLog(txHash, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("app log of tx %s fetch: %w", txHash.StringLE(), err)
|
||||
}
|
||||
|
||||
return appLog, d.addApplicationLog(txHash, appLog)
|
||||
}
|
||||
|
||||
func (d *Chain) applicationLog(txHash util.Uint256) (res *result.ApplicationLog, err error) {
|
||||
err = d.db.View(func(tx *bbolt.Tx) error {
|
||||
bkt := tx.Bucket(logsBucket)
|
||||
if bkt == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data := bkt.Get(txHash.BytesLE())
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
res = new(result.ApplicationLog)
|
||||
return res.UnmarshalJSON(bkt.Get(txHash.BytesLE()))
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read tx %s from cache: %w", txHash.StringLE(), err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *Chain) addApplicationLog(txHash util.Uint256, appLog *result.ApplicationLog) error {
|
||||
err := d.db.Batch(func(tx *bbolt.Tx) error {
|
||||
val, err := appLog.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bkt, err := tx.CreateBucketIfNotExists(logsBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bkt.Put(txHash.BytesLE(), val)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot add tx %s to cache: %w", txHash.StringLE(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Chain) Notifications(txHash util.Uint256) ([]state.NotificationEvent, error) {
|
||||
appLog, err := d.ApplicationLog(txHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]state.NotificationEvent, 0)
|
||||
for _, execution := range appLog.Executions {
|
||||
res = append(res, execution.Events...)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *Chain) AllNotifications(b *block.Block) ([]state.NotificationEvent, error) {
|
||||
res := make([]state.NotificationEvent, 0)
|
||||
|
||||
appLog, err := d.ApplicationLog(b.Hash())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, execution := range appLog.Executions {
|
||||
res = append(res, execution.Events...)
|
||||
}
|
||||
|
||||
for _, tx := range b.Transactions {
|
||||
appLog, err = d.ApplicationLog(tx.Hash())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, execution := range appLog.Executions {
|
||||
res = append(res, execution.Events...)
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d *Chain) Close() {
|
||||
_ = d.db.Close()
|
||||
}
|
273
main.go
Normal file
273
main.go
Normal file
|
@ -0,0 +1,273 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/monza/internal/chain"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := &cli.App{
|
||||
Name: "monza",
|
||||
Usage: "monitor notification events in N3 compatible chains",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "run",
|
||||
Usage: "look up over subset of blocks to find notifications",
|
||||
UsageText: "monza run -r [endpoint] --from 101000 --to p1000 -n \"Transfer:gas\" -n \"newEpoch:*\"",
|
||||
Action: monza,
|
||||
Flags: []cli.Flag{
|
||||
endpointFlag,
|
||||
fromFlag,
|
||||
toFlag,
|
||||
notificationFlag,
|
||||
cacheFlag,
|
||||
workersFlag,
|
||||
disableProgressBarFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "stutter",
|
||||
Usage: "find stuttered blocks in subset",
|
||||
UsageText: "monza stutter -r [endpoint] --from 101000 --to p1000 --threshold 20s",
|
||||
Action: stutter,
|
||||
Flags: []cli.Flag{
|
||||
endpointFlag,
|
||||
fromFlag,
|
||||
toFlag,
|
||||
stutterThresholdFlag,
|
||||
cacheFlag,
|
||||
workersFlag,
|
||||
disableProgressBarFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "explore",
|
||||
Usage: "explore stuttered blocks in subset",
|
||||
UsageText: "monza explore -r [endpoint]",
|
||||
Action: explorer,
|
||||
Flags: []cli.Flag{
|
||||
endpointFlag,
|
||||
cacheFlag,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func monza(c *cli.Context) (err error) {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
|
||||
// parse blockchain info
|
||||
cacheDir := c.String(cacheFlagKey)
|
||||
if len(cacheDir) == 0 {
|
||||
cacheDir, err = defaultConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
blockchain, err := chain.Open(ctx, cacheDir, c.String(endpointFlagKey))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot initialize remote blockchain client: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
blockchain.Close()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// parse block indices
|
||||
from, to, err := parseInterval(c.String(fromFlagKey), c.String(toFlagKey), blockchain.Client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// parse notifications
|
||||
notifications, err := parseNotifications(c.StringSlice(notificationFlagKey), blockchain.Client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start monza
|
||||
return run(ctx, ¶ms{
|
||||
from: from,
|
||||
to: to,
|
||||
blockchain: blockchain,
|
||||
notifications: notifications,
|
||||
workers: int(c.Uint64(workersFlagKey)),
|
||||
disableBar: c.Bool(disableProgressBarFlagKey),
|
||||
})
|
||||
}
|
||||
|
||||
type params struct {
|
||||
from, to uint32
|
||||
blockchain *chain.Chain
|
||||
notifications map[string]*util.Uint160
|
||||
workers int
|
||||
disableBar bool
|
||||
}
|
||||
|
||||
func run(ctx context.Context, p *params) error {
|
||||
err := cacheBlocks(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := p.from; i < p.to; i++ {
|
||||
b, err := p.blockchain.Block(i)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot fetch block %d: %w", i, err)
|
||||
}
|
||||
|
||||
notifications, err := p.blockchain.AllNotifications(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot fetch notifications from block %d: %w", i, err)
|
||||
}
|
||||
|
||||
for _, ev := range notifications {
|
||||
contract, ok := p.notifications[ev.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if contract != nil && !contract.Equals(ev.ScriptHash) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch ev.Name {
|
||||
case "Transfer":
|
||||
PrintTransfer(b, ev)
|
||||
case "NewEpoch":
|
||||
PrintNewEpoch(b, ev)
|
||||
case "AddPeerSuccess":
|
||||
PrintAddPeerSuccess(b, ev)
|
||||
case "UpdateState":
|
||||
PrintUpdateState(b, ev)
|
||||
default:
|
||||
PrintEvent(b, ev, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cacheBlocks(ctx context.Context, p *params) error {
|
||||
if p.workers <= 0 {
|
||||
return fmt.Errorf("invalid amount of workers %d", p.workers)
|
||||
}
|
||||
|
||||
var bar *progressbar.ProgressBar
|
||||
if !p.disableBar {
|
||||
bar = progressbar.NewOptions(int(p.to-p.from),
|
||||
progressbar.OptionSetDescription("syncing"),
|
||||
progressbar.OptionSetWriter(os.Stderr),
|
||||
progressbar.OptionSetWidth(10),
|
||||
progressbar.OptionThrottle(65*time.Millisecond),
|
||||
progressbar.OptionShowCount(),
|
||||
progressbar.OptionShowIts(),
|
||||
progressbar.OptionSetItsString("blocks"),
|
||||
progressbar.OptionOnCompletion(func() {
|
||||
_, _ = fmt.Fprint(os.Stderr, "\n")
|
||||
}),
|
||||
progressbar.OptionSpinnerType(14),
|
||||
progressbar.OptionSetWidth(50),
|
||||
progressbar.OptionSetTheme(progressbar.Theme{
|
||||
Saucer: "#",
|
||||
SaucerHead: "#",
|
||||
SaucerPadding: " ",
|
||||
BarStart: "[",
|
||||
BarEnd: "]",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
jobCh := make(chan uint32)
|
||||
errCh := make(chan error)
|
||||
wgCh := make(chan struct{})
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
for i := 0; i < p.workers; i++ {
|
||||
go func(ctx context.Context, ch <-chan uint32, out chan<- error) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case block, ok := <-ch:
|
||||
wg.Add(1)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
b, err := p.blockchain.Block(block)
|
||||
if err != nil {
|
||||
out <- err
|
||||
return
|
||||
}
|
||||
_, err = p.blockchain.AllNotifications(b)
|
||||
if err != nil {
|
||||
out <- err
|
||||
return
|
||||
}
|
||||
if bar != nil {
|
||||
_ = bar.Add(1)
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
}
|
||||
}(ctx, jobCh, errCh)
|
||||
}
|
||||
|
||||
for i := p.from; i < p.to; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.New("interrupted")
|
||||
case err := <-errCh:
|
||||
return err
|
||||
case jobCh <- i:
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(wgCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.New("interrupted")
|
||||
case err := <-errCh:
|
||||
return err
|
||||
case <-wgCh:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func defaultConfigDir() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatalf("cannot determine home dir for default config path: %s", err)
|
||||
}
|
||||
|
||||
p := path.Join(home, ".config")
|
||||
p = path.Join(p, "monza")
|
||||
|
||||
return p, os.MkdirAll(p, os.ModePerm)
|
||||
}
|
98
stutter.go
Normal file
98
stutter.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/monza/internal/chain"
|
||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func stutter(c *cli.Context) (err error) {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
|
||||
// parse blockchain info
|
||||
cacheDir := c.String(cacheFlagKey)
|
||||
if len(cacheDir) == 0 {
|
||||
cacheDir, err = defaultConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
blockchain, err := chain.Open(ctx, cacheDir, c.String(endpointFlagKey))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot initialize remote blockchain client: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
blockchain.Close()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// parse block indices
|
||||
from, to, err := parseInterval(c.String(fromFlagKey), c.String(toFlagKey), blockchain.Client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
threshold := c.Duration(stutterThresholdFlagKey)
|
||||
|
||||
// need at least two blocks
|
||||
if to-from < 2 {
|
||||
return errors.New("range must contain at least two blocks")
|
||||
}
|
||||
|
||||
// fetch blocks
|
||||
err = cacheBlocks(ctx, ¶ms{
|
||||
from: from,
|
||||
to: to,
|
||||
blockchain: blockchain,
|
||||
workers: int(c.Uint64(workersFlagKey)),
|
||||
disableBar: c.Bool(disableProgressBarFlagKey),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// process blocks one by one
|
||||
var (
|
||||
prev, curr *block.Block
|
||||
prevTS, currTS time.Time
|
||||
lastStutterBlock uint32
|
||||
)
|
||||
|
||||
for i := from; i < to; i++ {
|
||||
b, err := blockchain.Block(i)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot fetch block %d: %w", i, err)
|
||||
}
|
||||
|
||||
prev, prevTS = curr, currTS
|
||||
curr = b
|
||||
currTS = time.Unix(int64(b.Timestamp/1e3), 0)
|
||||
if prev == nil { // first block case
|
||||
continue
|
||||
}
|
||||
|
||||
blockDelta := currTS.Sub(prevTS)
|
||||
if blockDelta <= threshold {
|
||||
continue
|
||||
}
|
||||
|
||||
skippedBlocks := prev.Index - lastStutterBlock
|
||||
if lastStutterBlock > 0 && skippedBlocks > 1 {
|
||||
fmt.Printf("-- skipped %d blocks --\n", skippedBlocks-1)
|
||||
}
|
||||
|
||||
PrintBlock(prev, "")
|
||||
PrintBlock(curr, fmt.Sprintf("<- stutter for %s", blockDelta))
|
||||
lastStutterBlock = curr.Index
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in a new issue