From 2b3902e1bb693596e03b3d7eeb4d2d0c838bfee6 Mon Sep 17 00:00:00 2001 From: Dzmitry Hil Date: Wed, 13 Dec 2023 18:04:30 +0300 Subject: [PATCH] Add bridge bootstrapping and start CLI (#61) Add bridge bootstrapping and start CLI * Add a new bridge client which will be used by the CLI * Add bridge bootstrapping logic to init the bridge XRPL account and deploy contract * Integrate bootstrapping into the integration tests * Update docs --- README.md | 118 +++++- integration-tests/contract.go | 5 +- integration-tests/processes/env_test.go | 131 +++--- .../processes/ticket_allocation_test.go | 2 +- integration-tests/xrpl.go | 61 +-- integration-tests/xrpl/rpc_test.go | 24 +- integration-tests/xrpl/scanner_test.go | 14 +- relayer/client/bridge.go | 381 ++++++++++++++++++ relayer/client/bridge_test.go | 41 ++ relayer/cmd/cli/cli.go | 275 ++++++++++++- relayer/cmd/main.go | 17 +- relayer/logger/logger.go | 5 + .../xrpl_tx_submitter_operation_tx.go | 31 +- relayer/runner/runner.go | 143 +++---- relayer/runner/runner_test.go | 4 +- relayer/xrpl/constatns.go | 7 + relayer/{processes => xrpl}/fee.go | 2 +- relayer/xrpl/rpc.go | 54 +++ relayer/xrpl/signer.go | 2 +- 19 files changed, 1034 insertions(+), 283 deletions(-) create mode 100644 relayer/client/bridge.go create mode 100644 relayer/client/bridge_test.go rename relayer/{processes => xrpl}/fee.go (96%) diff --git a/README.md b/README.md index efaae8a4..061bd8d2 100644 --- a/README.md +++ b/README.md @@ -20,36 +20,130 @@ make build-relayer make build-relayer-docker ``` -## Init relayer +## Bootstrap the bridge -### Init relayer default config +### Init relayer (for each relayer) -The relayer uses `relayer.yaml` for its work. The file contains all required setting which can be adjusted. -To init the default config call. +#### Set env variables used in the following instruction ```bash -./coreumbridge-xrpl-relayer init +export COREUM_CHAIN_ID={Coreum chain id} +export COREUM_GRPC_URL={Coreum GRPC URL} +export XRPL_RPC_URL={XRPL RPC URL} ``` -The command will generate the default `relayer.yaml` config in the `$HOME/.coreumbridge-xrpl-relayer`. -Optionally you can provide `--home` to set different home directory. +#### Init the config and generate the relayer keys -## Run relayer in docker +```bash +./coreumbridge-xrpl-relayer init --coreum-chain-id $COREUM_CHAIN_ID --coreum-grpc-url $COREUM_GRPC_URL --xrpl-rpc-url $XRPL_RPC_URL +./coreumbridge-xrpl-relayer keys add coreum-relayer --keyring-dir $HOME/.coreumbridge-xrpl-relayer/keys +./coreumbridge-xrpl-relayer keys add xrpl-relayer --keyring-dir $HOME/.coreumbridge-xrpl-relayer/keys +``` -If relayer docker image is not built, build it. +The `coreum-relayer` and `xrpl-relayer` are key names set by default in the `relayer.yaml`. If for some reason you want +to update them, then updated them in the `relayer.yaml` as well. + +#### Extract data for the contract deployment + +```bash +./coreumbridge-xrpl-relayer relayer-keys-info --keyring-dir $HOME/.coreumbridge-xrpl-relayer/keys +``` + +Output example: + +```bash +2023-12-10T18:04:55.235+0300 info cli/cli.go:205 Key info {"coreumAddress": "core1dukhz42p4qxkrtxg8ap7nj6wn3f2lqjqwf8gny", "xrplAddress": "r3YU6MLbmnxnLwCrRQYBAbaXmBR1RgK5mu", "xrplPubKey": "02ED720F8BF89D333CF7C4EAC763DA6EB7051895924DEB33AD34E87A624FE6B8F0"} +``` -### Add relayer key +The output contains the `coreumAddress`, `xrplAddress` and `xrplPubKey` used for the contract deployment. +Create the account with the `coreumAddress` by sending some tokens to in on the Coreum chain and XRPL account with the +`xrplAddress` by sending 10XRP tokens and to it. Once the accounts are created share the keys info with contract +deployer. + +### Run bootstrapping + +#### Generate new key which will be used for the bridge bootstrapping + +```bash +./coreumbridge-xrpl-relayer keys add bridge-account --keyring-dir $HOME/.coreumbridge-xrpl-relayer/keys +``` + +#### Fund the Coreum account ```bash -docker run --rm -it -v ${PWD}/keys:/keys coreumbridge-xrpl-relayer:local keys add coreumbridge-xrpl-relayer --keyring-dir /keys +./coreumbridge-xrpl-relayer keys show -a bridge-account --keyring-dir $HOME/.coreumbridge-xrpl-relayer/keys ``` +Get the Coreum address from the output and fund it on the Coreum side. +The balance should cover the token issuance fee and fee for the deployment transaction. + +#### Generate config template + +```bash +export RELAYERS_COUNT={Relayes count to be used} +./coreumbridge-xrpl-relayer bootstrap-bridge bootstraping.yaml --key-name bridge-account --init-only --relayers-count $RELAYERS_COUNT --keyring-dir $HOME/.coreumbridge-xrpl-relayer/keys +``` + +The output will print the XRPL bridge address and min XRPL bridge account balance. Fund it and proceed to the nex step. + +#### Modify the `bootstraping.yaml` config + +Collect the config from the relayer and modify the bootstrapping config. + +Config example: + +```yaml +owner: "" +admin: "" +relayers: + - coreum_address: "" + xrpl_address: "" + xrpl_pub_key: "" +evidence_threshold: 0 +used_ticket_sequence_threshold: 150 +trust_set_limit_amount: "100000000000000000000000000000000000" +contract_bytecode_path: "" +skip_xrpl_balance_validation: false +``` + +If you don't have the contract bytecode download it. + +#### Run the bootstrapping + +```bash +./coreumbridge-xrpl-relayer bootstrap-bridge bootstraping.yaml --key-name bridge-account --keyring-dir $HOME/.coreumbridge-xrpl-relayer/keys +``` + +Once the command is executed get the bridge contract address from the output and share among the relayers to update in +the relayers config. + +#### Remove the bridge-account key + +```bash +./coreumbridge-xrpl-relayer keys delete bridge-account --keyring-dir $HOME/.coreumbridge-xrpl-relayer/keys +``` + +## Run relayer in docker + +If relayer docker image is not built, build it. + +### Run relayer + ### Run relayer ```bash -docker run -it --name coreumbridge-xrpl-relayer -v ${PWD}/keys:/keys coreumbridge-xrpl-relayer:local start --keyring-dir /keys +docker run -dit --name coreumbridge-xrpl-relayer \ + -v $HOME/.coreumbridge-xrpl-relayer:/.coreumbridge-xrpl-relayer \ + coreumbridge-xrpl-relayer:local start \ + --keyring-dir /.coreumbridge-xrpl-relayer/keys \ + --home /.coreumbridge-xrpl-relayer + +docker attach coreumbridge-xrpl-relayer ``` +Once you are attached, press any key and enter the keyring password. +It is expected that at that time the relayer is initialized and its keys are generated and accounts are funded. + ### Restart running instance ```bash diff --git a/integration-tests/contract.go b/integration-tests/contract.go index ca8c4ad7..9276a54c 100644 --- a/integration-tests/contract.go +++ b/integration-tests/contract.go @@ -13,7 +13,8 @@ import ( "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/coreum" ) -const compiledContractFilePath = "../../build/coreumbridge_xrpl.wasm" +// CompiledContractFilePath is bridge contract file path. +const CompiledContractFilePath = "../../build/coreumbridge_xrpl.wasm" // DeployAndInstantiateContract deploys and instantiates the contract. func DeployAndInstantiateContract( @@ -62,7 +63,7 @@ func DeployAndInstantiateContract( func readBuiltContract(t *testing.T) []byte { t.Helper() - body, err := os.ReadFile(compiledContractFilePath) + body, err := os.ReadFile(CompiledContractFilePath) require.NoError(t, err) return body diff --git a/integration-tests/processes/env_test.go b/integration-tests/processes/env_test.go index 37073b89..d45713fa 100644 --- a/integration-tests/processes/env_test.go +++ b/integration-tests/processes/env_test.go @@ -25,6 +25,7 @@ import ( coreumconfig "github.com/CoreumFoundation/coreum/v3/pkg/config" coreumintegration "github.com/CoreumFoundation/coreum/v3/testutil/integration" integrationtests "github.com/CoreumFoundation/coreumbridge-xrpl/integration-tests" + bridgeclient "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/client" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/coreum" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/runner" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/xrpl" @@ -34,9 +35,8 @@ import ( type RunnerEnvConfig struct { AwaitTimeout time.Duration SigningThreshold int - RelayerNumber int + RelayersCount int MaliciousRelayerNumber int - DisableMasterKey bool UsedTicketSequenceThreshold int TrustSetLimitAmount sdkmath.Int } @@ -46,11 +46,10 @@ func DefaultRunnerEnvConfig() RunnerEnvConfig { return RunnerEnvConfig{ AwaitTimeout: 15 * time.Second, SigningThreshold: 2, - RelayerNumber: 3, + RelayersCount: 3, MaliciousRelayerNumber: 0, - DisableMasterKey: true, UsedTicketSequenceThreshold: 150, - TrustSetLimitAmount: sdkmath.NewIntWithDecimal(1, 30), + TrustSetLimitAmount: sdkmath.NewIntWithDecimal(1, 35), } } @@ -61,6 +60,7 @@ type RunnerEnv struct { ContractClient *coreum.ContractClient Chains integrationtests.Chains ContractOwner sdk.AccAddress + BridgeClient *bridgeclient.BridgeClient RunnersParallelGroup *parallel.Group Runners []*runner.Runner } @@ -72,40 +72,65 @@ func NewRunnerEnv(ctx context.Context, t *testing.T, cfg RunnerEnvConfig, chains ctx, t, chains.Coreum, - cfg.RelayerNumber, + cfg.RelayersCount, ) bridgeXRPLAddress, relayerXRPLAddresses, relayerXRPLPubKeys := genBridgeXRPLAccountWithRelayers( ctx, t, chains.XRPL, - cfg.RelayerNumber, - uint32(cfg.SigningThreshold), - cfg.DisableMasterKey, + cfg.RelayersCount, ) - contractRelayer := make([]coreum.Relayer, 0, cfg.RelayerNumber) - for i := 0; i < cfg.RelayerNumber; i++ { - contractRelayer = append(contractRelayer, coreum.Relayer{ - CoreumAddress: relayerCoreumAddresses[i], - XRPLAddress: relayerXRPLAddresses[i].String(), - XRPLPubKey: relayerXRPLPubKeys[i].String(), + contractOwner := chains.Coreum.GenAccount() + // fund to cover the fees + chains.Coreum.FundAccountWithOptions(ctx, t, contractOwner, coreumintegration.BalancesOptions{ + Amount: chains.Coreum.QueryAssetFTParams(ctx, t).IssueFee.Amount.AddRaw(1_000_000), + }) + + contractClient := coreum.NewContractClient( + coreum.DefaultContractClientConfig(sdk.AccAddress(nil)), + chains.Log, + chains.Coreum.ClientContext, + ) + xrplTxSigner := xrpl.NewKeyringTxSigner(chains.XRPL.GetSignerKeyring()) + bridgeClient := bridgeclient.NewBridgeClient( + chains.Log, + chains.Coreum.ClientContext, + contractClient, + chains.XRPL.RPCClient(), + xrplTxSigner, + ) + + bootstrappingRelayers := make([]bridgeclient.RelayerBootstrappingConfig, 0) + for i := 0; i < cfg.RelayersCount; i++ { + relayerCoreumAddress := relayerCoreumAddresses[i] + relayerXRPLAddress := relayerXRPLAddresses[i] + relayerXRPLPubKey := relayerXRPLPubKeys[i] + bootstrappingRelayers = append(bootstrappingRelayers, bridgeclient.RelayerBootstrappingConfig{ + CoreumAddress: relayerCoreumAddress.String(), + XRPLAddress: relayerXRPLAddress.String(), + XRPLPubKey: relayerXRPLPubKey.String(), }) } - contractOwner, contractClient := integrationtests.DeployAndInstantiateContract( - ctx, - t, - chains, - contractRelayer, - cfg.SigningThreshold, - cfg.UsedTicketSequenceThreshold, - cfg.TrustSetLimitAmount, - bridgeXRPLAddress.String(), - ) + bootstrappingCfg := bridgeclient.BootstrappingConfig{ + Owner: contractOwner.String(), + Admin: contractOwner.String(), + Relayers: bootstrappingRelayers, + EvidenceThreshold: cfg.SigningThreshold, + UsedTicketSequenceThreshold: cfg.UsedTicketSequenceThreshold, + TrustSetLimitAmount: cfg.TrustSetLimitAmount.String(), + ContractByteCodePath: integrationtests.CompiledContractFilePath, + SkipXRPLBalanceValidation: true, + } + + contractAddress, err := bridgeClient.Bootstrap(ctx, contractOwner, bridgeXRPLAddress.String(), bootstrappingCfg) + require.NoError(t, err) + require.NoError(t, contractClient.SetContractAddress(contractAddress)) - runners := make([]*runner.Runner, 0, cfg.RelayerNumber) + runners := make([]*runner.Runner, 0, cfg.RelayersCount) // add correct relayers - for i := 0; i < cfg.RelayerNumber-cfg.MaliciousRelayerNumber; i++ { + for i := 0; i < cfg.RelayersCount-cfg.MaliciousRelayerNumber; i++ { runners = append( runners, createDevRunner( @@ -120,7 +145,7 @@ func NewRunnerEnv(ctx context.Context, t *testing.T, cfg RunnerEnvConfig, chains } // add malicious relayers // we keep the relayer indexes to make all config valid apart from the XRPL signing - for i := cfg.RelayerNumber - cfg.MaliciousRelayerNumber; i < cfg.RelayerNumber; i++ { + for i := cfg.RelayersCount - cfg.MaliciousRelayerNumber; i < cfg.RelayersCount; i++ { maliciousXRPLAddress := chains.XRPL.GenAccount(ctx, t, 0) runners = append( runners, @@ -141,6 +166,7 @@ func NewRunnerEnv(ctx context.Context, t *testing.T, cfg RunnerEnvConfig, chains ContractClient: contractClient, Chains: chains, ContractOwner: contractOwner, + BridgeClient: bridgeClient, RunnersParallelGroup: parallel.NewGroup(ctx), Runners: runners, } @@ -378,54 +404,22 @@ func genBridgeXRPLAccountWithRelayers( t *testing.T, xrplChain integrationtests.XRPLChain, signersCount int, - signerQuorum uint32, - disableMasterKey bool, ) (rippledata.Account, []rippledata.Account, []rippledata.PublicKey) { t.Helper() // some fee to cover simple txs all extras must be allocated in the test bridgeXRPLAddress := xrplChain.GenAccount(ctx, t, 0.5) t.Logf("Bridge account is generated, address:%s", bridgeXRPLAddress.String()) - signerEntries := make([]rippledata.SignerEntry, 0, signersCount) signerAccounts := make([]rippledata.Account, 0, signersCount) signerPubKeys := make([]rippledata.PublicKey, 0, signersCount) for i := 0; i < signersCount; i++ { signerAcc := xrplChain.GenAccount(ctx, t, 0) signerAccounts = append(signerAccounts, signerAcc) t.Logf("Signer %d is generated, address:%s", i+1, signerAcc.String()) - signerEntries = append(signerEntries, rippledata.SignerEntry{ - SignerEntry: rippledata.SignerEntryItem{ - Account: &signerAcc, - SignerWeight: lo.ToPtr(uint16(1)), - }, - }) signerPubKeys = append(signerPubKeys, xrplChain.GetSignerPubKey(t, signerAcc)) } // fund for the signers SignerListSet xrplChain.FundAccountForSignerListSet(ctx, t, bridgeXRPLAddress, signersCount) - signerListSetTx := rippledata.SignerListSet{ - SignerQuorum: signerQuorum, - SignerEntries: signerEntries, - TxBase: rippledata.TxBase{ - TransactionType: rippledata.SIGNER_LIST_SET, - }, - } - require.NoError(t, xrplChain.AutoFillSignAndSubmitTx(ctx, t, &signerListSetTx, bridgeXRPLAddress)) - t.Logf("The bridge signers set is updated") - - if disableMasterKey { - // disable master key - disableMasterKeyTx := rippledata.AccountSet{ - TxBase: rippledata.TxBase{ - Account: bridgeXRPLAddress, - TransactionType: rippledata.ACCOUNT_SET, - }, - SetFlag: lo.ToPtr(uint32(rippledata.TxSetDisableMaster)), - } - require.NoError(t, xrplChain.AutoFillSignAndSubmitTx(ctx, t, &disableMasterKeyTx, bridgeXRPLAddress)) - t.Logf("Bridge account master key is disabled") - } - return bridgeXRPLAddress, signerAccounts, signerPubKeys } @@ -439,14 +433,12 @@ func createDevRunner( ) *runner.Runner { t.Helper() - const ( - relayerCoreumKeyName = "coreum" - relayerXRPLKeyName = "xrpl" - ) - encodingConfig := coreumconfig.NewEncodingConfig(coreumapp.ModuleBasics) kr := keyring.NewInMemory(encodingConfig.Codec) + relayerRunnerCfg := runner.DefaultConfig() + relayerRunnerCfg.LoggingConfig.Level = "info" + // reimport coreum key coreumKr := chains.Coreum.ClientContext.Keyring() keyInfo, err := coreumKr.KeyByAddress(relayerCoreumAddress) @@ -454,7 +446,7 @@ func createDevRunner( pass := uuid.NewString() armor, err := coreumKr.ExportPrivKeyArmor(keyInfo.Name, pass) require.NoError(t, err) - require.NoError(t, kr.ImportPrivKey(relayerCoreumKeyName, armor, pass)) + require.NoError(t, kr.ImportPrivKey(relayerRunnerCfg.Coreum.RelayerKeyName, armor, pass)) // reimport XRPL key xrplKr := chains.XRPL.GetSignerKeyring() @@ -462,18 +454,13 @@ func createDevRunner( require.NoError(t, err) armor, err = xrplKr.ExportPrivKeyArmor(keyInfo.Name, pass) require.NoError(t, err) - require.NoError(t, kr.ImportPrivKey(relayerXRPLKeyName, armor, pass)) - - relayerRunnerCfg := runner.DefaultConfig() - relayerRunnerCfg.LoggingConfig.Level = "info" + require.NoError(t, kr.ImportPrivKey(relayerRunnerCfg.XRPL.MultiSignerKeyName, armor, pass)) - relayerRunnerCfg.XRPL.MultiSignerKeyName = relayerXRPLKeyName relayerRunnerCfg.XRPL.RPC.URL = chains.XRPL.Config().RPCAddress // make the scanner fast relayerRunnerCfg.XRPL.Scanner.RetryDelay = 500 * time.Millisecond relayerRunnerCfg.Coreum.GRPC.URL = chains.Coreum.Config().GRPCAddress - relayerRunnerCfg.Coreum.RelayerKeyName = relayerCoreumKeyName relayerRunnerCfg.Coreum.Contract.ContractAddress = contractAddress.String() // We use high gas adjustment since our relayers might send transactions in one block. // They estimate gas based on the same state, but since transactions are executed one by one the next transaction uses diff --git a/integration-tests/processes/ticket_allocation_test.go b/integration-tests/processes/ticket_allocation_test.go index ac001391..e526b093 100644 --- a/integration-tests/processes/ticket_allocation_test.go +++ b/integration-tests/processes/ticket_allocation_test.go @@ -144,7 +144,7 @@ func TestTicketsAllocationRecoveryWithMaliciousRelayers(t *testing.T) { envCfg := DefaultRunnerEnvConfig() // add malicious relayers to the config - envCfg.RelayerNumber = 5 + envCfg.RelayersCount = 5 envCfg.MaliciousRelayerNumber = 2 envCfg.SigningThreshold = 3 diff --git a/integration-tests/xrpl.go b/integration-tests/xrpl.go index 2a74cee8..c448a20c 100644 --- a/integration-tests/xrpl.go +++ b/integration-tests/xrpl.go @@ -29,11 +29,6 @@ const ( // XRPCurrencyCode is XRP toke currency code on XRPL chain. XRPCurrencyCode = "XRP" - xrplTxFee = "100" - xrplReserveToActivateAccount = float64(10) - xrplReservePerTicket = float64(2) - xrplReservePerSigner = float64(2) - ecdsaKeyType = rippledata.ECDSA faucetKeyringKeyName = "faucet" ) @@ -135,7 +130,7 @@ func (c XRPLChain) GenEmptyAccount(t *testing.T) rippledata.Account { func (c XRPLChain) CreateAccount(ctx context.Context, t *testing.T, acc rippledata.Account, amount float64) { t.Helper() // amount to activate the account and some tokens on top - c.FundAccount(ctx, t, acc, amount+xrplReserveToActivateAccount) + c.FundAccount(ctx, t, acc, amount+xrpl.ReserveToActivateAccount) } // GetSignerKeyring returns signer keyring. @@ -154,21 +149,21 @@ func (c XRPLChain) GetSignerPubKey(t *testing.T, acc rippledata.Account) rippled func (c XRPLChain) ActivateAccount(ctx context.Context, t *testing.T, acc rippledata.Account) { t.Helper() - c.FundAccount(ctx, t, acc, xrplReserveToActivateAccount) + c.FundAccount(ctx, t, acc, xrpl.ReserveToActivateAccount) } // FundAccountForTicketAllocation funds the provided account with the amount required for the ticket allocation. func (c XRPLChain) FundAccountForTicketAllocation( ctx context.Context, t *testing.T, acc rippledata.Account, ticketsNumber uint32, ) { - c.FundAccount(ctx, t, acc, xrplReservePerTicket*float64(ticketsNumber)) + c.FundAccount(ctx, t, acc, xrpl.ReservePerTicket*float64(ticketsNumber)) } // FundAccountForSignerListSet funds the provided account with the amount required for the multi-signing set. func (c XRPLChain) FundAccountForSignerListSet( ctx context.Context, t *testing.T, acc rippledata.Account, singersCount int, ) { - c.FundAccount(ctx, t, acc, xrplReservePerSigner*float64(singersCount)) + c.FundAccount(ctx, t, acc, xrpl.ReservePerSigner*float64(singersCount)) } // FundAccount funds the provided account with the provided amount. @@ -194,7 +189,7 @@ func (c XRPLChain) FundAccount(ctx context.Context, t *testing.T, acc rippledata require.NoError(t, c.signer.Sign(&fundXrpTx, faucetKeyringKeyName)) t.Logf("Funding account, account address: %s, amount: %f", acc, amount) - require.NoError(t, c.SubmitTx(ctx, t, &fundXrpTx)) + require.NoError(t, c.RPCClient().SubmitAndAwaitSuccess(ctx, &fundXrpTx)) t.Logf("The account %s is funded", acc) } @@ -224,55 +219,13 @@ func (c XRPLChain) SignAndSubmitTx( t.Helper() require.NoError(t, c.signer.Sign(tx, acc.String())) - return c.SubmitTx(ctx, t, tx) + return c.RPCClient().SubmitAndAwaitSuccess(ctx, tx) } // AutoFillTx add seq number and fee for the transaction. func (c XRPLChain) AutoFillTx(ctx context.Context, t *testing.T, tx rippledata.Transaction, sender rippledata.Account) { t.Helper() - - accInfo, err := c.rpcClient.AccountInfo(ctx, sender) - require.NoError(t, err) - // update base settings - base := tx.GetBase() - fee, err := rippledata.NewValue(xrplTxFee, true) - require.NoError(t, err) - base.Fee = *fee - base.Account = sender - base.Sequence = *accInfo.AccountData.Sequence -} - -// SubmitTx submits tx a waits for its result. -func (c XRPLChain) SubmitTx(ctx context.Context, t *testing.T, tx rippledata.Transaction) error { - t.Helper() - - t.Logf("Submitting transaction, hash:%s", tx.GetHash()) - // submit the transaction - res, err := c.rpcClient.Submit(ctx, tx) - if err != nil { - return err - } - if !res.EngineResult.Success() { - return errors.Errorf("the tx submition is failed, %+v", res) - } - - retryCtx, retryCtxCancel := context.WithTimeout(ctx, time.Minute) - defer retryCtxCancel() - - t.Logf("Transaction is submitted waitig for hash:%s", tx.GetHash()) - return retry.Do(retryCtx, 250*time.Millisecond, func() error { - reqCtx, reqCtxCancel := context.WithTimeout(ctx, 3*time.Second) - defer reqCtxCancel() - txRes, err := c.rpcClient.Tx(reqCtx, *tx.GetHash()) - if err != nil { - return retry.Retryable(err) - } - - if !txRes.Validated { - return retry.Retryable(errors.Errorf("transaction is not validated")) - } - return nil - }) + require.NoError(t, c.rpcClient.AutoFillTx(ctx, tx, sender)) } // GetAccountBalance returns account balance for the provided issuer and currency. diff --git a/integration-tests/xrpl/rpc_test.go b/integration-tests/xrpl/rpc_test.go index 138c2cf5..2591ee0a 100644 --- a/integration-tests/xrpl/rpc_test.go +++ b/integration-tests/xrpl/rpc_test.go @@ -152,7 +152,7 @@ func TestMultisigPayment(t *testing.T) { "Recipient account balance before: %s", chains.XRPL.GetAccountBalances(ctx, t, xrpPaymentTxTwoSigners.Destination), ) - require.NoError(t, chains.XRPL.SubmitTx(ctx, t, &xrpPaymentTxTwoSigners)) + require.NoError(t, chains.XRPL.RPCClient().SubmitAndAwaitSuccess(ctx, &xrpPaymentTxTwoSigners)) t.Logf( "Recipient account balance after: %s", chains.XRPL.GetAccountBalances(ctx, t, xrpPaymentTxTwoSigners.Destination), @@ -161,7 +161,7 @@ func TestMultisigPayment(t *testing.T) { // try to submit with three signers (the transaction won't be accepted) require.ErrorContains( t, - chains.XRPL.SubmitTx(ctx, t, &xrpPaymentTxThreeSigners), + chains.XRPL.RPCClient().SubmitAndAwaitSuccess(ctx, &xrpPaymentTxThreeSigners), "This sequence number has already passed", ) } @@ -311,7 +311,7 @@ func TestCreateAndUseTicketForTicketsCreationWithMultisigning(t *testing.T) { createTicketsTx = buildCreateTicketsTxForMultiSigning(ctx, t, chains.XRPL, ticketsToCreate, 0, nil, multisigAcc) require.NoError(t, rippledata.SetSigners(&createTicketsTx, signer1)) - require.NoError(t, chains.XRPL.SubmitTx(ctx, t, &createTicketsTx)) + require.NoError(t, chains.XRPL.RPCClient().SubmitAndAwaitSuccess(ctx, &createTicketsTx)) txRes, err := chains.XRPL.RPCClient().Tx(ctx, *createTicketsTx.GetHash()) require.NoError(t, err) @@ -329,7 +329,7 @@ func TestCreateAndUseTicketForTicketsCreationWithMultisigning(t *testing.T) { ) require.NoError(t, rippledata.SetSigners(&createTicketsTx, signer1)) - require.NoError(t, chains.XRPL.SubmitTx(ctx, t, &createTicketsTx)) + require.NoError(t, chains.XRPL.RPCClient().SubmitAndAwaitSuccess(ctx, &createTicketsTx)) txRes, err = chains.XRPL.RPCClient().Tx(ctx, *createTicketsTx.GetHash()) require.NoError(t, err) @@ -375,7 +375,7 @@ func TestCreateAndUseTicketForMultisigningKeysRotation(t *testing.T) { createTicketsTx = buildCreateTicketsTxForMultiSigning(ctx, t, chains.XRPL, ticketsToCreate, 0, nil, multisigAcc) require.NoError(t, rippledata.SetSigners(&createTicketsTx, signer1)) - require.NoError(t, chains.XRPL.SubmitTx(ctx, t, &createTicketsTx)) + require.NoError(t, chains.XRPL.RPCClient().SubmitAndAwaitSuccess(ctx, &createTicketsTx)) txRes, err := chains.XRPL.RPCClient().Tx(ctx, *createTicketsTx.GetHash()) require.NoError(t, err) @@ -392,7 +392,7 @@ func TestCreateAndUseTicketForMultisigningKeysRotation(t *testing.T) { ctx, t, chains.XRPL, signer2Acc, createdTickets[0].TicketSequence, multisigAcc, ) require.NoError(t, rippledata.SetSigners(&updateSignerListSetTx, signer1)) - require.NoError(t, chains.XRPL.SubmitTx(ctx, t, &updateSignerListSetTx)) + require.NoError(t, chains.XRPL.RPCClient().SubmitAndAwaitSuccess(ctx, &updateSignerListSetTx)) // try to sign and send with previous signer restoreSignerListSetTx := buildUpdateSignerListSetTxForMultiSigning( @@ -406,7 +406,7 @@ func TestCreateAndUseTicketForMultisigningKeysRotation(t *testing.T) { require.NoError(t, rippledata.SetSigners(&restoreSignerListSetTx, signer1)) require.ErrorContains( t, - chains.XRPL.SubmitTx(ctx, t, &restoreSignerListSetTx), + chains.XRPL.RPCClient().SubmitAndAwaitSuccess(ctx, &restoreSignerListSetTx), "A signature is provided for a non-signer", ) @@ -420,7 +420,7 @@ func TestCreateAndUseTicketForMultisigningKeysRotation(t *testing.T) { ctx, t, chains.XRPL, signer1Acc, createdTickets[1].TicketSequence, multisigAcc, ) require.NoError(t, rippledata.SetSigners(&restoreSignerListSetTx, signer2)) - require.NoError(t, chains.XRPL.SubmitTx(ctx, t, &restoreSignerListSetTx)) + require.NoError(t, chains.XRPL.RPCClient().SubmitAndAwaitSuccess(ctx, &restoreSignerListSetTx)) } func TestMultisigWithMasterKeyRemoval(t *testing.T) { @@ -489,7 +489,7 @@ func TestMultisigWithMasterKeyRemoval(t *testing.T) { require.NoError(t, rippledata.SetSigners(&xrpPaymentTx, signer1, signer2)) t.Logf("Recipient account balance before: %s", chains.XRPL.GetAccountBalances(ctx, t, xrpPaymentTx.Destination)) - require.NoError(t, chains.XRPL.SubmitTx(ctx, t, &xrpPaymentTx)) + require.NoError(t, chains.XRPL.RPCClient().SubmitAndAwaitSuccess(ctx, &xrpPaymentTx)) t.Logf("Recipient account balance after: %s", chains.XRPL.GetAccountBalances(ctx, t, xrpPaymentTx.Destination)) } @@ -528,7 +528,7 @@ func TestCreateAndUseUsedTicketAndSequencesWithMultisigning(t *testing.T) { createTicketsTx = buildCreateTicketsTxForMultiSigning(ctx, t, chains.XRPL, ticketsToCreate, 0, nil, multisigAcc) require.NoError(t, rippledata.SetSigners(&createTicketsTx, signer1)) - require.NoError(t, chains.XRPL.SubmitTx(ctx, t, &createTicketsTx)) + require.NoError(t, chains.XRPL.RPCClient().SubmitAndAwaitSuccess(ctx, &createTicketsTx)) txRes, err := chains.XRPL.RPCClient().Tx(ctx, *createTicketsTx.GetHash()) require.NoError(t, err) @@ -542,7 +542,7 @@ func TestCreateAndUseUsedTicketAndSequencesWithMultisigning(t *testing.T) { createTicketsTx = buildCreateTicketsTxForMultiSigning(ctx, t, chains.XRPL, ticketsToCreate, 0, usedTicket, multisigAcc) require.NoError(t, rippledata.SetSigners(&createTicketsTx, signer1)) - require.NoError(t, chains.XRPL.SubmitTx(ctx, t, &createTicketsTx)) + require.NoError(t, chains.XRPL.RPCClient().SubmitAndAwaitSuccess(ctx, &createTicketsTx)) txRes, err = chains.XRPL.RPCClient().Tx(ctx, *createTicketsTx.GetHash()) require.NoError(t, err) @@ -564,7 +564,7 @@ func TestCreateAndUseUsedTicketAndSequencesWithMultisigning(t *testing.T) { signer1 = chains.XRPL.Multisign(t, &createTicketsTx, signer1Acc) createTicketsTx = buildCreateTicketsTxForMultiSigning(ctx, t, chains.XRPL, ticketsToCreate, *seqNo, nil, multisigAcc) require.NoError(t, rippledata.SetSigners(&createTicketsTx, signer1)) - require.NoError(t, chains.XRPL.SubmitTx(ctx, t, &createTicketsTx)) + require.NoError(t, chains.XRPL.RPCClient().SubmitAndAwaitSuccess(ctx, &createTicketsTx)) createdTickets = integrationtests.ExtractTicketsFromMeta(txRes) require.Len(t, createdTickets, int(ticketsToCreate)) diff --git a/integration-tests/xrpl/scanner_test.go b/integration-tests/xrpl/scanner_test.go index a6847789..2d38db6c 100644 --- a/integration-tests/xrpl/scanner_test.go +++ b/integration-tests/xrpl/scanner_test.go @@ -114,12 +114,11 @@ func TestRecentHistoryScanAccountTx(t *testing.T) { spawn("scan", parallel.Continue, func(ctx context.Context) error { return scanner.ScanTxs(ctx, txsCh) }) - // write and exist - spawn("write", parallel.Exit, func(ctx context.Context) error { + spawn("write", parallel.Continue, func(ctx context.Context) error { writtenTxHashes = sendMultipleTxs(ctx, t, chains.XRPL, txsCount, senderAcc, recipientAcc) return nil }) - spawn("wait", parallel.Continue, func(ctx context.Context) error { + spawn("wait", parallel.Exit, func(ctx context.Context) error { t.Logf("Waiting for %d transactions to be scanned by the scanner", txsCount) for tx := range txsCh { receivedTxHashes[tx.GetHash().String()] = struct{}{} @@ -153,7 +152,14 @@ func sendMultipleTxs( TransactionType: rippledata.PAYMENT, }, } - require.NoError(t, xrplChain.AutoFillSignAndSubmitTx(ctx, t, &xrpPaymentTx, senderAcc)) + + err = xrplChain.AutoFillSignAndSubmitTx(ctx, t, &xrpPaymentTx, senderAcc) + if errors.Is(err, context.Canceled) { + // we add the hash here since we cancel the context once we read it + writtenTxHashes[xrpPaymentTx.Hash.String()] = struct{}{} + return writtenTxHashes + } + require.NoError(t, err) writtenTxHashes[xrpPaymentTx.Hash.String()] = struct{}{} } t.Logf("Successfully sent %d transactions", len(writtenTxHashes)) diff --git a/relayer/client/bridge.go b/relayer/client/bridge.go new file mode 100644 index 00000000..a5ef192f --- /dev/null +++ b/relayer/client/bridge.go @@ -0,0 +1,381 @@ +//nolint:tagliatelle // yaml spec +package client + +import ( + "context" + "io" + "os" + "path/filepath" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/pkg/errors" + rippledata "github.com/rubblelabs/ripple/data" + "github.com/samber/lo" + "gopkg.in/yaml.v3" + + "github.com/CoreumFoundation/coreum/v3/pkg/client" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/coreum" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/logger" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/xrpl" +) + +const ( + ticketsToAllocate = 250 + minBalanceToCoverFee = float64(1) +) + +// ContractClient is the interface for the contract client. +type ContractClient interface { + DeployAndInstantiate( + ctx context.Context, + sender sdk.AccAddress, + byteCode []byte, + config coreum.InstantiationConfig, + ) (sdk.AccAddress, error) + GetContractConfig(ctx context.Context) (coreum.ContractConfig, error) +} + +// XRPLRPCClient is XRPL RPC client interface. +type XRPLRPCClient interface { + AccountInfo(ctx context.Context, acc rippledata.Account) (xrpl.AccountInfoResult, error) + AutoFillTx(ctx context.Context, tx rippledata.Transaction, sender rippledata.Account) error + Submit(ctx context.Context, tx rippledata.Transaction) (xrpl.SubmitResult, error) + SubmitAndAwaitSuccess(ctx context.Context, tx rippledata.Transaction) error +} + +// XRPLTxSigner is XRPL transaction signer. +type XRPLTxSigner interface { + Account(keyName string) (rippledata.Account, error) + Sign(tx rippledata.Transaction, keyName string) error +} + +// RelayerBootstrappingConfig is relayer config used for the bootstrapping. +type RelayerBootstrappingConfig struct { + CoreumAddress string `yaml:"coreum_address"` + XRPLAddress string `yaml:"xrpl_address"` + XRPLPubKey string `yaml:"xrpl_pub_key"` +} + +// BootstrappingConfig the struct contains the setting for the bridge XRPL account creation and contract deployment. +type BootstrappingConfig struct { + Owner string `yaml:"owner"` + Admin string `yaml:"admin"` + Relayers []RelayerBootstrappingConfig `yaml:"relayers"` + EvidenceThreshold int `yaml:"evidence_threshold"` + UsedTicketSequenceThreshold int `yaml:"used_ticket_sequence_threshold"` + TrustSetLimitAmount string `yaml:"trust_set_limit_amount"` + ContractByteCodePath string `yaml:"contract_bytecode_path"` + SkipXRPLBalanceValidation bool `yaml:"-"` +} + +// DefaultBootstrappingConfig returns default BootstrappingConfig. +func DefaultBootstrappingConfig() BootstrappingConfig { + return BootstrappingConfig{ + Owner: "", + Admin: "", + Relayers: []RelayerBootstrappingConfig{{}}, + EvidenceThreshold: 0, + UsedTicketSequenceThreshold: 150, + TrustSetLimitAmount: sdkmath.NewIntWithDecimal(1, 35).String(), + ContractByteCodePath: "", + SkipXRPLBalanceValidation: false, + } +} + +// BridgeClient is the service responsible for the bridge bootstrapping. +type BridgeClient struct { + log logger.Logger + clientCtx client.Context + contractClient ContractClient + xrplRPCClient XRPLRPCClient + xrplTxSigner XRPLTxSigner +} + +// NewBridgeClient returns a new instance of the BridgeClient. +func NewBridgeClient( + log logger.Logger, + clientCtx client.Context, + contractClient ContractClient, + xrplRPCClient XRPLRPCClient, + xrplTxSigner XRPLTxSigner, +) *BridgeClient { + return &BridgeClient{ + log: log, + clientCtx: clientCtx, + contractClient: contractClient, + xrplRPCClient: xrplRPCClient, + xrplTxSigner: xrplTxSigner, + } +} + +// Bootstrap creates initial XRPL bridge multi-signing account the disabled master key, +// enabled rippling on it deploys the bridge contract with the provided settings. +func (b *BridgeClient) Bootstrap( + ctx context.Context, + senderAddress sdk.AccAddress, + bridgeAccountKeyName string, + cfg BootstrappingConfig, +) (sdk.AccAddress, error) { + xrplBridgeAccount, err := b.xrplTxSigner.Account(bridgeAccountKeyName) + if err != nil { + return nil, err + } + b.log.Info( + ctx, + "XRPL account details", + logger.StringField("keyName", bridgeAccountKeyName), + logger.StringField("xrplAddress", xrplBridgeAccount.String()), + ) + if !cfg.SkipXRPLBalanceValidation { + if err = b.validateXRPLBridgeAccountBalance(ctx, len(cfg.Relayers), xrplBridgeAccount); err != nil { + return nil, err + } + } + // validate the config and fill required objects + relayers, xrplSignerEntries, err := b.buildRelayersFromBootstrappingConfig(ctx, cfg) + if err != nil { + return nil, err + } + // prepare deployment config + contactByteCode, err := os.ReadFile(cfg.ContractByteCodePath) + if err != nil { + return nil, errors.Wrapf(err, "failed to get contract bytecode by path:%s", cfg.ContractByteCodePath) + } + owner, err := sdk.AccAddressFromBech32(cfg.Owner) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse owner") + } + admin, err := sdk.AccAddressFromBech32(cfg.Admin) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse admin") + } + trustSetLimitAmount, ok := sdkmath.NewIntFromString(cfg.TrustSetLimitAmount) + if !ok { + return nil, + errors.Wrapf( + err, + "failed to convert trustSetLimitAmount to sdkmth.Int, trustSetLimitAmount:%s", + trustSetLimitAmount, + ) + } + instantiationCfg := coreum.InstantiationConfig{ + Owner: owner, + Admin: admin, + Relayers: relayers, + EvidenceThreshold: cfg.EvidenceThreshold, + UsedTicketSequenceThreshold: cfg.UsedTicketSequenceThreshold, + TrustSetLimitAmount: trustSetLimitAmount, + BridgeXRPLAddress: xrplBridgeAccount.String(), + } + b.log.Info(ctx, "Deploying contract", logger.AnyField("settings", instantiationCfg)) + contractAddress, err := b.contractClient.DeployAndInstantiate(ctx, senderAddress, contactByteCode, instantiationCfg) + b.log.Info(ctx, "Contract is deployed successfully", logger.StringField("address", contractAddress.String())) + if err != nil { + return nil, errors.Wrap(err, "failed to deploy contract") + } + + if err := b.setUpXRPLBridgeAccount(ctx, bridgeAccountKeyName, cfg, xrplSignerEntries); err != nil { + return nil, err + } + + b.log.Info(ctx, "The XRPL bridge account is ready", logger.StringField("address", xrplBridgeAccount.String())) + return contractAddress, nil +} + +func (b *BridgeClient) buildRelayersFromBootstrappingConfig( + ctx context.Context, + cfg BootstrappingConfig, +) ([]coreum.Relayer, []rippledata.SignerEntry, error) { + coreumAuthClient := authtypes.NewQueryClient(b.clientCtx) + relayers := make([]coreum.Relayer, 0, len(cfg.Relayers)) + xrplSignerEntries := make([]rippledata.SignerEntry, 0) + for _, relayer := range cfg.Relayers { + if _, err := coreumAuthClient.Account(ctx, &authtypes.QueryAccountRequest{ + Address: relayer.CoreumAddress, + }); err != nil { + return nil, nil, errors.Wrapf(err, "failed to get coreum account by address:%s", relayer.CoreumAddress) + } + xrplAddress, err := rippledata.NewAccountFromAddress(relayer.XRPLAddress) + if err != nil { + return nil, nil, errors.Wrapf( + err, + "failed to convert XRPL address string to rippledata.Account, address:%s", + relayer.XRPLAddress, + ) + } + xrplAccInfo, err := b.xrplRPCClient.AccountInfo(ctx, *xrplAddress) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to get XRPL account by address:%s", xrplAddress.String()) + } + if xrplAccInfo.AccountData.Balance.Float() < xrpl.ReserveToActivateAccount { + return nil, nil, errors.Errorf( + "insufficient XRPL relayer account balance, required:%f, current:%f", + xrpl.ReserveToActivateAccount, xrplAccInfo.AccountData.Balance.Float(), + ) + } + relayerCoreumAddress, err := sdk.AccAddressFromBech32(relayer.CoreumAddress) + if err != nil { + return nil, nil, errors.Wrapf(err, "failed to parse relayerCoreumAddress:%s", relayer.CoreumAddress) + } + relayers = append(relayers, coreum.Relayer{ + CoreumAddress: relayerCoreumAddress, + XRPLAddress: relayer.XRPLAddress, + XRPLPubKey: relayer.XRPLPubKey, + }) + xrplSignerEntries = append(xrplSignerEntries, rippledata.SignerEntry{ + SignerEntry: rippledata.SignerEntryItem{ + Account: xrplAddress, + SignerWeight: lo.ToPtr(uint16(1)), + }, + }) + } + + return relayers, xrplSignerEntries, nil +} + +func (b *BridgeClient) validateXRPLBridgeAccountBalance( + ctx context.Context, + relayersCount int, + xrplBridgeAccount rippledata.Account, +) error { + requiredXRPLBalance := ComputeXRPLBrideAccountBalance(relayersCount) + b.log.Info( + ctx, + "Compute required XRPL bridge account balance to init the account", + logger.Float64Field("requiredBalance", requiredXRPLBalance), + ) + xrplBridgeAccountInfo, err := b.xrplRPCClient.AccountInfo(ctx, xrplBridgeAccount) + if err != nil { + return err + } + xrplBridgeAccountBalance := xrplBridgeAccountInfo.AccountData.Balance + b.log.Info( + ctx, + "Got XRPL bridge account balance", + logger.Float64Field("balance", xrplBridgeAccountBalance.Float()), + ) + if xrplBridgeAccountBalance.Float() < requiredXRPLBalance { + return errors.Errorf( + "insufficient XRPL bridge account balance, required:%f, current:%f", + requiredXRPLBalance, xrplBridgeAccountBalance.Float(), + ) + } + + return nil +} + +func (b *BridgeClient) setUpXRPLBridgeAccount( + ctx context.Context, + bridgeAccountKeyName string, + cfg BootstrappingConfig, + xrplSignerEntries []rippledata.SignerEntry, +) error { + xrplBridgeAccount, err := b.xrplTxSigner.Account(bridgeAccountKeyName) + if err != nil { + return err + } + + b.log.Info(ctx, "Enabling rippling") + enableRipplingTx := rippledata.AccountSet{ + SetFlag: lo.ToPtr(uint32(rippledata.TxDefaultRipple)), + TxBase: rippledata.TxBase{ + TransactionType: rippledata.ACCOUNT_SET, + }, + } + if err := b.autoFillSignSubmitAndAwaitXRPLTx(ctx, &enableRipplingTx, bridgeAccountKeyName); err != nil { + return err + } + + b.log.Info(ctx, "Setting signers rippling") + signerListSetTx := rippledata.SignerListSet{ + SignerQuorum: uint32(cfg.EvidenceThreshold), + SignerEntries: xrplSignerEntries, + TxBase: rippledata.TxBase{ + TransactionType: rippledata.SIGNER_LIST_SET, + }, + } + if err := b.autoFillSignSubmitAndAwaitXRPLTx(ctx, &signerListSetTx, bridgeAccountKeyName); err != nil { + return err + } + + b.log.Info(ctx, "Disabling master key") + disableMasterKeyTx := rippledata.AccountSet{ + TxBase: rippledata.TxBase{ + Account: xrplBridgeAccount, + TransactionType: rippledata.ACCOUNT_SET, + }, + SetFlag: lo.ToPtr(uint32(rippledata.TxSetDisableMaster)), + } + return b.autoFillSignSubmitAndAwaitXRPLTx(ctx, &disableMasterKeyTx, bridgeAccountKeyName) +} + +// ComputeXRPLBrideAccountBalance computes the min balance required by the XRPL bridge account. +func ComputeXRPLBrideAccountBalance(signersCount int) float64 { + return minBalanceToCoverFee + + xrpl.ReserveToActivateAccount + + ticketsToAllocate*xrpl.ReservePerTicket + + float64(signersCount)*xrpl.ReservePerSigner +} + +// InitBootstrappingConfig creates default bootstrapping config yaml file. +func InitBootstrappingConfig(filePath string) error { + if err := os.MkdirAll(filepath.Dir(filePath), 0o700); err != nil { + return errors.Errorf("failed to create dirs by path:%s", filePath) + } + + file, err := os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600) + if err != nil { + return errors.Wrapf(err, "failed to create config file, path:%s", filePath) + } + defer file.Close() + yamlStringConfig, err := yaml.Marshal(DefaultBootstrappingConfig()) + if err != nil { + return errors.Wrap(err, "failed convert default config to yaml") + } + if _, err := file.Write(yamlStringConfig); err != nil { + return errors.Wrapf(err, "failed to write yaml config file, path:%s", filePath) + } + + return nil +} + +// ReadBootstrappingConfig reads config yaml file. +func ReadBootstrappingConfig(filePath string) (BootstrappingConfig, error) { + file, err := os.OpenFile(filePath, os.O_RDONLY, 0o600) + defer file.Close() //nolint:staticcheck //we accept the error ignoring + if errors.Is(err, os.ErrNotExist) { + return BootstrappingConfig{}, errors.Errorf("config file does not exist, path:%s", filePath) + } + fileBytes, err := io.ReadAll(file) + if err != nil { + return BootstrappingConfig{}, errors.Wrapf(err, "failed to read bytes from file does not exist, path:%s", filePath) + } + + var config BootstrappingConfig + if err := yaml.Unmarshal(fileBytes, &config); err != nil { + return BootstrappingConfig{}, errors.Wrapf(err, "failed to unmarshal file to yaml, path:%s", filePath) + } + + return config, nil +} + +func (b *BridgeClient) autoFillSignSubmitAndAwaitXRPLTx( + ctx context.Context, + tx rippledata.Transaction, + signerKeyName string, +) error { + sender, err := b.xrplTxSigner.Account(signerKeyName) + if err != nil { + return err + } + if err := b.xrplRPCClient.AutoFillTx(ctx, tx, sender); err != nil { + return err + } + if err := b.xrplTxSigner.Sign(tx, signerKeyName); err != nil { + return err + } + + return b.xrplRPCClient.SubmitAndAwaitSuccess(ctx, tx) +} diff --git a/relayer/client/bridge_test.go b/relayer/client/bridge_test.go new file mode 100644 index 00000000..0832a2a5 --- /dev/null +++ b/relayer/client/bridge_test.go @@ -0,0 +1,41 @@ +package client_test + +import ( + "path" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/client" +) + +func TestInitAndReadBootstrappingConfig(t *testing.T) { + t.Parallel() + + defaultCfg := client.DefaultBootstrappingConfig() + yamlStringConfig, err := yaml.Marshal(defaultCfg) + require.NoError(t, err) + require.Equal(t, getDefaultConfigString(), string(yamlStringConfig)) + filePath := path.Join(t.TempDir(), "bootstrapping.yaml") + require.NoError(t, client.InitBootstrappingConfig(filePath)) + readConfig, err := client.ReadBootstrappingConfig(filePath) + require.NoError(t, err) + + require.Equal(t, defaultCfg, readConfig) +} + +// the func returns the default config snapshot. +func getDefaultConfigString() string { + return `owner: "" +admin: "" +relayers: + - coreum_address: "" + xrpl_address: "" + xrpl_pub_key: "" +evidence_threshold: 0 +used_ticket_sequence_threshold: 150 +trust_set_limit_amount: "100000000000000000000000000000000000" +contract_bytecode_path: "" +` +} diff --git a/relayer/cmd/cli/cli.go b/relayer/cmd/cli/cli.go index 003068e3..5bd058d2 100644 --- a/relayer/cmd/cli/cli.go +++ b/relayer/cmd/cli/cli.go @@ -4,7 +4,6 @@ import ( "bufio" "os" "path" - "time" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" @@ -12,8 +11,12 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/CoreumFoundation/coreum/v3/pkg/config" + "github.com/CoreumFoundation/coreum/v3/pkg/config/constant" + bridgeclient "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/client" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/logger" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/runner" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/xrpl" ) const ( @@ -21,8 +24,18 @@ const ( DefaultHomeDir = ".coreumbridge-xrpl-relayer" // FlagHome is home flag. FlagHome = "home" - // That key name is constant here temporary, we will take it from the relayer config later. - relayerKeyName = "coreumbridge-xrpl-relayer" + // FlagKeyName is key name flag. + FlagKeyName = "key-name" + // FlagCoreumChainID is chain-id flag. + FlagCoreumChainID = "coreum-chain-id" + // FlagCoreumGRPCURL is Coreum GRPC URL flag. + FlagCoreumGRPCURL = "coreum-grpc-url" + // FlagXRPLRPCURL is XRPL RPC URL flag. + FlagXRPLRPCURL = "xrpl-rpc-url" + // FlagInitOnly is init only flag. + FlagInitOnly = "init-only" + // FlagRelayersCount is relayers count flag. + FlagRelayersCount = "relayers-count" ) // InitCmd returns the init cmd. @@ -40,8 +53,29 @@ func InitCmd() *cobra.Command { if err != nil { return err } - log.Info(ctx, "Generating default settings", logger.StringField("home", home)) - if err = runner.InitConfig(home, runner.DefaultConfig()); err != nil { + log.Info(ctx, "Generating settings", logger.StringField("home", home)) + + chainID, err := cmd.Flags().GetString(FlagCoreumChainID) + if err != nil { + return errors.Wrapf(err, "failed to read %s", FlagCoreumChainID) + } + coreumGRPCURL, err := cmd.Flags().GetString(FlagCoreumGRPCURL) + if err != nil { + return errors.Wrapf(err, "failed to read %s", FlagCoreumGRPCURL) + } + + xrplRPCURL, err := cmd.Flags().GetString(FlagXRPLRPCURL) + if err != nil { + return errors.Wrapf(err, "failed to read %s", FlagXRPLRPCURL) + } + + cfg := runner.DefaultConfig() + cfg.Coreum.Network.ChainID = chainID + cfg.Coreum.GRPC.URL = coreumGRPCURL + + cfg.XRPL.RPC.URL = xrplRPCURL + + if err = runner.InitConfig(home, cfg); err != nil { return err } log.Info(ctx, "Settings are generated successfully") @@ -49,6 +83,10 @@ func InitCmd() *cobra.Command { }, } + addCoreumChainIDFlag(cmd) + cmd.PersistentFlags().String(FlagXRPLRPCURL, "", "XRPL RPC address") + cmd.PersistentFlags().String(FlagCoreumGRPCURL, "", "Coreum GRPC address.") + addHomeFlag(cmd) return cmd @@ -72,37 +110,224 @@ func StartCmd() *cobra.Command { input := bufio.NewScanner(os.Stdin) input.Scan() - // that code is just for an example and will be replaced later + rnr, err := getRunnerFromHome(cmd) + if err != nil { + return err + } + return rnr.Processor.StartProcesses(ctx, rnr.Processes.XRPLTxSubmitter, rnr.Processes.XRPLTxObserver) + }, + } + addHomeFlag(cmd) + addKeyringFlags(cmd) + + return cmd +} + +// KeyringCmd returns cosmos keyring cmd inti with the correct keys home. +func KeyringCmd() (*cobra.Command, error) { + // we set it for the keyring manually since it doesn't use the runner which does it for other CLI commands + cmd := keys.Commands(DefaultHomeDir) + for _, childCmd := range cmd.Commands() { + childCmd.PreRunE = func(cmd *cobra.Command, args []string) error { + return setCoreumConfigFromHomeFlag(cmd) + } + } + + return cmd, nil +} + +// XRPLKeyInfoCmd prints the XRPL key info. +func XRPLKeyInfoCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "relayer-keys-info", + Short: "Prints the coreum and XRPL relayer key info.", + RunE: func(cmd *cobra.Command, args []string) error { + if err := setCoreumConfigFromHomeFlag(cmd); err != nil { + return err + } + + ctx := cmd.Context() + log, err := getConsoleLogger() + if err != nil { + return err + } clientCtx, err := client.GetClientQueryContext(cmd) if err != nil { return errors.Wrap(err, "failed to get client context") } - keyRecord, err := clientCtx.Keyring.Key(relayerKeyName) + + // XRPL + cfg, err := getRelayerHomeRunnerConfig(cmd) if err != nil { - return errors.Wrap(err, "failed to get key from keyring") + return err } - address, err := keyRecord.GetAddress() + + kr := clientCtx.Keyring + xrplKeyringTxSigner := xrpl.NewKeyringTxSigner(kr) + + xrplAddress, err := xrplKeyringTxSigner.Account(cfg.XRPL.MultiSignerKeyName) + if err != nil { + return err + } + + xrplPubKey, err := xrplKeyringTxSigner.PubKey(cfg.XRPL.MultiSignerKeyName) if err != nil { - return errors.Wrap(err, "failed to get address from the key record") + return err } - for { - select { - case <-ctx.Done(): - return nil - case <-time.After(time.Second): - log.Info(ctx, "Address from the keyring extracted.", logger.StringField("address", address.String())) + + // Coreum + coreumKeyRecord, err := kr.Key(cfg.Coreum.RelayerKeyName) + if err != nil { + return errors.Wrapf(err, "failed to get coreum key, keyName:%s", cfg.Coreum.RelayerKeyName) + } + coreumAddress, err := coreumKeyRecord.GetAddress() + if err != nil { + return errors.Wrapf(err, "failed to get coreum address from key, keyName:%s", cfg.Coreum.RelayerKeyName) + } + + log.Info( + ctx, + "Keys info", + logger.StringField("coreumAddress", coreumAddress.String()), + logger.StringField("xrplAddress", xrplAddress.String()), + logger.StringField("xrplPubKey", xrplPubKey.String()), + ) + + return nil + }, + } + addKeyringFlags(cmd) + addKeyNameFlag(cmd) + addHomeFlag(cmd) + + return cmd +} + +// BootstrapBridge safely creates XRPL bridge account with all required settings and deploys the bridge contract. +func BootstrapBridge() *cobra.Command { + cmd := &cobra.Command{ + Use: "bootstrap-bridge [config-path]", + Args: cobra.ExactArgs(1), + Short: "Sets up the XRPL bridge account with all required settings and deploys the bridge contract", + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return errors.Wrap(err, "failed to get client context") + } + + log, err := getConsoleLogger() + if err != nil { + return err + } + + keyName, err := cmd.Flags().GetString(FlagKeyName) + if err != nil { + return errors.Wrapf(err, "failed to get %s", FlagKeyName) + } + + kr := clientCtx.Keyring + xrplKeyringTxSigner := xrpl.NewKeyringTxSigner(kr) + xrplBridgeAddress, err := xrplKeyringTxSigner.Account(keyName) + if err != nil { + return err + } + log.Info(ctx, "XRPL bridge address", logger.AnyField("address", xrplBridgeAddress.String())) + + filePath := args[0] + initOnly, err := cmd.Flags().GetBool(FlagInitOnly) + if err != nil { + return errors.Wrapf(err, "failed to get %s", FlagInitOnly) + } + if initOnly { + log.Info(ctx, "Initializing default bootstrapping config", logger.AnyField("path", filePath)) + if err := bridgeclient.InitBootstrappingConfig(filePath); err != nil { + return err + } + relayersCount, err := cmd.Flags().GetInt(FlagRelayersCount) + if err != nil { + return errors.Wrapf(err, "failed to get %s", FlagRelayersCount) } + if relayersCount > 0 { + minXrplBridgeBalance := bridgeclient.ComputeXRPLBrideAccountBalance(relayersCount) + log.Info(ctx, "Computed minimum XRPL bridge balance", logger.Float64Field("balance", minXrplBridgeBalance)) + } + + return nil + } + + rnr, err := getRunnerFromHome(cmd) + if err != nil { + return err + } + record, err := clientCtx.Keyring.Key(keyName) + if err != nil { + return errors.Wrapf(err, "failed to get key by name:%s", keyName) } + addr, err := record.GetAddress() + if err != nil { + return errors.Wrapf(err, "failed to address for key name:%s", keyName) + } + cfg, err := bridgeclient.ReadBootstrappingConfig(filePath) + if err != nil { + return err + } + log.Info(ctx, "Bootstrapping XRPL bridge", logger.AnyField("config", cfg)) + log.Info(ctx, "Press any key to continue.") + input := bufio.NewScanner(os.Stdin) + input.Scan() + + _, err = rnr.BridgeClient.Bootstrap(ctx, addr, keyName, cfg) + return err }, } addKeyringFlags(cmd) + addKeyNameFlag(cmd) + addHomeFlag(cmd) + + cmd.PersistentFlags().Bool(FlagInitOnly, false, "Init default config") + cmd.PersistentFlags().Int(FlagRelayersCount, 0, "Relayers count") return cmd } -// KeyringCmd returns cosmos keyring cmd inti with the correct keys home. -func KeyringCmd() *cobra.Command { - return keys.Commands(path.Join(DefaultHomeDir, "keys")) +func getRunnerFromHome(cmd *cobra.Command) (*runner.Runner, error) { + cfg, err := getRelayerHomeRunnerConfig(cmd) + if err != nil { + return nil, err + } + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return nil, errors.Wrap(err, "failed to get client context") + } + rnr, err := runner.NewRunner(cmd.Context(), cfg, clientCtx.Keyring) + if err != nil { + return nil, err + } + + return rnr, nil +} + +func setCoreumConfigFromHomeFlag(cmd *cobra.Command) error { + cfg, err := getRelayerHomeRunnerConfig(cmd) + if err != nil { + return err + } + network, err := config.NetworkConfigByChainID(constant.ChainID(cfg.Coreum.Network.ChainID)) + if err != nil { + return err + } + network.SetSDKConfig() + + return nil +} + +func getRelayerHomeRunnerConfig(cmd *cobra.Command) (runner.Config, error) { + home, err := getRelayerHome(cmd) + if err != nil { + return runner.Config{}, err + } + return runner.ReadConfig(home) } func getRelayerHome(cmd *cobra.Command) (string, error) { @@ -136,6 +361,10 @@ func addHomeFlag(cmd *cobra.Command) { cmd.PersistentFlags().String(FlagHome, DefaultHomeDir, "Relayer home directory") } +func addCoreumChainIDFlag(cmd *cobra.Command) *string { + return cmd.PersistentFlags().String(FlagCoreumChainID, string(runner.DefaultCoreumChainID), "Default coreum chain ID") +} + func addKeyringFlags(cmd *cobra.Command) { cmd.PersistentFlags().String( flags.FlagKeyringBackend, @@ -144,9 +373,11 @@ func addKeyringFlags(cmd *cobra.Command) { ) cmd.PersistentFlags().String( flags.FlagKeyringDir, - path.Join(DefaultHomeDir, "keys"), - "The client Keyring directory; if omitted, the default 'home' directory will be used", - ) + DefaultHomeDir, "The client Keyring directory; if omitted, the default 'home' directory will be used") +} + +func addKeyNameFlag(cmd *cobra.Command) { + cmd.PersistentFlags().String(FlagKeyName, "", "Key name from the keyring") } // returns the console logger initialised with the default logger config but with set `console` format. diff --git a/relayer/cmd/main.go b/relayer/cmd/main.go index 6a51d655..6a5209b9 100644 --- a/relayer/cmd/main.go +++ b/relayer/cmd/main.go @@ -16,7 +16,10 @@ import ( func main() { run.Tool("CoreumbridgeXRPLRelayer", func(ctx context.Context) error { - rootCmd := RootCmd(ctx) + rootCmd, err := RootCmd(ctx) + if err != nil { + return err + } if err := rootCmd.Execute(); err != nil && !errors.Is(err, context.Canceled) { return err } @@ -26,7 +29,7 @@ func main() { } // RootCmd returns the root cmd. -func RootCmd(ctx context.Context) *cobra.Command { +func RootCmd(ctx context.Context) (*cobra.Command, error) { encodingConfig := config.NewEncodingConfig(coreumapp.ModuleBasics) clientCtx := client.Context{}. WithCodec(encodingConfig.Codec). @@ -42,7 +45,13 @@ func RootCmd(ctx context.Context) *cobra.Command { cmd.AddCommand(cli.InitCmd()) cmd.AddCommand(cli.StartCmd()) - cmd.AddCommand(cli.KeyringCmd()) + keyringCmd, err := cli.KeyringCmd() + if err != nil { + return nil, err + } + cmd.AddCommand(keyringCmd) + cmd.AddCommand(cli.XRPLKeyInfoCmd()) + cmd.AddCommand(cli.BootstrapBridge()) - return cmd + return cmd, nil } diff --git a/relayer/logger/logger.go b/relayer/logger/logger.go index 0abff8b9..991d4e7b 100644 --- a/relayer/logger/logger.go +++ b/relayer/logger/logger.go @@ -68,6 +68,11 @@ func ByteStringField(key string, value []byte) Field { return convertZapFieldToField(zap.ByteString(key, value)) } +// Float64Field constructs a field with the given key and value. +func Float64Field(key string, value float64) Field { + return convertZapFieldToField(zap.Float64(key, value)) +} + // Error is shorthand for the common idiom NamedError("error", err). func Error(err error) Field { return convertZapFieldToField(zap.Error(err)) diff --git a/relayer/processes/xrpl_tx_submitter_operation_tx.go b/relayer/processes/xrpl_tx_submitter_operation_tx.go index 0e27032e..3d7d27c1 100644 --- a/relayer/processes/xrpl_tx_submitter_operation_tx.go +++ b/relayer/processes/xrpl_tx_submitter_operation_tx.go @@ -5,6 +5,7 @@ import ( rippledata "github.com/rubblelabs/ripple/data" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/coreum" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/xrpl" ) // BuildTicketCreateTxForMultiSigning builds TicketCreate transaction operation from the contract operation. @@ -27,7 +28,7 @@ func BuildTicketCreateTxForMultiSigning( // important for the multi-signing tx.TxBase.SigningPubKey = &rippledata.PublicKey{} - fee, err := GetTxFee(&tx) + fee, err := xrpl.GetTxFee(&tx) if err != nil { return nil, err } @@ -61,7 +62,7 @@ func BuildTrustSetTxForMultiSigning( // important for the multi-signing tx.TxBase.SigningPubKey = &rippledata.PublicKey{} - fee, err := GetTxFee(&tx) + fee, err := xrpl.GetTxFee(&tx) if err != nil { return nil, err } @@ -94,30 +95,6 @@ func BuildCoreumToXRPLXRPLOriginatedTokenTransferPaymentTxForMultiSigning( return &tx, nil } -// BuildCoreumToXRPLCoreumOriginatedTokenTransferPaymentTxForMultiSigning builds Payment transaction for coreum -// originated token from the contract operation. -func BuildCoreumToXRPLCoreumOriginatedTokenTransferPaymentTxForMultiSigning( - bridgeXRPLAddress rippledata.Account, - operation coreum.Operation, -) (*rippledata.Payment, error) { - coreumToXRPLTransferOperationType := operation.OperationType.CoreumToXRPLTransfer - value, err := ConvertXRPLOriginatedTokenCoreumAmountToXRPLAmount( - coreumToXRPLTransferOperationType.Amount, - coreumToXRPLTransferOperationType.Issuer, - coreumToXRPLTransferOperationType.Currency, - ) - if err != nil { - return nil, err - } - - tx, err := buildPaymentTx(bridgeXRPLAddress, operation, value) - if err != nil { - return nil, err - } - - return &tx, nil -} - func buildPaymentTx( bridgeXRPLAddress rippledata.Account, operation coreum.Operation, @@ -143,7 +120,7 @@ func buildPaymentTx( // important for the multi-signing tx.TxBase.SigningPubKey = &rippledata.PublicKey{} - fee, err := GetTxFee(&tx) + fee, err := xrpl.GetTxFee(&tx) if err != nil { return rippledata.Payment{}, err } diff --git a/relayer/runner/runner.go b/relayer/runner/runner.go index b80ecc84..d5d5ecdb 100644 --- a/relayer/runner/runner.go +++ b/relayer/runner/runner.go @@ -26,6 +26,7 @@ import ( coreumchainclient "github.com/CoreumFoundation/coreum/v3/pkg/client" coreumchainconfig "github.com/CoreumFoundation/coreum/v3/pkg/config" coreumchainconstant "github.com/CoreumFoundation/coreum/v3/pkg/config/constant" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/client" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/coreum" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/logger" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/processes" @@ -36,8 +37,8 @@ const ( configVersion = "v1" // ConfigFileName is file name used for the relayer config. ConfigFileName = "relayer.yaml" - - defaultCoreumChainID = coreumchainconstant.ChainIDMain + // DefaultCoreumChainID is default chain id. + DefaultCoreumChainID = coreumchainconstant.ChainIDMain ) // ******************** Config ******************** @@ -144,7 +145,7 @@ func DefaultConfig() Config { LoggingConfig: LoggingConfig(logger.DefaultZapLoggerConfig()), XRPL: XRPLConfig{ // empty be default - MultiSignerKeyName: "", + MultiSignerKeyName: "xrpl-relayer", HTTPClient: HTTPClientConfig(toolshttp.DefaultClientConfig()), RPC: XRPLRPCConfig{ // empty be default @@ -162,14 +163,13 @@ func DefaultConfig() Config { }, Coreum: CoreumConfig{ - // empty be default - RelayerKeyName: "", + RelayerKeyName: "coreum-relayer", GRPC: CoreumGRPCConfig{ // empty be default URL: "", }, Network: CoreumNetworkConfig{ - ChainID: string(defaultCoreumChainID), + ChainID: string(DefaultCoreumChainID), }, Contract: CoreumContractConfig{ // empty be default @@ -209,6 +209,8 @@ type Runner struct { CoreumContractClient *coreum.ContractClient CoreumChainNetworkConfig coreumchainconfig.NetworkConfig + BridgeClient *client.BridgeClient + Processes Processes Processor *processes.Processor } @@ -217,11 +219,15 @@ type Runner struct { // //nolint:funlen // the func contains sequential object initialisation func NewRunner(ctx context.Context, cfg Config, kr keyring.Keyring) (*Runner, error) { + rnr := &Runner{} zapLogger, err := logger.NewZapLogger(logger.ZapLoggerConfig(cfg.LoggingConfig)) if err != nil { return nil, err } + rnr.Log = zapLogger + retryableXRPLRPCHTTPClient := toolshttp.NewRetryableClient(toolshttp.RetryableClientConfig(cfg.XRPL.HTTPClient)) + rnr.RetryableHTTPClient = &retryableXRPLRPCHTTPClient var contractAddress sdk.AccAddress if cfg.Coreum.Contract.ContractAddress != "" { @@ -258,9 +264,8 @@ func NewRunner(ctx context.Context, cfg Config, kr keyring.Keyring) (*Runner, er clientContext = clientContext.WithGRPCClient(grpcClient) } - var coreumChainNetworkConfig coreumchainconfig.NetworkConfig if cfg.Coreum.Network.ChainID != "" { - coreumChainNetworkConfig, err = coreumchainconfig.NetworkConfigByChainID( + coreumChainNetworkConfig, err := coreumchainconfig.NetworkConfigByChainID( coreumchainconstant.ChainID(cfg.Coreum.Network.ChainID), ) if err != nil { @@ -271,6 +276,7 @@ func NewRunner(ctx context.Context, cfg Config, kr keyring.Keyring) (*Runner, er ) } clientContext = clientContext.WithChainID(cfg.Coreum.Network.ChainID) + rnr.CoreumChainNetworkConfig = coreumChainNetworkConfig } var relayerAddress sdk.AccAddress @@ -288,79 +294,78 @@ func NewRunner(ctx context.Context, cfg Config, kr keyring.Keyring) (*Runner, er ) } } - contractClient := coreum.NewContractClient(contractClientCfg, zapLogger, clientContext) - contractConfig, err := contractClient.GetContractConfig(ctx) - if err != nil { - return nil, errors.Wrapf(err, "failed to get contract config for the runner intialization") - } + contractClient := coreum.NewContractClient(contractClientCfg, zapLogger, clientContext) + rnr.CoreumContractClient = contractClient xrplRPCClientCfg := xrpl.RPCClientConfig(cfg.XRPL.RPC) xrplRPCClient := xrpl.NewRPCClient(xrplRPCClientCfg, zapLogger, retryableXRPLRPCHTTPClient) - bridgeXRPLAddress, err := rippledata.NewAccountFromAddress(contractConfig.BridgeXRPLAddress) - if err != nil { - return nil, errors.Wrapf(err, "failed to get xrpl account from string, string:%s", contractConfig.BridgeXRPLAddress) - } - xrplScanner := xrpl.NewAccountScanner(xrpl.AccountScannerConfig{ - Account: *bridgeXRPLAddress, - RecentScanEnabled: cfg.XRPL.Scanner.RecentScanEnabled, - RecentScanWindow: cfg.XRPL.Scanner.RecentScanWindow, - RepeatRecentScan: cfg.XRPL.Scanner.RepeatRecentScan, - FullScanEnabled: cfg.XRPL.Scanner.FullScanEnabled, - RepeatFullScan: cfg.XRPL.Scanner.RepeatFullScan, - RetryDelay: cfg.XRPL.Scanner.RetryDelay, - }, zapLogger, xrplRPCClient) + rnr.XRPLRPCClient = xrplRPCClient var xrplKeyringTxSigner *xrpl.KeyringTxSigner if kr != nil { xrplKeyringTxSigner = xrpl.NewKeyringTxSigner(kr) } + bridgeClient := client.NewBridgeClient(zapLogger, clientContext, contractClient, xrplRPCClient, xrplKeyringTxSigner) + rnr.BridgeClient = bridgeClient - processor := processes.NewProcessor(zapLogger) - runnerProcesses := Processes{ - XRPLTxObserver: processes.ProcessWithOptions{ - Process: processes.NewXRPLTxObserver( - processes.XRPLTxObserverConfig{ - BridgeXRPLAddress: *bridgeXRPLAddress, - RelayerCoreumAddress: relayerAddress, - }, - zapLogger, - xrplScanner, - contractClient, - ), - Name: "xrpl_tx_observer", - IsRestartableOnError: true, - }, - XRPLTxSubmitter: processes.ProcessWithOptions{ - Process: processes.NewXRPLTxSubmitter( - processes.XRPLTxSubmitterConfig{ - BridgeXRPLAddress: *bridgeXRPLAddress, - RelayerCoreumAddress: relayerAddress, - XRPLTxSignerKeyName: cfg.XRPL.MultiSignerKeyName, - RepeatRecentScan: true, - RepeatDelay: cfg.Processes.XRPLTxSubmitter.RepeatDelay, - }, - zapLogger, - contractClient, - xrplRPCClient, - xrplKeyringTxSigner, - ), - Name: "xrpl_tx_submitter", - IsRestartableOnError: true, - }, + if cfg.Coreum.Contract.ContractAddress != "" { + contractConfig, err := contractClient.GetContractConfig(ctx) + if err != nil { + return nil, errors.Wrapf(err, "failed to get contract config for the runner intialization") + } + + bridgeXRPLAddress, err := rippledata.NewAccountFromAddress(contractConfig.BridgeXRPLAddress) + if err != nil { + return nil, errors.Wrapf(err, "failed to get xrpl account from string, string:%s", contractConfig.BridgeXRPLAddress) + } + xrplScanner := xrpl.NewAccountScanner(xrpl.AccountScannerConfig{ + Account: *bridgeXRPLAddress, + RecentScanEnabled: cfg.XRPL.Scanner.RecentScanEnabled, + RecentScanWindow: cfg.XRPL.Scanner.RecentScanWindow, + RepeatRecentScan: cfg.XRPL.Scanner.RepeatRecentScan, + FullScanEnabled: cfg.XRPL.Scanner.FullScanEnabled, + RepeatFullScan: cfg.XRPL.Scanner.RepeatFullScan, + RetryDelay: cfg.XRPL.Scanner.RetryDelay, + }, zapLogger, xrplRPCClient) + rnr.XRPLAccountScanner = xrplScanner + + rnr.Processor = processes.NewProcessor(zapLogger) + rnr.Processes = Processes{ + XRPLTxObserver: processes.ProcessWithOptions{ + Process: processes.NewXRPLTxObserver( + processes.XRPLTxObserverConfig{ + BridgeXRPLAddress: *bridgeXRPLAddress, + RelayerCoreumAddress: relayerAddress, + }, + zapLogger, + xrplScanner, + contractClient, + ), + Name: "xrpl_tx_observer", + IsRestartableOnError: true, + }, + XRPLTxSubmitter: processes.ProcessWithOptions{ + Process: processes.NewXRPLTxSubmitter( + processes.XRPLTxSubmitterConfig{ + BridgeXRPLAddress: *bridgeXRPLAddress, + RelayerCoreumAddress: relayerAddress, + XRPLTxSignerKeyName: cfg.XRPL.MultiSignerKeyName, + RepeatRecentScan: true, + RepeatDelay: cfg.Processes.XRPLTxSubmitter.RepeatDelay, + }, + zapLogger, + contractClient, + xrplRPCClient, + xrplKeyringTxSigner, + ), + Name: "xrpl_tx_submitter", + IsRestartableOnError: true, + }, + } } - return &Runner{ - Log: zapLogger, - RetryableHTTPClient: &retryableXRPLRPCHTTPClient, - XRPLRPCClient: xrplRPCClient, - XRPLAccountScanner: xrplScanner, - CoreumContractClient: contractClient, - CoreumChainNetworkConfig: coreumChainNetworkConfig, - - Processes: runnerProcesses, - Processor: processor, - }, nil + return rnr, nil } // SetCoreumSDKConfig cosmos sdk config for the set coreum network. diff --git a/relayer/runner/runner_test.go b/relayer/runner/runner_test.go index 8aa889e5..008aba94 100644 --- a/relayer/runner/runner_test.go +++ b/relayer/runner/runner_test.go @@ -43,7 +43,7 @@ logging: level: info format: console xrpl: - multi_signer_key_name: "" + multi_signer_key_name: xrpl-relayer http_client: request_timeout: 5s do_timeout: 30s @@ -59,7 +59,7 @@ xrpl: repeat_full_scan: true retry_delay: 10s coreum: - relayer_key_name: "" + relayer_key_name: coreum-relayer grpc: url: "" network: diff --git a/relayer/xrpl/constatns.go b/relayer/xrpl/constatns.go index 8dc5d909..c5a55b06 100644 --- a/relayer/xrpl/constatns.go +++ b/relayer/xrpl/constatns.go @@ -24,6 +24,13 @@ const ( TemTxResultPrefix = "tem" ) +// Reserves. +var ( + ReserveToActivateAccount = float64(10) + ReservePerTicket = float64(2) + ReservePerSigner = float64(2) +) + const ( // XRPLIssuedTokenDecimals is XRPL decimals used on the coreum. XRPLIssuedTokenDecimals = 15 diff --git a/relayer/processes/fee.go b/relayer/xrpl/fee.go similarity index 96% rename from relayer/processes/fee.go rename to relayer/xrpl/fee.go index 922194d5..fa68df29 100644 --- a/relayer/processes/fee.go +++ b/relayer/xrpl/fee.go @@ -1,4 +1,4 @@ -package processes +package xrpl import ( "github.com/pkg/errors" diff --git a/relayer/xrpl/rpc.go b/relayer/xrpl/rpc.go index 4843dae5..2e7c50b3 100644 --- a/relayer/xrpl/rpc.go +++ b/relayer/xrpl/rpc.go @@ -7,10 +7,12 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/pkg/errors" rippledata "github.com/rubblelabs/ripple/data" + "github.com/CoreumFoundation/coreum-tools/pkg/retry" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/logger" ) @@ -321,3 +323,55 @@ func (c *RPCClient) callRPC(ctx context.Context, method string, params, result a return nil } + +// AutoFillTx add seq number and fee for the transaction. +func (c *RPCClient) AutoFillTx(ctx context.Context, tx rippledata.Transaction, sender rippledata.Account) error { + accInfo, err := c.AccountInfo(ctx, sender) + if err != nil { + return err + } + // update base settings + base := tx.GetBase() + fee, err := GetTxFee(tx) + if err != nil { + return err + } + base.Fee = fee + base.Account = sender + base.Sequence = *accInfo.AccountData.Sequence + + return nil +} + +// SubmitAndAwaitSuccess submits tx a waits for its result, if result is not success returns an error. +func (c *RPCClient) SubmitAndAwaitSuccess(ctx context.Context, tx rippledata.Transaction) error { + c.log.Info(ctx, "Submitting transaction", logger.StringField("txHash", tx.GetHash().String())) + // submit the transaction + res, err := c.Submit(ctx, tx) + if err != nil { + return err + } + if !res.EngineResult.Success() { + return errors.Errorf("the tx submition is failed, %+v", res) + } + + retryCtx, retryCtxCancel := context.WithTimeout(ctx, time.Minute) + defer retryCtxCancel() + c.log.Info( + ctx, + "Transaction is submitted waiting for tx to be accepted", + logger.StringField("txHash", tx.GetHash().String()), + ) + return retry.Do(retryCtx, 250*time.Millisecond, func() error { + reqCtx, reqCtxCancel := context.WithTimeout(ctx, 3*time.Second) + defer reqCtxCancel() + txRes, err := c.Tx(reqCtx, *tx.GetHash()) + if err != nil { + return retry.Retryable(err) + } + if !txRes.Validated { + return retry.Retryable(errors.Errorf("transaction is not validated")) + } + return nil + }) +} diff --git a/relayer/xrpl/signer.go b/relayer/xrpl/signer.go index 6cb9bedb..4117c7d5 100644 --- a/relayer/xrpl/signer.go +++ b/relayer/xrpl/signer.go @@ -129,7 +129,7 @@ func (s *KeyringTxSigner) GetKeyring() keyring.Keyring { func (s *KeyringTxSigner) extractXRPLPrivKey(keyName string) (xrplPrivKey, error) { key, err := s.kr.Key(keyName) if err != nil { - return xrplPrivKey{}, errors.Wrapf(err, "failed to get key xrpl from the keyring, key name:%s", keyName) + return xrplPrivKey{}, errors.Wrapf(err, "failed to get key from the keyring, key name:%s", keyName) } rl := key.GetLocal() if rl.PrivKey == nil {