From 5051e62ae134591256c33cce21df561d8a1c2077 Mon Sep 17 00:00:00 2001 From: Tanut Lertwarachai Date: Tue, 13 Jan 2026 11:25:02 +0700 Subject: [PATCH 1/8] refactor for next chain --- internal/relayertest/mocks/chain_provider.go | 176 ------------------- internal/relayertest/mocks/wallet.go | 3 +- relayer/app.go | 45 +++-- relayer/app_test.go | 115 ++++++++---- relayer/chains/evm/keys.go | 92 ++-------- relayer/chains/evm/keys_test.go | 39 +++- relayer/chains/evm/signer.go | 11 +- relayer/chains/keys.go | 65 +++++++ relayer/chains/provider.go | 18 -- relayer/chains/signer.go | 17 ++ relayer/chains/{evm => }/signer_test.go | 8 +- relayer/wallet/geth/wallet.go | 13 +- relayer/wallet/geth/wallet_test.go | 11 +- relayer/wallet/wallet.go | 6 +- 14 files changed, 264 insertions(+), 355 deletions(-) create mode 100644 relayer/chains/keys.go create mode 100644 relayer/chains/signer.go rename relayer/chains/{evm => }/signer_test.go (92%) diff --git a/internal/relayertest/mocks/chain_provider.go b/internal/relayertest/mocks/chain_provider.go index a00d074..9445f22 100644 --- a/internal/relayertest/mocks/chain_provider.go +++ b/internal/relayertest/mocks/chain_provider.go @@ -59,65 +59,6 @@ func (mr *MockChainProviderMockRecorder) AddKeyByMnemonic(keyName, mnemonic, coi return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddKeyByMnemonic", reflect.TypeOf((*MockChainProvider)(nil).AddKeyByMnemonic), keyName, mnemonic, coinType, account, index) } -// AddKeyByPrivateKey mocks base method. -func (m *MockChainProvider) AddKeyByPrivateKey(keyName, privateKeyHex string) (*types0.Key, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddKeyByPrivateKey", keyName, privateKeyHex) - ret0, _ := ret[0].(*types0.Key) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AddKeyByPrivateKey indicates an expected call of AddKeyByPrivateKey. -func (mr *MockChainProviderMockRecorder) AddKeyByPrivateKey(keyName, privateKeyHex any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddKeyByPrivateKey", reflect.TypeOf((*MockChainProvider)(nil).AddKeyByPrivateKey), keyName, privateKeyHex) -} - -// AddRemoteSignerKey mocks base method. -func (m *MockChainProvider) AddRemoteSignerKey(keyName, addr, url string, key *string) (*types0.Key, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddRemoteSignerKey", keyName, addr, url, key) - ret0, _ := ret[0].(*types0.Key) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AddRemoteSignerKey indicates an expected call of AddRemoteSignerKey. -func (mr *MockChainProviderMockRecorder) AddRemoteSignerKey(keyName, addr, url, key any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRemoteSignerKey", reflect.TypeOf((*MockChainProvider)(nil).AddRemoteSignerKey), keyName, addr, url, key) -} - -// DeleteKey mocks base method. -func (m *MockChainProvider) DeleteKey(keyName string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteKey", keyName) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteKey indicates an expected call of DeleteKey. -func (mr *MockChainProviderMockRecorder) DeleteKey(keyName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteKey", reflect.TypeOf((*MockChainProvider)(nil).DeleteKey), keyName) -} - -// ExportPrivateKey mocks base method. -func (m *MockChainProvider) ExportPrivateKey(keyName string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ExportPrivateKey", keyName) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ExportPrivateKey indicates an expected call of ExportPrivateKey. -func (mr *MockChainProviderMockRecorder) ExportPrivateKey(keyName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportPrivateKey", reflect.TypeOf((*MockChainProvider)(nil).ExportPrivateKey), keyName) -} - // GetChainName mocks base method. func (m *MockChainProvider) GetChainName() string { m.ctrl.T.Helper() @@ -146,20 +87,6 @@ func (mr *MockChainProviderMockRecorder) Init(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockChainProvider)(nil).Init), ctx) } -// ListKeys mocks base method. -func (m *MockChainProvider) ListKeys() []*types0.Key { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListKeys") - ret0, _ := ret[0].([]*types0.Key) - return ret0 -} - -// ListKeys indicates an expected call of ListKeys. -func (mr *MockChainProviderMockRecorder) ListKeys() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListKeys", reflect.TypeOf((*MockChainProvider)(nil).ListKeys)) -} - // LoadSigners mocks base method. func (m *MockChainProvider) LoadSigners() error { m.ctrl.T.Helper() @@ -230,21 +157,6 @@ func (mr *MockChainProviderMockRecorder) SetDatabase(database any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDatabase", reflect.TypeOf((*MockChainProvider)(nil).SetDatabase), database) } -// ShowKey mocks base method. -func (m *MockChainProvider) ShowKey(keyName string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ShowKey", keyName) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ShowKey indicates an expected call of ShowKey. -func (mr *MockChainProviderMockRecorder) ShowKey(keyName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShowKey", reflect.TypeOf((*MockChainProvider)(nil).ShowKey), keyName) -} - // MockKeyProvider is a mock of KeyProvider interface. type MockKeyProvider struct { ctrl *gomock.Controller @@ -284,79 +196,6 @@ func (mr *MockKeyProviderMockRecorder) AddKeyByMnemonic(keyName, mnemonic, coinT return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddKeyByMnemonic", reflect.TypeOf((*MockKeyProvider)(nil).AddKeyByMnemonic), keyName, mnemonic, coinType, account, index) } -// AddKeyByPrivateKey mocks base method. -func (m *MockKeyProvider) AddKeyByPrivateKey(keyName, privateKeyHex string) (*types0.Key, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddKeyByPrivateKey", keyName, privateKeyHex) - ret0, _ := ret[0].(*types0.Key) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AddKeyByPrivateKey indicates an expected call of AddKeyByPrivateKey. -func (mr *MockKeyProviderMockRecorder) AddKeyByPrivateKey(keyName, privateKeyHex any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddKeyByPrivateKey", reflect.TypeOf((*MockKeyProvider)(nil).AddKeyByPrivateKey), keyName, privateKeyHex) -} - -// AddRemoteSignerKey mocks base method. -func (m *MockKeyProvider) AddRemoteSignerKey(keyName, addr, url string, key *string) (*types0.Key, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddRemoteSignerKey", keyName, addr, url, key) - ret0, _ := ret[0].(*types0.Key) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AddRemoteSignerKey indicates an expected call of AddRemoteSignerKey. -func (mr *MockKeyProviderMockRecorder) AddRemoteSignerKey(keyName, addr, url, key any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRemoteSignerKey", reflect.TypeOf((*MockKeyProvider)(nil).AddRemoteSignerKey), keyName, addr, url, key) -} - -// DeleteKey mocks base method. -func (m *MockKeyProvider) DeleteKey(keyName string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteKey", keyName) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteKey indicates an expected call of DeleteKey. -func (mr *MockKeyProviderMockRecorder) DeleteKey(keyName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteKey", reflect.TypeOf((*MockKeyProvider)(nil).DeleteKey), keyName) -} - -// ExportPrivateKey mocks base method. -func (m *MockKeyProvider) ExportPrivateKey(keyName string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ExportPrivateKey", keyName) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ExportPrivateKey indicates an expected call of ExportPrivateKey. -func (mr *MockKeyProviderMockRecorder) ExportPrivateKey(keyName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportPrivateKey", reflect.TypeOf((*MockKeyProvider)(nil).ExportPrivateKey), keyName) -} - -// ListKeys mocks base method. -func (m *MockKeyProvider) ListKeys() []*types0.Key { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListKeys") - ret0, _ := ret[0].([]*types0.Key) - return ret0 -} - -// ListKeys indicates an expected call of ListKeys. -func (mr *MockKeyProviderMockRecorder) ListKeys() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListKeys", reflect.TypeOf((*MockKeyProvider)(nil).ListKeys)) -} - // LoadSigners mocks base method. func (m *MockKeyProvider) LoadSigners() error { m.ctrl.T.Helper() @@ -370,18 +209,3 @@ func (mr *MockKeyProviderMockRecorder) LoadSigners() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadSigners", reflect.TypeOf((*MockKeyProvider)(nil).LoadSigners)) } - -// ShowKey mocks base method. -func (m *MockKeyProvider) ShowKey(keyName string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ShowKey", keyName) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ShowKey indicates an expected call of ShowKey. -func (mr *MockKeyProviderMockRecorder) ShowKey(keyName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShowKey", reflect.TypeOf((*MockKeyProvider)(nil).ShowKey), keyName) -} diff --git a/internal/relayertest/mocks/wallet.go b/internal/relayertest/mocks/wallet.go index 0306bc5..05b5cf1 100644 --- a/internal/relayertest/mocks/wallet.go +++ b/internal/relayertest/mocks/wallet.go @@ -10,7 +10,6 @@ package mocks import ( - ecdsa "crypto/ecdsa" reflect "reflect" wallet "github.com/bandprotocol/falcon/relayer/wallet" @@ -167,7 +166,7 @@ func (mr *MockWalletMockRecorder) GetSigners() *gomock.Call { } // SavePrivateKey mocks base method. -func (m *MockWallet) SavePrivateKey(name string, privKey *ecdsa.PrivateKey) (string, error) { +func (m *MockWallet) SavePrivateKey(name, privKey string) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SavePrivateKey", name, privKey) ret0, _ := ret[0].(string) diff --git a/relayer/app.go b/relayer/app.go index c7d7954..66635e3 100644 --- a/relayer/app.go +++ b/relayer/app.go @@ -15,6 +15,7 @@ import ( "github.com/bandprotocol/falcon/relayer/logger" "github.com/bandprotocol/falcon/relayer/store" "github.com/bandprotocol/falcon/relayer/types" + "github.com/bandprotocol/falcon/relayer/wallet" ) var _ Application = &App{} @@ -107,7 +108,6 @@ func (a *App) InitTargetChain(chainName string) error { ) return err } - cp, err := chainConfig.NewChainProvider(chainName, a.Log, wallet, a.Alert) if err != nil { a.Log.Error("Cannot create chain provider", @@ -276,12 +276,12 @@ func (a *App) AddKeyByPrivateKey(chainName string, keyName string, privateKey st return nil, err } - cp, err := a.getChainProvider(chainName) + w, err := a.getWallet(chainName) if err != nil { return nil, err } - return cp.AddKeyByPrivateKey(keyName, privateKey) + return chains.AddKeyByPrivateKey(w, keyName, privateKey) } // AddKeyByMnemonic adds a new key to the chain provider using a mnemonic phrase. @@ -313,12 +313,12 @@ func (a *App) AddRemoteSignerKey( url string, key *string, ) (*chainstypes.Key, error) { - cp, err := a.getChainProvider(chainName) + w, err := a.getWallet(chainName) if err != nil { return nil, err } - return cp.AddRemoteSignerKey(keyName, addr, url, key) + return chains.AddRemoteSignerKey(w, keyName, addr, url, key) } // DeleteKey deletes the key from the chain provider. @@ -327,12 +327,12 @@ func (a *App) DeleteKey(chainName string, keyName string) error { return err } - cp, err := a.getChainProvider(chainName) + w, err := a.getWallet(chainName) if err != nil { return err } - return cp.DeleteKey(keyName) + return chains.DeleteKey(w, keyName) } // ExportKey exports the private key from the chain provider. @@ -341,32 +341,32 @@ func (a *App) ExportKey(chainName string, keyName string) (string, error) { return "", err } - cp, err := a.getChainProvider(chainName) + w, err := a.getWallet(chainName) if err != nil { return "", err } - return cp.ExportPrivateKey(keyName) + return chains.ExportPrivateKey(w, keyName) } // ListKeys retrieves the list of keys from the chain provider. func (a *App) ListKeys(chainName string) ([]*chainstypes.Key, error) { - cp, err := a.getChainProvider(chainName) + w, err := a.getWallet(chainName) if err != nil { return nil, err } - return cp.ListKeys(), nil + return chains.ListKeys(w), nil } // ShowKey retrieves the key information from the chain provider. func (a *App) ShowKey(chainName string, keyName string) (string, error) { - cp, err := a.getChainProvider(chainName) + w, err := a.getWallet(chainName) if err != nil { return "", err } - return cp.ShowKey(keyName) + return chains.ShowKey(w, keyName) } // QueryBalance retrieves the balance of the key from the chain provider. @@ -512,6 +512,25 @@ func (a *App) getChainProvider(chainName string) (chains.ChainProvider, error) { return cp, nil } +// getWallet retrieves the wallet for the given chain name. +func (a *App) getWallet(chainName string) (wallet.Wallet, error) { + if a.Config == nil { + return nil, fmt.Errorf("config is not initialized") + } + + chainConfig, ok := a.Config.TargetChains[chainName] + if !ok { + return nil, fmt.Errorf("chain name does not exist: %s", chainName) + } + + w, err := a.Store.NewWallet(chainConfig.GetChainType(), chainName, a.Passphrase) + if err != nil { + return nil, err + } + + return w, nil +} + // getTunnelsByIDs retrieves the tunnels by given tunnel IDs. func (a *App) getTunnelsByIDs(ctx context.Context, tunnelIDs []uint64) ([]bandtypes.Tunnel, error) { var tunnels []bandtypes.Tunnel diff --git a/relayer/app_test.go b/relayer/app_test.go index 277faed..2520567 100644 --- a/relayer/app_test.go +++ b/relayer/app_test.go @@ -26,6 +26,7 @@ import ( "github.com/bandprotocol/falcon/relayer/config" "github.com/bandprotocol/falcon/relayer/logger" "github.com/bandprotocol/falcon/relayer/types" + "github.com/bandprotocol/falcon/relayer/wallet" ) type AppTestSuite struct { @@ -36,6 +37,8 @@ type AppTestSuite struct { chainProvider *mocks.MockChainProvider client *mocks.MockClient store *mocks.MockStore + wallet *mocks.MockWallet + ctrl *gomock.Controller passphrase string hashedPassphrase []byte @@ -43,15 +46,16 @@ type AppTestSuite struct { // SetupTest sets up the test suite by creating a temporary directory and declare mock objects. func (s *AppTestSuite) SetupTest() { - ctrl := gomock.NewController(s.T()) + s.ctrl = gomock.NewController(s.T()) log := logger.NewZapLogWrapper(zap.NewNop().Sugar()) // mock objects. - s.chainProviderConfig = mocks.NewMockChainProviderConfig(ctrl) - s.chainProvider = mocks.NewMockChainProvider(ctrl) - s.client = mocks.NewMockClient(ctrl) + s.chainProviderConfig = mocks.NewMockChainProviderConfig(s.ctrl) + s.chainProvider = mocks.NewMockChainProvider(s.ctrl) + s.client = mocks.NewMockClient(s.ctrl) s.client.EXPECT().Init(gomock.Any()).Return(nil).AnyTimes() - s.store = mocks.NewMockStore(ctrl) + s.store = mocks.NewMockStore(s.ctrl) + s.wallet = mocks.NewMockWallet(s.ctrl) cfg := config.Config{ BandChain: band.Config{ @@ -70,6 +74,7 @@ func (s *AppTestSuite) SetupTest() { h.Write([]byte(s.passphrase)) s.hashedPassphrase = h.Sum(nil) s.store.EXPECT().GetHashedPassphrase().Return(s.hashedPassphrase, nil).AnyTimes() + s.chainProviderConfig.EXPECT().GetChainType().Return(chainstypes.ChainTypeEVM).AnyTimes() s.app = &relayer.App{ Log: log, @@ -479,12 +484,15 @@ func (s *AppTestSuite) TestAddKey() { coinType: 60, out: chainstypes.NewKey("", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", ""), preprocess: func() { - s.chainProvider.EXPECT(). - AddKeyByPrivateKey( + s.store.EXPECT(). + NewWallet(chainstypes.ChainTypeEVM, "testnet_evm", s.passphrase). + Return(s.wallet, nil) + s.wallet.EXPECT(). + SavePrivateKey( "testkey", "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ). - Return(chainstypes.NewKey("", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", ""), nil) + Return("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", nil) }, }, { @@ -494,12 +502,15 @@ func (s *AppTestSuite) TestAddKey() { privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", // anvil coinType: 60, preprocess: func() { - s.chainProvider.EXPECT(). - AddKeyByPrivateKey( + s.store.EXPECT(). + NewWallet(chainstypes.ChainTypeEVM, "testnet_evm", s.passphrase). + Return(s.wallet, nil) + s.wallet.EXPECT(). + SavePrivateKey( "testkey", "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ). - Return(nil, fmt.Errorf("add key error")) + Return("", fmt.Errorf("add key error")) }, err: fmt.Errorf("add key error"), }, @@ -550,7 +561,10 @@ func (s *AppTestSuite) TestDeleteKey() { chainName: "testnet_evm", keyName: "testkey", preprocess: func() { - s.chainProvider.EXPECT(). + s.store.EXPECT(). + NewWallet(chainstypes.ChainTypeEVM, "testnet_evm", s.passphrase). + Return(s.wallet, nil) + s.wallet.EXPECT(). DeleteKey("testkey"). Return(nil) }, @@ -560,7 +574,10 @@ func (s *AppTestSuite) TestDeleteKey() { chainName: "testnet_evm", keyName: "testkey", preprocess: func() { - s.chainProvider.EXPECT(). + s.store.EXPECT(). + NewWallet(chainstypes.ChainTypeEVM, "testnet_evm", s.passphrase). + Return(s.wallet, nil) + s.wallet.EXPECT(). DeleteKey("testkey"). Return(fmt.Errorf("delete key error")) }, @@ -607,8 +624,15 @@ func (s *AppTestSuite) TestExportKey() { chainName: "testnet_evm", keyName: "testkey", preprocess: func() { - s.chainProvider.EXPECT(). - ExportPrivateKey("testkey"). + s.store.EXPECT(). + NewWallet(chainstypes.ChainTypeEVM, "testnet_evm", s.passphrase). + Return(s.wallet, nil) + signer := mocks.NewMockSigner(s.ctrl) + s.wallet.EXPECT(). + GetSigner("testkey"). + Return(signer, true) + signer.EXPECT(). + ExportPrivateKey(). Return("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", nil) }, out: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", @@ -618,8 +642,15 @@ func (s *AppTestSuite) TestExportKey() { chainName: "testnet_evm", keyName: "testkey", preprocess: func() { - s.chainProvider.EXPECT(). - ExportPrivateKey("testkey"). + s.store.EXPECT(). + NewWallet(chainstypes.ChainTypeEVM, "testnet_evm", s.passphrase). + Return(s.wallet, nil) + signer := mocks.NewMockSigner(s.ctrl) + s.wallet.EXPECT(). + GetSigner("testkey"). + Return(signer, true) + signer.EXPECT(). + ExportPrivateKey(). Return("", fmt.Errorf("export key error")) }, err: fmt.Errorf("export key error"), @@ -661,12 +692,26 @@ func (s *AppTestSuite) TestListKeys() { name: "success", in: "testnet_evm", preprocess: func() { - s.chainProvider.EXPECT(). - ListKeys(). - Return([]*chainstypes.Key{ - chainstypes.NewKey("", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "testkey1"), - chainstypes.NewKey("", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92267", "testkey2"), - }) + s.store.EXPECT(). + NewWallet(chainstypes.ChainTypeEVM, "testnet_evm", s.passphrase). + Return(s.wallet, nil) + signer1 := mocks.NewMockSigner(s.ctrl) + signer2 := mocks.NewMockSigner(s.ctrl) + s.wallet.EXPECT(). + GetSigners(). + Return([]wallet.Signer{signer1, signer2}) + signer1.EXPECT(). + GetAddress(). + Return("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266") + signer1.EXPECT(). + GetName(). + Return("testkey1") + signer2.EXPECT(). + GetAddress(). + Return("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92267") + signer2.EXPECT(). + GetName(). + Return("testkey2") }, out: []*chainstypes.Key{ chainstypes.NewKey("", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "testkey1"), @@ -711,9 +756,16 @@ func (s *AppTestSuite) TestShowKey() { chainName: "testnet_evm", keyName: "testkey", preprocess: func() { - s.chainProvider.EXPECT(). - ShowKey("testkey"). - Return("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92267", nil) + s.store.EXPECT(). + NewWallet(chainstypes.ChainTypeEVM, "testnet_evm", s.passphrase). + Return(s.wallet, nil) + signer := mocks.NewMockSigner(s.ctrl) + s.wallet.EXPECT(). + GetSigner("testkey"). + Return(signer, true) + signer.EXPECT(). + GetAddress(). + Return("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92267") }, out: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92267", }, @@ -722,12 +774,15 @@ func (s *AppTestSuite) TestShowKey() { chainName: "testnet_evm", keyName: "testkey", preprocess: func() { - s.chainProvider.EXPECT(). - ShowKey("testkey"). - Return("", fmt.Errorf("show key error")) + s.store.EXPECT(). + NewWallet(chainstypes.ChainTypeEVM, "testnet_evm", s.passphrase). + Return(s.wallet, nil) + s.wallet.EXPECT(). + GetSigner("testkey"). + Return(nil, false) }, out: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92267", - err: fmt.Errorf("show key error"), + err: fmt.Errorf("key name does not exist"), }, { name: "chain name does not exist", diff --git a/relayer/chains/evm/keys.go b/relayer/chains/evm/keys.go index dbf2bb7..e08aee2 100644 --- a/relayer/chains/evm/keys.go +++ b/relayer/chains/evm/keys.go @@ -1,10 +1,8 @@ package evm import ( - "crypto/ecdsa" "fmt" - "github.com/ethereum/go-ethereum/crypto" hdwallet "github.com/miguelmota/go-ethereum-hdwallet" chainstypes "github.com/bandprotocol/falcon/relayer/chains/types" @@ -37,109 +35,41 @@ func (cp *EVMChainProvider) AddKeyByMnemonic( } // Generate private key using mnemonic - priv, err := generatePrivateKey(mnemonic, coinType, account, index) + privHex, err := generatePrivateKeyHex(mnemonic, coinType, account, index) if err != nil { return nil, err } - return cp.finalizeKeyAddition(keyName, priv, generatedMnemonic) -} - -// AddKeyByPrivateKey adds a key using a raw private key. -func (cp *EVMChainProvider) AddKeyByPrivateKey(keyName, privateKey string) (*chainstypes.Key, error) { - // Convert private key from hex - priv, err := crypto.HexToECDSA(StripPrivateKeyPrefix(privateKey)) + addr, err := cp.Wallet.SavePrivateKey(keyName, privHex) if err != nil { return nil, err } - // No mnemonic is used, so pass an empty string - return cp.finalizeKeyAddition(keyName, priv, "") -} - -// finalizeKeyAddition stores the private key and initializes the sender. -func (cp *EVMChainProvider) finalizeKeyAddition( - keyName string, - priv *ecdsa.PrivateKey, - mnemonic string, -) (*chainstypes.Key, error) { - addr, err := cp.Wallet.SavePrivateKey(keyName, priv) - if err != nil { - return nil, err - } - - return chainstypes.NewKey(mnemonic, addr, ""), nil -} - -// AddRemoteSignerKey adds a remote signer with the given name, address, and URL. -func (cp *EVMChainProvider) AddRemoteSignerKey(keyName, addr, url string, key *string) (*chainstypes.Key, error) { - if err := cp.Wallet.SaveRemoteSignerKey(keyName, addr, url, key); err != nil { - return nil, err - } - return chainstypes.NewKey("", addr, ""), nil -} - -// DeleteKey deletes the given key name from the key store and removes its information. -func (cp *EVMChainProvider) DeleteKey(keyName string) error { - return cp.Wallet.DeleteKey(keyName) -} - -// ExportPrivateKey exports private key of given key name. -func (cp *EVMChainProvider) ExportPrivateKey(keyName string) (string, error) { - signer, ok := cp.Wallet.GetSigner(keyName) - if !ok { - return "", fmt.Errorf("key name not exist: %s", keyName) - } - - return signer.ExportPrivateKey() -} - -// ListKeys lists all keys. -func (cp *EVMChainProvider) ListKeys() []*chainstypes.Key { - signers := cp.Wallet.GetSigners() - - res := make([]*chainstypes.Key, 0, len(signers)) - for _, signer := range signers { - key := chainstypes.NewKey("", signer.GetAddress(), signer.GetName()) - res = append(res, key) - } - - return res + return chainstypes.NewKey(generatedMnemonic, addr, ""), nil } -// ShowKey shows key by the given name. -func (cp *EVMChainProvider) ShowKey(keyName string) (string, error) { - signer, ok := cp.Wallet.GetSigner(keyName) - if !ok { - return "", fmt.Errorf("key name does not exist: %s", keyName) - } - - return signer.GetAddress(), nil -} - -// generatePrivateKey generates private key from given mnemonic. -func generatePrivateKey( +// generatePrivateKeyHex generates private key hex from given mnemonic. +func generatePrivateKeyHex( mnemonic string, coinType uint32, account uint, index uint, -) (*ecdsa.PrivateKey, error) { +) (string, error) { wallet, err := hdwallet.NewFromMnemonic(mnemonic) if err != nil { - return nil, err + return "", err } hdPath := fmt.Sprintf(hdPathTemplate, coinType, account, index) path := hdwallet.MustParseDerivationPath(hdPath) accs, err := wallet.Derive(path, true) if err != nil { - return nil, err + return "", err } - - privatekey, err := wallet.PrivateKey(accs) + privatekeyHex, err := wallet.PrivateKeyHex(accs) if err != nil { - return nil, err + return "", err } - return privatekey, nil + return privatekeyHex, nil } diff --git a/relayer/chains/evm/keys_test.go b/relayer/chains/evm/keys_test.go index 3480797..4d774cb 100644 --- a/relayer/chains/evm/keys_test.go +++ b/relayer/chains/evm/keys_test.go @@ -3,10 +3,12 @@ package evm_test import ( "fmt" "testing" + "time" "github.com/stretchr/testify/suite" "go.uber.org/zap" + "github.com/bandprotocol/falcon/relayer/chains" "github.com/bandprotocol/falcon/relayer/chains/evm" chaintypes "github.com/bandprotocol/falcon/relayer/chains/types" "github.com/bandprotocol/falcon/relayer/logger" @@ -21,6 +23,24 @@ const ( testMnemonic = "repeat sugar clarify visa chief soon walnut kangaroo rude parrot height piano spoil desk basket swim income catalog more plunge supreme above later worry" ) +var evmCfg = &evm.EVMChainProviderConfig{ + BaseChainProviderConfig: chains.BaseChainProviderConfig{ + Endpoints: []string{"http://localhost:8545"}, + ChainType: chaintypes.ChainTypeEVM, + MaxRetry: 3, + ChainID: 31337, + TunnelRouterAddress: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + QueryTimeout: 3 * time.Second, + ExecuteTimeout: 3 * time.Second, + }, + BlockConfirmation: 5, + WaitingTxDuration: time.Second * 3, + CheckingTxInterval: time.Second, + LivelinessCheckingInterval: 15 * time.Minute, + GasType: evm.GasTypeEIP1559, + GasMultiplier: 1.1, +} + func TestKeysTestSuite(t *testing.T) { suite.Run(t, new(KeysTestSuite)) } @@ -87,7 +107,7 @@ func (s *KeysTestSuite) TestAddKeyByPrivateKey() { for _, tc := range testcases { s.T().Run(tc.name, func(t *testing.T) { - key, err := s.chainProvider.AddKeyByPrivateKey(tc.input.keyName, tc.input.privKey) + key, err := chains.AddKeyByPrivateKey(s.wallet, tc.input.keyName, tc.input.privKey) if tc.err != nil { s.Require().ErrorContains(err, tc.err.Error()) @@ -226,7 +246,8 @@ func (s *KeysTestSuite) TestAddRemoteSignerKey() { for _, tc := range testcases { s.T().Run(tc.name, func(t *testing.T) { - key, err := s.chainProvider.AddRemoteSignerKey( + key, err := chains.AddRemoteSignerKey( + s.wallet, tc.input.keyName, tc.input.addr, tc.input.url, @@ -241,13 +262,13 @@ func (s *KeysTestSuite) TestAddRemoteSignerKey() { func (s *KeysTestSuite) TestDeleteKey() { // Add key to delete - _, err := s.chainProvider.AddKeyByPrivateKey(testKey, testPrivateKey) + _, err := chains.AddKeyByPrivateKey(s.wallet, testKey, testPrivateKey) s.Require().NoError(err) s.loadChainProvider() // Delete the key - err = s.chainProvider.DeleteKey(testKey) + err = chains.DeleteKey(s.wallet, testKey) s.Require().NoError(err) } @@ -263,7 +284,7 @@ func (s *KeysTestSuite) TestExportPrivateKey() { name: "success", keyName: testKey, setup: func() { - _, err := s.chainProvider.AddKeyByPrivateKey(testKey, testPrivateKey) + _, err := chains.AddKeyByPrivateKey(s.wallet, testKey, testPrivateKey) s.Require().NoError(err) }, }, @@ -281,7 +302,7 @@ func (s *KeysTestSuite) TestExportPrivateKey() { tc.setup() } s.loadChainProvider() - exported, err := s.chainProvider.ExportPrivateKey(tc.keyName) + exported, err := chains.ExportPrivateKey(s.wallet, tc.keyName) if tc.wantErr { s.Require().ErrorContains(err, tc.errSubstr) return @@ -327,7 +348,7 @@ func (s *KeysTestSuite) TestListKeys() { s.loadChainProvider() // List all keys - actual := s.chainProvider.ListKeys() + actual := chains.ListKeys(s.wallet) s.Require().Equal(2, len(actual)) expected1 := chaintypes.NewKey("", key1.Address, keyName1) @@ -362,7 +383,7 @@ func (s *KeysTestSuite) TestShowKey() { name: "success", keyName: testKey, setup: func() { - _, err := s.chainProvider.AddKeyByPrivateKey(testKey, testPrivateKey) + _, err := chains.AddKeyByPrivateKey(s.wallet, testKey, testPrivateKey) s.Require().NoError(err) }, }, @@ -380,7 +401,7 @@ func (s *KeysTestSuite) TestShowKey() { tc.setup() } s.loadChainProvider() - address, err := s.chainProvider.ShowKey(tc.keyName) + address, err := chains.ShowKey(s.wallet, tc.keyName) if tc.wantErr { s.Require().ErrorContains(err, tc.errSubstr) return diff --git a/relayer/chains/evm/signer.go b/relayer/chains/evm/signer.go index 1e92179..27441b4 100644 --- a/relayer/chains/evm/signer.go +++ b/relayer/chains/evm/signer.go @@ -1,18 +1,11 @@ package evm import ( - "github.com/bandprotocol/falcon/relayer/wallet" + "github.com/bandprotocol/falcon/relayer/chains" ) // LoadSigners initializes the Signer channel with all configured wallet signers. func (cp *EVMChainProvider) LoadSigners() error { - signers := cp.Wallet.GetSigners() - signerChannel := make(chan wallet.Signer, len(signers)) - - for _, signer := range signers { - signerChannel <- signer - } - - cp.FreeSigners = signerChannel + cp.FreeSigners = chains.LoadSigners(cp.Wallet) return nil } diff --git a/relayer/chains/keys.go b/relayer/chains/keys.go new file mode 100644 index 0000000..b33a279 --- /dev/null +++ b/relayer/chains/keys.go @@ -0,0 +1,65 @@ +package chains + +import ( + "fmt" + + chainstypes "github.com/bandprotocol/falcon/relayer/chains/types" + "github.com/bandprotocol/falcon/relayer/wallet" +) + +// AddKeyByPrivateKey adds a key using a raw private key. +func AddKeyByPrivateKey(w wallet.Wallet, keyName, privateKey string) (*chainstypes.Key, error) { + addr, err := w.SavePrivateKey(keyName, privateKey) + if err != nil { + return nil, err + } + + return chainstypes.NewKey("", addr, ""), nil +} + +// AddRemoteSignerKey adds a remote signer with the given name, address, and URL. +func AddRemoteSignerKey(w wallet.Wallet, keyName, addr, url string, key *string) (*chainstypes.Key, error) { + if err := w.SaveRemoteSignerKey(keyName, addr, url, key); err != nil { + return nil, err + } + + return chainstypes.NewKey("", addr, ""), nil +} + +// DeleteKey deletes the given key name from the key store and removes its information. +func DeleteKey(w wallet.Wallet, keyName string) error { + return w.DeleteKey(keyName) +} + +// ExportPrivateKey exports private key of given key name. +func ExportPrivateKey(w wallet.Wallet, keyName string) (string, error) { + signer, ok := w.GetSigner(keyName) + if !ok { + return "", fmt.Errorf("key name not exist: %s", keyName) + } + + return signer.ExportPrivateKey() +} + +// ListKeys lists all keys. +func ListKeys(w wallet.Wallet) []*chainstypes.Key { + signers := w.GetSigners() + + res := make([]*chainstypes.Key, 0, len(signers)) + for _, signer := range signers { + key := chainstypes.NewKey("", signer.GetAddress(), signer.GetName()) + res = append(res, key) + } + + return res +} + +// ShowKey shows key by the given name. +func ShowKey(w wallet.Wallet, keyName string) (string, error) { + signer, ok := w.GetSigner(keyName) + if !ok { + return "", fmt.Errorf("key name does not exist: %s", keyName) + } + + return signer.GetAddress(), nil +} diff --git a/relayer/chains/provider.go b/relayer/chains/provider.go index e591cd8..a279fa3 100644 --- a/relayer/chains/provider.go +++ b/relayer/chains/provider.go @@ -50,24 +50,6 @@ type KeyProvider interface { index uint, ) (*chainstypes.Key, error) - // AddKeyByPrivateKey adds a key using a private key. - AddKeyByPrivateKey(keyName string, privateKeyHex string) (*chainstypes.Key, error) - - // AddRemoteSignerKey adds a key using a remote signer’s address and a Falcon KMS URL. - AddRemoteSignerKey(keyName string, addr string, url string, key *string) (*chainstypes.Key, error) - - // DeleteKey deletes the key information and private key - DeleteKey(keyName string) error - - // ExportPrivateKey exports private key of specified key name. - ExportPrivateKey(keyName string) (string, error) - - // ListKeys lists all keys - ListKeys() []*chainstypes.Key - - // ShowKey shows the address of the given key - ShowKey(keyName string) (string, error) - // LoadSigners loads signers to prepare to relay the packet LoadSigners() error } diff --git a/relayer/chains/signer.go b/relayer/chains/signer.go new file mode 100644 index 0000000..161fb2b --- /dev/null +++ b/relayer/chains/signer.go @@ -0,0 +1,17 @@ +package chains + +import ( + "github.com/bandprotocol/falcon/relayer/wallet" +) + +// LoadSigners returns the Signer channel with all configured wallet signers. +func LoadSigners(w wallet.Wallet) chan wallet.Signer { + signers := w.GetSigners() + signerChannel := make(chan wallet.Signer, len(signers)) + + for _, signer := range signers { + signerChannel <- signer + } + + return signerChannel +} diff --git a/relayer/chains/evm/signer_test.go b/relayer/chains/signer_test.go similarity index 92% rename from relayer/chains/evm/signer_test.go rename to relayer/chains/signer_test.go index a60aa0a..d1fefd4 100644 --- a/relayer/chains/evm/signer_test.go +++ b/relayer/chains/signer_test.go @@ -1,4 +1,4 @@ -package evm_test +package chains_test import ( "testing" @@ -69,15 +69,15 @@ func (s *SenderTestSuite) SetupTest() { wallet, err := geth.NewGethWallet("", s.homePath, s.chainName) s.Require().NoError(err) - chainProvider, err := evm.NewEVMChainProvider(s.chainName, client, evmCfg, log, wallet, nil) + _, err = evm.NewEVMChainProvider(s.chainName, client, evmCfg, log, wallet, nil) s.Require().NoError(err) // Add two mock keys to the chain provider - _, err = chainProvider.AddKeyByPrivateKey(keyName1, privateKey1) + _, err = chains.AddKeyByPrivateKey(wallet, keyName1, privateKey1) s.Require().NoError(err) testKey := "testKey" - _, err = chainProvider.AddRemoteSignerKey(keyName2, address2, url, &testKey) + _, err = chains.AddRemoteSignerKey(wallet, keyName2, address2, url, &testKey) s.Require().NoError(err) } diff --git a/relayer/wallet/geth/wallet.go b/relayer/wallet/geth/wallet.go index bb10cd5..8fafc10 100644 --- a/relayer/wallet/geth/wallet.go +++ b/relayer/wallet/geth/wallet.go @@ -1,9 +1,9 @@ package geth import ( - "crypto/ecdsa" "fmt" "path" + "strings" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -106,14 +106,19 @@ func NewGethWallet(passphrase, homePath, chainName string) (*GethWallet, error) } // SavePrivateKey imports the ECDSA key into the keystore and writes its signer record. -func (w *GethWallet) SavePrivateKey(name string, privKey *ecdsa.PrivateKey) (addr string, err error) { +func (w *GethWallet) SavePrivateKey(name string, privKey string) (addr string, err error) { // check if the key name exists if _, ok := w.Signers[name]; ok { return "", fmt.Errorf("key name exists: %s", name) } + privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(privKey, "0x")) + if err != nil { + return "", err + } + // derive the Ethereum address from the pubkey and check exist or not - addr = crypto.PubkeyToAddress(privKey.PublicKey).Hex() + addr = crypto.PubkeyToAddress(privateKey.PublicKey).Hex() // check if the address is already added if w.IsAddressExist(addr) { @@ -121,7 +126,7 @@ func (w *GethWallet) SavePrivateKey(name string, privKey *ecdsa.PrivateKey) (add } // save the signer - _, err = w.Store.ImportECDSA(privKey, w.Passphrase) + _, err = w.Store.ImportECDSA(privateKey, w.Passphrase) if err != nil { return "", err } diff --git a/relayer/wallet/geth/wallet_test.go b/relayer/wallet/geth/wallet_test.go index fce16f3..000b428 100644 --- a/relayer/wallet/geth/wallet_test.go +++ b/relayer/wallet/geth/wallet_test.go @@ -1,6 +1,7 @@ package geth_test import ( + "encoding/hex" "os" "path" "path/filepath" @@ -40,6 +41,7 @@ func (s *WalletTestSuite) TestSavePrivateKey() { priv, err := crypto.GenerateKey() s.Require().NoError(err) addrHex := crypto.PubkeyToAddress(priv.PublicKey).Hex() + privHex := "0x" + hex.EncodeToString(crypto.FromECDSA(priv)) tests := []struct { name string @@ -52,7 +54,7 @@ func (s *WalletTestSuite) TestSavePrivateKey() { { "duplicate name fails", "alice", func(w *geth.GethWallet) { - _, err := w.SavePrivateKey("alice", priv) + _, err := w.SavePrivateKey("alice", privHex) s.Require().NoError(err) }, true, "key name exists", @@ -60,7 +62,7 @@ func (s *WalletTestSuite) TestSavePrivateKey() { { "duplicate address fails", "bob", func(w *geth.GethWallet) { - _, err := w.SavePrivateKey("a", priv) + _, err := w.SavePrivateKey("a", privHex) s.Require().NoError(err) }, true, "address exists", @@ -76,7 +78,7 @@ func (s *WalletTestSuite) TestSavePrivateKey() { w, _ = geth.NewGethWallet(s.passphrase, home, s.chainName) } - gotAddr, err := w.SavePrivateKey(tc.keyName, priv) + gotAddr, err := w.SavePrivateKey(tc.keyName, privHex) if tc.wantErr { s.Error(err) s.Contains(err.Error(), tc.errSubstr) @@ -165,6 +167,7 @@ func (s *WalletTestSuite) TestDeleteKey() { priv, err := crypto.GenerateKey() s.Require().NoError(err) addrHex := crypto.PubkeyToAddress(priv.PublicKey).Hex() + privHex := hex.EncodeToString(crypto.FromECDSA(priv)) testKey := "testKey" @@ -178,7 +181,7 @@ func (s *WalletTestSuite) TestDeleteKey() { { "delete local succeeds", func(w *geth.GethWallet) { - _, err := w.SavePrivateKey("alice", priv) + _, err := w.SavePrivateKey("alice", privHex) s.Require().NoError(err) }, "alice", false, "", diff --git a/relayer/wallet/wallet.go b/relayer/wallet/wallet.go index cefbfa2..63ca91f 100644 --- a/relayer/wallet/wallet.go +++ b/relayer/wallet/wallet.go @@ -1,9 +1,5 @@ package wallet -import ( - "crypto/ecdsa" -) - type Signer interface { ExportPrivateKey() (string, error) GetName() string @@ -12,7 +8,7 @@ type Signer interface { } type Wallet interface { - SavePrivateKey(name string, privKey *ecdsa.PrivateKey) (addr string, err error) + SavePrivateKey(name string, privKey string) (addr string, err error) SaveRemoteSignerKey(name, addr, url string, key *string) error DeleteKey(name string) error GetSigners() []Signer From 162a23c328986f0c82e1c6dd31ade6642cc0f187 Mon Sep 17 00:00:00 2001 From: Tanut Lertwarachai Date: Wed, 14 Jan 2026 13:58:01 +0700 Subject: [PATCH 2/8] fix copilot comment --- relayer/chains/keys.go | 2 +- relayer/chains/provider.go | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/relayer/chains/keys.go b/relayer/chains/keys.go index b33a279..66766ae 100644 --- a/relayer/chains/keys.go +++ b/relayer/chains/keys.go @@ -35,7 +35,7 @@ func DeleteKey(w wallet.Wallet, keyName string) error { func ExportPrivateKey(w wallet.Wallet, keyName string) (string, error) { signer, ok := w.GetSigner(keyName) if !ok { - return "", fmt.Errorf("key name not exist: %s", keyName) + return "", fmt.Errorf("key name does not exist: %s", keyName) } return signer.ExportPrivateKey() diff --git a/relayer/chains/provider.go b/relayer/chains/provider.go index a279fa3..9412c39 100644 --- a/relayer/chains/provider.go +++ b/relayer/chains/provider.go @@ -15,7 +15,6 @@ type ChainProviders map[string]ChainProvider // ChainProvider defines the interface for the chain interaction with the destination chain. type ChainProvider interface { - KeyProvider // Init initialize to the chain. Init(ctx context.Context) error @@ -37,10 +36,7 @@ type ChainProvider interface { // GetChainName retrieves the chain name from the chain provider. GetChainName() string -} -// KeyProvider defines the interface for the key interaction with destination chain -type KeyProvider interface { // AddKeyByMnemonic adds a key using a mnemonic phrase. AddKeyByMnemonic( keyName string, From 81fddd71c461a95aff39605c829609b134509fa9 Mon Sep 17 00:00:00 2001 From: Tanut Lertwarachai Date: Wed, 14 Jan 2026 14:01:15 +0700 Subject: [PATCH 3/8] fix wrong test result --- relayer/chains/evm/keys_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/relayer/chains/evm/keys_test.go b/relayer/chains/evm/keys_test.go index 4d774cb..b76fbc9 100644 --- a/relayer/chains/evm/keys_test.go +++ b/relayer/chains/evm/keys_test.go @@ -289,10 +289,10 @@ func (s *KeysTestSuite) TestExportPrivateKey() { }, }, { - name: "key name not exist", + name: "key name does not exist", keyName: "doesNotExist", wantErr: true, - errSubstr: "key name not exist", + errSubstr: "key name does not exist", }, } From 3ece56fc14613f5a5317aa01a44a6c7ab570423f Mon Sep 17 00:00:00 2001 From: Tanut Lertwarachai Date: Fri, 30 Jan 2026 11:53:59 +0700 Subject: [PATCH 4/8] add xrpl --- cmd/keys.go | 19 +- go.mod | 23 +- go.sum | 37 ++- internal/relayertest/constants.go | 14 +- internal/relayertest/mocks/chain_provider.go | 67 +---- relayer/app.go | 33 +- relayer/app_test.go | 9 +- relayer/band/client.go | 1 + relayer/band/client_test.go | 1 + relayer/band/types/packet.go | 3 + relayer/chains/client.go | 18 -- relayer/chains/config.go | 2 - relayer/chains/evm/config.go | 1 + relayer/chains/evm/keys_test.go | 15 +- relayer/chains/evm/provider.go | 15 +- relayer/chains/evm/provider_test.go | 14 +- relayer/chains/provider.go | 15 +- relayer/chains/registry.go | 25 -- relayer/chains/signer_test.go | 14 +- relayer/chains/types/chain_type.go | 6 +- relayer/chains/types/tunnel.go | 16 +- relayer/chains/xrpl/client.go | 217 ++++++++++++++ relayer/chains/xrpl/config.go | 52 ++++ relayer/chains/xrpl/keys.go | 53 ++++ relayer/chains/xrpl/provider.go | 298 +++++++++++++++++++ relayer/chains/xrpl/utils.go | 52 ++++ relayer/config/config.go | 15 + relayer/store/filesystem.go | 3 + relayer/tunnel_relayer.go | 45 ++- relayer/tunnel_relayer_test.go | 43 +++ relayer/types.go | 6 +- relayer/wallet/xrpl/config.go | 54 ++++ relayer/wallet/xrpl/helper.go | 24 ++ relayer/wallet/xrpl/keyring.go | 56 ++++ relayer/wallet/xrpl/local_signer.go | 54 ++++ relayer/wallet/xrpl/remote_signer.go | 47 +++ relayer/wallet/xrpl/wallet.go | 208 +++++++++++++ 37 files changed, 1375 insertions(+), 200 deletions(-) delete mode 100644 relayer/chains/client.go delete mode 100644 relayer/chains/registry.go create mode 100644 relayer/chains/xrpl/client.go create mode 100644 relayer/chains/xrpl/config.go create mode 100644 relayer/chains/xrpl/keys.go create mode 100644 relayer/chains/xrpl/provider.go create mode 100644 relayer/chains/xrpl/utils.go create mode 100644 relayer/wallet/xrpl/config.go create mode 100644 relayer/wallet/xrpl/helper.go create mode 100644 relayer/wallet/xrpl/keyring.go create mode 100644 relayer/wallet/xrpl/local_signer.go create mode 100644 relayer/wallet/xrpl/remote_signer.go create mode 100644 relayer/wallet/xrpl/wallet.go diff --git a/cmd/keys.go b/cmd/keys.go index 9c1ebea..b0e6401 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -488,22 +488,33 @@ func addKey( // Add key to the keychain if input.PrivateKey != "" { - return app.AddKeyByPrivateKey(chainName, keyName, input.PrivateKey) + address, err := app.AddKeyByPrivateKey(chainName, keyName, input.PrivateKey) + if err != nil { + return nil, err + } + return chainstypes.NewKey("", address, ""), nil } else if input.RemoteSigner.Address != "" && input.RemoteSigner.Url != "" { - return app.AddRemoteSignerKey( + if err := app.AddRemoteSignerKey( chainName, keyName, input.RemoteSigner.Address, input.RemoteSigner.Url, input.RemoteSigner.Key, - ) + ); err != nil { + return nil, err + } + return chainstypes.NewKey("", input.RemoteSigner.Address, ""), nil } else { - return app.AddKeyByMnemonic( + mnemonic, address, err := app.AddKeyByMnemonic( chainName, keyName, input.Mnemonic, uint32(input.CoinType), uint(input.Account), uint(input.Index), ) + if err != nil { + return nil, err + } + return chainstypes.NewKey(mnemonic, address, ""), nil } } diff --git a/go.mod b/go.mod index 1e83644..0df0d31 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,13 @@ module github.com/bandprotocol/falcon -go 1.24.2 +go 1.24.3 require ( cosmossdk.io/math v1.4.0 cosmossdk.io/x/tx v0.13.7 + github.com/99designs/keyring v1.2.1 + github.com/Peersyst/xrpl-go v0.1.14 + github.com/bsv-blockchain/go-sdk v1.2.9 github.com/charmbracelet/huh v0.7.0 github.com/cometbft/cometbft v0.38.19 github.com/cosmos/cosmos-sdk v0.50.14 @@ -40,7 +43,6 @@ require ( cosmossdk.io/store v1.1.1 // indirect filippo.io/edwards25519 v1.0.0 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect - github.com/99designs/keyring v1.2.1 // indirect github.com/DataDog/zstd v1.5.5 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/StackExchange/wmi v1.2.1 // indirect @@ -83,6 +85,7 @@ require ( github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/crypto/ripemd160 v1.0.2 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dgraph-io/badger/v4 v4.2.0 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect @@ -126,6 +129,7 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/jmhodges/levigo v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect @@ -137,6 +141,8 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect @@ -167,19 +173,20 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/crypto v0.38.0 // indirect + golang.org/x/crypto v0.44.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.40.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect diff --git a/go.sum b/go.sum index 83029da..2abb3ef 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Peersyst/xrpl-go v0.1.14 h1:4iCyLCzTnpp+1cjvRm536edsPW3brHfNLS4wDT6an3A= +github.com/Peersyst/xrpl-go v0.1.14/go.mod h1:QwEypVCDdluBo6P4jgSq0cC0+OYspFQCHOHEeaCAH2c= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= @@ -57,6 +59,8 @@ github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2 github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bsv-blockchain/go-sdk v1.2.9 h1:LwFzuts+J5X7A+ECx0LNowtUgIahCkNNlXckdiEMSDk= +github.com/bsv-blockchain/go-sdk v1.2.9/go.mod h1:KiHWa/hblo3Bzr+IsX11v0sn1E6elGbNX0VXl5mOq6E= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= @@ -191,6 +195,8 @@ github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpO github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/crypto/ripemd160 v1.0.2 h1:TvGTmUBHDU75OHro9ojPLK+Yv7gDl2hnUvRocRCjsys= +github.com/decred/dcrd/crypto/ripemd160 v1.0.2/go.mod h1:uGfjDyePSpa75cSQLzNdVmWlbQMBuiJkvXw/MNKRY4M= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= @@ -401,6 +407,8 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -460,9 +468,12 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= @@ -613,6 +624,8 @@ github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9f github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= @@ -655,8 +668,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= @@ -682,8 +695,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -693,8 +706,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -720,15 +733,15 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/relayertest/constants.go b/internal/relayertest/constants.go index 159da08..3e076cc 100644 --- a/internal/relayertest/constants.go +++ b/internal/relayertest/constants.go @@ -35,14 +35,14 @@ var CustomCfg = config.Config{ TargetChains: config.ChainProviderConfigs{ "testnet": &evm.EVMChainProviderConfig{ BaseChainProviderConfig: chains.BaseChainProviderConfig{ - Endpoints: []string{"http://localhost:8545"}, - ChainType: chainstypes.ChainTypeEVM, - MaxRetry: 3, - ChainID: 31337, - TunnelRouterAddress: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", - QueryTimeout: 3 * time.Second, - ExecuteTimeout: 3 * time.Second, + Endpoints: []string{"http://localhost:8545"}, + ChainType: chainstypes.ChainTypeEVM, + MaxRetry: 3, + ChainID: 31337, + QueryTimeout: 3 * time.Second, + ExecuteTimeout: 3 * time.Second, }, + TunnelRouterAddress: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", BlockConfirmation: 5, WaitingTxDuration: time.Second * 3, CheckingTxInterval: time.Second, diff --git a/internal/relayertest/mocks/chain_provider.go b/internal/relayertest/mocks/chain_provider.go index 9445f22..0b96623 100644 --- a/internal/relayertest/mocks/chain_provider.go +++ b/internal/relayertest/mocks/chain_provider.go @@ -59,6 +59,20 @@ func (mr *MockChainProviderMockRecorder) AddKeyByMnemonic(keyName, mnemonic, coi return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddKeyByMnemonic", reflect.TypeOf((*MockChainProvider)(nil).AddKeyByMnemonic), keyName, mnemonic, coinType, account, index) } +// ChainType mocks base method. +func (m *MockChainProvider) ChainType() types0.ChainType { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChainType") + ret0, _ := ret[0].(types0.ChainType) + return ret0 +} + +// ChainType indicates an expected call of ChainType. +func (mr *MockChainProviderMockRecorder) ChainType() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChainType", reflect.TypeOf((*MockChainProvider)(nil).ChainType)) +} + // GetChainName mocks base method. func (m *MockChainProvider) GetChainName() string { m.ctrl.T.Helper() @@ -156,56 +170,3 @@ func (mr *MockChainProviderMockRecorder) SetDatabase(database any) *gomock.Call mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDatabase", reflect.TypeOf((*MockChainProvider)(nil).SetDatabase), database) } - -// MockKeyProvider is a mock of KeyProvider interface. -type MockKeyProvider struct { - ctrl *gomock.Controller - recorder *MockKeyProviderMockRecorder - isgomock struct{} -} - -// MockKeyProviderMockRecorder is the mock recorder for MockKeyProvider. -type MockKeyProviderMockRecorder struct { - mock *MockKeyProvider -} - -// NewMockKeyProvider creates a new mock instance. -func NewMockKeyProvider(ctrl *gomock.Controller) *MockKeyProvider { - mock := &MockKeyProvider{ctrl: ctrl} - mock.recorder = &MockKeyProviderMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockKeyProvider) EXPECT() *MockKeyProviderMockRecorder { - return m.recorder -} - -// AddKeyByMnemonic mocks base method. -func (m *MockKeyProvider) AddKeyByMnemonic(keyName, mnemonic string, coinType uint32, account, index uint) (*types0.Key, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddKeyByMnemonic", keyName, mnemonic, coinType, account, index) - ret0, _ := ret[0].(*types0.Key) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AddKeyByMnemonic indicates an expected call of AddKeyByMnemonic. -func (mr *MockKeyProviderMockRecorder) AddKeyByMnemonic(keyName, mnemonic, coinType, account, index any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddKeyByMnemonic", reflect.TypeOf((*MockKeyProvider)(nil).AddKeyByMnemonic), keyName, mnemonic, coinType, account, index) -} - -// LoadSigners mocks base method. -func (m *MockKeyProvider) LoadSigners() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LoadSigners") - ret0, _ := ret[0].(error) - return ret0 -} - -// LoadSigners indicates an expected call of LoadSigners. -func (mr *MockKeyProviderMockRecorder) LoadSigners() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadSigners", reflect.TypeOf((*MockKeyProvider)(nil).LoadSigners)) -} diff --git a/relayer/app.go b/relayer/app.go index 66635e3..f259452 100644 --- a/relayer/app.go +++ b/relayer/app.go @@ -271,17 +271,22 @@ func (a *App) GetChainConfig(chainName string) (chains.ChainProviderConfig, erro } // AddKeyByPrivateKey adds a new key to the chain provider using a private key. -func (a *App) AddKeyByPrivateKey(chainName string, keyName string, privateKey string) (*chainstypes.Key, error) { +func (a *App) AddKeyByPrivateKey(chainName string, keyName string, privateKey string) (string, error) { if err := a.Store.ValidatePassphrase(a.Passphrase); err != nil { - return nil, err + return "", err } w, err := a.getWallet(chainName) if err != nil { - return nil, err + return "", err + } + + key, err := chains.AddKeyByPrivateKey(w, keyName, privateKey) + if err != nil { + return "", err } - return chains.AddKeyByPrivateKey(w, keyName, privateKey) + return key.Address, nil } // AddKeyByMnemonic adds a new key to the chain provider using a mnemonic phrase. @@ -292,17 +297,22 @@ func (a *App) AddKeyByMnemonic( coinType uint32, account uint, index uint, -) (*chainstypes.Key, error) { +) (string, string, error) { if err := a.Store.ValidatePassphrase(a.Passphrase); err != nil { - return nil, err + return "", "", err } cp, err := a.getChainProvider(chainName) if err != nil { - return nil, err + return "", "", err + } + + key, err := cp.AddKeyByMnemonic(keyName, mnemonic, coinType, account, index) + if err != nil { + return "", "", err } - return cp.AddKeyByMnemonic(keyName, mnemonic, coinType, account, index) + return key.Mnemonic, key.Address, nil } // AddRemoteSignerKey adds a new remote signer key to the chain provider. @@ -312,13 +322,14 @@ func (a *App) AddRemoteSignerKey( addr string, url string, key *string, -) (*chainstypes.Key, error) { +) error { w, err := a.getWallet(chainName) if err != nil { - return nil, err + return err } - return chains.AddRemoteSignerKey(w, keyName, addr, url, key) + _, err = chains.AddRemoteSignerKey(w, keyName, addr, url, key) + return err } // DeleteKey deletes the key from the chain provider. diff --git a/relayer/app_test.go b/relayer/app_test.go index 2520567..3afba1e 100644 --- a/relayer/app_test.go +++ b/relayer/app_test.go @@ -336,7 +336,7 @@ func (s *AppTestSuite) TestQueryTunnelInfo() { false, "0xc0ffee254729296a45a3885639AC7E10F9d54979", ) - mockTunnelChainInfo := chainstypes.NewTunnel(1, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", false) + mockTunnelChainInfo := chainstypes.NewTunnel(1, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", false, 0, nil) testcases := []struct { name string @@ -428,6 +428,7 @@ func (s *AppTestSuite) TestQueryTunnelPacketInfo() { signalPrices, signingInfo, nil, + time.Now().Unix(), ) // Set up the mock expectation @@ -439,7 +440,7 @@ func (s *AppTestSuite) TestQueryTunnelPacketInfo() { packet, err := s.app.QueryTunnelPacketInfo(context.Background(), 1, 1) // Create the expected packet structure for comparison - expected := bandtypes.NewPacket(1, 1, signalPrices, signingInfo, nil) + expected := bandtypes.NewPacket(1, 1, signalPrices, signingInfo, nil, time.Now().Unix()) // Assertions s.Require().NoError(err) @@ -473,7 +474,7 @@ func (s *AppTestSuite) TestAddKey() { account uint index uint err error - out *chainstypes.Key + out string preprocess func() }{ { @@ -482,7 +483,7 @@ func (s *AppTestSuite) TestAddKey() { keyName: "testkey", privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", // anvil coinType: 60, - out: chainstypes.NewKey("", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", ""), + out: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", preprocess: func() { s.store.EXPECT(). NewWallet(chainstypes.ChainTypeEVM, "testnet_evm", s.passphrase). diff --git a/relayer/band/client.go b/relayer/band/client.go index fc244d9..c4341f0 100644 --- a/relayer/band/client.go +++ b/relayer/band/client.go @@ -306,6 +306,7 @@ func (c *client) GetTunnelPacket(ctx context.Context, tunnelID uint64, sequence signalPrices, currentGroupSigning, incomingGroupSigning, + resPacket.Packet.CreatedAt, ), nil } diff --git a/relayer/band/client_test.go b/relayer/band/client_test.go index 868dfea..a37f9f8 100644 --- a/relayer/band/client_test.go +++ b/relayer/band/client_test.go @@ -252,6 +252,7 @@ func (s *ClientTestSuite) TestGetTSSTunnelPacket() { expectedSignalPrices, expectedCurrentGroupSigning, nil, + time.Now().Unix(), ) // actual result diff --git a/relayer/band/types/packet.go b/relayer/band/types/packet.go index 37b55e3..7224a94 100644 --- a/relayer/band/types/packet.go +++ b/relayer/band/types/packet.go @@ -13,6 +13,7 @@ type Packet struct { SignalPrices []SignalPrice `json:"signal_prices"` CurrentGroupSigning *Signing `json:"current_group_signing"` IncomingGroupSigning *Signing `json:"incoming_group_signing"` + CreatedAt int64 `json:"-"` } // NewPacket creates a new Packet instance. @@ -22,6 +23,7 @@ func NewPacket( signalPrices []SignalPrice, currentGroupSigning *Signing, incomingGroupSigning *Signing, + createdAt int64, ) *Packet { return &Packet{ TunnelID: tunnelID, @@ -29,5 +31,6 @@ func NewPacket( SignalPrices: signalPrices, CurrentGroupSigning: currentGroupSigning, IncomingGroupSigning: incomingGroupSigning, + CreatedAt: createdAt, } } diff --git a/relayer/chains/client.go b/relayer/chains/client.go deleted file mode 100644 index 6dd82dc..0000000 --- a/relayer/chains/client.go +++ /dev/null @@ -1,18 +0,0 @@ -package chains - -import "math/big" - -// Client defines the interface for the target chain client -type Client interface { - // GetNonce returns the nonce of the given address - GetNonce(address string) (uint64, error) - - // BroadcastTx broadcasts the given raw transaction - BroadcastTx(rawTx string) (string, error) - - // GetBalances returns the balances of the given accounts - GetBalances(accounts []string) ([]*big.Int, error) - - // GetTunnelNonce returns the nonce of the given tunnel - GetTunnelNonce(targetAddress string, tunnelID uint64) (uint64, error) -} diff --git a/relayer/chains/config.go b/relayer/chains/config.go index 557b555..8c1e1d2 100644 --- a/relayer/chains/config.go +++ b/relayer/chains/config.go @@ -17,8 +17,6 @@ type BaseChainProviderConfig struct { QueryTimeout time.Duration `mapstructure:"query_timeout" toml:"query_timeout"` ExecuteTimeout time.Duration `mapstructure:"execute_timeout" toml:"execute_timeout"` ChainID uint64 `mapstructure:"chain_id" toml:"chain_id"` - - TunnelRouterAddress string `mapstructure:"tunnel_router_address" toml:"tunnel_router_address"` } // ChainProviderConfigs is a collection of ChainProviderConfig interfaces (mapped by chainName) diff --git a/relayer/chains/evm/config.go b/relayer/chains/evm/config.go index 614beb4..5d6e6d4 100644 --- a/relayer/chains/evm/config.go +++ b/relayer/chains/evm/config.go @@ -16,6 +16,7 @@ var _ chains.ChainProviderConfig = &EVMChainProviderConfig{} type EVMChainProviderConfig struct { chains.BaseChainProviderConfig `mapstructure:",squash"` + TunnelRouterAddress string `mapstructure:"tunnel_router_address" toml:"tunnel_router_address"` BlockConfirmation uint64 `mapstructure:"block_confirmation" toml:"block_confirmation"` WaitingTxDuration time.Duration `mapstructure:"waiting_tx_duration" toml:"waiting_tx_duration"` LivelinessCheckingInterval time.Duration `mapstructure:"liveliness_checking_interval" toml:"liveliness_checking_interval"` diff --git a/relayer/chains/evm/keys_test.go b/relayer/chains/evm/keys_test.go index b76fbc9..9af3446 100644 --- a/relayer/chains/evm/keys_test.go +++ b/relayer/chains/evm/keys_test.go @@ -25,14 +25,15 @@ const ( var evmCfg = &evm.EVMChainProviderConfig{ BaseChainProviderConfig: chains.BaseChainProviderConfig{ - Endpoints: []string{"http://localhost:8545"}, - ChainType: chaintypes.ChainTypeEVM, - MaxRetry: 3, - ChainID: 31337, - TunnelRouterAddress: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", - QueryTimeout: 3 * time.Second, - ExecuteTimeout: 3 * time.Second, + Endpoints: []string{"http://localhost:8545"}, + ChainType: chaintypes.ChainTypeEVM, + MaxRetry: 3, + ChainID: 31337, + + QueryTimeout: 3 * time.Second, + ExecuteTimeout: 3 * time.Second, }, + TunnelRouterAddress: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", BlockConfirmation: 5, WaitingTxDuration: time.Second * 3, CheckingTxInterval: time.Second, diff --git a/relayer/chains/evm/provider.go b/relayer/chains/evm/provider.go index 8466c93..05786ee 100644 --- a/relayer/chains/evm/provider.go +++ b/relayer/chains/evm/provider.go @@ -133,13 +133,9 @@ func (cp *EVMChainProvider) QueryTunnelInfo( return nil, fmt.Errorf("[EVMProvider] failed to query contract: %w", err) } - return &types.Tunnel{ - ID: tunnelID, - TargetAddress: tunnelDestinationAddr, - IsActive: info.IsActive, - LatestSequence: info.LatestSequence, - Balance: info.Balance, - }, nil + tunnel := types.NewTunnel(tunnelID, tunnelDestinationAddr, info.IsActive, info.LatestSequence, info.Balance) + + return tunnel, nil } // RelayPacket relays the packet from the source chain to the destination chain. @@ -851,6 +847,11 @@ func (cp *EVMChainProvider) GetChainName() string { return cp.ChainName } +// ChainType retrieves the chain type from the chain provider. +func (cp *EVMChainProvider) ChainType() types.ChainType { + return types.ChainTypeEVM +} + // queryRelayerGasFee queries the relayer gas fee being set on tunnel router. func (cp *EVMChainProvider) queryRelayerGasFee(ctx context.Context) (*big.Int, error) { calldata, err := cp.TunnelRouterABI.Pack("gasFee") diff --git a/relayer/chains/evm/provider_test.go b/relayer/chains/evm/provider_test.go index c9c839a..19d6dac 100644 --- a/relayer/chains/evm/provider_test.go +++ b/relayer/chains/evm/provider_test.go @@ -28,14 +28,14 @@ import ( var baseEVMCfg = &evm.EVMChainProviderConfig{ BaseChainProviderConfig: chains.BaseChainProviderConfig{ - Endpoints: []string{"http://localhost:8545"}, - ChainType: chaintypes.ChainTypeEVM, - MaxRetry: 3, - ChainID: 31337, - TunnelRouterAddress: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", - QueryTimeout: 3 * time.Second, - ExecuteTimeout: 3 * time.Second, + Endpoints: []string{"http://localhost:8545"}, + ChainType: chaintypes.ChainTypeEVM, + MaxRetry: 3, + ChainID: 31337, + QueryTimeout: 3 * time.Second, + ExecuteTimeout: 3 * time.Second, }, + TunnelRouterAddress: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", BlockConfirmation: 5, WaitingTxDuration: time.Second * 3, CheckingTxInterval: time.Second, diff --git a/relayer/chains/provider.go b/relayer/chains/provider.go index 9412c39..d01ea18 100644 --- a/relayer/chains/provider.go +++ b/relayer/chains/provider.go @@ -7,7 +7,6 @@ import ( bandtypes "github.com/bandprotocol/falcon/relayer/band/types" chainstypes "github.com/bandprotocol/falcon/relayer/chains/types" "github.com/bandprotocol/falcon/relayer/db" - "github.com/bandprotocol/falcon/relayer/logger" ) // ChainProviders is a collection of ChainProvider interfaces (mapped by chainName) @@ -37,6 +36,9 @@ type ChainProvider interface { // GetChainName retrieves the chain name from the chain provider. GetChainName() string + // GetChainType retrieves the chain type from the chain provider. + ChainType() chainstypes.ChainType + // AddKeyByMnemonic adds a key using a mnemonic phrase. AddKeyByMnemonic( keyName string, @@ -49,14 +51,3 @@ type ChainProvider interface { // LoadSigners loads signers to prepare to relay the packet LoadSigners() error } - -// BaseChainProvider is a base object for connecting with the chain network. -type BaseChainProvider struct { - log logger.Logger - - Config ChainProviderConfig - ChainName string - ChainID string - - debug bool -} diff --git a/relayer/chains/registry.go b/relayer/chains/registry.go deleted file mode 100644 index 79b7a7d..0000000 --- a/relayer/chains/registry.go +++ /dev/null @@ -1,25 +0,0 @@ -package chains - -import "fmt" - -// Registry is a collection of chain clients. -type Registry struct { - Chains map[string]Client -} - -// NewRegistry creates a new chain registry. -func NewRegistry() *Registry { - return &Registry{ - Chains: make(map[string]Client), - } -} - -// Register registers a chain client to the registry. -func (r *Registry) Register(chainID string, client Client) error { - if _, ok := r.Chains[chainID]; !ok { - return fmt.Errorf("chain %s already registered", chainID) - } - - r.Chains[chainID] = client - return nil -} diff --git a/relayer/chains/signer_test.go b/relayer/chains/signer_test.go index d1fefd4..ef07a05 100644 --- a/relayer/chains/signer_test.go +++ b/relayer/chains/signer_test.go @@ -26,14 +26,14 @@ const ( var evmCfg = &evm.EVMChainProviderConfig{ BaseChainProviderConfig: chains.BaseChainProviderConfig{ - Endpoints: []string{"http://localhost:8545"}, - ChainType: chainstypes.ChainTypeEVM, - MaxRetry: 3, - ChainID: 31337, - TunnelRouterAddress: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", - QueryTimeout: 3 * time.Second, - ExecuteTimeout: 3 * time.Second, + Endpoints: []string{"http://localhost:8545"}, + ChainType: chainstypes.ChainTypeEVM, + MaxRetry: 3, + ChainID: 31337, + QueryTimeout: 3 * time.Second, + ExecuteTimeout: 3 * time.Second, }, + TunnelRouterAddress: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", BlockConfirmation: 5, WaitingTxDuration: time.Second * 3, CheckingTxInterval: time.Second, diff --git a/relayer/chains/types/chain_type.go b/relayer/chains/types/chain_type.go index 011ef01..d27305e 100644 --- a/relayer/chains/types/chain_type.go +++ b/relayer/chains/types/chain_type.go @@ -15,10 +15,12 @@ type ChainType int const ( ChainTypeUndefined ChainType = iota ChainTypeEVM + ChainTypeXRPL ) var chainTypeNameMap = map[ChainType]string{ - ChainTypeEVM: "evm", + ChainTypeEVM: "evm", + ChainTypeXRPL: "xrpl", } var nameToChainTypeMap map[string]ChainType @@ -67,7 +69,7 @@ func (c ChainType) MarshalText() ([]byte, error) { // Scan scans string value into ChainType, implements sql.Scanner interface. // (needs to manually creates `chain_type` type in a database first -// by "CREATE TYPE chain_type AS ENUM ('evm')") +// by "CREATE TYPE chain_type AS ENUM ('evm', 'xrpl')") func (c *ChainType) Scan(value interface{}) error { str, ok := value.(string) if !ok { diff --git a/relayer/chains/types/tunnel.go b/relayer/chains/types/tunnel.go index aaddc76..52cda5f 100644 --- a/relayer/chains/types/tunnel.go +++ b/relayer/chains/types/tunnel.go @@ -12,10 +12,18 @@ type Tunnel struct { } // NewTunnel creates a new tunnel object. -func NewTunnel(id uint64, targetAddress string, isActive bool) *Tunnel { +func NewTunnel( + id uint64, + targetAddress string, + isActive bool, + latestSequence uint64, + balance *big.Int, +) *Tunnel { return &Tunnel{ - ID: id, - TargetAddress: targetAddress, - IsActive: isActive, + ID: id, + TargetAddress: targetAddress, + IsActive: isActive, + LatestSequence: latestSequence, + Balance: balance, } } diff --git a/relayer/chains/xrpl/client.go b/relayer/chains/xrpl/client.go new file mode 100644 index 0000000..1a3c812 --- /dev/null +++ b/relayer/chains/xrpl/client.go @@ -0,0 +1,217 @@ +package xrpl + +import ( + "context" + "fmt" + "math/big" + "strings" + "sync" + "time" + + xrplaccount "github.com/Peersyst/xrpl-go/xrpl/queries/account" + "github.com/Peersyst/xrpl-go/xrpl/queries/common" + "github.com/Peersyst/xrpl-go/xrpl/queries/utility" + "github.com/Peersyst/xrpl-go/xrpl/rpc" + "github.com/Peersyst/xrpl-go/xrpl/transaction" + "github.com/Peersyst/xrpl-go/xrpl/transaction/types" + + "github.com/bandprotocol/falcon/relayer/alert" + "github.com/bandprotocol/falcon/relayer/logger" +) + +// Client handles XRPL JSON-RPC interactions. +type Client struct { + ChainName string + Endpoints []string + QueryTimeout time.Duration + ExecuteTimeout time.Duration + + Log logger.Logger + alert alert.Alert + + rpcClient *rpc.Client + + mu sync.RWMutex + selectedEndpoint string +} + +// NewClient creates a new XRPL client from config. +func NewClient(chainName string, cfg *XRPLChainProviderConfig, log logger.Logger, alert alert.Alert) *Client { + return &Client{ + ChainName: chainName, + Endpoints: cfg.Endpoints, + QueryTimeout: cfg.QueryTimeout, + ExecuteTimeout: cfg.ExecuteTimeout, + Log: log.With("chain_name", chainName), + alert: alert, + } +} + +func (c *Client) getSelectedEndpoint() string { + c.mu.RLock() + defer c.mu.RUnlock() + return c.selectedEndpoint +} + +func (c *Client) setSelectedEndpoint(endpoint string) { + c.mu.Lock() + defer c.mu.Unlock() + c.selectedEndpoint = endpoint +} + +func (c *Client) getRPCClient() (*rpc.Client, error) { + c.mu.RLock() + defer c.mu.RUnlock() + if c.rpcClient == nil { + return nil, fmt.Errorf("xrpl rpc client not initialized") + } + return c.rpcClient, nil +} + +func (c *Client) setRPCClient(client *rpc.Client) { + c.mu.Lock() + defer c.mu.Unlock() + c.rpcClient = client +} + +// Connect selects a responsive endpoint by pinging the server. +func (c *Client) Connect(ctx context.Context) error { + return c.ping(ctx) +} + +// Ping checks connectivity to the XRPL endpoint. +func (c *Client) ping(ctx context.Context) error { + endpoints := make([]string, 0, len(c.Endpoints)) + if selected := c.getSelectedEndpoint(); selected != "" { + endpoints = append(endpoints, selected) + } + for _, endpoint := range c.Endpoints { + if endpoint == c.getSelectedEndpoint() { + continue + } + endpoints = append(endpoints, endpoint) + } + + var lastErr error + for _, endpoint := range endpoints { + if err := ctx.Err(); err != nil { + return err + } + + timeout := c.QueryTimeout + if c.ExecuteTimeout > timeout { + timeout = c.ExecuteTimeout + } + var opts []rpc.ConfigOpt + if timeout > 0 { + opts = append(opts, rpc.WithTimeout(timeout)) + } + cfg, err := rpc.NewClientConfig(endpoint, opts...) + if err != nil { + lastErr = err + c.Log.Warn("XRPL endpoint error", "endpoint", endpoint, err) + continue + } + + client := rpc.NewClient(cfg) + _, err = client.Ping(&utility.PingRequest{}) + if err == nil { + if c.getSelectedEndpoint() != endpoint { + c.Log.Info("Connected to XRPL endpoint", "endpoint", endpoint) + } + c.setSelectedEndpoint(endpoint) + c.setRPCClient(client) + return nil + } + + lastErr = err + c.Log.Warn("XRPL endpoint error", "endpoint", endpoint, err) + } + + return lastErr +} + +// GetAccountSequenceNumber fetches the sequence for the given account. +func (c *Client) GetAccountSequenceNumber(ctx context.Context, account string) (uint64, error) { + if err := ctx.Err(); err != nil { + return 0, err + } + client, err := c.getRPCClient() + if err != nil { + return 0, err + } + result, err := client.GetAccountInfo(&xrplaccount.InfoRequest{ + Account: types.Address(account), + LedgerIndex: common.Validated, + Strict: true, + }) + if err != nil { + return 0, err + } + + return uint64(result.AccountData.Sequence), nil +} + +// GetBalance fetches the XRP balance for the given account (drops). +func (c *Client) GetBalance(ctx context.Context, account string) (*big.Int, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + client, err := c.getRPCClient() + if err != nil { + return nil, err + } + result, err := client.GetAccountInfo(&xrplaccount.InfoRequest{ + Account: types.Address(account), + LedgerIndex: common.Validated, + Strict: true, + }) + if err != nil { + return nil, err + } + + b := new(big.Int) + b, ok := b.SetString(result.AccountData.Balance.String(), 10) + if !ok { + return nil, fmt.Errorf("failed to parse balance of %s (%s)", account, result.AccountData.Balance.String()) + } + + return b, nil +} + +// Autofill completes a transaction with missing Sequence, Fee, and LastLedgerSequence fields. +func (c *Client) Autofill(tx *transaction.FlatTransaction) error { + client, err := c.getRPCClient() + if err != nil { + return err + } + return client.Autofill(tx) +} + +// BroadcastTx submits a signed tx blob and returns its hash. +func (c *Client) BroadcastTx(ctx context.Context, txBlob string) (string, error) { + client, err := c.getRPCClient() + if err != nil { + return "", err + } + + result, err := client.SubmitTxBlob(txBlob, false) + if err != nil { + return "", err + } + + if !strings.HasPrefix(result.EngineResult, "tes") { + return "", fmt.Errorf( + "failed to broadcast with engine result %s: %s", + result.EngineResult, + result.EngineResultMessage, + ) + } + + txHash, ok := result.Tx["hash"].(string) + if !ok || txHash == "" { + return "", fmt.Errorf("missing tx hash in submit response") + } + + return txHash, nil +} diff --git a/relayer/chains/xrpl/config.go b/relayer/chains/xrpl/config.go new file mode 100644 index 0000000..0581e9c --- /dev/null +++ b/relayer/chains/xrpl/config.go @@ -0,0 +1,52 @@ +package xrpl + +import ( + "fmt" + + "github.com/bandprotocol/falcon/relayer/alert" + "github.com/bandprotocol/falcon/relayer/chains" + "github.com/bandprotocol/falcon/relayer/chains/types" + "github.com/bandprotocol/falcon/relayer/logger" + "github.com/bandprotocol/falcon/relayer/wallet" +) + +var _ chains.ChainProviderConfig = &XRPLChainProviderConfig{} + +// XRPLChainProviderConfig is the configuration for the XRPL chain provider. +type XRPLChainProviderConfig struct { + chains.BaseChainProviderConfig `mapstructure:",squash"` + + OracleID uint16 `mapstructure:"oracle_id" toml:"oracle_id"` + Fee string `mapstructure:"fee" toml:"fee"` + PriceScale uint32 `mapstructure:"price_scale" toml:"price_scale"` +} + +// NewChainProvider creates a new XRPL chain provider. +func (cpc *XRPLChainProviderConfig) NewChainProvider( + chainName string, + log logger.Logger, + wallet wallet.Wallet, + alert alert.Alert, +) (chains.ChainProvider, error) { + client := NewClient(chainName, cpc, log, alert) + + return NewXRPLChainProvider(chainName, client, cpc, log, wallet, alert) +} + +// Validate validates the XRPL chain provider configuration. +func (cpc *XRPLChainProviderConfig) Validate() error { + if len(cpc.Endpoints) == 0 { + return fmt.Errorf("endpoints is required") + } + if cpc.OracleID == 0 { + return fmt.Errorf("oracle_id is required") + } + if cpc.Fee == "" { + return fmt.Errorf("fee is required") + } + return nil +} + +func (cpc *XRPLChainProviderConfig) GetChainType() types.ChainType { + return types.ChainTypeXRPL +} diff --git a/relayer/chains/xrpl/keys.go b/relayer/chains/xrpl/keys.go new file mode 100644 index 0000000..4822ce3 --- /dev/null +++ b/relayer/chains/xrpl/keys.go @@ -0,0 +1,53 @@ +package xrpl + +import ( + "fmt" + + xrplwallet "github.com/Peersyst/xrpl-go/xrpl/wallet" + "github.com/bsv-blockchain/go-sdk/compat/bip39" + + chainstypes "github.com/bandprotocol/falcon/relayer/chains/types" +) + +const ( + xrplMnemonicEntropyBits = 256 + xrplDefaultCoinType = 144 +) + +// AddKeyByMnemonic adds a key using a mnemonic phrase. +func (cp *XRPLChainProvider) AddKeyByMnemonic( + keyName string, + mnemonic string, + coinType uint32, + account uint, + index uint, +) (*chainstypes.Key, error) { + if coinType != xrplDefaultCoinType || account != 0 || index != 0 { + return nil, fmt.Errorf("xrpl mnemonic derivation only supports m/44'/144'/0'/0/0") + } + + generatedMnemonic := "" + if mnemonic == "" { + entropy, err := bip39.NewEntropy(xrplMnemonicEntropyBits) + if err != nil { + return nil, err + } + mnemonic, err = bip39.NewMnemonic(entropy) + if err != nil { + return nil, err + } + generatedMnemonic = mnemonic + } + + w, err := xrplwallet.FromMnemonic(mnemonic) + if err != nil { + return nil, err + } + + addr, err := cp.Wallet.SavePrivateKey(keyName, w.PrivateKey) + if err != nil { + return nil, err + } + + return chainstypes.NewKey(generatedMnemonic, addr, ""), nil +} diff --git a/relayer/chains/xrpl/provider.go b/relayer/chains/xrpl/provider.go new file mode 100644 index 0000000..61f3f00 --- /dev/null +++ b/relayer/chains/xrpl/provider.go @@ -0,0 +1,298 @@ +package xrpl + +import ( + "context" + "fmt" + "math/big" + "time" + + binarycodec "github.com/Peersyst/xrpl-go/binary-codec" + ledger "github.com/Peersyst/xrpl-go/xrpl/ledger-entry-types" + "github.com/Peersyst/xrpl-go/xrpl/transaction" + xrpltypes "github.com/Peersyst/xrpl-go/xrpl/transaction/types" + "github.com/shopspring/decimal" + + "github.com/bandprotocol/falcon/internal/relayermetrics" + "github.com/bandprotocol/falcon/relayer/alert" + bandtypes "github.com/bandprotocol/falcon/relayer/band/types" + "github.com/bandprotocol/falcon/relayer/chains" + "github.com/bandprotocol/falcon/relayer/chains/types" + "github.com/bandprotocol/falcon/relayer/db" + "github.com/bandprotocol/falcon/relayer/logger" + "github.com/bandprotocol/falcon/relayer/wallet" +) + +var _ chains.ChainProvider = (*XRPLChainProvider)(nil) + +// XRPLChainProvider handles interactions with XRPL. +type XRPLChainProvider struct { + Config *XRPLChainProviderConfig + ChainName string + // OracleAccount is derived from the XRPL wallet signers at runtime. + OracleAccount string + + Client *Client + + Log logger.Logger + + Wallet wallet.Wallet + DB db.Database + + Alert alert.Alert + + FreeSigners chan wallet.Signer + + nonceInterval time.Duration +} + +// NewXRPLChainProvider creates a new XRPL chain provider. +func NewXRPLChainProvider( + chainName string, + client *Client, + cfg *XRPLChainProviderConfig, + log logger.Logger, + wallet wallet.Wallet, + alert alert.Alert, +) (*XRPLChainProvider, error) { + if cfg.PriceScale == 0 { + cfg.PriceScale = 9 + } + if cfg.PriceScale > uint32(ledger.PriceDataScaleMax) { + return nil, fmt.Errorf( + "price_scale %d exceeds max %d", + cfg.PriceScale, + ledger.PriceDataScaleMax, + ) + } + + return &XRPLChainProvider{ + Config: cfg, + ChainName: chainName, + Client: client, + Log: log.With("chain_name", chainName), + Wallet: wallet, + Alert: alert, + nonceInterval: time.Second, + }, nil +} + +// Init connects to the XRPL chain. +func (cp *XRPLChainProvider) Init(ctx context.Context) error { + if err := cp.Client.Connect(ctx); err != nil { + return err + } + + return nil +} + +// SetDatabase assigns the given database instance. +func (cp *XRPLChainProvider) SetDatabase(database db.Database) { + cp.DB = database +} + +// QueryTunnelInfo returns a best-effort tunnel info for XRPL. +func (cp *XRPLChainProvider) QueryTunnelInfo( + ctx context.Context, + tunnelID uint64, + tunnelDestinationAddr string, +) (*types.Tunnel, error) { + tunnel := types.NewTunnel(tunnelID, tunnelDestinationAddr, true, 0, nil) + return tunnel, nil +} + +// RelayPacket relays the packet to XRPL OracleSet transaction. +func (cp *XRPLChainProvider) RelayPacket(ctx context.Context, packet *bandtypes.Packet) error { + if cp.FreeSigners == nil { + return fmt.Errorf("signers not loaded") + } + signer := <-cp.FreeSigners + defer func() { + cp.FreeSigners <- signer + }() + + log := cp.Log.With( + "tunnel_id", packet.TunnelID, + "sequence", packet.Sequence, + "signer_address", signer.GetAddress(), + ) + + var lastErr error + var err error + sequence := uint64(0) + for retryCount := 1; retryCount <= cp.Config.MaxRetry; retryCount++ { + log.Info("Relaying a message", "retry_count", retryCount) + + if sequence == 0 { + sequence, err = cp.Client.GetAccountSequenceNumber(ctx, signer.GetAddress()) + if err != nil { + log.Error("Get account sequence number error", "retry_count", retryCount, err) + lastErr = err + time.Sleep(cp.nonceInterval) + continue + } + } + + tx, err := cp.buildOracleSetTx(packet, signer.GetAddress(), sequence) + if err != nil { + log.Error("Build OracleSet transaction error", "retry_count", retryCount, err) + lastErr = err + continue + } + + if err := cp.Client.Autofill(&tx); err != nil { + log.Error("Autofill transaction error", "retry_count", retryCount, err) + lastErr = err + continue + } + + encodedTx, err := binarycodec.Encode(tx) + if err != nil { + log.Error("Encode transaction error", "retry_count", retryCount, err) + lastErr = err + continue + } + + txBlobBytes, err := signer.Sign([]byte(encodedTx)) + if err != nil { + log.Error("Sign transaction error", "retry_count", retryCount, err) + lastErr = err + continue + } + + txHash, err := cp.Client.BroadcastTx(ctx, string(txBlobBytes)) + if err != nil { + log.Error("Broadcast transaction error", "retry_count", retryCount, err) + lastErr = err + continue + } + + log.Info( + "Packet is successfully relayed", + "tx_hash", txHash, + "retry_count", retryCount, + ) + + cp.saveRelayTx(packet, txHash) + relayermetrics.IncTxsCount(packet.TunnelID, cp.ChainName, types.TX_STATUS_SUCCESS.String()) + + return nil + } + + alert.HandleAlert( + cp.Alert, + alert.NewTopic(alert.RelayTxErrorMsg).WithTunnelID(packet.TunnelID).WithChainName(cp.ChainName), + lastErr.Error(), + ) + return fmt.Errorf("failed to relay packet after %d attempts", cp.Config.MaxRetry) +} + +// QueryBalance queries balance by given key name from the destination chain. +func (cp *XRPLChainProvider) QueryBalance(ctx context.Context, keyName string) (*big.Int, error) { + signer, ok := cp.Wallet.GetSigner(keyName) + if !ok { + cp.Log.Error("Key name does not exist", "key_name", keyName) + return nil, fmt.Errorf("key name does not exist: %s", keyName) + } + + return cp.Client.GetBalance(ctx, signer.GetAddress()) +} + +// GetChainName retrieves the chain name from the chain provider. +func (cp *XRPLChainProvider) GetChainName() string { return cp.ChainName } + +// ChainType retrieves the chain type from the chain provider. +func (cp *XRPLChainProvider) ChainType() types.ChainType { + return types.ChainTypeXRPL +} + +// LoadSigners loads signers to prepare to relay the packet. +func (cp *XRPLChainProvider) LoadSigners() error { + cp.FreeSigners = chains.LoadSigners(cp.Wallet) + return nil +} + +func (cp *XRPLChainProvider) buildOracleSetTx( + packet *bandtypes.Packet, + signerAddress string, + sequence uint64, +) (transaction.FlatTransaction, error) { + providerHex, err := stringToHex("Band Protocol", 0) + if err != nil { + return transaction.FlatTransaction{}, err + } + dataClassHex, err := stringToHex("currency", 0) + if err != nil { + return transaction.FlatTransaction{}, err + } + + priceDataSeries := make([]ledger.PriceDataWrapper, 0, len(packet.SignalPrices)) + + for _, p := range packet.SignalPrices { + baseAsset, quoteAsset, err := parseAssetsFromSignal(p.SignalID) + if err != nil { + return transaction.FlatTransaction{}, err + } + + priceDataSeries = append(priceDataSeries, ledger.PriceDataWrapper{ + PriceData: ledger.PriceData{ + BaseAsset: baseAsset, + QuoteAsset: quoteAsset, + AssetPrice: p.Price, + Scale: uint8(cp.Config.PriceScale), + }, + }) + } + + tx := &transaction.OracleSet{ + BaseTx: transaction.BaseTx{ + Account: xrpltypes.Address(signerAddress), + TransactionType: transaction.OracleSetTx, + Sequence: uint32(sequence), + Fee: xrpltypes.XRPCurrencyAmount(12), + }, + OracleDocumentID: uint32(cp.Config.OracleID), + LastUpdatedTime: uint32(time.Now().Unix()), + Provider: providerHex, + AssetClass: dataClassHex, + PriceDataSeries: priceDataSeries, + } + + return tx.Flatten(), nil +} + +func (cp *XRPLChainProvider) saveRelayTx(packet *bandtypes.Packet, txHash string) { + signalPrices := make([]db.SignalPrice, 0, len(packet.SignalPrices)) + for _, p := range packet.SignalPrices { + signalPrices = append(signalPrices, *db.NewSignalPrice(p.SignalID, p.Price)) + } + + tx := db.NewTransaction( + txHash, + packet.TunnelID, + packet.Sequence, + cp.ChainName, + types.ChainTypeXRPL, + cp.OracleAccount, + types.TX_STATUS_SUCCESS, + decimal.NullDecimal{}, + decimal.NullDecimal{}, + decimal.NullDecimal{}, + signalPrices, + nil, + ) + + if cp.DB == nil { + return + } + + if err := cp.DB.AddOrUpdateTransaction(tx); err != nil { + cp.Log.Error("Save transaction error", err) + alert.HandleAlert(cp.Alert, alert.NewTopic(alert.SaveDatabaseErrorMsg). + WithTunnelID(tx.TunnelID). + WithChainName(cp.ChainName), err.Error()) + } else { + alert.HandleReset(cp.Alert, alert.NewTopic(alert.SaveDatabaseErrorMsg). + WithTunnelID(tx.TunnelID). + WithChainName(cp.ChainName)) + } +} diff --git a/relayer/chains/xrpl/utils.go b/relayer/chains/xrpl/utils.go new file mode 100644 index 0000000..ee23f74 --- /dev/null +++ b/relayer/chains/xrpl/utils.go @@ -0,0 +1,52 @@ +package xrpl + +import ( + "encoding/hex" + "fmt" + "strings" +) + +func stringToHex(str string, length int) (string, error) { + encoded := strings.ToUpper(hex.EncodeToString([]byte(str))) + if length != 0 && len(encoded) > length { + return "", fmt.Errorf("hex string length %d exceeds expected length %d", len(encoded), length) + } + for length != 0 && len(encoded) < length { + encoded += "0" + } + return encoded, nil +} + +func parseAssetsFromSignal(signalID string) (string, string, error) { + parts := strings.Split(signalID, ":") + core := parts[len(parts)-1] + assets := strings.Split(core, "-") + if len(assets) != 2 { + return "", "", fmt.Errorf("invalid signal_id format: %s", signalID) + } + base := strings.TrimSpace(assets[0]) + quote := strings.TrimSpace(assets[1]) + if base == "" || quote == "" { + return "", "", fmt.Errorf("invalid signal_id format: %s", signalID) + } + + baseAsset := base + if len(base) != 3 { + var err error + baseAsset, err = stringToHex(base, 40) + if err != nil { + return "", "", err + } + } + + quoteAsset := quote + if len(quote) != 3 { + var err error + quoteAsset, err = stringToHex(quote, 40) + if err != nil { + return "", "", err + } + } + + return baseAsset, quoteAsset, nil +} diff --git a/relayer/config/config.go b/relayer/config/config.go index 0c349ab..44dcea2 100644 --- a/relayer/config/config.go +++ b/relayer/config/config.go @@ -13,6 +13,7 @@ import ( "github.com/bandprotocol/falcon/relayer/chains" "github.com/bandprotocol/falcon/relayer/chains/evm" chainstypes "github.com/bandprotocol/falcon/relayer/chains/types" + "github.com/bandprotocol/falcon/relayer/chains/xrpl" ) // ChainProviderConfigs is a collection of ChainProviderConfig interfaces (mapped by chainName) @@ -75,6 +76,20 @@ func ParseChainProviderConfig(w ChainProviderConfigWrapper) (chains.ChainProvide return nil, err } + cfg = &newCfg + case chainstypes.ChainTypeXRPL: + var newCfg xrpl.XRPLChainProviderConfig + + decoderConfig.Result = &newCfg + decoder, err := mapstructure.NewDecoder(&decoderConfig) + if err != nil { + return nil, err + } + + if err := decoder.Decode(w); err != nil { + return nil, err + } + cfg = &newCfg default: return cfg, fmt.Errorf("unsupported chain type: %s", typeName) diff --git a/relayer/store/filesystem.go b/relayer/store/filesystem.go index a3711de..2a7e9e1 100644 --- a/relayer/store/filesystem.go +++ b/relayer/store/filesystem.go @@ -13,6 +13,7 @@ import ( "github.com/bandprotocol/falcon/relayer/config" "github.com/bandprotocol/falcon/relayer/wallet" "github.com/bandprotocol/falcon/relayer/wallet/geth" + "github.com/bandprotocol/falcon/relayer/wallet/xrpl" ) var _ Store = &FileSystem{} @@ -105,6 +106,8 @@ func (fs *FileSystem) NewWallet(chainType chainstypes.ChainType, chainName, pass switch chainType { case chainstypes.ChainTypeEVM: return geth.NewGethWallet(passphrase, fs.HomePath, chainName) + case chainstypes.ChainTypeXRPL: + return xrpl.NewXRPLWallet(passphrase, fs.HomePath, chainName) default: return nil, fmt.Errorf("unsupported chain type: %s", chainType) } diff --git a/relayer/tunnel_relayer.go b/relayer/tunnel_relayer.go index 64d7352..7dac9a1 100644 --- a/relayer/tunnel_relayer.go +++ b/relayer/tunnel_relayer.go @@ -36,6 +36,7 @@ type TunnelRelayer struct { isTargetChainActive bool penaltySkipRemaining uint + lastRelayedSeq uint64 mu *sync.Mutex } @@ -57,6 +58,7 @@ func NewTunnelRelayer( Alert: alert, isTargetChainActive: false, penaltySkipRemaining: 0, + lastRelayedSeq: 0, mu: &sync.Mutex{}, } } @@ -172,7 +174,17 @@ func (t *TunnelRelayer) getNextPacketSequence(ctx context.Context, isForce bool) WithChainName(t.TargetChainProvider.GetChainName()), ) - t.updateRelayerMetrics(tunnelInfo, targetContractInfo) + var targetLatestSeq uint64 + + switch t.TargetChainProvider.ChainType() { + case chaintypes.ChainTypeEVM: + targetLatestSeq = targetContractInfo.LatestSequence + case chaintypes.ChainTypeXRPL: + // For XRPL, we use the lastRelayedSeq to track the latest sequence + targetLatestSeq = t.lastRelayedSeq + } + + t.updateRelayerMetrics(tunnelInfo, targetContractInfo, targetLatestSeq) // check if the target contract is active t.isTargetChainActive = targetContractInfo.IsActive @@ -181,8 +193,17 @@ func (t *TunnelRelayer) getNextPacketSequence(ctx context.Context, isForce bool) return 0, nil } - // end process if current packet is already relayed - latestSeq := targetContractInfo.LatestSequence + // check that target contract always relays packets or not + if t.TargetChainProvider.ChainType() == chaintypes.ChainTypeXRPL { + if t.lastRelayedSeq >= tunnelInfo.LatestSequence { + t.Log.Debug("No new packet to relay", "sequence", t.lastRelayedSeq) + return 0, nil + } + + return tunnelInfo.LatestSequence, nil + } + + latestSeq := targetLatestSeq nextSeq := latestSeq + 1 if tunnelInfo.LatestSequence < nextSeq { t.Log.Debug("No new packet to relay", "sequence", latestSeq) @@ -196,10 +217,11 @@ func (t *TunnelRelayer) getNextPacketSequence(ctx context.Context, isForce bool) func (t *TunnelRelayer) updateRelayerMetrics( tunnelInfo *types.Tunnel, targetContractInfo *chaintypes.Tunnel, + targetLatestSeq uint64, ) { // update the metric for unrelayed packets based on the difference // between the latest sequences on BandChain and the target chain - unrelayedPackets := tunnelInfo.LatestSequence - targetContractInfo.LatestSequence + unrelayedPackets := tunnelInfo.LatestSequence - targetLatestSeq relayermetrics.SetUnrelayedPackets(t.TunnelID, unrelayedPackets) // update the metric for the number of active target contracts @@ -223,6 +245,9 @@ func (t *TunnelRelayer) relayPacket(ctx context.Context, packet *types.Packet) e // Increment the metric for successfully relayed packets relayermetrics.IncPacketsRelayedSuccess(t.TunnelID) t.Log.Info("Successfully relayed packet", "sequence", packet.Sequence) + if t.TargetChainProvider.ChainType() == chaintypes.ChainTypeXRPL { + t.lastRelayedSeq = packet.Sequence + } return nil } @@ -236,7 +261,9 @@ func (t *TunnelRelayer) getTunnelPacket(ctx context.Context, seq uint64) (*types if err != nil { alert.HandleAlert( t.Alert, - alert.NewTopic(alert.GetTunnelPacketErrorMsg).WithTunnelID(t.TunnelID).WithChainName(t.TargetChainProvider.GetChainName()), + alert.NewTopic(alert.GetTunnelPacketErrorMsg). + WithTunnelID(t.TunnelID). + WithChainName(t.TargetChainProvider.GetChainName()), err.Error(), ) t.Log.Error("Failed to get packet", "sequence", seq, err) @@ -244,7 +271,9 @@ func (t *TunnelRelayer) getTunnelPacket(ctx context.Context, seq uint64) (*types } alert.HandleReset( t.Alert, - alert.NewTopic(alert.GetTunnelPacketErrorMsg).WithTunnelID(t.TunnelID).WithChainName(t.TargetChainProvider.GetChainName()), + alert.NewTopic(alert.GetTunnelPacketErrorMsg). + WithTunnelID(t.TunnelID). + WithChainName(t.TargetChainProvider.GetChainName()), ) // Check signing status; if it is waiting, wait for the completion of the EVM signature. // If it is not success (Failed or Undefined), return error. @@ -270,7 +299,9 @@ func (t *TunnelRelayer) getTunnelPacket(ctx context.Context, seq uint64) (*types } alert.HandleReset( t.Alert, - alert.NewTopic(alert.PacketSigningStatusErrorMsg).WithTunnelID(t.TunnelID).WithChainName(t.TargetChainProvider.GetChainName()), + alert.NewTopic(alert.PacketSigningStatusErrorMsg). + WithTunnelID(t.TunnelID). + WithChainName(t.TargetChainProvider.GetChainName()), ) return packet, nil diff --git a/relayer/tunnel_relayer_test.go b/relayer/tunnel_relayer_test.go index 79a22cd..7aa1db7 100644 --- a/relayer/tunnel_relayer_test.go +++ b/relayer/tunnel_relayer_test.go @@ -130,6 +130,7 @@ func createMockPacket( signalPrices, currentGroupSigning, incomingGroupSigning, + time.Now().Unix(), ) } @@ -139,6 +140,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { preprocess func() err error relayStatus relayer.RelayStatus + chainType chaintypes.ChainType }{ { name: "success", @@ -162,6 +164,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { s.mockQueryTunnelInfo(defaultTargetChainSequence+1, true, defaultContractAddress) }, relayStatus: relayer.RelayStatusSuccess, + chainType: chaintypes.ChainTypeEVM, }, { name: "failed to get tunnel on band client", @@ -172,6 +175,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { }, err: fmt.Errorf("failed to get tunnel"), relayStatus: relayer.RelayStatusFailed, + chainType: chaintypes.ChainTypeEVM, }, { name: "failed to query chain tunnel info", @@ -183,6 +187,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { }, err: fmt.Errorf("failed to query tunnel info"), relayStatus: relayer.RelayStatusFailed, + chainType: chaintypes.ChainTypeEVM, }, { name: "target chain not active", @@ -192,6 +197,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { }, err: nil, relayStatus: relayer.RelayStatusSkipped, + chainType: chaintypes.ChainTypeEVM, }, { name: "no new packet to relay", @@ -201,6 +207,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { }, err: nil, relayStatus: relayer.RelayStatusSkipped, + chainType: chaintypes.ChainTypeEVM, }, { name: "fail to get a new packet", @@ -214,6 +221,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { }, err: fmt.Errorf("failed to get packet"), relayStatus: relayer.RelayStatusFailed, + chainType: chaintypes.ChainTypeEVM, }, { name: "fallen signing status of current group but incoming group success", @@ -237,6 +245,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { s.mockQueryTunnelInfo(defaultTargetChainSequence+1, true, defaultContractAddress) }, relayStatus: relayer.RelayStatusSuccess, + chainType: chaintypes.ChainTypeEVM, }, { name: "incoming group signing status fallen", @@ -257,6 +266,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { }, err: fmt.Errorf(("signing status is not success")), relayStatus: relayer.RelayStatusFailed, + chainType: chaintypes.ChainTypeEVM, }, { name: "signing status is waiting", @@ -296,6 +306,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { }, err: nil, relayStatus: relayer.RelayStatusSuccess, + chainType: chaintypes.ChainTypeEVM, }, { name: "failed to relay packet", @@ -317,11 +328,43 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { }, err: fmt.Errorf("failed to relay packet"), relayStatus: relayer.RelayStatusFailed, + chainType: chaintypes.ChainTypeEVM, + }, + { + name: "xrpl relays latest sequence when behind", + preprocess: func() { + bandLatest := uint64(3) + s.mockGetTunnel(bandLatest) + s.mockQueryTunnelInfo(defaultTargetChainSequence, true, defaultContractAddress) + + packet := createMockPacket( + s.tunnelRelayer.TunnelID, + bandLatest, + int32(tss.SIGNING_STATUS_SUCCESS), + -1, + ) + s.client.EXPECT(). + GetTunnelPacket(gomock.Any(), s.tunnelRelayer.TunnelID, bandLatest). + Return(packet, nil) + s.chainProvider.EXPECT().RelayPacket(gomock.Any(), packet).Return(nil) + + // Check and relay the packet for the second time + s.mockGetTunnel(bandLatest) + s.mockQueryTunnelInfo(defaultTargetChainSequence, true, defaultContractAddress) + }, + relayStatus: relayer.RelayStatusSuccess, + chainType: chaintypes.ChainTypeXRPL, }, } for _, tc := range testcases { s.T().Run(tc.name, func(t *testing.T) { + chainType := tc.chainType + if chainType == chaintypes.ChainTypeUndefined { + chainType = chaintypes.ChainTypeEVM + } + s.chainProvider.EXPECT().ChainType().Return(chainType).AnyTimes() + if tc.preprocess != nil { tc.preprocess() } diff --git a/relayer/types.go b/relayer/types.go index 16e6015..7494af1 100644 --- a/relayer/types.go +++ b/relayer/types.go @@ -44,7 +44,7 @@ type Application interface { DeleteChainConfig(chainName string) error GetChainConfig(chainName string) (chains.ChainProviderConfig, error) - AddKeyByPrivateKey(chainName string, keyName string, privateKey string) (*chainstypes.Key, error) + AddKeyByPrivateKey(chainName string, keyName string, privateKey string) (string, error) AddKeyByMnemonic( chainName string, keyName string, @@ -52,14 +52,14 @@ type Application interface { coinType uint32, account uint, index uint, - ) (*chainstypes.Key, error) + ) (string, string, error) AddRemoteSignerKey( chainName string, keyName string, address string, url string, key *string, - ) (*chainstypes.Key, error) + ) error DeleteKey(chainName string, keyName string) error ListKeys(chainName string) ([]*chainstypes.Key, error) ExportKey(chainName string, keyName string) (string, error) diff --git a/relayer/wallet/xrpl/config.go b/relayer/wallet/xrpl/config.go new file mode 100644 index 0000000..3225877 --- /dev/null +++ b/relayer/wallet/xrpl/config.go @@ -0,0 +1,54 @@ +package xrpl + +import ( + toml "github.com/pelletier/go-toml/v2" + + "github.com/bandprotocol/falcon/internal/os" +) + +// KeyRecord stores XRPL signer info on disk. +type KeyRecord struct { + Type string `toml:"type"` + Address string `toml:"address,omitempty"` + Url string `toml:"url,omitempty"` + Key *string `toml:"key,omitempty"` +} + +// NewKeyRecord creates a new KeyRecord. +func NewKeyRecord(signerType, address, url string, key *string) KeyRecord { + return KeyRecord{ + Type: signerType, + Address: address, + Url: url, + Key: key, + } +} + +// LoadKeyRecord loads all files in `path/*.toml` into KeyRecord. +func LoadKeyRecord(path string) (map[string]KeyRecord, error) { + filePaths, err := os.ListFilePaths(path) + if err != nil { + return nil, err + } + + keyRecords := make(map[string]KeyRecord) + for _, filePath := range filePaths { + b, err := os.ReadFileIfExist(filePath) + if err != nil { + return nil, err + } + + var keyRecord KeyRecord + if err := toml.Unmarshal(b, &keyRecord); err != nil { + return nil, err + } + + name, err := ExtractKeyName(filePath) + if err != nil { + return nil, err + } + keyRecords[name] = keyRecord + } + + return keyRecords, nil +} diff --git a/relayer/wallet/xrpl/helper.go b/relayer/wallet/xrpl/helper.go new file mode 100644 index 0000000..187eed1 --- /dev/null +++ b/relayer/wallet/xrpl/helper.go @@ -0,0 +1,24 @@ +package xrpl + +import ( + "fmt" + "path" + "strings" +) + +// ExtractKeyName returns the filename (the key name) without its extension, or an error if empty. +func ExtractKeyName(filePath string) (string, error) { + fileName := path.Base(filePath) + + keyName := strings.TrimSuffix(fileName, path.Ext(fileName)) + if keyName == "" { + return "", fmt.Errorf("wrong keyname format") + } + + return keyName, nil +} + +// getXRPLKeyDir returns the key record directory. +func getXRPLKeyDir(homePath, chainName string) []string { + return []string{homePath, "keys", chainName, "metadata"} +} diff --git a/relayer/wallet/xrpl/keyring.go b/relayer/wallet/xrpl/keyring.go new file mode 100644 index 0000000..b311d1a --- /dev/null +++ b/relayer/wallet/xrpl/keyring.go @@ -0,0 +1,56 @@ +package xrpl + +import ( + "fmt" + "path" + + "github.com/99designs/keyring" +) + +const ( + xrplKeyringService = "falcon-xrpl" + xrplKeyringDirName = "priv" +) + +func openXRPLKeyring(passphrase, homePath, chainName string) (keyring.Keyring, error) { + return keyring.Open(keyring.Config{ + ServiceName: xrplKeyringService, + AllowedBackends: []keyring.BackendType{keyring.FileBackend}, + FileDir: path.Join(homePath, "keys", chainName, xrplKeyringDirName), + FilePasswordFunc: func(_ string) (string, error) { + return passphrase, nil + }, + }) +} + +func xrplKeyringKey(chainName, name string) string { + return fmt.Sprintf("xrpl/%s/%s", chainName, name) +} + +func getXRPLSecret(kr keyring.Keyring, chainName, name string) (string, error) { + item, err := kr.Get(xrplKeyringKey(chainName, name)) + if err != nil { + if err == keyring.ErrKeyNotFound { + return "", fmt.Errorf("missing secret for key %s", name) + } + return "", err + } + return string(item.Data), nil +} + +func setXRPLSecret(kr keyring.Keyring, chainName, name, secret string) error { + return kr.Set(keyring.Item{ + Key: xrplKeyringKey(chainName, name), + Data: []byte(secret), + Label: fmt.Sprintf("XRPL secret %s/%s", chainName, name), + Description: "XRPL signer seed", + }) +} + +func deleteXRPLSecret(kr keyring.Keyring, chainName, name string) error { + err := kr.Remove(xrplKeyringKey(chainName, name)) + if err == keyring.ErrKeyNotFound { + return nil + } + return err +} diff --git a/relayer/wallet/xrpl/local_signer.go b/relayer/wallet/xrpl/local_signer.go new file mode 100644 index 0000000..b241551 --- /dev/null +++ b/relayer/wallet/xrpl/local_signer.go @@ -0,0 +1,54 @@ +package xrpl + +import ( + binarycodec "github.com/Peersyst/xrpl-go/binary-codec" + xrplwallet "github.com/Peersyst/xrpl-go/xrpl/wallet" + + "github.com/bandprotocol/falcon/relayer/wallet" +) + +var _ wallet.Signer = (*LocalSigner)(nil) + +// LocalSigner uses a local XRPL secret for signing. +type LocalSigner struct { + Name string + Wallet xrplwallet.Wallet +} + +// NewLocalSigner creates a new LocalSigner. +func NewLocalSigner(name string, w xrplwallet.Wallet) *LocalSigner { + return &LocalSigner{ + Name: name, + Wallet: w, + } +} + +// ExportPrivateKey returns the decrypted XRPL secret. +func (l *LocalSigner) ExportPrivateKey() (string, error) { + return l.Wallet.PrivateKey, nil +} + +// GetName returns the signer's key name. +func (l *LocalSigner) GetName() string { + return l.Name +} + +// GetAddress returns the signer's classic address. +func (l *LocalSigner) GetAddress() (addr string) { + return l.Wallet.ClassicAddress.String() +} + +// Sign signs the provided transaction payload and returns the signed tx blob. +func (l *LocalSigner) Sign(data []byte) ([]byte, error) { + tx, err := binarycodec.Decode(string(data)) + if err != nil { + return nil, err + } + + txBlob, _, err := l.Wallet.Sign(tx) + if err != nil { + return nil, err + } + + return []byte(txBlob), nil +} diff --git a/relayer/wallet/xrpl/remote_signer.go b/relayer/wallet/xrpl/remote_signer.go new file mode 100644 index 0000000..d7e8652 --- /dev/null +++ b/relayer/wallet/xrpl/remote_signer.go @@ -0,0 +1,47 @@ +package xrpl + +import ( + "fmt" + + "github.com/bandprotocol/falcon/relayer/wallet" +) + +var _ wallet.Signer = (*RemoteSigner)(nil) + +// RemoteSigner is a placeholder for XRPL remote signers. +type RemoteSigner struct { + Name string + Address string + Url string + Key *string +} + +// NewRemoteSigner creates a new RemoteSigner instance. +func NewRemoteSigner(name, address, url string, key *string) *RemoteSigner { + return &RemoteSigner{ + Name: name, + Address: address, + Url: url, + Key: key, + } +} + +// ExportPrivateKey always returns an error for remote signer. +func (r *RemoteSigner) ExportPrivateKey() (string, error) { + return "", fmt.Errorf("cannot extract private key from remote signer") +} + +// GetName returns the signer's key name. +func (r *RemoteSigner) GetName() string { + return r.Name +} + +// GetAddress returns the signer's address. +func (r *RemoteSigner) GetAddress() (addr string) { + return r.Address +} + +// Sign is unsupported for XRPL remote signers. +func (r *RemoteSigner) Sign(data []byte) ([]byte, error) { + return nil, fmt.Errorf("xrpl remote signer is not supported") +} diff --git a/relayer/wallet/xrpl/wallet.go b/relayer/wallet/xrpl/wallet.go new file mode 100644 index 0000000..994cdfe --- /dev/null +++ b/relayer/wallet/xrpl/wallet.go @@ -0,0 +1,208 @@ +package xrpl + +import ( + "fmt" + "path" + + addresscodec "github.com/Peersyst/xrpl-go/address-codec" + xrplwallet "github.com/Peersyst/xrpl-go/xrpl/wallet" + toml "github.com/pelletier/go-toml/v2" + + "github.com/bandprotocol/falcon/internal/os" + "github.com/bandprotocol/falcon/relayer/wallet" +) + +var _ wallet.Wallet = &XRPLWallet{} + +const ( + LocalSignerType = "local" + RemoteSignerType = "remote" +) + +// XRPLWallet manages local and remote signers for a specific chain. +type XRPLWallet struct { + Passphrase string + Signers map[string]wallet.Signer + HomePath string + ChainName string +} + +// NewXRPLWallet creates a new XRPLWallet instance. +func NewXRPLWallet(passphrase, homePath, chainName string) (*XRPLWallet, error) { + keyRecordDir := path.Join(getXRPLKeyDir(homePath, chainName)...) + keyRecords, err := LoadKeyRecord(keyRecordDir) + if err != nil { + return nil, err + } + + kr, err := openXRPLKeyring(passphrase, homePath, chainName) + if err != nil { + return nil, err + } + + signers := make(map[string]wallet.Signer) + for name, record := range keyRecords { + var signer wallet.Signer + switch record.Type { + case LocalSignerType: + secret, err := getXRPLSecret(kr, chainName, name) + if err != nil { + return nil, err + } + + w, err := xrplwallet.FromSecret(secret) + if err != nil { + return nil, err + } + + signer = NewLocalSigner(name, w) + case RemoteSignerType: + if record.Address == "" { + return nil, fmt.Errorf("missing address for key %s", name) + } + if !addresscodec.IsValidClassicAddress(record.Address) { + return nil, fmt.Errorf("invalid address: %s", record.Address) + } + + signer = NewRemoteSigner(name, record.Address, record.Url, record.Key) + default: + return nil, fmt.Errorf( + "unsupported signer type %s for chain %s, key %s", + record.Type, + chainName, + name, + ) + } + + signers[name] = signer + } + + return &XRPLWallet{ + Passphrase: passphrase, + Signers: signers, + HomePath: homePath, + ChainName: chainName, + }, nil +} + +// SavePrivateKey stores the secret in keyring and writes its record. +func (w *XRPLWallet) SavePrivateKey(name string, privKey string) (addr string, err error) { + if _, ok := w.Signers[name]; ok { + return "", fmt.Errorf("key name exists: %s", name) + } + + privWallet, err := xrplwallet.FromSecret(privKey) + if err != nil { + return + } + + addr = privWallet.ClassicAddress.String() + + if w.IsAddressExist(addr) { + return "", fmt.Errorf("address exists: %s", addr) + } + + kr, err := openXRPLKeyring(w.Passphrase, w.HomePath, w.ChainName) + if err != nil { + return "", err + } + + if err := setXRPLSecret(kr, w.ChainName, name, privKey); err != nil { + return "", err + } + + record := NewKeyRecord(LocalSignerType, "", "", nil) + if err := w.saveKeyRecord(name, record); err != nil { + return "", err + } + + return addr, nil +} + +// SaveRemoteSignerKey registers a remote signer under the given name. +func (w *XRPLWallet) SaveRemoteSignerKey(name, address, url string, key *string) error { + if _, ok := w.Signers[name]; ok { + return fmt.Errorf("key name exists: %s", name) + } + + if !addresscodec.IsValidClassicAddress(address) { + return fmt.Errorf("invalid address: %s", address) + } + + if w.IsAddressExist(address) { + return fmt.Errorf("address exists: %s", address) + } + + record := NewKeyRecord(RemoteSignerType, address, url, key) + if err := w.saveKeyRecord(name, record); err != nil { + return err + } + + return nil +} + +// DeleteKey removes the signer named name, deleting its record. +func (w *XRPLWallet) DeleteKey(name string) error { + if _, ok := w.Signers[name]; !ok { + return fmt.Errorf("key name does not exist: %s", name) + } + + if _, ok := w.Signers[name].(*LocalSigner); ok { + kr, err := openXRPLKeyring(w.Passphrase, w.HomePath, w.ChainName) + if err != nil { + return err + } + if err := deleteXRPLSecret(kr, w.ChainName, name); err != nil { + return err + } + } + + if err := w.deleteKeyRecord(name); err != nil { + return err + } + + return nil +} + +// GetSigners lists all signers. +func (w *XRPLWallet) GetSigners() []wallet.Signer { + signers := make([]wallet.Signer, 0, len(w.Signers)) + for _, signer := range w.Signers { + signers = append(signers, signer) + } + + return signers +} + +// GetSigner returns the signer with the given name and a flag indicating if it was found. +func (w *XRPLWallet) GetSigner(name string) (wallet.Signer, bool) { + signer, ok := w.Signers[name] + return signer, ok +} + +// IsAddressExist returns true if the given address is already added. +func (w *XRPLWallet) IsAddressExist(address string) bool { + for _, signer := range w.Signers { + if signer.GetAddress() == address { + return true + } + } + return false +} + +// saveKeyRecord writes the KeyRecord to the file. +func (w *XRPLWallet) saveKeyRecord(name string, record KeyRecord) error { + b, err := toml.Marshal(record) + if err != nil { + return err + } + + return os.Write(b, append(getXRPLKeyDir(w.HomePath, w.ChainName), fmt.Sprintf("%s.toml", name))) +} + +// deleteKeyRecord deletes the KeyRecord file. +func (w *XRPLWallet) deleteKeyRecord(name string) error { + dir := path.Join(getXRPLKeyDir(w.HomePath, w.ChainName)...) + filePath := path.Join(dir, fmt.Sprintf("%s.toml", name)) + return os.DeletePath(filePath) +} From 55cd602e182cfb2927e8d88b7c0e7ea163cce4b3 Mon Sep 17 00:00:00 2001 From: Tanut Lertwarachai Date: Fri, 30 Jan 2026 15:53:31 +0700 Subject: [PATCH 5/8] refactor --- README.md | 9 ++-- cmd/flags.go | 2 +- cmd/keys.go | 25 +++++------ internal/relayertest/mocks/wallet.go | 27 +++++++++--- relayer/app.go | 32 ++++++++------ relayer/app_test.go | 8 ++-- relayer/chains/evm/keys.go | 5 ++- relayer/chains/keys.go | 14 +++++- relayer/chains/xrpl/keys.go | 18 ++------ relayer/tunnel_relayer_test.go | 10 ++++- relayer/types.go | 6 +-- relayer/wallet/geth/wallet.go | 34 +++++++++++++-- relayer/wallet/geth/wallet_test.go | 10 ++--- relayer/wallet/wallet.go | 3 +- relayer/wallet/xrpl/config.go | 20 +++++---- relayer/wallet/xrpl/wallet.go | 64 +++++++++++++++++++++++++--- 16 files changed, 200 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 5c354bb..5eaabf0 100644 --- a/README.md +++ b/README.md @@ -233,14 +233,15 @@ falcon keys add testnet testkey There are 3 options for user to add key ``` Choose how to add a key -> Private key (provide an existing private key) +> Secret (XRPL seed or EVM private key) Mnemonic (recover from an existing mnemonic phrase) - Generate new address (no private key or mnemonic needed) + Generate new address (no secret or mnemonic needed) ``` -If you already have a private key and want to retrieve key from it, you can choose `Private key` option. +If you already have a secret and want to retrieve key from it, you can choose `Secret` option. +XRPL uses a seed, while EVM chains use a private key. ``` -Enter your private key +Enter your secret (XRPL seed or EVM private key) > ``` diff --git a/cmd/flags.go b/cmd/flags.go index 08b225b..64337d1 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -8,7 +8,7 @@ const ( FlagLogFormat = "log-format" flagFile = "file" - flagPrivateKey = "private-key" + flagPrivateKey = "secret" flagMnemonic = "mnemonic" flagCoinType = "coin-type" flagWalletAccount = "account" diff --git a/cmd/keys.go b/cmd/keys.go index b0e6401..097f7aa 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -14,9 +14,9 @@ import ( ) const ( - privateKeyLabel = "Private key (provide an existing private key)" + privateKeyLabel = "Secret (XRPL seed or EVM private key)" mnemonicLabel = "Mnemonic (recover from an existing mnemonic phrase)" - defaultLabel = "Generate new address (no private key or mnemonic needed)" + defaultLabel = "Generate new address (no secret or mnemonic needed)" ) const ( @@ -118,7 +118,7 @@ keys add eth test-key`), }, } - cmd.Flags().String(flagPrivateKey, "", "add key with the given private key") + cmd.Flags().String(flagPrivateKey, "", "add key with the given secret (XRPL seed or EVM private key)") cmd.Flags().String(flagMnemonic, "", "add key with the given mnemonic") cmd.Flags().Uint64(flagCoinType, defaultCoinType, "coin type number for HD derivation") cmd.Flags().Uint64(flagWalletAccount, 0, "account number in the HD derivation path") @@ -306,7 +306,7 @@ func validateAddKeyInput(input *AddKeyInput) error { return nil } -// showHuhPrompt shows a prompt to the user to input a private key, mnemonic for generating or +// showHuhPrompt shows a prompt to the user to input a secret, mnemonic for generating or // inserting a user's key. func showHuhPrompt() (input *AddKeyInput, err error) { input = &AddKeyInput{} @@ -392,7 +392,7 @@ func showHuhPrompt() (input *AddKeyInput, err error) { switch selection { case privateKeyResult: privateKeyPrompt := huh.NewGroup(huh.NewInput(). - Title("Enter your private key"). + Title("Enter your secret (XRPL seed or EVM private key)"). Value(&input.PrivateKey)) form := huh.NewForm(privateKeyPrompt) @@ -488,24 +488,25 @@ func addKey( // Add key to the keychain if input.PrivateKey != "" { - address, err := app.AddKeyByPrivateKey(chainName, keyName, input.PrivateKey) + key, err := app.AddKeyByPrivateKey(chainName, keyName, input.PrivateKey) if err != nil { return nil, err } - return chainstypes.NewKey("", address, ""), nil + return key, nil } else if input.RemoteSigner.Address != "" && input.RemoteSigner.Url != "" { - if err := app.AddRemoteSignerKey( + key, err := app.AddRemoteSignerKey( chainName, keyName, input.RemoteSigner.Address, input.RemoteSigner.Url, input.RemoteSigner.Key, - ); err != nil { + ) + if err != nil { return nil, err } - return chainstypes.NewKey("", input.RemoteSigner.Address, ""), nil + return key, nil } else { - mnemonic, address, err := app.AddKeyByMnemonic( + key, err := app.AddKeyByMnemonic( chainName, keyName, input.Mnemonic, uint32(input.CoinType), @@ -515,6 +516,6 @@ func addKey( if err != nil { return nil, err } - return chainstypes.NewKey(mnemonic, address, ""), nil + return key, nil } } diff --git a/internal/relayertest/mocks/wallet.go b/internal/relayertest/mocks/wallet.go index 05b5cf1..3c797a1 100644 --- a/internal/relayertest/mocks/wallet.go +++ b/internal/relayertest/mocks/wallet.go @@ -165,19 +165,34 @@ func (mr *MockWalletMockRecorder) GetSigners() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSigners", reflect.TypeOf((*MockWallet)(nil).GetSigners)) } -// SavePrivateKey mocks base method. -func (m *MockWallet) SavePrivateKey(name, privKey string) (string, error) { +// SaveByMnemonic mocks base method. +func (m *MockWallet) SaveByMnemonic(name, mnemonic string) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SavePrivateKey", name, privKey) + ret := m.ctrl.Call(m, "SaveByMnemonic", name, mnemonic) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } -// SavePrivateKey indicates an expected call of SavePrivateKey. -func (mr *MockWalletMockRecorder) SavePrivateKey(name, privKey any) *gomock.Call { +// SaveByMnemonic indicates an expected call of SaveByMnemonic. +func (mr *MockWalletMockRecorder) SaveByMnemonic(name, mnemonic any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SavePrivateKey", reflect.TypeOf((*MockWallet)(nil).SavePrivateKey), name, privKey) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveByMnemonic", reflect.TypeOf((*MockWallet)(nil).SaveByMnemonic), name, mnemonic) +} + +// SaveBySecret mocks base method. +func (m *MockWallet) SaveBySecret(name, secret string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveBySecret", name, secret) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SaveBySecret indicates an expected call of SaveBySecret. +func (mr *MockWalletMockRecorder) SaveBySecret(name, secret any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveBySecret", reflect.TypeOf((*MockWallet)(nil).SaveBySecret), name, secret) } // SaveRemoteSignerKey mocks base method. diff --git a/relayer/app.go b/relayer/app.go index f259452..f72929f 100644 --- a/relayer/app.go +++ b/relayer/app.go @@ -271,22 +271,22 @@ func (a *App) GetChainConfig(chainName string) (chains.ChainProviderConfig, erro } // AddKeyByPrivateKey adds a new key to the chain provider using a private key. -func (a *App) AddKeyByPrivateKey(chainName string, keyName string, privateKey string) (string, error) { +func (a *App) AddKeyByPrivateKey(chainName string, keyName string, privateKey string) (*chainstypes.Key, error) { if err := a.Store.ValidatePassphrase(a.Passphrase); err != nil { - return "", err + return nil, err } w, err := a.getWallet(chainName) if err != nil { - return "", err + return nil, err } key, err := chains.AddKeyByPrivateKey(w, keyName, privateKey) if err != nil { - return "", err + return nil, err } - return key.Address, nil + return key, nil } // AddKeyByMnemonic adds a new key to the chain provider using a mnemonic phrase. @@ -297,22 +297,22 @@ func (a *App) AddKeyByMnemonic( coinType uint32, account uint, index uint, -) (string, string, error) { +) (*chainstypes.Key, error) { if err := a.Store.ValidatePassphrase(a.Passphrase); err != nil { - return "", "", err + return nil, err } cp, err := a.getChainProvider(chainName) if err != nil { - return "", "", err + return nil, err } key, err := cp.AddKeyByMnemonic(keyName, mnemonic, coinType, account, index) if err != nil { - return "", "", err + return nil, err } - return key.Mnemonic, key.Address, nil + return key, nil } // AddRemoteSignerKey adds a new remote signer key to the chain provider. @@ -322,14 +322,18 @@ func (a *App) AddRemoteSignerKey( addr string, url string, key *string, -) error { +) (*chainstypes.Key, error) { w, err := a.getWallet(chainName) if err != nil { - return err + return nil, err } - _, err = chains.AddRemoteSignerKey(w, keyName, addr, url, key) - return err + remoteKey, err := chains.AddRemoteSignerKey(w, keyName, addr, url, key) + if err != nil { + return nil, err + } + + return remoteKey, nil } // DeleteKey deletes the key from the chain provider. diff --git a/relayer/app_test.go b/relayer/app_test.go index 3afba1e..a435e3b 100644 --- a/relayer/app_test.go +++ b/relayer/app_test.go @@ -474,7 +474,7 @@ func (s *AppTestSuite) TestAddKey() { account uint index uint err error - out string + out *chainstypes.Key preprocess func() }{ { @@ -483,13 +483,13 @@ func (s *AppTestSuite) TestAddKey() { keyName: "testkey", privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", // anvil coinType: 60, - out: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + out: chainstypes.NewKey("", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", ""), preprocess: func() { s.store.EXPECT(). NewWallet(chainstypes.ChainTypeEVM, "testnet_evm", s.passphrase). Return(s.wallet, nil) s.wallet.EXPECT(). - SavePrivateKey( + SaveBySecret( "testkey", "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ). @@ -507,7 +507,7 @@ func (s *AppTestSuite) TestAddKey() { NewWallet(chainstypes.ChainTypeEVM, "testnet_evm", s.passphrase). Return(s.wallet, nil) s.wallet.EXPECT(). - SavePrivateKey( + SaveBySecret( "testkey", "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ). diff --git a/relayer/chains/evm/keys.go b/relayer/chains/evm/keys.go index e08aee2..2c980d0 100644 --- a/relayer/chains/evm/keys.go +++ b/relayer/chains/evm/keys.go @@ -5,6 +5,7 @@ import ( hdwallet "github.com/miguelmota/go-ethereum-hdwallet" + "github.com/bandprotocol/falcon/relayer/chains" chainstypes "github.com/bandprotocol/falcon/relayer/chains/types" ) @@ -27,7 +28,7 @@ func (cp *EVMChainProvider) AddKeyByMnemonic( var err error generatedMnemonic := "" if mnemonic == "" { - mnemonic, err = hdwallet.NewMnemonic(mnemonicSize) + mnemonic, err = chains.GenerateMnemonic(mnemonicSize) if err != nil { return nil, err } @@ -40,7 +41,7 @@ func (cp *EVMChainProvider) AddKeyByMnemonic( return nil, err } - addr, err := cp.Wallet.SavePrivateKey(keyName, privHex) + addr, err := cp.Wallet.SaveBySecret(keyName, privHex) if err != nil { return nil, err } diff --git a/relayer/chains/keys.go b/relayer/chains/keys.go index 66766ae..21719d5 100644 --- a/relayer/chains/keys.go +++ b/relayer/chains/keys.go @@ -3,13 +3,25 @@ package chains import ( "fmt" + "github.com/bsv-blockchain/go-sdk/compat/bip39" + chainstypes "github.com/bandprotocol/falcon/relayer/chains/types" "github.com/bandprotocol/falcon/relayer/wallet" ) +// GenerateMnemonic creates a BIP-39 mnemonic with the requested entropy size. +func GenerateMnemonic(bitSize int) (string, error) { + entropy, err := bip39.NewEntropy(bitSize) + if err != nil { + return "", err + } + + return bip39.NewMnemonic(entropy) +} + // AddKeyByPrivateKey adds a key using a raw private key. func AddKeyByPrivateKey(w wallet.Wallet, keyName, privateKey string) (*chainstypes.Key, error) { - addr, err := w.SavePrivateKey(keyName, privateKey) + addr, err := w.SaveBySecret(keyName, privateKey) if err != nil { return nil, err } diff --git a/relayer/chains/xrpl/keys.go b/relayer/chains/xrpl/keys.go index 4822ce3..b96d129 100644 --- a/relayer/chains/xrpl/keys.go +++ b/relayer/chains/xrpl/keys.go @@ -3,9 +3,7 @@ package xrpl import ( "fmt" - xrplwallet "github.com/Peersyst/xrpl-go/xrpl/wallet" - "github.com/bsv-blockchain/go-sdk/compat/bip39" - + "github.com/bandprotocol/falcon/relayer/chains" chainstypes "github.com/bandprotocol/falcon/relayer/chains/types" ) @@ -27,24 +25,16 @@ func (cp *XRPLChainProvider) AddKeyByMnemonic( } generatedMnemonic := "" + var err error if mnemonic == "" { - entropy, err := bip39.NewEntropy(xrplMnemonicEntropyBits) - if err != nil { - return nil, err - } - mnemonic, err = bip39.NewMnemonic(entropy) + mnemonic, err = chains.GenerateMnemonic(xrplMnemonicEntropyBits) if err != nil { return nil, err } generatedMnemonic = mnemonic } - w, err := xrplwallet.FromMnemonic(mnemonic) - if err != nil { - return nil, err - } - - addr, err := cp.Wallet.SavePrivateKey(keyName, w.PrivateKey) + addr, err := cp.Wallet.SaveByMnemonic(keyName, mnemonic) if err != nil { return nil, err } diff --git a/relayer/tunnel_relayer_test.go b/relayer/tunnel_relayer_test.go index 7aa1db7..00bb4cb 100644 --- a/relayer/tunnel_relayer_test.go +++ b/relayer/tunnel_relayer_test.go @@ -135,6 +135,14 @@ func createMockPacket( } func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { + var currentChainType chaintypes.ChainType + s.chainProvider.EXPECT(). + ChainType(). + DoAndReturn(func() chaintypes.ChainType { + return currentChainType + }). + AnyTimes() + testcases := []struct { name string preprocess func() @@ -363,7 +371,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { if chainType == chaintypes.ChainTypeUndefined { chainType = chaintypes.ChainTypeEVM } - s.chainProvider.EXPECT().ChainType().Return(chainType).AnyTimes() + currentChainType = chainType if tc.preprocess != nil { tc.preprocess() diff --git a/relayer/types.go b/relayer/types.go index 7494af1..16e6015 100644 --- a/relayer/types.go +++ b/relayer/types.go @@ -44,7 +44,7 @@ type Application interface { DeleteChainConfig(chainName string) error GetChainConfig(chainName string) (chains.ChainProviderConfig, error) - AddKeyByPrivateKey(chainName string, keyName string, privateKey string) (string, error) + AddKeyByPrivateKey(chainName string, keyName string, privateKey string) (*chainstypes.Key, error) AddKeyByMnemonic( chainName string, keyName string, @@ -52,14 +52,14 @@ type Application interface { coinType uint32, account uint, index uint, - ) (string, string, error) + ) (*chainstypes.Key, error) AddRemoteSignerKey( chainName string, keyName string, address string, url string, key *string, - ) error + ) (*chainstypes.Key, error) DeleteKey(chainName string, keyName string) error ListKeys(chainName string) ([]*chainstypes.Key, error) ExportKey(chainName string, keyName string) (string, error) diff --git a/relayer/wallet/geth/wallet.go b/relayer/wallet/geth/wallet.go index 8fafc10..97304c6 100644 --- a/relayer/wallet/geth/wallet.go +++ b/relayer/wallet/geth/wallet.go @@ -9,6 +9,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + hdwallet "github.com/miguelmota/go-ethereum-hdwallet" toml "github.com/pelletier/go-toml/v2" "github.com/bandprotocol/falcon/internal/os" @@ -20,6 +21,8 @@ var _ wallet.Wallet = &GethWallet{} const ( LocalSignerType = "local" RemoteSignerType = "remote" + + defaultEVMHDPath = "m/44'/60'/0'/0/0" ) // GethWallet manages local and remote signers for a specific chain. @@ -105,14 +108,14 @@ func NewGethWallet(passphrase, homePath, chainName string) (*GethWallet, error) }, nil } -// SavePrivateKey imports the ECDSA key into the keystore and writes its signer record. -func (w *GethWallet) SavePrivateKey(name string, privKey string) (addr string, err error) { +// SaveBySecret imports the ECDSA key into the keystore and writes its signer record. +func (w *GethWallet) SaveBySecret(name string, secret string) (addr string, err error) { // check if the key name exists if _, ok := w.Signers[name]; ok { return "", fmt.Errorf("key name exists: %s", name) } - privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(privKey, "0x")) + privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(secret, "0x")) if err != nil { return "", err } @@ -139,6 +142,31 @@ func (w *GethWallet) SavePrivateKey(name string, privKey string) (addr string, e return addr, nil } +// SaveByMnemonic derives the ECDSA key from the mnemonic and stores it as a local signer. +func (w *GethWallet) SaveByMnemonic(name string, mnemonic string) (addr string, err error) { + if mnemonic == "" { + return "", fmt.Errorf("mnemonic is empty") + } + + hdWallet, err := hdwallet.NewFromMnemonic(mnemonic) + if err != nil { + return "", err + } + + derivationPath := hdwallet.MustParseDerivationPath(defaultEVMHDPath) + account, err := hdWallet.Derive(derivationPath, true) + if err != nil { + return "", err + } + + privHex, err := hdWallet.PrivateKeyHex(account) + if err != nil { + return "", err + } + + return w.SaveBySecret(name, privHex) +} + // SaveRemoteSignerKey registers a remote signer under the given name, // storing its address and service URL as on‐disk records. func (w *GethWallet) SaveRemoteSignerKey(name, address, url string, key *string) error { diff --git a/relayer/wallet/geth/wallet_test.go b/relayer/wallet/geth/wallet_test.go index 000b428..da7f016 100644 --- a/relayer/wallet/geth/wallet_test.go +++ b/relayer/wallet/geth/wallet_test.go @@ -37,7 +37,7 @@ func (s *WalletTestSuite) newWallet() (*geth.GethWallet, string) { return w, home } -func (s *WalletTestSuite) TestSavePrivateKey() { +func (s *WalletTestSuite) TestSaveBySecret() { priv, err := crypto.GenerateKey() s.Require().NoError(err) addrHex := crypto.PubkeyToAddress(priv.PublicKey).Hex() @@ -54,7 +54,7 @@ func (s *WalletTestSuite) TestSavePrivateKey() { { "duplicate name fails", "alice", func(w *geth.GethWallet) { - _, err := w.SavePrivateKey("alice", privHex) + _, err := w.SaveBySecret("alice", privHex) s.Require().NoError(err) }, true, "key name exists", @@ -62,7 +62,7 @@ func (s *WalletTestSuite) TestSavePrivateKey() { { "duplicate address fails", "bob", func(w *geth.GethWallet) { - _, err := w.SavePrivateKey("a", privHex) + _, err := w.SaveBySecret("a", privHex) s.Require().NoError(err) }, true, "address exists", @@ -78,7 +78,7 @@ func (s *WalletTestSuite) TestSavePrivateKey() { w, _ = geth.NewGethWallet(s.passphrase, home, s.chainName) } - gotAddr, err := w.SavePrivateKey(tc.keyName, privHex) + gotAddr, err := w.SaveBySecret(tc.keyName, privHex) if tc.wantErr { s.Error(err) s.Contains(err.Error(), tc.errSubstr) @@ -181,7 +181,7 @@ func (s *WalletTestSuite) TestDeleteKey() { { "delete local succeeds", func(w *geth.GethWallet) { - _, err := w.SavePrivateKey("alice", privHex) + _, err := w.SaveBySecret("alice", privHex) s.Require().NoError(err) }, "alice", false, "", diff --git a/relayer/wallet/wallet.go b/relayer/wallet/wallet.go index 63ca91f..2b2f1f5 100644 --- a/relayer/wallet/wallet.go +++ b/relayer/wallet/wallet.go @@ -8,7 +8,8 @@ type Signer interface { } type Wallet interface { - SavePrivateKey(name string, privKey string) (addr string, err error) + SaveBySecret(name string, secret string) (addr string, err error) + SaveByMnemonic(name string, mnemonic string) (addr string, err error) SaveRemoteSignerKey(name, addr, url string, key *string) error DeleteKey(name string) error GetSigners() []Signer diff --git a/relayer/wallet/xrpl/config.go b/relayer/wallet/xrpl/config.go index 3225877..393c782 100644 --- a/relayer/wallet/xrpl/config.go +++ b/relayer/wallet/xrpl/config.go @@ -8,19 +8,21 @@ import ( // KeyRecord stores XRPL signer info on disk. type KeyRecord struct { - Type string `toml:"type"` - Address string `toml:"address,omitempty"` - Url string `toml:"url,omitempty"` - Key *string `toml:"key,omitempty"` + Type string `toml:"type"` + Address string `toml:"address,omitempty"` + Url string `toml:"url,omitempty"` + Key *string `toml:"key,omitempty"` + SaveMethod string `toml:"save_method,omitempty"` } // NewKeyRecord creates a new KeyRecord. -func NewKeyRecord(signerType, address, url string, key *string) KeyRecord { +func NewKeyRecord(signerType, address, url string, key *string, saveMethod string) KeyRecord { return KeyRecord{ - Type: signerType, - Address: address, - Url: url, - Key: key, + Type: signerType, + Address: address, + Url: url, + Key: key, + SaveMethod: saveMethod, } } diff --git a/relayer/wallet/xrpl/wallet.go b/relayer/wallet/xrpl/wallet.go index 994cdfe..011c12a 100644 --- a/relayer/wallet/xrpl/wallet.go +++ b/relayer/wallet/xrpl/wallet.go @@ -17,6 +17,9 @@ var _ wallet.Wallet = &XRPLWallet{} const ( LocalSignerType = "local" RemoteSignerType = "remote" + + SaveMethodSeed = "seed" + SaveMethodMnemonic = "mnemonic" ) // XRPLWallet manages local and remote signers for a specific chain. @@ -50,7 +53,17 @@ func NewXRPLWallet(passphrase, homePath, chainName string) (*XRPLWallet, error) return nil, err } - w, err := xrplwallet.FromSecret(secret) + var w xrplwallet.Wallet + var wptr *xrplwallet.Wallet + switch record.SaveMethod { + case SaveMethodMnemonic: + wptr, err = xrplwallet.FromMnemonic(secret) + w = *wptr + case SaveMethodSeed: + w, err = xrplwallet.FromSecret(secret) + default: + return nil, fmt.Errorf("unsupported save method %s for key %s", record.SaveMethod, name) + } if err != nil { return nil, err } @@ -85,13 +98,13 @@ func NewXRPLWallet(passphrase, homePath, chainName string) (*XRPLWallet, error) }, nil } -// SavePrivateKey stores the secret in keyring and writes its record. -func (w *XRPLWallet) SavePrivateKey(name string, privKey string) (addr string, err error) { +// SaveBySecret stores the secret in keyring and writes its record. +func (w *XRPLWallet) SaveBySecret(name string, secret string) (addr string, err error) { if _, ok := w.Signers[name]; ok { return "", fmt.Errorf("key name exists: %s", name) } - privWallet, err := xrplwallet.FromSecret(privKey) + privWallet, err := xrplwallet.FromSecret(secret) if err != nil { return } @@ -107,11 +120,48 @@ func (w *XRPLWallet) SavePrivateKey(name string, privKey string) (addr string, e return "", err } - if err := setXRPLSecret(kr, w.ChainName, name, privKey); err != nil { + if err := setXRPLSecret(kr, w.ChainName, name, secret); err != nil { + return "", err + } + + record := NewKeyRecord(LocalSignerType, "", "", nil, SaveMethodSeed) + if err := w.saveKeyRecord(name, record); err != nil { + return "", err + } + + return addr, nil +} + +// SaveByMnemonic stores the mnemonic in keyring and writes its record. +func (w *XRPLWallet) SaveByMnemonic(name string, mnemonic string) (addr string, err error) { + if _, ok := w.Signers[name]; ok { + return "", fmt.Errorf("key name exists: %s", name) + } + if mnemonic == "" { + return "", fmt.Errorf("mnemonic is empty") + } + + mnWallet, err := xrplwallet.FromMnemonic(mnemonic) + if err != nil { + return "", err + } + + addr = mnWallet.ClassicAddress.String() + + if w.IsAddressExist(addr) { + return "", fmt.Errorf("address exists: %s", addr) + } + + kr, err := openXRPLKeyring(w.Passphrase, w.HomePath, w.ChainName) + if err != nil { + return "", err + } + + if err := setXRPLSecret(kr, w.ChainName, name, mnemonic); err != nil { return "", err } - record := NewKeyRecord(LocalSignerType, "", "", nil) + record := NewKeyRecord(LocalSignerType, "", "", nil, SaveMethodMnemonic) if err := w.saveKeyRecord(name, record); err != nil { return "", err } @@ -133,7 +183,7 @@ func (w *XRPLWallet) SaveRemoteSignerKey(name, address, url string, key *string) return fmt.Errorf("address exists: %s", address) } - record := NewKeyRecord(RemoteSignerType, address, url, key) + record := NewKeyRecord(RemoteSignerType, address, url, key, "") if err := w.saveKeyRecord(name, record); err != nil { return err } From 914949be7e8a8b24c5befeedc7ea015293a590b9 Mon Sep 17 00:00:00 2001 From: Tanut Lertwarachai Date: Fri, 30 Jan 2026 16:35:43 +0700 Subject: [PATCH 6/8] add test --- cmd/keys.go | 18 +++--------------- internal/relayertest/mocks/wallet.go | 8 ++++---- relayer/chains/xrpl/keys.go | 2 +- relayer/tunnel_relayer_test.go | 11 +++++++++++ relayer/wallet/geth/wallet.go | 17 ++++++++++++----- relayer/wallet/wallet.go | 2 +- relayer/wallet/xrpl/wallet.go | 13 ++++++++++++- 7 files changed, 44 insertions(+), 27 deletions(-) diff --git a/cmd/keys.go b/cmd/keys.go index 097f7aa..5eabaeb 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -488,34 +488,22 @@ func addKey( // Add key to the keychain if input.PrivateKey != "" { - key, err := app.AddKeyByPrivateKey(chainName, keyName, input.PrivateKey) - if err != nil { - return nil, err - } - return key, nil + return app.AddKeyByPrivateKey(chainName, keyName, input.PrivateKey) } else if input.RemoteSigner.Address != "" && input.RemoteSigner.Url != "" { - key, err := app.AddRemoteSignerKey( + return app.AddRemoteSignerKey( chainName, keyName, input.RemoteSigner.Address, input.RemoteSigner.Url, input.RemoteSigner.Key, ) - if err != nil { - return nil, err - } - return key, nil } else { - key, err := app.AddKeyByMnemonic( + return app.AddKeyByMnemonic( chainName, keyName, input.Mnemonic, uint32(input.CoinType), uint(input.Account), uint(input.Index), ) - if err != nil { - return nil, err - } - return key, nil } } diff --git a/internal/relayertest/mocks/wallet.go b/internal/relayertest/mocks/wallet.go index 3c797a1..14d7a15 100644 --- a/internal/relayertest/mocks/wallet.go +++ b/internal/relayertest/mocks/wallet.go @@ -166,18 +166,18 @@ func (mr *MockWalletMockRecorder) GetSigners() *gomock.Call { } // SaveByMnemonic mocks base method. -func (m *MockWallet) SaveByMnemonic(name, mnemonic string) (string, error) { +func (m *MockWallet) SaveByMnemonic(name, mnemonic string, coinType uint32, account, index uint) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SaveByMnemonic", name, mnemonic) + ret := m.ctrl.Call(m, "SaveByMnemonic", name, mnemonic, coinType, account, index) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // SaveByMnemonic indicates an expected call of SaveByMnemonic. -func (mr *MockWalletMockRecorder) SaveByMnemonic(name, mnemonic any) *gomock.Call { +func (mr *MockWalletMockRecorder) SaveByMnemonic(name, mnemonic, coinType, account, index any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveByMnemonic", reflect.TypeOf((*MockWallet)(nil).SaveByMnemonic), name, mnemonic) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveByMnemonic", reflect.TypeOf((*MockWallet)(nil).SaveByMnemonic), name, mnemonic, coinType, account, index) } // SaveBySecret mocks base method. diff --git a/relayer/chains/xrpl/keys.go b/relayer/chains/xrpl/keys.go index b96d129..08a9cbd 100644 --- a/relayer/chains/xrpl/keys.go +++ b/relayer/chains/xrpl/keys.go @@ -34,7 +34,7 @@ func (cp *XRPLChainProvider) AddKeyByMnemonic( generatedMnemonic = mnemonic } - addr, err := cp.Wallet.SaveByMnemonic(keyName, mnemonic) + addr, err := cp.Wallet.SaveByMnemonic(keyName, mnemonic, coinType, account, index) if err != nil { return nil, err } diff --git a/relayer/tunnel_relayer_test.go b/relayer/tunnel_relayer_test.go index 00bb4cb..2216642 100644 --- a/relayer/tunnel_relayer_test.go +++ b/relayer/tunnel_relayer_test.go @@ -363,6 +363,17 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { relayStatus: relayer.RelayStatusSuccess, chainType: chaintypes.ChainTypeXRPL, }, + { + name: "xrpl not relays when last relayed sequence equal Band latest sequence", + preprocess: func() { + bandLatest := uint64(0) + s.mockGetTunnel(bandLatest) + s.mockQueryTunnelInfo(defaultTargetChainSequence, true, defaultContractAddress) + }, + err: nil, + relayStatus: relayer.RelayStatusSkipped, + chainType: chaintypes.ChainTypeXRPL, + }, } for _, tc := range testcases { diff --git a/relayer/wallet/geth/wallet.go b/relayer/wallet/geth/wallet.go index 97304c6..c8c7693 100644 --- a/relayer/wallet/geth/wallet.go +++ b/relayer/wallet/geth/wallet.go @@ -22,7 +22,7 @@ const ( LocalSignerType = "local" RemoteSignerType = "remote" - defaultEVMHDPath = "m/44'/60'/0'/0/0" + hdPathTemplate = "m/44'/%d'/%d'/0/%d" ) // GethWallet manages local and remote signers for a specific chain. @@ -143,7 +143,13 @@ func (w *GethWallet) SaveBySecret(name string, secret string) (addr string, err } // SaveByMnemonic derives the ECDSA key from the mnemonic and stores it as a local signer. -func (w *GethWallet) SaveByMnemonic(name string, mnemonic string) (addr string, err error) { +func (w *GethWallet) SaveByMnemonic( + name string, + mnemonic string, + coinType uint32, + account uint, + index uint, +) (addr string, err error) { if mnemonic == "" { return "", fmt.Errorf("mnemonic is empty") } @@ -153,13 +159,14 @@ func (w *GethWallet) SaveByMnemonic(name string, mnemonic string) (addr string, return "", err } - derivationPath := hdwallet.MustParseDerivationPath(defaultEVMHDPath) - account, err := hdWallet.Derive(derivationPath, true) + hdPath := fmt.Sprintf(hdPathTemplate, coinType, account, index) + derivationPath := hdwallet.MustParseDerivationPath(hdPath) + ethAccount, err := hdWallet.Derive(derivationPath, true) if err != nil { return "", err } - privHex, err := hdWallet.PrivateKeyHex(account) + privHex, err := hdWallet.PrivateKeyHex(ethAccount) if err != nil { return "", err } diff --git a/relayer/wallet/wallet.go b/relayer/wallet/wallet.go index 2b2f1f5..497c91f 100644 --- a/relayer/wallet/wallet.go +++ b/relayer/wallet/wallet.go @@ -9,7 +9,7 @@ type Signer interface { type Wallet interface { SaveBySecret(name string, secret string) (addr string, err error) - SaveByMnemonic(name string, mnemonic string) (addr string, err error) + SaveByMnemonic(name string, mnemonic string, coinType uint32, account uint, index uint) (addr string, err error) SaveRemoteSignerKey(name, addr, url string, key *string) error DeleteKey(name string) error GetSigners() []Signer diff --git a/relayer/wallet/xrpl/wallet.go b/relayer/wallet/xrpl/wallet.go index 011c12a..fb27eae 100644 --- a/relayer/wallet/xrpl/wallet.go +++ b/relayer/wallet/xrpl/wallet.go @@ -20,6 +20,8 @@ const ( SaveMethodSeed = "seed" SaveMethodMnemonic = "mnemonic" + + xrplDefaultCoinType = 144 ) // XRPLWallet manages local and remote signers for a specific chain. @@ -133,10 +135,19 @@ func (w *XRPLWallet) SaveBySecret(name string, secret string) (addr string, err } // SaveByMnemonic stores the mnemonic in keyring and writes its record. -func (w *XRPLWallet) SaveByMnemonic(name string, mnemonic string) (addr string, err error) { +func (w *XRPLWallet) SaveByMnemonic( + name string, + mnemonic string, + coinType uint32, + account uint, + index uint, +) (addr string, err error) { if _, ok := w.Signers[name]; ok { return "", fmt.Errorf("key name exists: %s", name) } + if coinType != xrplDefaultCoinType || account != 0 || index != 0 { + return "", fmt.Errorf("xrpl mnemonic derivation only supports m/44'/144'/0'/0/0") + } if mnemonic == "" { return "", fmt.Errorf("mnemonic is empty") } From e5bd189ea29eaa911676e34c96cb93022a675378 Mon Sep 17 00:00:00 2001 From: Tanut Lertwarachai Date: Fri, 30 Jan 2026 16:48:58 +0700 Subject: [PATCH 7/8] fix metrics --- relayer/tunnel_relayer.go | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/relayer/tunnel_relayer.go b/relayer/tunnel_relayer.go index 7dac9a1..0ae62ce 100644 --- a/relayer/tunnel_relayer.go +++ b/relayer/tunnel_relayer.go @@ -36,7 +36,7 @@ type TunnelRelayer struct { isTargetChainActive bool penaltySkipRemaining uint - lastRelayedSeq uint64 + lastRelayedSeq *uint64 mu *sync.Mutex } @@ -58,7 +58,7 @@ func NewTunnelRelayer( Alert: alert, isTargetChainActive: false, penaltySkipRemaining: 0, - lastRelayedSeq: 0, + lastRelayedSeq: nil, mu: &sync.Mutex{}, } } @@ -174,11 +174,12 @@ func (t *TunnelRelayer) getNextPacketSequence(ctx context.Context, isForce bool) WithChainName(t.TargetChainProvider.GetChainName()), ) - var targetLatestSeq uint64 + var targetLatestSeq *uint64 switch t.TargetChainProvider.ChainType() { case chaintypes.ChainTypeEVM: - targetLatestSeq = targetContractInfo.LatestSequence + latestSeq := targetContractInfo.LatestSequence + targetLatestSeq = &latestSeq case chaintypes.ChainTypeXRPL: // For XRPL, we use the lastRelayedSeq to track the latest sequence targetLatestSeq = t.lastRelayedSeq @@ -195,15 +196,15 @@ func (t *TunnelRelayer) getNextPacketSequence(ctx context.Context, isForce bool) // check that target contract always relays packets or not if t.TargetChainProvider.ChainType() == chaintypes.ChainTypeXRPL { - if t.lastRelayedSeq >= tunnelInfo.LatestSequence { - t.Log.Debug("No new packet to relay", "sequence", t.lastRelayedSeq) + if t.lastRelayedSeq == nil || (t.lastRelayedSeq != nil && *t.lastRelayedSeq >= tunnelInfo.LatestSequence) { + t.Log.Debug("No new packet to relay", "sequence", *t.lastRelayedSeq) return 0, nil } return tunnelInfo.LatestSequence, nil } - latestSeq := targetLatestSeq + latestSeq := *targetLatestSeq nextSeq := latestSeq + 1 if tunnelInfo.LatestSequence < nextSeq { t.Log.Debug("No new packet to relay", "sequence", latestSeq) @@ -217,12 +218,14 @@ func (t *TunnelRelayer) getNextPacketSequence(ctx context.Context, isForce bool) func (t *TunnelRelayer) updateRelayerMetrics( tunnelInfo *types.Tunnel, targetContractInfo *chaintypes.Tunnel, - targetLatestSeq uint64, + targetLatestSeq *uint64, ) { - // update the metric for unrelayed packets based on the difference - // between the latest sequences on BandChain and the target chain - unrelayedPackets := tunnelInfo.LatestSequence - targetLatestSeq - relayermetrics.SetUnrelayedPackets(t.TunnelID, unrelayedPackets) + if targetLatestSeq != nil { + // update the metric for unrelayed packets based on the difference + // between the latest sequences on BandChain and the target chain + unrelayedPackets := tunnelInfo.LatestSequence - *targetLatestSeq + relayermetrics.SetUnrelayedPackets(t.TunnelID, unrelayedPackets) + } // update the metric for the number of active target contracts if targetContractInfo.IsActive && !t.isTargetChainActive { @@ -246,7 +249,8 @@ func (t *TunnelRelayer) relayPacket(ctx context.Context, packet *types.Packet) e relayermetrics.IncPacketsRelayedSuccess(t.TunnelID) t.Log.Info("Successfully relayed packet", "sequence", packet.Sequence) if t.TargetChainProvider.ChainType() == chaintypes.ChainTypeXRPL { - t.lastRelayedSeq = packet.Sequence + latestSeq := packet.Sequence + t.lastRelayedSeq = &latestSeq } return nil From 36bdd61f20e8eeed4afbafb16a89a97b4ce3e5ed Mon Sep 17 00:00:00 2001 From: Tanut Lertwarachai Date: Sat, 31 Jan 2026 13:39:00 +0700 Subject: [PATCH 8/8] fix copilot comment --- relayer/chains/xrpl/provider.go | 1 + relayer/tunnel_relayer.go | 5 ++++- relayer/wallet/xrpl/local_signer.go | 4 ++-- relayer/wallet/xrpl/wallet.go | 6 +++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/relayer/chains/xrpl/provider.go b/relayer/chains/xrpl/provider.go index 61f3f00..e1e9652 100644 --- a/relayer/chains/xrpl/provider.go +++ b/relayer/chains/xrpl/provider.go @@ -122,6 +122,7 @@ func (cp *XRPLChainProvider) RelayPacket(ctx context.Context, packet *bandtypes. for retryCount := 1; retryCount <= cp.Config.MaxRetry; retryCount++ { log.Info("Relaying a message", "retry_count", retryCount) + // If it is the first attempt or previous attempt failed due to sequence error, fetch the latest account sequence number. if sequence == 0 { sequence, err = cp.Client.GetAccountSequenceNumber(ctx, signer.GetAddress()) if err != nil { diff --git a/relayer/tunnel_relayer.go b/relayer/tunnel_relayer.go index 0ae62ce..e214b1f 100644 --- a/relayer/tunnel_relayer.go +++ b/relayer/tunnel_relayer.go @@ -196,7 +196,7 @@ func (t *TunnelRelayer) getNextPacketSequence(ctx context.Context, isForce bool) // check that target contract always relays packets or not if t.TargetChainProvider.ChainType() == chaintypes.ChainTypeXRPL { - if t.lastRelayedSeq == nil || (t.lastRelayedSeq != nil && *t.lastRelayedSeq >= tunnelInfo.LatestSequence) { + if t.lastRelayedSeq != nil && *t.lastRelayedSeq >= tunnelInfo.LatestSequence { t.Log.Debug("No new packet to relay", "sequence", *t.lastRelayedSeq) return 0, nil } @@ -220,6 +220,9 @@ func (t *TunnelRelayer) updateRelayerMetrics( targetContractInfo *chaintypes.Tunnel, targetLatestSeq *uint64, ) { + // Specifically for XRPL, if it is the first time relaying (targetLatestSeq is nil) + // dont't set unwelayed packets metrics + // because we don't know the latest sequence on the target chain if targetLatestSeq != nil { // update the metric for unrelayed packets based on the difference // between the latest sequences on BandChain and the target chain diff --git a/relayer/wallet/xrpl/local_signer.go b/relayer/wallet/xrpl/local_signer.go index b241551..0b113e5 100644 --- a/relayer/wallet/xrpl/local_signer.go +++ b/relayer/wallet/xrpl/local_signer.go @@ -12,11 +12,11 @@ var _ wallet.Signer = (*LocalSigner)(nil) // LocalSigner uses a local XRPL secret for signing. type LocalSigner struct { Name string - Wallet xrplwallet.Wallet + Wallet *xrplwallet.Wallet } // NewLocalSigner creates a new LocalSigner. -func NewLocalSigner(name string, w xrplwallet.Wallet) *LocalSigner { +func NewLocalSigner(name string, w *xrplwallet.Wallet) *LocalSigner { return &LocalSigner{ Name: name, Wallet: w, diff --git a/relayer/wallet/xrpl/wallet.go b/relayer/wallet/xrpl/wallet.go index fb27eae..f972fc0 100644 --- a/relayer/wallet/xrpl/wallet.go +++ b/relayer/wallet/xrpl/wallet.go @@ -55,14 +55,14 @@ func NewXRPLWallet(passphrase, homePath, chainName string) (*XRPLWallet, error) return nil, err } - var w xrplwallet.Wallet var wptr *xrplwallet.Wallet + var w xrplwallet.Wallet switch record.SaveMethod { case SaveMethodMnemonic: wptr, err = xrplwallet.FromMnemonic(secret) - w = *wptr case SaveMethodSeed: w, err = xrplwallet.FromSecret(secret) + wptr = &w default: return nil, fmt.Errorf("unsupported save method %s for key %s", record.SaveMethod, name) } @@ -70,7 +70,7 @@ func NewXRPLWallet(passphrase, homePath, chainName string) (*XRPLWallet, error) return nil, err } - signer = NewLocalSigner(name, w) + signer = NewLocalSigner(name, wptr) case RemoteSignerType: if record.Address == "" { return nil, fmt.Errorf("missing address for key %s", name)