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 9c1ebea..5eabaeb 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) 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 a00d074..0b96623 100644 --- a/internal/relayertest/mocks/chain_provider.go +++ b/internal/relayertest/mocks/chain_provider.go @@ -59,63 +59,18 @@ 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) { +// ChainType mocks base method. +func (m *MockChainProvider) ChainType() types0.ChainType { 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) + ret := m.ctrl.Call(m, "ChainType") + ret0, _ := ret[0].(types0.ChainType) 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 { +// 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, "ExportPrivateKey", reflect.TypeOf((*MockChainProvider)(nil).ExportPrivateKey), keyName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChainType", reflect.TypeOf((*MockChainProvider)(nil).ChainType)) } // GetChainName mocks base method. @@ -146,20 +101,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() @@ -229,159 +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) } - -// 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 - 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) -} - -// 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() - 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)) -} - -// 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..14d7a15 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" @@ -166,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 string, privKey *ecdsa.PrivateKey) (string, error) { +// SaveByMnemonic mocks base method. +func (m *MockWallet) SaveByMnemonic(name, mnemonic string, coinType uint32, account, index uint) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SavePrivateKey", name, privKey) + ret := m.ctrl.Call(m, "SaveByMnemonic", name, mnemonic, coinType, account, index) 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, coinType, account, index 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, coinType, account, index) +} + +// 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 c7d7954..f72929f 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,17 @@ 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) + key, err := chains.AddKeyByPrivateKey(w, keyName, privateKey) + if err != nil { + return nil, err + } + + return key, nil } // AddKeyByMnemonic adds a new key to the chain provider using a mnemonic phrase. @@ -302,7 +307,12 @@ func (a *App) AddKeyByMnemonic( return nil, err } - return cp.AddKeyByMnemonic(keyName, mnemonic, coinType, account, index) + key, err := cp.AddKeyByMnemonic(keyName, mnemonic, coinType, account, index) + if err != nil { + return nil, err + } + + return key, nil } // AddRemoteSignerKey adds a new remote signer key to the chain provider. @@ -313,12 +323,17 @@ 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 + } + + remoteKey, err := chains.AddRemoteSignerKey(w, keyName, addr, url, key) if err != nil { return nil, err } - return cp.AddRemoteSignerKey(keyName, addr, url, key) + return remoteKey, nil } // DeleteKey deletes the key from the chain provider. @@ -327,12 +342,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 +356,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 +527,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..a435e3b 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, @@ -331,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 @@ -423,6 +428,7 @@ func (s *AppTestSuite) TestQueryTunnelPacketInfo() { signalPrices, signingInfo, nil, + time.Now().Unix(), ) // Set up the mock expectation @@ -434,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) @@ -479,12 +485,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(). + SaveBySecret( "testkey", "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ). - Return(chainstypes.NewKey("", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", ""), nil) + Return("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", nil) }, }, { @@ -494,12 +503,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(). + SaveBySecret( "testkey", "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", ). - Return(nil, fmt.Errorf("add key error")) + Return("", fmt.Errorf("add key error")) }, err: fmt.Errorf("add key error"), }, @@ -550,7 +562,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 +575,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 +625,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 +643,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 +693,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 +757,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 +775,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/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.go b/relayer/chains/evm/keys.go index dbf2bb7..2c980d0 100644 --- a/relayer/chains/evm/keys.go +++ b/relayer/chains/evm/keys.go @@ -1,12 +1,11 @@ package evm import ( - "crypto/ecdsa" "fmt" - "github.com/ethereum/go-ethereum/crypto" hdwallet "github.com/miguelmota/go-ethereum-hdwallet" + "github.com/bandprotocol/falcon/relayer/chains" chainstypes "github.com/bandprotocol/falcon/relayer/chains/types" ) @@ -29,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 } @@ -37,109 +36,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.SaveBySecret(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..9af3446 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,25 @@ 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, + + QueryTimeout: 3 * time.Second, + ExecuteTimeout: 3 * time.Second, + }, + TunnelRouterAddress: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + 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 +108,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 +247,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 +263,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,15 +285,15 @@ 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) }, }, { - 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", }, } @@ -281,7 +303,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 +349,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 +384,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 +402,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/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/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..21719d5 --- /dev/null +++ b/relayer/chains/keys.go @@ -0,0 +1,77 @@ +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.SaveBySecret(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 does 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..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) @@ -15,7 +14,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 +35,10 @@ 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 { + // GetChainType retrieves the chain type from the chain provider. + ChainType() chainstypes.ChainType + // AddKeyByMnemonic adds a key using a mnemonic phrase. AddKeyByMnemonic( keyName string, @@ -50,35 +48,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 } - -// 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.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 83% rename from relayer/chains/evm/signer_test.go rename to relayer/chains/signer_test.go index a60aa0a..ef07a05 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" @@ -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, @@ -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/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..08a9cbd --- /dev/null +++ b/relayer/chains/xrpl/keys.go @@ -0,0 +1,43 @@ +package xrpl + +import ( + "fmt" + + "github.com/bandprotocol/falcon/relayer/chains" + 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 := "" + var err error + if mnemonic == "" { + mnemonic, err = chains.GenerateMnemonic(xrplMnemonicEntropyBits) + if err != nil { + return nil, err + } + generatedMnemonic = mnemonic + } + + addr, err := cp.Wallet.SaveByMnemonic(keyName, mnemonic, coinType, account, index) + 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..e1e9652 --- /dev/null +++ b/relayer/chains/xrpl/provider.go @@ -0,0 +1,299 @@ +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 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 { + 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..e214b1f 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: nil, mu: &sync.Mutex{}, } } @@ -172,7 +174,18 @@ 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: + latestSeq := targetContractInfo.LatestSequence + targetLatestSeq = &latestSeq + 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 +194,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 != 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 nextSeq := latestSeq + 1 if tunnelInfo.LatestSequence < nextSeq { t.Log.Debug("No new packet to relay", "sequence", latestSeq) @@ -196,11 +218,17 @@ 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 - relayermetrics.SetUnrelayedPackets(t.TunnelID, unrelayedPackets) + // 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 + unrelayedPackets := tunnelInfo.LatestSequence - *targetLatestSeq + relayermetrics.SetUnrelayedPackets(t.TunnelID, unrelayedPackets) + } // update the metric for the number of active target contracts if targetContractInfo.IsActive && !t.isTargetChainActive { @@ -223,6 +251,10 @@ 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 { + latestSeq := packet.Sequence + t.lastRelayedSeq = &latestSeq + } return nil } @@ -236,7 +268,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 +278,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 +306,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..2216642 100644 --- a/relayer/tunnel_relayer_test.go +++ b/relayer/tunnel_relayer_test.go @@ -130,15 +130,25 @@ func createMockPacket( signalPrices, currentGroupSigning, incomingGroupSigning, + time.Now().Unix(), ) } 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() err error relayStatus relayer.RelayStatus + chainType chaintypes.ChainType }{ { name: "success", @@ -162,6 +172,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 +183,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 +195,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 +205,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { }, err: nil, relayStatus: relayer.RelayStatusSkipped, + chainType: chaintypes.ChainTypeEVM, }, { name: "no new packet to relay", @@ -201,6 +215,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { }, err: nil, relayStatus: relayer.RelayStatusSkipped, + chainType: chaintypes.ChainTypeEVM, }, { name: "fail to get a new packet", @@ -214,6 +229,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 +253,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 +274,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 +314,7 @@ func (s *TunnelRelayerTestSuite) TestCheckAndRelay() { }, err: nil, relayStatus: relayer.RelayStatusSuccess, + chainType: chaintypes.ChainTypeEVM, }, { name: "failed to relay packet", @@ -317,11 +336,54 @@ 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, + }, + { + 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 { s.T().Run(tc.name, func(t *testing.T) { + chainType := tc.chainType + if chainType == chaintypes.ChainTypeUndefined { + chainType = chaintypes.ChainTypeEVM + } + currentChainType = chainType + if tc.preprocess != nil { tc.preprocess() } diff --git a/relayer/wallet/geth/wallet.go b/relayer/wallet/geth/wallet.go index bb10cd5..c8c7693 100644 --- a/relayer/wallet/geth/wallet.go +++ b/relayer/wallet/geth/wallet.go @@ -1,14 +1,15 @@ package geth import ( - "crypto/ecdsa" "fmt" "path" + "strings" "github.com/ethereum/go-ethereum/accounts" "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" + + hdPathTemplate = "m/44'/%d'/%d'/0/%d" ) // GethWallet manages local and remote signers for a specific chain. @@ -105,15 +108,20 @@ 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 *ecdsa.PrivateKey) (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(secret, "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 +129,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 } @@ -134,6 +142,38 @@ func (w *GethWallet) SavePrivateKey(name string, privKey *ecdsa.PrivateKey) (add 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, + coinType uint32, + account uint, + index uint, +) (addr string, err error) { + if mnemonic == "" { + return "", fmt.Errorf("mnemonic is empty") + } + + hdWallet, err := hdwallet.NewFromMnemonic(mnemonic) + if err != nil { + return "", err + } + + 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(ethAccount) + 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 fce16f3..da7f016 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" @@ -36,10 +37,11 @@ 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() + 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.SaveBySecret("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.SaveBySecret("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.SaveBySecret(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.SaveBySecret("alice", privHex) s.Require().NoError(err) }, "alice", false, "", diff --git a/relayer/wallet/wallet.go b/relayer/wallet/wallet.go index cefbfa2..497c91f 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,8 @@ type Signer interface { } type Wallet interface { - SavePrivateKey(name string, privKey *ecdsa.PrivateKey) (addr string, err error) + SaveBySecret(name string, secret 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/config.go b/relayer/wallet/xrpl/config.go new file mode 100644 index 0000000..393c782 --- /dev/null +++ b/relayer/wallet/xrpl/config.go @@ -0,0 +1,56 @@ +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"` + SaveMethod string `toml:"save_method,omitempty"` +} + +// NewKeyRecord creates a new KeyRecord. +func NewKeyRecord(signerType, address, url string, key *string, saveMethod string) KeyRecord { + return KeyRecord{ + Type: signerType, + Address: address, + Url: url, + Key: key, + SaveMethod: saveMethod, + } +} + +// 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..0b113e5 --- /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..f972fc0 --- /dev/null +++ b/relayer/wallet/xrpl/wallet.go @@ -0,0 +1,269 @@ +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" + + SaveMethodSeed = "seed" + SaveMethodMnemonic = "mnemonic" + + xrplDefaultCoinType = 144 +) + +// 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 + } + + var wptr *xrplwallet.Wallet + var w xrplwallet.Wallet + switch record.SaveMethod { + case SaveMethodMnemonic: + wptr, err = xrplwallet.FromMnemonic(secret) + case SaveMethodSeed: + w, err = xrplwallet.FromSecret(secret) + wptr = &w + default: + return nil, fmt.Errorf("unsupported save method %s for key %s", record.SaveMethod, name) + } + if err != nil { + return nil, err + } + + signer = NewLocalSigner(name, wptr) + 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 +} + +// 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(secret) + 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, 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, + 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") + } + + 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, SaveMethodMnemonic) + 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) +}