diff --git a/.gitignore b/.gitignore index 45e4e6a..05cedba 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ target/ .idea/ .vscode/ .DS_Store +data/ diff --git a/.releaserc.yml b/.releaserc.yml index 594dfc5..20cbdf1 100644 --- a/.releaserc.yml +++ b/.releaserc.yml @@ -8,7 +8,7 @@ plugins: - preset: conventionalcommits - - "@semantic-release/changelog" - changelogFile: CHANGELOG.md - changelogTitle: "# ØKP4 template-go" + changelogTitle: "# Axone Wallet Extractor Changelog" - - "@google/semantic-release-replace-plugin" - replacements: - files: [version] @@ -25,18 +25,18 @@ plugins: make build-go-all - - "@semantic-release/github" - assets: - - name: template-go_darwin_amd64 + - name: wallet-extractor_darwin_amd64 label: Binary - Darwin amd64 - path: "./target/dist/darwin/amd64/template-go" - - name: template-go_darwin_arm64 + path: "./target/dist/darwin/amd64/wallet-extractor" + - name: wallet-extractor_darwin_arm64 label: Binary - Darwin arm64 - path: "./target/dist/darwin/arm64/template-go" - - name: template-go_linux_amd64 + path: "./target/dist/darwin/arm64/wallet-extractor" + - name: wallet-extractor_linux_amd64 label: Binary - Linux amd64 - path: "./target/dist/linux/amd64/template-go" - - name: template-go_windows_amd64.exe + path: "./target/dist/linux/amd64/wallet-extractor" + - name: wallet-extractor_windows_amd64.exe label: Binary - Windows amd64 - path: "./target/dist/windows/amd64/template-go.exe" + path: "./target/dist/windows/amd64/wallet-extractor.exe" - - "@semantic-release/git" - assets: - CHANGELOG.md diff --git a/Makefile b/Makefile index f8ac087..cac5f97 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # ℹ Freely based on: https://gist.github.com/thomaspoignant/5b72d579bd5f311904d973652180c705 # Constants -BINARY_NAME = template-go +BINARY_NAME = $(shell basename `pwd`) TARGET_FOLDER = target DIST_FOLDER = $(TARGET_FOLDER)/dist DOCKER_IMAGE_GOLANG_CI = golangci/golangci-lint:v1.62 @@ -17,13 +17,13 @@ COLOR_RESET = $(shell tput -Txterm sgr0) VERSION := $(shell cat version) COMMIT := $(shell git log -1 --format='%H') LD_FLAGS = \ - -X okp4/template-go/internal/version.Name=$(BINARY_NAME) \ - -X okp4/template-go/internal/version.Version=$(VERSION) \ - -X okp4/template-go/internal/version.Commit=$(COMMIT) + -X github.com/axone-protocol/wallet-extractor/internal/version.Name=$(BINARY_NAME) \ + -X github.com/axone-protocol/wallet-extractor/internal/version.Version=$(VERSION) \ + -X github.com/axone-protocol/wallet-extractor/internal/version.Commit=$(COMMIT) BUILD_FLAGS := -ldflags '$(LD_FLAGS)' # Commands -GO_BUiLD := CGO_ENABLED=0 go build $(BUILD_FLAGS) +GO_BUILD := CGO_ENABLED=0 go build $(BUILD_FLAGS) # Environments ENVIRONMENTS = \ @@ -103,5 +103,5 @@ help: ## Show this help. # $2: architecture (GOARCH) # $3: filename of the executable generated define build-go - GOOS=$1 GOARCH=$2 $(GO_BUiLD) -o $3 ${CMD_ROOT} + GOOS=$1 GOARCH=$2 $(GO_BUILD) -o $3 ${CMD_ROOT} endef diff --git a/cmd/delegators.go b/cmd/delegators.go new file mode 100644 index 0000000..2f0e9f5 --- /dev/null +++ b/cmd/delegators.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "github.com/axone-protocol/wallet-extractor/pkg/delegators" + "github.com/spf13/cobra" +) + +const ( + flagChainName = "chain-name" +) + +var extractDelegatorsCmd = &cobra.Command{ + Use: "delegators [source] [dest]", + Short: "Extract all delegators into CSV files", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + chainName, _ := cmd.Flags().GetString(flagChainName) + + return delegators.Extract(chainName, args[0], args[1]) + }, +} + +func init() { + extractCmd.AddCommand(extractDelegatorsCmd) + + extractDelegatorsCmd.Flags().StringP(flagChainName, "n", "cosmos", "Name of the chain") +} diff --git a/cmd/extract.go b/cmd/extract.go new file mode 100644 index 0000000..e8f1695 --- /dev/null +++ b/cmd/extract.go @@ -0,0 +1,14 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var extractCmd = &cobra.Command{ + Use: "extract", + Short: "Extract data from a chain (snapshot)", +} + +func init() { + rootCmd.AddCommand(extractCmd) +} diff --git a/cmd/root.go b/cmd/root.go index d622d89..20a85ea 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,7 +8,7 @@ import ( // rootCmd represents the base command when called without any subcommands. var rootCmd = &cobra.Command{ - Use: "template-go", + Use: "wallet-extractor", Short: "A template fo Golang projects", Long: "A template fo Golang projects", } diff --git a/go.mod b/go.mod index 613ad6a..792f7f0 100644 --- a/go.mod +++ b/go.mod @@ -65,6 +65,7 @@ require ( github.com/go-kit/kit v0.13.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect diff --git a/go.sum b/go.sum index 65a565f..bc876c1 100644 --- a/go.sum +++ b/go.sum @@ -269,6 +269,8 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= diff --git a/pkg/delegators/extract.go b/pkg/delegators/extract.go new file mode 100644 index 0000000..3250f5a --- /dev/null +++ b/pkg/delegators/extract.go @@ -0,0 +1,159 @@ +package delegators + +import ( + "bufio" + "fmt" + "os" + "path" + "strings" + + "github.com/axone-protocol/wallet-extractor/pkg/keeper" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/gocarina/gocsv" + + "cosmossdk.io/log" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/bech32" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +func Extract(chainName, src, dst string) error { + logger := log.NewLogger(os.Stderr) + + logger.Info("Extracting wallets", "source", src, "destination", dst) + + keepers, err := keeper.OpenStore(src, logger) + if err != nil { + return err + } + + ctx := sdk.NewContext(keepers.Store, cmtproto.Header{}, false, keepers.Logger) + + validators, err := keepers.Staking.GetAllValidators(ctx) + if err != nil { + panic(err) + } + + keepers.Logger.Info("Analyzing validators", "count", len(validators)) + + err = extractChainMetadata(ctx, chainName, keepers, dst) + if err != nil { + return err + } + + return extractDelegators(ctx, chainName, keepers, validators, dst) +} + +func extractDelegators( + ctx sdk.Context, chainName string, keepers *keeper.Keepers, validators []stakingtypes.Validator, destination string, +) error { + file, err := os.OpenFile(path.Join(destination, "delegations.csv"), os.O_RDWR|os.O_CREATE|os.O_APPEND, os.ModePerm) + if err != nil { + return err + } + defer file.Close() + writer := bufio.NewWriter(file) + defer writer.Flush() + + prefix, err := guessPrefixFromValoper(validators[0].OperatorAddress) + if err != nil { + return err + } + + config := sdk.GetConfig() + + keepers.Bank.IterateAllBalances(ctx, func(addr sdk.AccAddress, _ sdk.Coin) (stop bool) { + for _, val := range validators { + if config.GetBech32AccountAddrPrefix() != prefix { + config.SetBech32PrefixForValidator( + fmt.Sprintf("%svaloper", prefix), + fmt.Sprintf("%svaloperpub", prefix), + ) + } + + valAddr, err := sdk.ValAddressFromBech32(val.OperatorAddress) + if err != nil { + panic(err) + } + delegation, err := keepers.Staking.GetDelegation(ctx, addr, valAddr) + if err != nil { + continue + } + + record := Delegations{ + ChainName: chainName, + DelegatorNativeAddr: delegation.DelegatorAddress, + DelegatorCosmosAddr: convertAndEncodeMust("cosmos", delegation.DelegatorAddress), + DelegatorAxoneAddr: convertAndEncodeMust("axone", delegation.DelegatorAddress), + ValidatorAddr: delegation.ValidatorAddress, + Shares: delegation.Shares.String(), + } + + v, err := gocsv.MarshalStringWithoutHeaders(&[]Delegations{record}) + if err != nil { + panic(err) + } + + _, err = writer.WriteString(v) + if err != nil { + panic(err) + } + } + + return false + }) + + return nil +} + +func extractChainMetadata( + _ sdk.Context, chainName string, keepers *keeper.Keepers, destination string, +) error { + file, err := os.OpenFile(path.Join(destination, "metadatas.csv"), os.O_RDWR|os.O_CREATE|os.O_APPEND, os.ModePerm) + if err != nil { + return err + } + defer file.Close() + writer := bufio.NewWriter(file) + defer writer.Flush() + + record := Chains{ + Name: chainName, + StoreVersion: fmt.Sprintf("%d", keepers.Store.LastCommitID().Version), + StoreHash: fmt.Sprintf("%X", keepers.Store.LastCommitID().Hash), + } + + v, err := gocsv.MarshalStringWithoutHeaders(&[]Chains{record}) + if err != nil { + panic(err) + } + + _, err = writer.WriteString(v) + if err != nil { + panic(err) + } + + return nil +} + +func convertAndEncodeMust(hrp string, bech string) string { + _, bytes, err := bech32.DecodeAndConvert(bech) + if err != nil { + panic(err) + } + + encoded, err := bech32.ConvertAndEncode(hrp, bytes) + if err != nil { + panic(err) + } + + return encoded +} + +func guessPrefixFromValoper(valoper string) (string, error) { + if idx := strings.Index(valoper, "valoper"); idx != -1 { + return valoper[:idx], nil + } + return "", fmt.Errorf("valoper not found in operator address: %s", valoper) +} diff --git a/pkg/delegators/record.go b/pkg/delegators/record.go new file mode 100644 index 0000000..4fa3a69 --- /dev/null +++ b/pkg/delegators/record.go @@ -0,0 +1,17 @@ +package delegators + +type Chains struct { + Name string `csv:"name"` + StoreVersion string `csv:"store_version"` + StoreHash string `csv:"store_hash"` +} + +type Delegations struct { + ChainName string `csv:"chain_name"` + DelegatorNativeAddr string `csv:"delegator_native_addr"` + DelegatorCosmosAddr string `csv:"delegator_cosmos_addr"` + DelegatorAxoneAddr string `csv:"delegator_axone_addr"` + + ValidatorAddr string `csv:"validator_addr"` + Shares string `csv:"shares"` +} diff --git a/pkg/keeper.go b/pkg/keeper/keeper.go similarity index 92% rename from pkg/keeper.go rename to pkg/keeper/keeper.go index bacb9a3..7bbfb09 100644 --- a/pkg/keeper.go +++ b/pkg/keeper/keeper.go @@ -1,4 +1,4 @@ -package pkg +package keeper import ( "fmt" @@ -42,15 +42,7 @@ type Keepers struct { } // OpenStore opens an existing store at the given path and returns the keepers for the store. -func OpenStore(dbPath, addressPrefix string) (*Keepers, error) { - config := sdk.GetConfig() - config.SetBech32PrefixForValidator( - fmt.Sprintf("%svaloper", addressPrefix), - fmt.Sprintf("%svaloperpub", addressPrefix), - ) - config.Seal() - - logger := log.NewNopLogger() +func OpenStore(dbPath string, logger log.Logger) (*Keepers, error) { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, @@ -105,6 +97,7 @@ func newCodec() (*codec.ProtoCodec, error) { return nil, err } std.RegisterInterfaces(interfaceRegistry) + interfaceRegistry.RegisterInterface("/cosmos.auth.v1beta1.BaseAccount", (*sdk.AccountI)(nil)) return codec.NewProtoCodec(interfaceRegistry), nil } @@ -154,6 +147,8 @@ func newStakingKeeper( func newCommitMultiStore( dbPath string, keys map[string]*storetypes.KVStoreKey, logger log.Logger, ) (storetypes.CommitMultiStore, error) { + logger.Debug("Opening store", "path", dbPath) + db, err := dbm.NewDB("application", dbm.GoLevelDBBackend, dbPath) if err != nil { return nil, err @@ -169,7 +164,8 @@ func newCommitMultiStore( } commitID := ms.LastCommitID() - logger.Info("Loaded store at version: %d, hash: %X\n", commitID.Version, commitID.Hash) + + logger.Debug("Store loaded", "version", commitID.Version, "hash", fmt.Sprintf("%x", commitID.Hash)) return ms, nil }