From 3189da33ed069510b8a375aca7ab43000db0e41f Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 2 May 2023 14:13:32 +0200 Subject: [PATCH 01/12] Add CLI command to list tBTC deposits We added a CLI command to list the tBTC deposits. The command queries the chain for revealed deposits and outputs their details to stdout. To list all deposits run: ``` keep-client wallet deposits ``` Following flags can be used with the command: - `--wallet
` - filter deposits only for given wallet - `--hide-swept` - doesn't show deposits that were already swept - `--sort-amount` - sort output by the deposit amount The command requires `ethereum` node details to be provided in the config. --- cmd/cmd.go | 1 + cmd/wallet.go | 100 +++++++++++++++++++++ cmd/wallet/deposits.go | 186 ++++++++++++++++++++++++++++++++++++++++ cmd/wallet/walletcmd.go | 7 ++ 4 files changed, 294 insertions(+) create mode 100644 cmd/wallet.go create mode 100644 cmd/wallet/deposits.go create mode 100644 cmd/wallet/walletcmd.go diff --git a/cmd/cmd.go b/cmd/cmd.go index fa428d0a47..484ae7dafa 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -36,6 +36,7 @@ func init() { PingCommand, EthereumCommand, MaintainerCommand, + WalletCommand, ) } diff --git a/cmd/wallet.go b/cmd/wallet.go new file mode 100644 index 0000000000..37a4282b0c --- /dev/null +++ b/cmd/wallet.go @@ -0,0 +1,100 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + walletcmd "github.com/keep-network/keep-core/cmd/wallet" + "github.com/keep-network/keep-core/config" + "github.com/keep-network/keep-core/pkg/chain/ethereum" +) + +var ( + walletFlagName = "wallet" + hideSweptFlagName = "hide-swept" + sortByAmountFlagName = "sort-amount" +) + +// WalletCommand contains the definition of tBTC wallets tools. +var WalletCommand = &cobra.Command{ + Use: "wallet", + Short: "tBTC wallets tools", + Long: "The tool exposes commands for interactions with tBTC wallets.", + TraverseChildren: true, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + if err := clientConfig.ReadConfig( + configFilePath, + cmd.Flags(), + config.General, config.Ethereum, config.BitcoinElectrum, + ); err != nil { + logger.Fatalf("error reading config: %v", err) + } + }, +} + +var depositsCommand = cobra.Command{ + Use: "deposits", + Short: "get deposits", + Long: "Gets tBTC deposits details from the chain and prints them.", + TraverseChildren: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + wallet, err := cmd.Flags().GetString(walletFlagName) + if err != nil { + return fmt.Errorf("failed to find wallet flag: %v", err) + } + + hideSwept, err := cmd.Flags().GetBool(hideSweptFlagName) + if err != nil { + return fmt.Errorf("failed to find show swept flag: %v", err) + } + + sortByAmount, err := cmd.Flags().GetBool(sortByAmountFlagName) + if err != nil { + return fmt.Errorf("failed to find show swept flag: %v", err) + } + + _, tbtcChain, _, _, _, err := ethereum.Connect(ctx, clientConfig.Ethereum) + if err != nil { + return fmt.Errorf( + "could not connect to Bitcoin difficulty chain: [%v]", + err, + ) + } + + return walletcmd.ListDeposits(tbtcChain, wallet, hideSwept, sortByAmount) + }, +} + +func init() { + initFlags( + WalletCommand, + &configFilePath, + clientConfig, + config.General, config.Ethereum, config.BitcoinElectrum, + ) + + // Wallet Command + WalletCommand.PersistentFlags().String( + walletFlagName, + "", + "wallet public key hash", + ) + + // Deposits Subcommand + depositsCommand.Flags().Bool( + hideSweptFlagName, + false, + "hide swept deposits", + ) + + depositsCommand.Flags().Bool( + sortByAmountFlagName, + false, + "sort by deposit amount", + ) + + WalletCommand.AddCommand(&depositsCommand) +} diff --git a/cmd/wallet/deposits.go b/cmd/wallet/deposits.go new file mode 100644 index 0000000000..54a064da0f --- /dev/null +++ b/cmd/wallet/deposits.go @@ -0,0 +1,186 @@ +package walletcmd + +import ( + "encoding/binary" + "encoding/hex" + "fmt" + "os" + "sort" + "text/tabwriter" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +type depositEntry struct { + wallet [20]byte + + depositKey string + revealedAtBlock uint64 + isSwept bool + + fundingTransactionHash bitcoin.Hash + fundingTransactionOutputIndex uint32 + amountBtc float64 +} + +// ListDeposits gets deposits from the chain. +func ListDeposits( + tbtcChain tbtc.Chain, + walletStr string, + hideSwept bool, + sortByAmount bool, +) error { + deposits, err := getDeposits(tbtcChain, walletStr) + if err != nil { + return fmt.Errorf( + "failed to get deposits: [%w]", + err, + ) + } + + if len(deposits) == 0 { + return fmt.Errorf("no deposits found") + } + + // Filter + if hideSwept { + deposits = removeSwept(deposits) + } + + // Order + sort.SliceStable(deposits, func(i, j int) bool { + return deposits[i].revealedAtBlock > deposits[j].revealedAtBlock + }) + + if sortByAmount { + sort.SliceStable(deposits, func(i, j int) bool { + return deposits[i].amountBtc < deposits[j].amountBtc + }) + } + + // Print + if err := printTable(deposits); err != nil { + return fmt.Errorf("failed to print deposits table: %v", err) + } + + return nil +} + +func removeSwept(deposits []depositEntry) []depositEntry { + result := []depositEntry{} + for _, deposit := range deposits { + if deposit.isSwept { + continue + } + result = append(result, deposit) + } + return result +} + +func getDeposits(tbtcChain tbtc.Chain, walletStr string) ([]depositEntry, error) { + logger.Infof("reading deposits from chain...") + + filter := &tbtc.DepositRevealedEventFilter{} + if len(walletStr) > 0 { + walletPublicKeyHash, err := hexToWalletPublicKeyHash(walletStr) + if err != nil { + return []depositEntry{}, fmt.Errorf("failed to extract wallet public key hash: %v", err) + } + + filter.WalletPublicKeyHash = [][20]byte{walletPublicKeyHash} + } + + depositRevealedEvent, err := tbtcChain.PastDepositRevealedEvents(filter) + if err != nil { + return []depositEntry{}, fmt.Errorf( + "failed to get past deposit revealed events: [%w]", + err, + ) + } + + logger.Infof("found %d DepositRevealed events", len(depositRevealedEvent)) + + result := make([]depositEntry, len(depositRevealedEvent)) + for i, event := range depositRevealedEvent { + logger.Debugf("getting details of deposit %d/%d", i+1, len(depositRevealedEvent)) + + depositKey := buildDepositKey(event.FundingTxHash, event.FundingOutputIndex) + + depositRequest, err := tbtcChain.GetDepositRequest(event.FundingTxHash, event.FundingOutputIndex) + if err != nil { + return result, fmt.Errorf( + "failed to get deposit request: [%w]", + err, + ) + } + + result[i] = depositEntry{ + wallet: event.WalletPublicKeyHash, + depositKey: depositKey, + revealedAtBlock: event.BlockNumber, + isSwept: depositRequest.SweptAt.After(depositRequest.RevealedAt), + fundingTransactionHash: event.FundingTxHash, + fundingTransactionOutputIndex: event.FundingOutputIndex, + amountBtc: convertSatToBtc(float64(depositRequest.Amount)), + } + } + + return result, nil +} + +func printTable(deposits []depositEntry) error { + w := tabwriter.NewWriter(os.Stdout, 2, 4, 1, ' ', tabwriter.AlignRight) + fmt.Fprintf(w, "index\twallet\tvalue (BTC)\tdeposit key\tfunding transaction\tswept\t\n") + + for i, deposit := range deposits { + fmt.Fprintf(w, "%d\t%s\t%.5f\t%s\t%s\t%v\t\n", + i, + "0x"+hex.EncodeToString(deposit.wallet[:]), + deposit.amountBtc, + deposit.depositKey, + fmt.Sprintf( + "%s:%d", + deposit.fundingTransactionHash.Hex(bitcoin.ReversedByteOrder), + deposit.fundingTransactionOutputIndex, + ), + deposit.isSwept, + ) + } + w.Flush() + + return nil +} + +func hexToWalletPublicKeyHash(str string) ([20]byte, error) { + walletHex, err := hexutil.Decode(str) + if err != nil { + return [20]byte{}, fmt.Errorf("failed to parse arguments: %w", err) + } + + var walletPublicKeyHash [20]byte + copy(walletPublicKeyHash[:], walletHex) + + return walletPublicKeyHash, nil +} + +func buildDepositKey( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, +) string { + fundingOutputIndexBytes := make([]byte, 4) + binary.BigEndian.PutUint32(fundingOutputIndexBytes, fundingOutputIndex) + + depositKey := crypto.Keccak256Hash( + append(fundingTxHash[:], fundingOutputIndexBytes...), + ) + + return depositKey.Hex() +} + +func convertSatToBtc(sats float64) float64 { + return sats / float64(100000000) +} diff --git a/cmd/wallet/walletcmd.go b/cmd/wallet/walletcmd.go new file mode 100644 index 0000000000..67e7828a2b --- /dev/null +++ b/cmd/wallet/walletcmd.go @@ -0,0 +1,7 @@ +package walletcmd + +import ( + "github.com/ipfs/go-log" +) + +var logger = log.Logger("keep-wallet-cmd") From f933ecb5ae8178798c157de2542f469ba101b8e2 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 2 May 2023 20:30:22 +0200 Subject: [PATCH 02/12] Handle w.Flush() error go scan was complaining about unhandled error --- cmd/wallet/deposits.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/wallet/deposits.go b/cmd/wallet/deposits.go index 54a064da0f..060446cdd4 100644 --- a/cmd/wallet/deposits.go +++ b/cmd/wallet/deposits.go @@ -150,7 +150,10 @@ func printTable(deposits []depositEntry) error { deposit.isSwept, ) } - w.Flush() + + if err := w.Flush(); err != nil { + return fmt.Errorf("failed to flush the writer: %v", err) + } return nil } From 82b381cab4843ae4b8f2137cc768b228a4a17130 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 5 May 2023 11:36:10 +0200 Subject: [PATCH 03/12] Update variables naming --- cmd/wallet/deposits.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cmd/wallet/deposits.go b/cmd/wallet/deposits.go index 060446cdd4..6d706703f7 100644 --- a/cmd/wallet/deposits.go +++ b/cmd/wallet/deposits.go @@ -16,11 +16,11 @@ import ( ) type depositEntry struct { - wallet [20]byte + walletPublicKeyHash [20]byte - depositKey string - revealedAtBlock uint64 - isSwept bool + depositKey string + revealBlock uint64 + isSwept bool fundingTransactionHash bitcoin.Hash fundingTransactionOutputIndex uint32 @@ -30,11 +30,11 @@ type depositEntry struct { // ListDeposits gets deposits from the chain. func ListDeposits( tbtcChain tbtc.Chain, - walletStr string, + walletPublicKeyHashString string, hideSwept bool, sortByAmount bool, ) error { - deposits, err := getDeposits(tbtcChain, walletStr) + deposits, err := getDeposits(tbtcChain, walletPublicKeyHashString) if err != nil { return fmt.Errorf( "failed to get deposits: [%w]", @@ -53,7 +53,7 @@ func ListDeposits( // Order sort.SliceStable(deposits, func(i, j int) bool { - return deposits[i].revealedAtBlock > deposits[j].revealedAtBlock + return deposits[i].revealBlock > deposits[j].revealBlock }) if sortByAmount { @@ -81,12 +81,12 @@ func removeSwept(deposits []depositEntry) []depositEntry { return result } -func getDeposits(tbtcChain tbtc.Chain, walletStr string) ([]depositEntry, error) { +func getDeposits(tbtcChain tbtc.Chain, walletPublicKeyHashString string) ([]depositEntry, error) { logger.Infof("reading deposits from chain...") filter := &tbtc.DepositRevealedEventFilter{} - if len(walletStr) > 0 { - walletPublicKeyHash, err := hexToWalletPublicKeyHash(walletStr) + if len(walletPublicKeyHashString) > 0 { + walletPublicKeyHash, err := hexToWalletPublicKeyHash(walletPublicKeyHashString) if err != nil { return []depositEntry{}, fmt.Errorf("failed to extract wallet public key hash: %v", err) } @@ -119,9 +119,9 @@ func getDeposits(tbtcChain tbtc.Chain, walletStr string) ([]depositEntry, error) } result[i] = depositEntry{ - wallet: event.WalletPublicKeyHash, + walletPublicKeyHash: event.WalletPublicKeyHash, depositKey: depositKey, - revealedAtBlock: event.BlockNumber, + revealBlock: event.BlockNumber, isSwept: depositRequest.SweptAt.After(depositRequest.RevealedAt), fundingTransactionHash: event.FundingTxHash, fundingTransactionOutputIndex: event.FundingOutputIndex, @@ -139,7 +139,7 @@ func printTable(deposits []depositEntry) error { for i, deposit := range deposits { fmt.Fprintf(w, "%d\t%s\t%.5f\t%s\t%s\t%v\t\n", i, - "0x"+hex.EncodeToString(deposit.wallet[:]), + "0x"+hex.EncodeToString(deposit.walletPublicKeyHash[:]), deposit.amountBtc, deposit.depositKey, fmt.Sprintf( From 3e13eec26d270d19d1cbe0ee3f4654840f3ad066 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 5 May 2023 11:38:42 +0200 Subject: [PATCH 04/12] Add TODO about bitcoin confirmations --- cmd/wallet/deposits.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/wallet/deposits.go b/cmd/wallet/deposits.go index 6d706703f7..645a74a604 100644 --- a/cmd/wallet/deposits.go +++ b/cmd/wallet/deposits.go @@ -25,6 +25,7 @@ type depositEntry struct { fundingTransactionHash bitcoin.Hash fundingTransactionOutputIndex uint32 amountBtc float64 + // TODO: Add Bitcoin confirmations number } // ListDeposits gets deposits from the chain. From fc1cf3a95a8e4d26711143d8e9c736dc13b0224f Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 5 May 2023 11:59:58 +0200 Subject: [PATCH 05/12] Compare SweptAt with 0 --- cmd/wallet/deposits.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/wallet/deposits.go b/cmd/wallet/deposits.go index 645a74a604..85bcb74715 100644 --- a/cmd/wallet/deposits.go +++ b/cmd/wallet/deposits.go @@ -123,7 +123,7 @@ func getDeposits(tbtcChain tbtc.Chain, walletPublicKeyHashString string) ([]depo walletPublicKeyHash: event.WalletPublicKeyHash, depositKey: depositKey, revealBlock: event.BlockNumber, - isSwept: depositRequest.SweptAt.After(depositRequest.RevealedAt), + isSwept: depositRequest.SweptAt.Unix() != 0, fundingTransactionHash: event.FundingTxHash, fundingTransactionOutputIndex: event.FundingOutputIndex, amountBtc: convertSatToBtc(float64(depositRequest.Amount)), From 4d2814372651b17f97247e287c8dc482c9c64f1e Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 8 May 2023 13:06:10 +0200 Subject: [PATCH 06/12] Get Bitcoin transaction confirmations Get number of bitcoin confirmations for the given transaction and output it in the table. --- cmd/wallet.go | 16 ++++++++++++++-- cmd/wallet/deposits.go | 29 ++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/cmd/wallet.go b/cmd/wallet.go index 37a4282b0c..cb30dfa070 100644 --- a/cmd/wallet.go +++ b/cmd/wallet.go @@ -7,6 +7,7 @@ import ( walletcmd "github.com/keep-network/keep-core/cmd/wallet" "github.com/keep-network/keep-core/config" + "github.com/keep-network/keep-core/pkg/bitcoin/electrum" "github.com/keep-network/keep-core/pkg/chain/ethereum" ) @@ -59,12 +60,23 @@ var depositsCommand = cobra.Command{ _, tbtcChain, _, _, _, err := ethereum.Connect(ctx, clientConfig.Ethereum) if err != nil { return fmt.Errorf( - "could not connect to Bitcoin difficulty chain: [%v]", + "could not connect to Ethereum chain: [%v]", err, ) } - return walletcmd.ListDeposits(tbtcChain, wallet, hideSwept, sortByAmount) + btcChain, err := electrum.Connect(ctx, clientConfig.Bitcoin.Electrum) + if err != nil { + return fmt.Errorf("could not connect to Electrum chain: [%v]", err) + } + + return walletcmd.ListDeposits( + tbtcChain, + btcChain, + wallet, + hideSwept, + sortByAmount, + ) }, } diff --git a/cmd/wallet/deposits.go b/cmd/wallet/deposits.go index 85bcb74715..0d590d4923 100644 --- a/cmd/wallet/deposits.go +++ b/cmd/wallet/deposits.go @@ -25,17 +25,22 @@ type depositEntry struct { fundingTransactionHash bitcoin.Hash fundingTransactionOutputIndex uint32 amountBtc float64 - // TODO: Add Bitcoin confirmations number + confirmations uint } // ListDeposits gets deposits from the chain. func ListDeposits( tbtcChain tbtc.Chain, + btcChain bitcoin.Chain, walletPublicKeyHashString string, hideSwept bool, sortByAmount bool, ) error { - deposits, err := getDeposits(tbtcChain, walletPublicKeyHashString) + deposits, err := getDeposits( + tbtcChain, + btcChain, + walletPublicKeyHashString, + ) if err != nil { return fmt.Errorf( "failed to get deposits: [%w]", @@ -82,7 +87,11 @@ func removeSwept(deposits []depositEntry) []depositEntry { return result } -func getDeposits(tbtcChain tbtc.Chain, walletPublicKeyHashString string) ([]depositEntry, error) { +func getDeposits( + tbtcChain tbtc.Chain, + btcChain bitcoin.Chain, + walletPublicKeyHashString string, +) ([]depositEntry, error) { logger.Infof("reading deposits from chain...") filter := &tbtc.DepositRevealedEventFilter{} @@ -119,6 +128,14 @@ func getDeposits(tbtcChain tbtc.Chain, walletPublicKeyHashString string) ([]depo ) } + confirmations, err := btcChain.GetTransactionConfirmations(event.FundingTxHash) + if err != nil { + logger.Errorf( + "failed to get bitcoin transaction confirmations: [%v]", + err, + ) + } + result[i] = depositEntry{ walletPublicKeyHash: event.WalletPublicKeyHash, depositKey: depositKey, @@ -127,6 +144,7 @@ func getDeposits(tbtcChain tbtc.Chain, walletPublicKeyHashString string) ([]depo fundingTransactionHash: event.FundingTxHash, fundingTransactionOutputIndex: event.FundingOutputIndex, amountBtc: convertSatToBtc(float64(depositRequest.Amount)), + confirmations: confirmations, } } @@ -135,10 +153,10 @@ func getDeposits(tbtcChain tbtc.Chain, walletPublicKeyHashString string) ([]depo func printTable(deposits []depositEntry) error { w := tabwriter.NewWriter(os.Stdout, 2, 4, 1, ' ', tabwriter.AlignRight) - fmt.Fprintf(w, "index\twallet\tvalue (BTC)\tdeposit key\tfunding transaction\tswept\t\n") + fmt.Fprintf(w, "index\twallet\tvalue (BTC)\tdeposit key\tfunding transaction\tconfirmations\tswept\t\n") for i, deposit := range deposits { - fmt.Fprintf(w, "%d\t%s\t%.5f\t%s\t%s\t%v\t\n", + fmt.Fprintf(w, "%d\t%s\t%.5f\t%s\t%s\t%d\t%t\t\n", i, "0x"+hex.EncodeToString(deposit.walletPublicKeyHash[:]), deposit.amountBtc, @@ -148,6 +166,7 @@ func printTable(deposits []depositEntry) error { deposit.fundingTransactionHash.Hex(bitcoin.ReversedByteOrder), deposit.fundingTransactionOutputIndex, ), + deposit.confirmations, deposit.isSwept, ) } From ebac502d4afe2e3b4b3750bf48b4cbc125bc8a04 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 8 May 2023 13:07:49 +0200 Subject: [PATCH 07/12] Add head and tail flags The flags can be used to query specific number of top or bottom deposits. By default deposits are sorted by the creation block, so with `--head X` user can select X oldest deposits and with `--tail Y` get Y newest deposits. When `--sort-amount --head X` flags are used only X smalest deposits are returned and with `--sort-amount --tail Y` only Y biggest deposits are returned. --- cmd/wallet.go | 26 +++++++++++++++++++ cmd/wallet/deposits.go | 58 ++++++++++++++++++++++++++++++------------ 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/cmd/wallet.go b/cmd/wallet.go index cb30dfa070..4aab030f6b 100644 --- a/cmd/wallet.go +++ b/cmd/wallet.go @@ -15,6 +15,8 @@ var ( walletFlagName = "wallet" hideSweptFlagName = "hide-swept" sortByAmountFlagName = "sort-amount" + headFlagName = "head" + tailFlagName = "tail" ) // WalletCommand contains the definition of tBTC wallets tools. @@ -57,6 +59,16 @@ var depositsCommand = cobra.Command{ return fmt.Errorf("failed to find show swept flag: %v", err) } + head, err := cmd.Flags().GetInt(headFlagName) + if err != nil { + return fmt.Errorf("failed to find head flag: %v", err) + } + + tail, err := cmd.Flags().GetInt(tailFlagName) + if err != nil { + return fmt.Errorf("failed to find tail flag: %v", err) + } + _, tbtcChain, _, _, _, err := ethereum.Connect(ctx, clientConfig.Ethereum) if err != nil { return fmt.Errorf( @@ -76,6 +88,8 @@ var depositsCommand = cobra.Command{ wallet, hideSwept, sortByAmount, + head, + tail, ) }, } @@ -108,5 +122,17 @@ func init() { "sort by deposit amount", ) + depositsCommand.Flags().Int( + headFlagName, + 0, + "get head of deposits", + ) + + depositsCommand.Flags().Int( + tailFlagName, + 0, + "get tail of deposits", + ) + WalletCommand.AddCommand(&depositsCommand) } diff --git a/cmd/wallet/deposits.go b/cmd/wallet/deposits.go index 0d590d4923..824508d523 100644 --- a/cmd/wallet/deposits.go +++ b/cmd/wallet/deposits.go @@ -35,11 +35,16 @@ func ListDeposits( walletPublicKeyHashString string, hideSwept bool, sortByAmount bool, + head int, + tail int, ) error { deposits, err := getDeposits( tbtcChain, btcChain, walletPublicKeyHashString, + sortByAmount, + head, + tail, ) if err != nil { return fmt.Errorf( @@ -57,17 +62,6 @@ func ListDeposits( deposits = removeSwept(deposits) } - // Order - sort.SliceStable(deposits, func(i, j int) bool { - return deposits[i].revealBlock > deposits[j].revealBlock - }) - - if sortByAmount { - sort.SliceStable(deposits, func(i, j int) bool { - return deposits[i].amountBtc < deposits[j].amountBtc - }) - } - // Print if err := printTable(deposits); err != nil { return fmt.Errorf("failed to print deposits table: %v", err) @@ -91,6 +85,9 @@ func getDeposits( tbtcChain tbtc.Chain, btcChain bitcoin.Chain, walletPublicKeyHashString string, + sortByAmount bool, + head int, + tail int, ) ([]depositEntry, error) { logger.Infof("reading deposits from chain...") @@ -104,7 +101,7 @@ func getDeposits( filter.WalletPublicKeyHash = [][20]byte{walletPublicKeyHash} } - depositRevealedEvent, err := tbtcChain.PastDepositRevealedEvents(filter) + allDepositRevealedEvents, err := tbtcChain.PastDepositRevealedEvents(filter) if err != nil { return []depositEntry{}, fmt.Errorf( "failed to get past deposit revealed events: [%w]", @@ -112,11 +109,40 @@ func getDeposits( ) } - logger.Infof("found %d DepositRevealed events", len(depositRevealedEvent)) + logger.Infof("found %d DepositRevealed events", len(allDepositRevealedEvents)) + + // Order + sort.SliceStable(allDepositRevealedEvents, func(i, j int) bool { + return allDepositRevealedEvents[i].BlockNumber > allDepositRevealedEvents[j].BlockNumber + }) + + if sortByAmount { + sort.SliceStable(allDepositRevealedEvents, func(i, j int) bool { + return allDepositRevealedEvents[i].Amount < allDepositRevealedEvents[j].Amount + }) + } + + // Filter + depositRevealedEvents := []*tbtc.DepositRevealedEvent{} + + if len(allDepositRevealedEvents) > head+tail && (head > 0 || tail > 0) { + // Head + depositRevealedEvents = append( + depositRevealedEvents, + allDepositRevealedEvents[:head]..., + ) + // Tail + depositRevealedEvents = append( + depositRevealedEvents, + allDepositRevealedEvents[len(allDepositRevealedEvents)-tail:]..., + ) + } else { + copy(depositRevealedEvents, allDepositRevealedEvents) + } - result := make([]depositEntry, len(depositRevealedEvent)) - for i, event := range depositRevealedEvent { - logger.Debugf("getting details of deposit %d/%d", i+1, len(depositRevealedEvent)) + result := make([]depositEntry, len(depositRevealedEvents)) + for i, event := range depositRevealedEvents { + logger.Debugf("getting details of deposit %d/%d", i+1, len(depositRevealedEvents)) depositKey := buildDepositKey(event.FundingTxHash, event.FundingOutputIndex) From 6e4e90ad3ba3b0bd864ca0ccf991ee7392295450 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 8 May 2023 13:31:20 +0200 Subject: [PATCH 08/12] Expose BuildDepositKey function The function can be used externally to build the deposit key based on the rules specific for the given chain implementation. --- cmd/wallet/deposits.go | 20 ++------------------ pkg/chain/ethereum/tbtc.go | 11 +++++++++-- pkg/tbtc/chain.go | 4 ++++ pkg/tbtc/chain_test.go | 12 +++++++++++- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/cmd/wallet/deposits.go b/cmd/wallet/deposits.go index 824508d523..b0d0243156 100644 --- a/cmd/wallet/deposits.go +++ b/cmd/wallet/deposits.go @@ -1,7 +1,6 @@ package walletcmd import ( - "encoding/binary" "encoding/hex" "fmt" "os" @@ -9,7 +8,6 @@ import ( "text/tabwriter" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/crypto" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/tbtc" @@ -144,7 +142,7 @@ func getDeposits( for i, event := range depositRevealedEvents { logger.Debugf("getting details of deposit %d/%d", i+1, len(depositRevealedEvents)) - depositKey := buildDepositKey(event.FundingTxHash, event.FundingOutputIndex) + depositKey := tbtcChain.BuildDepositKey(event.FundingTxHash, event.FundingOutputIndex) depositRequest, err := tbtcChain.GetDepositRequest(event.FundingTxHash, event.FundingOutputIndex) if err != nil { @@ -164,7 +162,7 @@ func getDeposits( result[i] = depositEntry{ walletPublicKeyHash: event.WalletPublicKeyHash, - depositKey: depositKey, + depositKey: hexutil.Encode(depositKey.Bytes()), revealBlock: event.BlockNumber, isSwept: depositRequest.SweptAt.Unix() != 0, fundingTransactionHash: event.FundingTxHash, @@ -216,20 +214,6 @@ func hexToWalletPublicKeyHash(str string) ([20]byte, error) { return walletPublicKeyHash, nil } -func buildDepositKey( - fundingTxHash bitcoin.Hash, - fundingOutputIndex uint32, -) string { - fundingOutputIndexBytes := make([]byte, 4) - binary.BigEndian.PutUint32(fundingOutputIndexBytes, fundingOutputIndex) - - depositKey := crypto.Keccak256Hash( - append(fundingTxHash[:], fundingOutputIndexBytes...), - ) - - return depositKey.Hex() -} - func convertSatToBtc(sats float64) float64 { return sats / float64(100000000) } diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 709af330f1..d289619b5c 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -864,8 +864,8 @@ func (tc *TbtcChain) IsDKGResultValid( // a boolean indicating whether the result is valid or not. The outcome parameter // must be a pointer to a struct containing a boolean flag as the first field. // -// TODO: Find a better way to get the validity flag. This would require -// changes in the contracts binding generator. +// TODO: Find a better way to get the validity flag. This would require changes +// in the contracts binding generator. func parseDkgResultValidationOutcome( outcome interface{}, ) (bool, error) { @@ -1091,6 +1091,13 @@ func computeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOutput) [32]byte { return mainUtxoHash } +func (tc *TbtcChain) BuildDepositKey( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, +) *big.Int { + return buildDepositKey(fundingTxHash, fundingOutputIndex) +} + func buildDepositKey( fundingTxHash bitcoin.Hash, fundingOutputIndex uint32, diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index 8d4d503bb8..98bb327354 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -208,6 +208,10 @@ type BridgeChain interface { // ComputeMainUtxoHash computes the hash of the provided main UTXO // according to the on-chain Bridge rules. ComputeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOutput) [32]byte + + // BuildDepositKey calculates a deposit key for the given funding transaction + // which is an unique identifier for a deposit on-chain. + BuildDepositKey(fundingTxHash bitcoin.Hash, fundingOutputIndex uint32) *big.Int } // HeartbeatRequestedEvent represents a Bridge heartbeat request event. diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 80269839d2..015739bd4e 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -14,6 +14,8 @@ import ( "sync" "time" + "golang.org/x/crypto/sha3" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/chain/local_v1" @@ -21,7 +23,6 @@ import ( "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/subscription" "github.com/keep-network/keep-core/pkg/tecdsa/dkg" - "golang.org/x/crypto/sha3" ) const localChainOperatorID = chain.OperatorID(1) @@ -551,6 +552,15 @@ func (lc *localChain) setDepositRequest( lc.depositRequests[requestKey] = request } +func (lc *localChain) BuildDepositKey( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, +) *big.Int { + depositKeyBytes := buildDepositRequestKey(fundingTxHash, fundingOutputIndex) + + return new(big.Int).SetBytes(depositKeyBytes[:]) +} + func buildDepositRequestKey( fundingTxHash bitcoin.Hash, fundingOutputIndex uint32, From 2d1009307696cdec294365212080840c9176b81b Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 8 May 2023 14:26:11 +0200 Subject: [PATCH 09/12] Move walletcmd to pkg/coordinator --- cmd/wallet.go | 4 ++-- cmd/wallet/walletcmd.go | 7 ------- pkg/coordinator/coordinator.go | 7 +++++++ {cmd/wallet => pkg/coordinator}/deposits.go | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 cmd/wallet/walletcmd.go create mode 100644 pkg/coordinator/coordinator.go rename {cmd/wallet => pkg/coordinator}/deposits.go (99%) diff --git a/cmd/wallet.go b/cmd/wallet.go index 4aab030f6b..57bd97be8e 100644 --- a/cmd/wallet.go +++ b/cmd/wallet.go @@ -5,10 +5,10 @@ import ( "github.com/spf13/cobra" - walletcmd "github.com/keep-network/keep-core/cmd/wallet" "github.com/keep-network/keep-core/config" "github.com/keep-network/keep-core/pkg/bitcoin/electrum" "github.com/keep-network/keep-core/pkg/chain/ethereum" + "github.com/keep-network/keep-core/pkg/coordinator" ) var ( @@ -82,7 +82,7 @@ var depositsCommand = cobra.Command{ return fmt.Errorf("could not connect to Electrum chain: [%v]", err) } - return walletcmd.ListDeposits( + return coordinator.ListDeposits( tbtcChain, btcChain, wallet, diff --git a/cmd/wallet/walletcmd.go b/cmd/wallet/walletcmd.go deleted file mode 100644 index 67e7828a2b..0000000000 --- a/cmd/wallet/walletcmd.go +++ /dev/null @@ -1,7 +0,0 @@ -package walletcmd - -import ( - "github.com/ipfs/go-log" -) - -var logger = log.Logger("keep-wallet-cmd") diff --git a/pkg/coordinator/coordinator.go b/pkg/coordinator/coordinator.go new file mode 100644 index 0000000000..fee65ecae5 --- /dev/null +++ b/pkg/coordinator/coordinator.go @@ -0,0 +1,7 @@ +package coordinator + +import ( + "github.com/ipfs/go-log" +) + +var logger = log.Logger("keep-coordinator") diff --git a/cmd/wallet/deposits.go b/pkg/coordinator/deposits.go similarity index 99% rename from cmd/wallet/deposits.go rename to pkg/coordinator/deposits.go index b0d0243156..afb2c57207 100644 --- a/cmd/wallet/deposits.go +++ b/pkg/coordinator/deposits.go @@ -1,4 +1,4 @@ -package walletcmd +package coordinator import ( "encoding/hex" From 3f4e2b3f01d8adf52ea32633485e043c240692ce Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 8 May 2023 14:27:53 +0200 Subject: [PATCH 10/12] Define internal hexutils package We define hexutils pacakge with Decode and Encode functions that are based on https://pkg.go.dev/github.com/ethereum/go-ethereum/common/hexutil package. --- pkg/coordinator/deposits.go | 7 +-- pkg/internal/hexutils/hexutils.go | 35 ++++++++++++ pkg/internal/hexutils/hexutils_test.go | 77 ++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 pkg/internal/hexutils/hexutils.go create mode 100644 pkg/internal/hexutils/hexutils_test.go diff --git a/pkg/coordinator/deposits.go b/pkg/coordinator/deposits.go index afb2c57207..cf61c4bb61 100644 --- a/pkg/coordinator/deposits.go +++ b/pkg/coordinator/deposits.go @@ -7,9 +7,8 @@ import ( "sort" "text/tabwriter" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/internal/hexutils" "github.com/keep-network/keep-core/pkg/tbtc" ) @@ -162,7 +161,7 @@ func getDeposits( result[i] = depositEntry{ walletPublicKeyHash: event.WalletPublicKeyHash, - depositKey: hexutil.Encode(depositKey.Bytes()), + depositKey: hexutils.Encode(depositKey.Bytes()), revealBlock: event.BlockNumber, isSwept: depositRequest.SweptAt.Unix() != 0, fundingTransactionHash: event.FundingTxHash, @@ -203,7 +202,7 @@ func printTable(deposits []depositEntry) error { } func hexToWalletPublicKeyHash(str string) ([20]byte, error) { - walletHex, err := hexutil.Decode(str) + walletHex, err := hexutils.Decode(str) if err != nil { return [20]byte{}, fmt.Errorf("failed to parse arguments: %w", err) } diff --git a/pkg/internal/hexutils/hexutils.go b/pkg/internal/hexutils/hexutils.go new file mode 100644 index 0000000000..fb2abc4689 --- /dev/null +++ b/pkg/internal/hexutils/hexutils.go @@ -0,0 +1,35 @@ +package hexutils + +import ( + "encoding/hex" + "fmt" +) + +// Decode decodes a hex string with 0x prefix. +func Decode(input string) ([]byte, error) { + if len(input) == 0 { + return nil, fmt.Errorf("empty hex string") + } + if has0xPrefix(input) { + input = input[2:] + } + + b, err := hex.DecodeString(input) + + if err != nil { + return nil, fmt.Errorf("failed to decode string [%s]", input) + } + return b, err +} + +// Encode encodes b as a hex string with 0x prefix. +func Encode(b []byte) string { + enc := make([]byte, len(b)*2+2) + copy(enc, "0x") + hex.Encode(enc[2:], b) + return string(enc) +} + +func has0xPrefix(input string) bool { + return len(input) >= 2 && input[0] == '0' && (input[1] == 'x' || input[1] == 'X') +} diff --git a/pkg/internal/hexutils/hexutils_test.go b/pkg/internal/hexutils/hexutils_test.go new file mode 100644 index 0000000000..3f5c644110 --- /dev/null +++ b/pkg/internal/hexutils/hexutils_test.go @@ -0,0 +1,77 @@ +package hexutils + +import ( + "bytes" + "fmt" + "reflect" + "testing" +) + +type unmarshalTest struct { + input string + want []byte + wantErr error // if set, decoding must fail +} + +type marshalTest struct { + input []byte + want string +} + +var ( + decodeBytesTests = []unmarshalTest{ + // invalid + {input: ``, wantErr: fmt.Errorf("empty hex string")}, + {input: `0`, wantErr: fmt.Errorf("failed to decode string [0]")}, + {input: `0x0`, wantErr: fmt.Errorf("failed to decode string [0]")}, + {input: `0x023`, wantErr: fmt.Errorf("failed to decode string [023]")}, + {input: `0xxx`, wantErr: fmt.Errorf("failed to decode string [xx]")}, + {input: `0x01zz01`, wantErr: fmt.Errorf("failed to decode string [01zz01]")}, + // valid + {input: `0x`, want: []byte{}}, + {input: `0X`, want: []byte{}}, + {input: `0x02`, want: []byte{0x02}}, + {input: `0X02`, want: []byte{0x02}}, + {input: `0xffffffffff`, want: []byte{0xff, 0xff, 0xff, 0xff, 0xff}}, + { + input: `0xffffffffffffffffffffffffffffffffffff`, + want: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, + {input: `00`, want: []byte{0x00}}, + {input: `02`, want: []byte{0x02}}, + {input: `ffffffffff`, want: []byte{0xff, 0xff, 0xff, 0xff, 0xff}}, + { + input: `ffffffffffffffffffffffffffffffffffff`, + want: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, + } + + encodeBytesTests = []marshalTest{ + {[]byte{}, "0x"}, + {[]byte{0}, "0x00"}, + {[]byte{0, 0, 1, 2}, "0x00000102"}, + } +) + +func TestDecode(t *testing.T) { + for _, test := range decodeBytesTests { + t.Run(test.input, func(t *testing.T) { + dec, err := Decode(test.input) + if !reflect.DeepEqual(err, test.wantErr) { + t.Fatalf("unexpected error\nexpected: %v\nactual: %v", test.wantErr, err) + } + if !bytes.Equal(test.want, dec) { + t.Errorf("unexpected result\nexpected: %v\nactual: %v", test.want, dec) + } + }) + } +} + +func TestEncode(t *testing.T) { + for _, test := range encodeBytesTests { + enc := Encode(test.input) + if enc != test.want { + t.Errorf("input %x: wrong encoding %s", test.input, enc) + } + } +} From a717cc2cdd6a7b9981ec9eeb4226f4aba8bfa76a Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 8 May 2023 14:34:45 +0200 Subject: [PATCH 11/12] Rename command wallet to coordinator --- cmd/cmd.go | 2 +- cmd/{wallet.go => coordinator.go} | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) rename cmd/{wallet.go => coordinator.go} (87%) diff --git a/cmd/cmd.go b/cmd/cmd.go index 484ae7dafa..1efbe1abf5 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -36,7 +36,7 @@ func init() { PingCommand, EthereumCommand, MaintainerCommand, - WalletCommand, + CoordinatorCommand, ) } diff --git a/cmd/wallet.go b/cmd/coordinator.go similarity index 87% rename from cmd/wallet.go rename to cmd/coordinator.go index 57bd97be8e..a4664e9436 100644 --- a/cmd/wallet.go +++ b/cmd/coordinator.go @@ -19,10 +19,10 @@ var ( tailFlagName = "tail" ) -// WalletCommand contains the definition of tBTC wallets tools. -var WalletCommand = &cobra.Command{ - Use: "wallet", - Short: "tBTC wallets tools", +// CoordinatorCommand contains the definition of tBTC Wallet Coordinator tools. +var CoordinatorCommand = &cobra.Command{ + Use: "coordinator", + Short: "tBTC Wallet Coordinator Tools", Long: "The tool exposes commands for interactions with tBTC wallets.", TraverseChildren: true, PersistentPreRun: func(cmd *cobra.Command, args []string) { @@ -38,7 +38,7 @@ var WalletCommand = &cobra.Command{ var depositsCommand = cobra.Command{ Use: "deposits", - Short: "get deposits", + Short: "get list of deposits", Long: "Gets tBTC deposits details from the chain and prints them.", TraverseChildren: true, RunE: func(cmd *cobra.Command, args []string) error { @@ -96,14 +96,14 @@ var depositsCommand = cobra.Command{ func init() { initFlags( - WalletCommand, + CoordinatorCommand, &configFilePath, clientConfig, config.General, config.Ethereum, config.BitcoinElectrum, ) - // Wallet Command - WalletCommand.PersistentFlags().String( + // Coordinator Command + CoordinatorCommand.PersistentFlags().String( walletFlagName, "", "wallet public key hash", @@ -134,5 +134,5 @@ func init() { "get tail of deposits", ) - WalletCommand.AddCommand(&depositsCommand) + CoordinatorCommand.AddCommand(&depositsCommand) } From a52261a43e98dd2228e337b68e1198bc206416f3 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Mon, 8 May 2023 15:31:42 +0200 Subject: [PATCH 12/12] Use append instead of copy --- pkg/coordinator/deposits.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/coordinator/deposits.go b/pkg/coordinator/deposits.go index cf61c4bb61..859dfd8093 100644 --- a/pkg/coordinator/deposits.go +++ b/pkg/coordinator/deposits.go @@ -134,7 +134,10 @@ func getDeposits( allDepositRevealedEvents[len(allDepositRevealedEvents)-tail:]..., ) } else { - copy(depositRevealedEvents, allDepositRevealedEvents) + depositRevealedEvents = append( + depositRevealedEvents, + allDepositRevealedEvents..., + ) } result := make([]depositEntry, len(depositRevealedEvents))