Some checks failed
Docs Deploy / build_and_deploy (push) Has been cancelled
Generate Docs / cli (push) Has been cancelled
Generate Config Doc / cli (push) Has been cancelled
Go formatting / go-formatting (push) Has been cancelled
Check links / markdown-link-check (push) Has been cancelled
Integration / pre-test (push) Has been cancelled
Integration / test on (push) Has been cancelled
Integration / status (push) Has been cancelled
Lint / Lint Go code (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
389 lines
9.5 KiB
Go
389 lines
9.5 KiB
Go
package chaincmdrunner
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cenkalti/backoff"
|
|
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/chaincmd"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cmdrunner/step"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cosmosver"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/errors"
|
|
)
|
|
|
|
// Start starts the blockchain.
|
|
func (r Runner) Start(ctx context.Context, args ...string) error {
|
|
return r.run(
|
|
ctx,
|
|
runOptions{wrappedStdErrMaxLen: 50000},
|
|
r.chainCmd.StartCommand(args...),
|
|
)
|
|
}
|
|
|
|
// Init inits the blockchain.
|
|
func (r Runner) Init(ctx context.Context, moniker string, args ...string) error {
|
|
return r.run(ctx, runOptions{}, r.chainCmd.InitCommand(moniker, args...))
|
|
}
|
|
|
|
// KV holds a key, value pair.
|
|
type KV struct {
|
|
key string
|
|
value string
|
|
}
|
|
|
|
// NewKV returns a new key, value pair.
|
|
func NewKV(key, value string) KV {
|
|
return KV{key, value}
|
|
}
|
|
|
|
var gentxRe = regexp.MustCompile(`(?m)"(.+?)"`)
|
|
|
|
func (r Runner) InPlace(ctx context.Context, newChainID, newOperatorAddress string, options ...chaincmd.InPlaceOption) error {
|
|
runOptions := runOptions{
|
|
stdout: os.Stdout,
|
|
stderr: os.Stderr,
|
|
}
|
|
return r.run(
|
|
ctx,
|
|
runOptions,
|
|
r.chainCmd.TestnetInPlaceCommand(newChainID, newOperatorAddress, options...),
|
|
)
|
|
}
|
|
|
|
// Initialize config directories & files for a multi-validator testnet locally.
|
|
func (r Runner) MultiNode(ctx context.Context, options ...chaincmd.MultiNodeOption) error {
|
|
runOptions := runOptions{
|
|
stdout: os.Stdout,
|
|
stderr: os.Stderr,
|
|
}
|
|
return r.run(
|
|
ctx,
|
|
runOptions,
|
|
r.chainCmd.TestnetMultiNodeCommand(options...),
|
|
)
|
|
}
|
|
|
|
// Gentx generates a genesis tx carrying a self delegation.
|
|
func (r Runner) Gentx(
|
|
ctx context.Context,
|
|
validatorName,
|
|
selfDelegation string,
|
|
options ...chaincmd.GentxOption,
|
|
) (gentxPath string, err error) {
|
|
b := newBuffer()
|
|
|
|
if err := r.run(ctx, runOptions{
|
|
stdout: b,
|
|
stderr: b,
|
|
stdin: os.Stdin,
|
|
}, r.chainCmd.GentxCommand(validatorName, selfDelegation, options...)); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return gentxRe.FindStringSubmatch(b.String())[1], nil
|
|
}
|
|
|
|
// CollectGentxs collects gentxs.
|
|
func (r Runner) CollectGentxs(ctx context.Context) error {
|
|
return r.run(ctx, runOptions{}, r.chainCmd.CollectGentxsCommand())
|
|
}
|
|
|
|
// ValidateGenesis validates genesis.
|
|
func (r Runner) ValidateGenesis(ctx context.Context) error {
|
|
return r.run(ctx, runOptions{}, r.chainCmd.ValidateGenesisCommand())
|
|
}
|
|
|
|
// UnsafeReset resets the blockchain database.
|
|
func (r Runner) UnsafeReset(ctx context.Context) error {
|
|
return r.run(ctx, runOptions{}, r.chainCmd.UnsafeResetCommand())
|
|
}
|
|
|
|
// ShowNodeID shows node id.
|
|
func (r Runner) ShowNodeID(ctx context.Context) (nodeID string, err error) {
|
|
b := newBuffer()
|
|
err = r.run(ctx, runOptions{stdout: b}, r.chainCmd.ShowNodeIDCommand())
|
|
nodeID = strings.TrimSpace(b.String())
|
|
return
|
|
}
|
|
|
|
// NodeStatus keeps info about node's status.
|
|
type NodeStatus struct {
|
|
ChainID string
|
|
}
|
|
|
|
// Status returns the node's status.
|
|
func (r Runner) Status(ctx context.Context) (NodeStatus, error) {
|
|
b := newBuffer()
|
|
|
|
if err := r.run(ctx, runOptions{stdout: b, stderr: b}, r.chainCmd.StatusCommand()); err != nil {
|
|
return NodeStatus{}, err
|
|
}
|
|
|
|
var chainID string
|
|
|
|
data, err := b.JSONEnsuredBytes()
|
|
if err != nil {
|
|
return NodeStatus{}, err
|
|
}
|
|
|
|
version := r.chainCmd.SDKVersion()
|
|
switch {
|
|
case version.GTE(cosmosver.StargateFortyVersion):
|
|
out := struct {
|
|
NodeInfo struct {
|
|
Network string `json:"network"`
|
|
} `json:"NodeInfo"`
|
|
}{}
|
|
|
|
if err := json.Unmarshal(data, &out); err != nil {
|
|
return NodeStatus{}, err
|
|
}
|
|
|
|
chainID = out.NodeInfo.Network
|
|
default:
|
|
out := struct {
|
|
NodeInfo struct {
|
|
Network string `json:"network"`
|
|
} `json:"node_info"`
|
|
}{}
|
|
|
|
if err := json.Unmarshal(data, &out); err != nil {
|
|
return NodeStatus{}, err
|
|
}
|
|
|
|
chainID = out.NodeInfo.Network
|
|
}
|
|
|
|
return NodeStatus{
|
|
ChainID: chainID,
|
|
}, nil
|
|
}
|
|
|
|
// BankSend sends amount from fromAccount to toAccount.
|
|
func (r Runner) BankSend(ctx context.Context, fromAccount, toAccount, amount string, options ...chaincmd.BankSendOption) (string, error) {
|
|
b := newBuffer()
|
|
opt := []step.Option{
|
|
r.chainCmd.BankSendCommand(fromAccount, toAccount, amount, options...),
|
|
}
|
|
|
|
if r.chainCmd.KeyringPassword() != "" {
|
|
input := newBuffer()
|
|
_, err := fmt.Fprintln(input, r.chainCmd.KeyringPassword())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
_, err = fmt.Fprintln(input, r.chainCmd.KeyringPassword())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
_, err = fmt.Fprintln(input, r.chainCmd.KeyringPassword())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
opt = append(opt, step.Write(input.Bytes()))
|
|
}
|
|
|
|
if err := r.run(ctx, runOptions{stdout: b}, opt...); err != nil {
|
|
if strings.Contains(err.Error(), "key not found") {
|
|
return "", errors.New("account doesn't have any balances")
|
|
}
|
|
|
|
return "", err
|
|
}
|
|
|
|
txResult, err := decodeTxResult(b)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if txResult.Code > 0 {
|
|
return "", errors.Errorf("cannot send tokens (SDK code %d): %s", txResult.Code, txResult.RawLog)
|
|
}
|
|
|
|
return txResult.TxHash, nil
|
|
}
|
|
|
|
// WaitTx waits until a tx is successfully added to a block and can be queried.
|
|
func (r Runner) WaitTx(ctx context.Context, txHash string, retryDelay time.Duration, maxRetry int) error {
|
|
retry := 0
|
|
|
|
// retry querying the request
|
|
checkTx := func() error {
|
|
b := newBuffer()
|
|
if err := r.run(ctx, runOptions{stdout: b}, r.chainCmd.QueryTxCommand(txHash)); err != nil {
|
|
// filter not found error and check for max retry
|
|
if !strings.Contains(err.Error(), "not found") {
|
|
return backoff.Permanent(err)
|
|
}
|
|
retry++
|
|
if retry == maxRetry {
|
|
return backoff.Permanent(errors.Errorf("can't retrieve tx %s", txHash))
|
|
}
|
|
return err
|
|
}
|
|
|
|
// parse tx and check code
|
|
txResult, err := decodeTxResult(b)
|
|
if err != nil {
|
|
return backoff.Permanent(err)
|
|
}
|
|
if txResult.Code != 0 {
|
|
return backoff.Permanent(errors.Errorf("tx %s failed: %s", txHash, txResult.RawLog))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
return backoff.Retry(checkTx, backoff.WithContext(backoff.NewConstantBackOff(retryDelay), ctx))
|
|
}
|
|
|
|
// Export exports the state of the chain into the specified file.
|
|
func (r Runner) Export(ctx context.Context, exportedFile string) error {
|
|
// Make sure the path exists
|
|
dir := filepath.Dir(exportedFile)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
stdout, stderr := newBuffer(), newBuffer()
|
|
if err := r.run(ctx, runOptions{stdout: stdout, stderr: stderr}, r.chainCmd.ExportCommand()); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Exported genesis is written on stderr from Cosmos-SDK v0.44.0
|
|
var exportedState []byte
|
|
if stdout.Len() > 0 {
|
|
exportedState = stdout.Bytes()
|
|
} else {
|
|
exportedState = stderr.Bytes()
|
|
}
|
|
|
|
// Save the new state
|
|
return os.WriteFile(exportedFile, exportedState, 0o600)
|
|
}
|
|
|
|
// EventSelector is used to query events.
|
|
type EventSelector struct {
|
|
typ string
|
|
attr string
|
|
value string
|
|
}
|
|
|
|
// NewEventSelector creates a new event selector.
|
|
func NewEventSelector(typ, addr, value string) EventSelector {
|
|
return EventSelector{typ, addr, value}
|
|
}
|
|
|
|
// Event represents a TX event.
|
|
type Event struct {
|
|
Type string
|
|
Attributes []EventAttribute
|
|
Time time.Time
|
|
}
|
|
|
|
// EventAttribute holds event's attributes.
|
|
type EventAttribute struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
// QueryTxByEvents queries tx events by event selectors.
|
|
func (r Runner) QueryTxByEvents(
|
|
ctx context.Context,
|
|
selectors ...EventSelector,
|
|
) ([]Event, error) {
|
|
if len(selectors) == 0 {
|
|
return nil, errors.New("event selector list should be greater than zero")
|
|
}
|
|
list := make([]string, len(selectors))
|
|
for i, event := range selectors {
|
|
list[i] = fmt.Sprintf("%s.%s=%s", event.typ, event.attr, event.value)
|
|
}
|
|
query := strings.Join(list, "&")
|
|
return r.QueryTx(ctx, r.chainCmd.QueryTxEventsCommand(query))
|
|
}
|
|
|
|
// QueryTxByQuery queries tx events by event selectors.
|
|
func (r Runner) QueryTxByQuery(
|
|
ctx context.Context,
|
|
selectors ...EventSelector,
|
|
) ([]Event, error) {
|
|
if len(selectors) == 0 {
|
|
return nil, errors.New("event selector list should be greater than zero")
|
|
}
|
|
list := make([]string, len(selectors))
|
|
for i, query := range selectors {
|
|
list[i] = fmt.Sprintf("%s.%s='%s'", query.typ, query.attr, query.value)
|
|
}
|
|
query := strings.Join(list, " AND ")
|
|
return r.QueryTx(ctx, r.chainCmd.QueryTxQueryCommand(query))
|
|
}
|
|
|
|
// QueryTx queries tx events/query selectors.
|
|
func (r Runner) QueryTx(
|
|
ctx context.Context,
|
|
option ...step.Option,
|
|
) ([]Event, error) {
|
|
// execute the command and parse the output.
|
|
b := newBuffer()
|
|
if err := r.run(ctx, runOptions{stdout: b}, option...); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := struct {
|
|
Txs []struct {
|
|
Logs []struct {
|
|
Events []struct {
|
|
Type string `json:"type"`
|
|
Attrs []struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
} `json:"attributes"`
|
|
} `json:"events"`
|
|
} `json:"logs"`
|
|
TimeStamp string `json:"timestamp"`
|
|
} `json:"txs"`
|
|
}{}
|
|
|
|
data, err := b.JSONEnsuredBytes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(data, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var events []Event
|
|
for _, tx := range out.Txs {
|
|
for _, log := range tx.Logs {
|
|
for _, e := range log.Events {
|
|
var attrs []EventAttribute
|
|
for _, attr := range e.Attrs {
|
|
attrs = append(attrs, EventAttribute{
|
|
Key: attr.Key,
|
|
Value: attr.Value,
|
|
})
|
|
}
|
|
|
|
txTime, err := time.Parse(time.RFC3339, tx.TimeStamp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
events = append(events, Event{
|
|
Type: e.Type,
|
|
Attributes: attrs,
|
|
Time: txTime,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return events, nil
|
|
}
|