diff --git a/integration-tests/processes/env_test.go b/integration-tests/processes/env_test.go index dd3f279d..27f0b41e 100644 --- a/integration-tests/processes/env_test.go +++ b/integration-tests/processes/env_test.go @@ -114,7 +114,7 @@ func NewRunnerEnv(ctx context.Context, t *testing.T, cfg RunnerEnvConfig, chains // 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), + Amount: chains.Coreum.QueryAssetFTParams(ctx, t).IssueFee.Amount.AddRaw(10_000_000), }) contractClient := coreum.NewContractClient( @@ -418,7 +418,8 @@ func (r *RunnerEnv) SendFromCoreumToXRPL( amount sdk.Coin, deliverAmount *sdkmath.Int, ) { - require.NoError(t, r.BridgeClient.SendFromCoreumToXRPL(ctx, sender, recipient, amount, deliverAmount)) + _, err := r.BridgeClient.SendFromCoreumToXRPL(ctx, sender, recipient, amount, deliverAmount) + require.NoError(t, err) } // SendFromXRPLToCoreum sends tokens form XRPL to Coreum. @@ -429,7 +430,8 @@ func (r *RunnerEnv) SendFromXRPLToCoreum( amount rippledata.Amount, recipient sdk.AccAddress, ) { - require.NoError(t, r.BridgeClient.SendFromXRPLToCoreum(ctx, senderKeyName, amount, recipient)) + _, err := r.BridgeClient.SendFromXRPLToCoreum(ctx, senderKeyName, amount, recipient) + require.NoError(t, err) } // SendXRPLPaymentTx sends Payment transaction. diff --git a/integration-tests/processes/sending_test.go b/integration-tests/processes/sending_test.go index 657489b5..ea4fa40e 100644 --- a/integration-tests/processes/sending_test.go +++ b/integration-tests/processes/sending_test.go @@ -1953,7 +1953,7 @@ func TestSendFromCoreumToXRPLProhibitedAddresses(t *testing.T) { runnerEnv.BridgeClient.UpdateProhibitedXRPLAddresses(ctx, runnerEnv.ContractOwner, prohibitedXRPLAddresses), ) - err = runnerEnv.BridgeClient.SendFromCoreumToXRPL( + _, err = runnerEnv.BridgeClient.SendFromCoreumToXRPL( ctx, coreumSenderAddress, xrplRecipientAddress, diff --git a/integration-tests/processes/sending_tracing_test.go b/integration-tests/processes/sending_tracing_test.go new file mode 100644 index 00000000..cfe0e896 --- /dev/null +++ b/integration-tests/processes/sending_tracing_test.go @@ -0,0 +1,160 @@ +//go:build integrationtests +// +build integrationtests + +package processes_test + +import ( + "testing" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + rippledata "github.com/rubblelabs/ripple/data" + "github.com/stretchr/testify/require" + + coreumintegration "github.com/CoreumFoundation/coreum/v4/testutil/integration" + integrationtests "github.com/CoreumFoundation/coreumbridge-xrpl/integration-tests" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/coreum" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/xrpl" +) + +func TestTraceXRPLToCoreumTransfer(t *testing.T) { + t.Parallel() + + ctx, chains := integrationtests.NewTestingContext(t) + + envCfg := DefaultRunnerEnvConfig() + runnerEnv := NewRunnerEnv(ctx, t, envCfg, chains) + runnerEnv.StartAllRunnerProcesses() + runnerEnv.AllocateTickets(ctx, t, uint32(200)) + + coreumRecipient := chains.Coreum.GenAccount() + xrplRecipientAddress := chains.XRPL.GenAccount(ctx, t, 0) + t.Logf("XRPL recipient: %s", xrplRecipientAddress.String()) + + xrplIssuerAddress := chains.XRPL.GenAccount(ctx, t, 1) + // enable to be able to send to any address + runnerEnv.EnableXRPLAccountRippling(ctx, t, xrplIssuerAddress) + registeredXRPLCurrency := integrationtests.GenerateXRPLCurrency(t) + registeredXRPLToken := runnerEnv.RegisterXRPLOriginatedToken( + ctx, + t, + xrplIssuerAddress, + registeredXRPLCurrency, + int32(6), + integrationtests.ConvertStringWithDecimalsToSDKInt(t, "1", 30), + sdk.ZeroInt(), + ) + + valueSentToCoreum, err := rippledata.NewValue("1.0", false) + require.NoError(t, err) + amountToSendToCoreum := rippledata.Amount{ + Value: valueSentToCoreum, + Currency: registeredXRPLCurrency, + Issuer: xrplIssuerAddress, + } + + txHash, err := runnerEnv.BridgeClient.SendFromXRPLToCoreum( + ctx, xrplIssuerAddress.String(), amountToSendToCoreum, coreumRecipient, + ) + require.NoError(t, err) + + runnerEnv.AwaitCoreumBalance( + ctx, + t, + coreumRecipient, + sdk.NewCoin( + registeredXRPLToken.CoreumDenom, + integrationtests.ConvertStringWithDecimalsToSDKInt( + t, + valueSentToCoreum.String(), + xrpl.XRPLIssuedTokenDecimals, + ), + ), + ) + + tracingInfo, err := runnerEnv.BridgeClient.GetXRPLToCoreumTracingInfo(ctx, txHash) + require.NoError(t, err) + require.NotNil(t, tracingInfo.CoreumTx) + require.Len(t, tracingInfo.EvidenceToTxs, 2) +} + +func TestTraceCoreumToXRPLTransfer(t *testing.T) { + t.Parallel() + + ctx, chains := integrationtests.NewTestingContext(t) + + xrplRecipient1Address := chains.XRPL.GenAccount(ctx, t, 0) + xrplRecipient2Address := chains.XRPL.GenAccount(ctx, t, 0) + + coreumSenderAddress := chains.Coreum.GenAccount() + issueFee := chains.Coreum.QueryAssetFTParams(ctx, t).IssueFee + chains.Coreum.FundAccountWithOptions(ctx, t, coreumSenderAddress, coreumintegration.BalancesOptions{ + Amount: issueFee.Amount.Add(sdkmath.NewIntWithDecimal(1, 7)), + }) + + envCfg := DefaultRunnerEnvConfig() + runnerEnv := NewRunnerEnv(ctx, t, envCfg, chains) + + // start relayers + runnerEnv.StartAllRunnerProcesses() + // recover tickets so we can register tokens + runnerEnv.AllocateTickets(ctx, t, 200) + + // issue asset ft and register it + sendingPrecision := int32(2) + tokenDecimals := uint32(4) + initialAmount := sdkmath.NewIntWithDecimal(1, 16) + maxHoldingAmount := sdkmath.NewIntWithDecimal(1, 16) + registeredCoreumOriginatedToken := runnerEnv.IssueAndRegisterCoreumOriginatedToken( + ctx, + t, + coreumSenderAddress, + tokenDecimals, + initialAmount, + sendingPrecision, + maxHoldingAmount, + sdkmath.ZeroInt(), + ) + + // send TrustSet to be able to receive coins from the bridge + xrplCurrency, err := rippledata.NewCurrency(registeredCoreumOriginatedToken.XRPLCurrency) + require.NoError(t, err) + runnerEnv.SendXRPLMaxTrustSetTx(ctx, t, xrplRecipient1Address, runnerEnv.BridgeXRPLAddress, xrplCurrency) + runnerEnv.SendXRPLMaxTrustSetTx(ctx, t, xrplRecipient2Address, runnerEnv.BridgeXRPLAddress, xrplCurrency) + + amountToSendToRecipient1 := sdkmath.NewInt(111111) + amountToSendToRecipient2 := sdkmath.NewInt(211111) + txRes, err := runnerEnv.ContractClient.MultiSendToXRPL( + ctx, + coreumSenderAddress, + []coreum.SendToXRPLRequest{ + { + Recipient: xrplRecipient1Address.String(), + DeliverAmount: nil, + Amount: sdk.NewCoin(registeredCoreumOriginatedToken.Denom, amountToSendToRecipient1), + }, + { + Recipient: xrplRecipient2Address.String(), + DeliverAmount: nil, + Amount: sdk.NewCoin(registeredCoreumOriginatedToken.Denom, amountToSendToRecipient2), + }, + }..., + ) + require.NoError(t, err) + runnerEnv.AwaitNoPendingOperations(ctx, t) + + // check the XRPL recipients balance + xrplRecipient1Balance := runnerEnv.Chains.XRPL.GetAccountBalance( + ctx, t, xrplRecipient1Address, runnerEnv.BridgeXRPLAddress, xrplCurrency, + ) + require.Equal(t, "11.11", xrplRecipient1Balance.Value.String()) + xrplRecipient2Balance := runnerEnv.Chains.XRPL.GetAccountBalance( + ctx, t, xrplRecipient2Address, runnerEnv.BridgeXRPLAddress, xrplCurrency, + ) + require.Equal(t, "21.11", xrplRecipient2Balance.Value.String()) + + tracingInfo, err := runnerEnv.BridgeClient.GetCoreumToXRPLTracingInfo(ctx, txRes.TxHash) + require.NoError(t, err) + require.Len(t, tracingInfo.XRPLTxs, 2) + require.Len(t, tracingInfo.EvidenceToTxs, 2) +} diff --git a/integration-tests/stress/env_test.go b/integration-tests/stress/env_test.go index cd09f12a..73b55edb 100644 --- a/integration-tests/stress/env_test.go +++ b/integration-tests/stress/env_test.go @@ -133,9 +133,8 @@ func (env *Env) FundCoreumAccountsWithXRP( coreumFaucetAccount := env.Chains.Coreum.GenAccount() - require.NoError( - t, env.BridgeClient.SendFromXRPLToCoreum(ctx, xrplFaucetAccount.String(), xrpAmount, coreumFaucetAccount), - ) + _, err = env.BridgeClient.SendFromXRPLToCoreum(ctx, xrplFaucetAccount.String(), xrpAmount, coreumFaucetAccount) + require.NoError(t, err) require.NoError(t, env.AwaitCoreumBalance( ctx, @@ -336,7 +335,7 @@ func (env *Env) AwaitContractCall(ctx context.Context, call func() error) error }) } -func (env *Env) callAdminAction(ctx context.Context, action func() error, rollbackAction func() error) error { +func (env *Env) callAdminAction(ctx context.Context, action, rollbackAction func() error) error { ctx, cancel := context.WithTimeout(ctx, env.Cfg.TestCaseTimeout) defer cancel() // use common BridgeClient to prevent sequence mismatch diff --git a/integration-tests/stress/stress_test.go b/integration-tests/stress/stress_test.go index 1dbac808..ba84bca8 100644 --- a/integration-tests/stress/stress_test.go +++ b/integration-tests/stress/stress_test.go @@ -115,9 +115,10 @@ func sendXRPFromXRPLAndBack(ctx context.Context, t *testing.T, env *Env) { ctx, cancel := context.WithTimeout(ctx, env.Cfg.TestCaseTimeout) defer cancel() - if err := bridgeClient.SendFromXRPLToCoreum( + _, err := bridgeClient.SendFromXRPLToCoreum( ctx, xrplAccount.String(), amountToSendFromXRPLtoCoreum, coreumAccount, - ); err != nil { + ) + if err != nil { return err } @@ -141,13 +142,14 @@ func sendXRPFromXRPLAndBack(ctx context.Context, t *testing.T, env *Env) { // send back to XRPL if err = env.AwaitContractCall(ctx, func() error { - return bridgeClient.SendFromCoreumToXRPL( + _, err := bridgeClient.SendFromCoreumToXRPL( ctx, coreumAccount, xrplAccount, coreumAmount, nil, ) + return err }); err != nil { return err } @@ -210,7 +212,7 @@ func sendWithFailureAndClaimRefund(ctx context.Context, t *testing.T, env *Env) xrplAccount := xrpl.GenPrivKeyTxSigner().Account() if err = env.AwaitContractCall(ctx, func() error { - return bridgeClient.SendFromCoreumToXRPL( + _, err := bridgeClient.SendFromCoreumToXRPL( ctx, coreumAccount, xrplAccount, @@ -219,6 +221,7 @@ func sendWithFailureAndClaimRefund(ctx context.Context, t *testing.T, env *Env) amountToSendFromCoreumXRPL), nil, ) + return err }); err != nil { return err } diff --git a/relayer/client/bridge.go b/relayer/client/bridge.go index 4ba4dd9d..8d3f353e 100644 --- a/relayer/client/bridge.go +++ b/relayer/client/bridge.go @@ -151,6 +151,14 @@ type ContractClient interface { sender sdk.AccAddress, codeID uint64, ) (*sdk.TxResponse, error) + GetXRPLToCoreumTracingInfo( + ctx context.Context, + xrplTxHash string, + ) (coreum.XRPLToCoreumTracingInfo, error) + GetCoreumToXRPLTracingInfo( + ctx context.Context, + coreumTxHash string, + ) (coreum.CoreumToXRPLTracingInfo, error) } // XRPLRPCClient is XRPL RPC client interface. @@ -171,6 +179,7 @@ type XRPLRPCClient interface { marker string, ) (xrpl.AccountLinesResult, error) GetXRPLBalances(ctx context.Context, acc rippledata.Account) ([]rippledata.Amount, error) + Tx(ctx context.Context, hash rippledata.Hash256) (xrpl.TxResult, error) } // XRPLTxSigner is XRPL transaction signer. @@ -232,6 +241,20 @@ func DefaultKeysRotationConfig() KeysRotationConfig { } } +// XRPLToCoreumTracingInfo is XRPL to Coreum tracing info. +type XRPLToCoreumTracingInfo struct { + XRPLTx rippledata.TransactionWithMetaData + CoreumTx *sdk.TxResponse + EvidenceToTxs []coreum.DataToTx[coreum.XRPLToCoreumTransferEvidence] +} + +// CoreumToXRPLTracingInfo is Coreum to XRPL tracing info. +type CoreumToXRPLTracingInfo struct { + CoreumTx sdk.TxResponse + XRPLTxs []rippledata.TransactionWithMetaData + EvidenceToTxs [][]coreum.DataToTx[coreum.XRPLTransactionResultEvidence] +} + // BridgeClient is the service responsible for the bridge bootstrapping. type BridgeClient struct { log logger.Logger @@ -597,7 +620,7 @@ func (b *BridgeClient) SendFromCoreumToXRPL( recipient rippledata.Account, amount sdk.Coin, deliverAmount *sdkmath.Int, -) error { +) (string, error) { logFields := []zap.Field{ zap.String("sender", sender.String()), zap.String("amount", amount.String()), @@ -613,11 +636,11 @@ func (b *BridgeClient) SendFromCoreumToXRPL( ) txRes, err := b.contractClient.SendToXRPL(ctx, sender, recipient.String(), amount, deliverAmount) if err != nil { - return err + return "", err } if txRes == nil { - return nil + return "", nil } b.log.Info( @@ -626,7 +649,7 @@ func (b *BridgeClient) SendFromCoreumToXRPL( zap.String("txHash", txRes.TxHash), ) - return nil + return txRes.TxHash, nil } // SendFromXRPLToCoreum sends tokens form XRPL to Coreum. @@ -635,10 +658,10 @@ func (b *BridgeClient) SendFromXRPLToCoreum( senderKeyName string, amount rippledata.Amount, recipient sdk.AccAddress, -) error { +) (string, error) { senderAccount, err := b.xrplTxSigner.Account(senderKeyName) if err != nil { - return err + return "", err } b.log.Info( @@ -651,11 +674,11 @@ func (b *BridgeClient) SendFromXRPLToCoreum( cfg, err := b.contractClient.GetContractConfig(ctx) if err != nil { - return err + return "", err } xrplBridgeAddress, err := rippledata.NewAccountFromAddress(cfg.BridgeXRPLAddress) if err != nil { - return errors.Wrapf( + return "", errors.Wrapf( err, "failed to convert BridgeXRPLAddress from contract to rippledata.Account, address:%s", cfg.BridgeXRPLAddress, @@ -664,7 +687,7 @@ func (b *BridgeClient) SendFromXRPLToCoreum( memo, err := xrpl.EncodeCoreumRecipientToMemo(recipient) if err != nil { - return err + return "", err } paymentTx := rippledata.Payment{ @@ -707,7 +730,11 @@ func (b *BridgeClient) SetXRPLTrustSet( }, } - return b.autoFillSignSubmitAndAwaitXRPLTx(ctx, &trustSetTx, senderKeyName) + if _, err := b.autoFillSignSubmitAndAwaitXRPLTx(ctx, &trustSetTx, senderKeyName); err != nil { + return err + } + + return nil } // UpdateCoreumToken updates Coreum token. @@ -1119,6 +1146,96 @@ func (b *BridgeClient) GetTransactionEvidences(ctx context.Context) ([]coreum.Tr return b.contractClient.GetTransactionEvidences(ctx) } +// GetXRPLToCoreumTracingInfo returns XRPL to Coreum tracing info. +func (b *BridgeClient) GetXRPLToCoreumTracingInfo( + ctx context.Context, + xrplTxHash string, +) (XRPLToCoreumTracingInfo, error) { + b.log.Info(ctx, "Getting XRPL to Coreum transfer tracing info") + xrplHash, err := rippledata.NewHash256(xrplTxHash) + if err != nil { + return XRPLToCoreumTracingInfo{}, errors.Wrapf(err, "invalid XRPL tx hash:%s", xrplTxHash) + } + + tx, err := b.xrplRPCClient.Tx(ctx, *xrplHash) + if err != nil { + return XRPLToCoreumTracingInfo{}, err + } + + if tx.GetType() != rippledata.PAYMENT.String() { + return XRPLToCoreumTracingInfo{}, errors.Errorf( + "invalid XRPL transaction type, expected %s, got: %s", rippledata.PAYMENT.String(), tx.GetType(), + ) + } + paymentTx, ok := tx.Transaction.(*rippledata.Payment) + if !ok { + return XRPLToCoreumTracingInfo{}, errors.Errorf("failed to cast tx to Payment, data:%+v", tx) + } + coreumRecipient := xrpl.DecodeCoreumRecipientFromMemo(paymentTx.Memos) + if coreumRecipient == nil { + return XRPLToCoreumTracingInfo{}, errors.New("XRPL tx memo does not include expected structure") + } + + contractCfg, err := b.contractClient.GetContractConfig(ctx) + if err != nil { + return XRPLToCoreumTracingInfo{}, err + } + + if paymentTx.Destination.String() != contractCfg.BridgeXRPLAddress { + return XRPLToCoreumTracingInfo{}, errors.New("the destination is not bridge XRPL address") + } + + coreumTracingInfo, err := b.contractClient.GetXRPLToCoreumTracingInfo(ctx, xrplTxHash) + if err != nil { + return XRPLToCoreumTracingInfo{}, err + } + + return XRPLToCoreumTracingInfo{ + XRPLTx: tx.TransactionWithMetaData, + CoreumTx: coreumTracingInfo.CoreumTx, + EvidenceToTxs: coreumTracingInfo.EvidenceToTxs, + }, nil +} + +// GetCoreumToXRPLTracingInfo returns Coreum to XRPL tracing info. +func (b *BridgeClient) GetCoreumToXRPLTracingInfo( + ctx context.Context, + coreumTxHash string, +) (CoreumToXRPLTracingInfo, error) { + b.log.Info(ctx, "Getting Coreum to XRPL transfer tracing info") + + tracingInfo, err := b.contractClient.GetCoreumToXRPLTracingInfo(ctx, coreumTxHash) + if err != nil { + return CoreumToXRPLTracingInfo{}, err + } + + coreumToXRPLTracingInfo := CoreumToXRPLTracingInfo{ + CoreumTx: tracingInfo.CoreumTx, + XRPLTxs: make([]rippledata.TransactionWithMetaData, 0), + EvidenceToTxs: tracingInfo.EvidenceToTxs, + } + + for _, xrplTxHashString := range tracingInfo.XRPLTxHashes { + xrplHash, err := rippledata.NewHash256(xrplTxHashString) + if err != nil { + return CoreumToXRPLTracingInfo{}, errors.Wrapf(err, "invalid XRPL tx hash:%s", xrplTxHashString) + } + tx, err := b.xrplRPCClient.Tx(ctx, *xrplHash) + if err != nil { + return CoreumToXRPLTracingInfo{}, err + } + if tx.GetType() != rippledata.PAYMENT.String() { + return CoreumToXRPLTracingInfo{}, errors.Errorf( + "invalid XRPL transaction type, expected %s, got: %s, hash:%s", + rippledata.PAYMENT.String(), tx.GetType(), xrplTxHashString, + ) + } + coreumToXRPLTracingInfo.XRPLTxs = append(coreumToXRPLTracingInfo.XRPLTxs, tx.TransactionWithMetaData) + } + + return coreumToXRPLTracingInfo, nil +} + func (b *BridgeClient) validateXRPLBridgeAccountBalance( ctx context.Context, xrplBridgeAccount rippledata.Account, @@ -1167,7 +1284,7 @@ func (b *BridgeClient) setUpXRPLBridgeAccount( TransactionType: rippledata.ACCOUNT_SET, }, } - if err := b.autoFillSignSubmitAndAwaitXRPLTx(ctx, &enableRipplingTx, bridgeAccountKeyName); err != nil { + if _, err := b.autoFillSignSubmitAndAwaitXRPLTx(ctx, &enableRipplingTx, bridgeAccountKeyName); err != nil { return err } @@ -1179,7 +1296,7 @@ func (b *BridgeClient) setUpXRPLBridgeAccount( TransactionType: rippledata.SIGNER_LIST_SET, }, } - if err := b.autoFillSignSubmitAndAwaitXRPLTx(ctx, &signerListSetTx, bridgeAccountKeyName); err != nil { + if _, err := b.autoFillSignSubmitAndAwaitXRPLTx(ctx, &signerListSetTx, bridgeAccountKeyName); err != nil { return err } @@ -1191,7 +1308,11 @@ func (b *BridgeClient) setUpXRPLBridgeAccount( }, SetFlag: lo.ToPtr(uint32(rippledata.TxSetDisableMaster)), } - return b.autoFillSignSubmitAndAwaitXRPLTx(ctx, &disableMasterKeyTx, bridgeAccountKeyName) + if _, err := b.autoFillSignSubmitAndAwaitXRPLTx(ctx, &disableMasterKeyTx, bridgeAccountKeyName); err != nil { + return err + } + + return nil } // ComputeXRPLBridgeAccountBalance computes the min balance required by the XRPL bridge account. @@ -1282,23 +1403,23 @@ func (b *BridgeClient) autoFillSignSubmitAndAwaitXRPLTx( ctx context.Context, tx rippledata.Transaction, signerKeyName string, -) error { +) (string, error) { sender, err := b.xrplTxSigner.Account(signerKeyName) if err != nil { - return err + return "", err } if err := b.xrplRPCClient.AutoFillTx(ctx, tx, sender, xrpl.MaxAllowedXRPLSigners); err != nil { - return err + return "", err } if err := b.xrplTxSigner.Sign(tx, signerKeyName); err != nil { - return err + return "", err } b.log.Info(ctx, "Submitting XRPL transaction", zap.String("txHash", tx.GetHash().String())) if err = b.xrplRPCClient.SubmitAndAwaitSuccess(ctx, tx); err != nil { - return err + return "", err } b.log.Info(ctx, "Successfully submitted transaction", zap.String("txHash", tx.GetHash().String())) - return nil + return tx.GetHash().String(), nil } diff --git a/relayer/cmd/cli/cli.go b/relayer/cmd/cli/cli.go index c201bbcb..82d244f0 100644 --- a/relayer/cmd/cli/cli.go +++ b/relayer/cmd/cli/cli.go @@ -142,13 +142,13 @@ type BridgeClient interface { recipient rippledata.Account, amount sdk.Coin, deliverAmount *sdkmath.Int, - ) error + ) (string, error) SendFromXRPLToCoreum( ctx context.Context, senderKeyName string, amount rippledata.Amount, recipient sdk.AccAddress, - ) error + ) (string, error) SetXRPLTrustSet( ctx context.Context, senderKeyName string, @@ -219,6 +219,11 @@ type BridgeClient interface { sender sdk.AccAddress, contractByteCodePath string, ) (*sdk.TxResponse, uint64, error) + GetXRPLToCoreumTracingInfo(ctx context.Context, xrplTxHash string) (bridgeclient.XRPLToCoreumTracingInfo, error) + GetCoreumToXRPLTracingInfo( + ctx context.Context, + coreumTxHash string, + ) (bridgeclient.CoreumToXRPLTracingInfo, error) } // BridgeClientProvider is function which returns the BridgeClient from the input cmd. diff --git a/relayer/cmd/cli/cli_mocks_test.go b/relayer/cmd/cli/cli_mocks_test.go index 5b15db4f..2c131efc 100644 --- a/relayer/cmd/cli/cli_mocks_test.go +++ b/relayer/cmd/cli/cli_mocks_test.go @@ -174,6 +174,21 @@ func (mr *MockBridgeClientMockRecorder) GetCoreumBalances(arg0, arg1 interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCoreumBalances", reflect.TypeOf((*MockBridgeClient)(nil).GetCoreumBalances), arg0, arg1) } +// GetCoreumToXRPLTracingInfo mocks base method. +func (m *MockBridgeClient) GetCoreumToXRPLTracingInfo(arg0 context.Context, arg1 string) (client.CoreumToXRPLTracingInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCoreumToXRPLTracingInfo", arg0, arg1) + ret0, _ := ret[0].(client.CoreumToXRPLTracingInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCoreumToXRPLTracingInfo indicates an expected call of GetCoreumToXRPLTracingInfo. +func (mr *MockBridgeClientMockRecorder) GetCoreumToXRPLTracingInfo(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCoreumToXRPLTracingInfo", reflect.TypeOf((*MockBridgeClient)(nil).GetCoreumToXRPLTracingInfo), arg0, arg1) +} + // GetFeesCollected mocks base method. func (m *MockBridgeClient) GetFeesCollected(arg0 context.Context, arg1 types.Address) (types.Coins, error) { m.ctrl.T.Helper() @@ -264,6 +279,21 @@ func (mr *MockBridgeClientMockRecorder) GetXRPLBalances(arg0, arg1 interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetXRPLBalances", reflect.TypeOf((*MockBridgeClient)(nil).GetXRPLBalances), arg0, arg1) } +// GetXRPLToCoreumTracingInfo mocks base method. +func (m *MockBridgeClient) GetXRPLToCoreumTracingInfo(arg0 context.Context, arg1 string) (client.XRPLToCoreumTracingInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetXRPLToCoreumTracingInfo", arg0, arg1) + ret0, _ := ret[0].(client.XRPLToCoreumTracingInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetXRPLToCoreumTracingInfo indicates an expected call of GetXRPLToCoreumTracingInfo. +func (mr *MockBridgeClientMockRecorder) GetXRPLToCoreumTracingInfo(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetXRPLToCoreumTracingInfo", reflect.TypeOf((*MockBridgeClient)(nil).GetXRPLToCoreumTracingInfo), arg0, arg1) +} + // HaltBridge mocks base method. func (m *MockBridgeClient) HaltBridge(arg0 context.Context, arg1 types.AccAddress) error { m.ctrl.T.Helper() @@ -365,11 +395,12 @@ func (mr *MockBridgeClientMockRecorder) RotateKeys(arg0, arg1, arg2 interface{}) } // SendFromCoreumToXRPL mocks base method. -func (m *MockBridgeClient) SendFromCoreumToXRPL(arg0 context.Context, arg1 types.AccAddress, arg2 data.Account, arg3 types.Coin, arg4 *math.Int) error { +func (m *MockBridgeClient) SendFromCoreumToXRPL(arg0 context.Context, arg1 types.AccAddress, arg2 data.Account, arg3 types.Coin, arg4 *math.Int) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SendFromCoreumToXRPL", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 } // SendFromCoreumToXRPL indicates an expected call of SendFromCoreumToXRPL. @@ -379,11 +410,12 @@ func (mr *MockBridgeClientMockRecorder) SendFromCoreumToXRPL(arg0, arg1, arg2, a } // SendFromXRPLToCoreum mocks base method. -func (m *MockBridgeClient) SendFromXRPLToCoreum(arg0 context.Context, arg1 string, arg2 data.Amount, arg3 types.AccAddress) error { +func (m *MockBridgeClient) SendFromXRPLToCoreum(arg0 context.Context, arg1 string, arg2 data.Amount, arg3 types.AccAddress) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SendFromXRPLToCoreum", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 } // SendFromXRPLToCoreum indicates an expected call of SendFromXRPLToCoreum. diff --git a/relayer/cmd/cli/coreum_cli.go b/relayer/cmd/cli/coreum_cli.go index bea2cd9e..47508c9d 100644 --- a/relayer/cmd/cli/coreum_cli.go +++ b/relayer/cmd/cli/coreum_cli.go @@ -72,6 +72,7 @@ func CoreumCmd(bcp BridgeClientProvider) (*cobra.Command, error) { coreumQueryCmd.AddCommand(PendingOperationsCmd(bcp)) coreumQueryCmd.AddCommand(ProhibitedXRPLAddressesCmd(bcp)) coreumQueryCmd.AddCommand(TransactionEvidencesCmd(bcp)) + coreumQueryCmd.AddCommand(TraceCoreumToXRPLTransfer(bcp)) AddHomeFlag(coreumQueryCmd) @@ -676,7 +677,8 @@ $ send-from-coreum-to-xrpl 1000000ucore rrrrrrrrrrrrrrrrrrrrrhoLvTp --%s sender return errors.Wrapf(err, "failed to convert recipient string to rippledata.Account: %s", args[1]) } - return bridgeClient.SendFromCoreumToXRPL(ctx, sender, *recipient, amount, deliverAmount) + _, err = bridgeClient.SendFromCoreumToXRPL(ctx, sender, *recipient, amount, deliverAmount) + return err }), } @@ -1001,6 +1003,42 @@ func TransactionEvidencesCmd(bcp BridgeClientProvider) *cobra.Command { } } +// TraceCoreumToXRPLTransfer prints Coreum to XRPL transfer tracing info. +func TraceCoreumToXRPLTransfer(bcp BridgeClientProvider) *cobra.Command { + return &cobra.Command{ + Use: "trace-coreum-to-xrpl-transfer [coreum tx hash]", + Short: "Coreum to XRPL transfer tracing info.", + Args: cobra.ExactArgs(1), + RunE: runBridgeCmd(bcp, + func(cmd *cobra.Command, args []string, components runner.Components, bridgeClient BridgeClient) error { + ctx := cmd.Context() + + xrplTxHash := args[0] + tracingInfo, err := bridgeClient.GetCoreumToXRPLTracingInfo(ctx, xrplTxHash) + if err != nil { + return err + } + + if len(tracingInfo.XRPLTxs) == 0 { + components.Log.Info( + ctx, + "No XRPL transactions found. The transfer might be in-progress.", + ) + return nil + } + + components.Log.Info( + ctx, + "Transfer is complete for XRPL txs.", + zap.Strings("xrplTxHashes", lo.Map(tracingInfo.XRPLTxs, func(tx rippledata.TransactionWithMetaData, _ int) string { + return tx.GetHash().String() + })), + ) + return nil + }), + } +} + // CoreumTxPreRun is Coreum transaction CMD pre-run function. func CoreumTxPreRun(bcp BridgeClientProvider) func(cmd *cobra.Command, args []string) error { return runBridgeCmd(bcp, diff --git a/relayer/cmd/cli/coreum_cli_test.go b/relayer/cmd/cli/coreum_cli_test.go index 8d2fd76b..886fff57 100644 --- a/relayer/cmd/cli/coreum_cli_test.go +++ b/relayer/cmd/cli/coreum_cli_test.go @@ -1247,6 +1247,30 @@ func TestTransactionEvidencesCmd(t *testing.T) { executeQueryCmd(t, cli.TransactionEvidencesCmd(mockBridgeClientProvider(bridgeClientMock)), initConfig(t)...) } +func TestTraceCoreumToXRPLTransfer(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + bridgeClientMock := NewMockBridgeClient(ctrl) + + coreumTxHash := "hash" + args := append(initConfig(t), coreumTxHash) + + bridgeClientMock.EXPECT().GetCoreumToXRPLTracingInfo(gomock.Any(), coreumTxHash). + Return(bridgeclient.CoreumToXRPLTracingInfo{ + XRPLTxs: []rippledata.TransactionWithMetaData{ + { + Transaction: &rippledata.Payment{ + TxBase: rippledata.TxBase{ + Hash: rippledata.Hash256{}, + }, + }, + }, + }, + }, nil) + executeQueryCmd(t, cli.TraceCoreumToXRPLTransfer(mockBridgeClientProvider(bridgeClientMock)), args...) +} + func executeCoreumTxCmd(t *testing.T, bcp cli.BridgeClientProvider, cmd *cobra.Command, args ...string) { cli.AddCoreumTxFlags(cmd) cmd.PreRunE = cli.CoreumTxPreRun(bcp) diff --git a/relayer/cmd/cli/xrpl_cli.go b/relayer/cmd/cli/xrpl_cli.go index 08dc4d2e..8a135867 100644 --- a/relayer/cmd/cli/xrpl_cli.go +++ b/relayer/cmd/cli/xrpl_cli.go @@ -40,6 +40,7 @@ func XRPLCmd(bcp BridgeClientProvider) (*cobra.Command, error) { Short: "XRPL queries.", } xrplQueryCmd.AddCommand(XRPLBalancesCmd(bcp)) + xrplQueryCmd.AddCommand(TraceXRPLToCoreumTransfer(bcp)) AddHomeFlag(xrplQueryCmd) keyringXRPLCmd, err := KeyringCmd(XRPLKeyringSuffix, xrpl.CoinType, @@ -109,7 +110,7 @@ $ send-from-xrpl-to-coreum 1000000 %s %s %s --%s sender return errors.Wrapf(err, "failed to get flag %s", FlagKeyName) } - return bridgeClient.SendFromXRPLToCoreum( + _, err = bridgeClient.SendFromXRPLToCoreum( ctx, keyName, rippledata.Amount{ @@ -119,6 +120,8 @@ $ send-from-xrpl-to-coreum 1000000 %s %s %s --%s sender }, recipient, ) + + return err }), } } @@ -213,3 +216,45 @@ func XRPLBalancesCmd(bcp BridgeClientProvider) *cobra.Command { }), } } + +// TraceXRPLToCoreumTransfer prints XRPL to Coreum transfer tracing info. +func TraceXRPLToCoreumTransfer(bcp BridgeClientProvider) *cobra.Command { + return &cobra.Command{ + Use: "trace-xrpl-to-coreum-transfer [xrpl tx hash]", + Short: "XRPL to Coreum transfer tracing info.", + Args: cobra.ExactArgs(1), + RunE: runBridgeCmd(bcp, + func(cmd *cobra.Command, args []string, components runner.Components, bridgeClient BridgeClient) error { + ctx := cmd.Context() + + xrplTxHash := args[0] + tracingInfo, err := bridgeClient.GetXRPLToCoreumTracingInfo(ctx, xrplTxHash) + if err != nil { + return err + } + + if tracingInfo.CoreumTx != nil { + components.Log.Info( + ctx, + "Transfer is complete.", + zap.String("coreumTxHah", tracingInfo.CoreumTx.TxHash), + ) + return nil + } + + contractCfg, err := bridgeClient.GetContractConfig(ctx) + if err != nil { + return err + } + + components.Log.Info( + ctx, + "Transfer is in progress.", + zap.Int("evidencesProvided", len(tracingInfo.EvidenceToTxs)), + zap.Uint32("threshold", contractCfg.EvidenceThreshold), + ) + + return nil + }), + } +} diff --git a/relayer/cmd/cli/xrpl_cli_test.go b/relayer/cmd/cli/xrpl_cli_test.go index 1ebf4a67..df6a7d01 100644 --- a/relayer/cmd/cli/xrpl_cli_test.go +++ b/relayer/cmd/cli/xrpl_cli_test.go @@ -3,11 +3,14 @@ package cli_test import ( "testing" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/golang/mock/gomock" rippledata "github.com/rubblelabs/ripple/data" "github.com/stretchr/testify/require" + bridgeclient "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/client" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/cmd/cli" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/coreum" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/xrpl" ) @@ -57,3 +60,29 @@ func TestXRPBalancesCmd(t *testing.T) { executeQueryCmd(t, cli.XRPLBalancesCmd(mockBridgeClientProvider(bridgeClientMock)), append(initConfig(t), account.String())...) } + +func TestTraceXRPLToCoreumTransfer(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + bridgeClientMock := NewMockBridgeClient(ctrl) + + xrplTxHash := "hash" + args := append(initConfig(t), xrplTxHash) + + // complete + bridgeClientMock.EXPECT().GetXRPLToCoreumTracingInfo(gomock.Any(), xrplTxHash). + Return(bridgeclient.XRPLToCoreumTracingInfo{ + CoreumTx: &sdk.TxResponse{}, + }, nil) + executeQueryCmd(t, cli.TraceXRPLToCoreumTransfer(mockBridgeClientProvider(bridgeClientMock)), args...) + + // pending + bridgeClientMock.EXPECT().GetContractConfig(gomock.Any()).Return(coreum.ContractConfig{}, nil) + bridgeClientMock.EXPECT().GetXRPLToCoreumTracingInfo(gomock.Any(), xrplTxHash). + Return(bridgeclient.XRPLToCoreumTracingInfo{ + CoreumTx: nil, + }, nil) + + executeQueryCmd(t, cli.TraceXRPLToCoreumTransfer(mockBridgeClientProvider(bridgeClientMock)), args...) +} diff --git a/relayer/coreum/contract.go b/relayer/coreum/contract.go index 86593a4a..4a3e0715 100644 --- a/relayer/coreum/contract.go +++ b/relayer/coreum/contract.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "strconv" "strings" "sync" "time" @@ -14,8 +15,12 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" sdk "github.com/cosmos/cosmos-sdk/types" cosmoserrors "github.com/cosmos/cosmos-sdk/types/errors" + grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" + sdktxtypes "github.com/cosmos/cosmos-sdk/types/tx" "github.com/pkg/errors" + "github.com/samber/lo" "go.uber.org/zap" + "google.golang.org/grpc/metadata" "github.com/CoreumFoundation/coreum-tools/pkg/retry" "github.com/CoreumFoundation/coreum/v4/pkg/client" @@ -27,6 +32,12 @@ import ( const ( contractLabel = "coreumbridge-xrpl" + + eventAttributeAction = "action" + eventAttributeHash = "hash" + eventAttributeThresholdReached = "threshold_reached" + eventAttributeOperationID = "operation_id" + eventValueSaveAction = "save_evidence" ) // ExecMethod is contract exec method. @@ -262,9 +273,9 @@ func (o Operation) GetOperationID() uint32 { // SendToXRPLRequest defines single request to send from coreum to XRPL. type SendToXRPLRequest struct { - Recipient string - Amount sdk.Coin - DeliverAmount *sdkmath.Int + Recipient string `json:"recipient"` + DeliverAmount *sdkmath.Int `json:"deliver_amount,omitempty"` + Amount sdk.Coin `json:"-"` } // SaveSignatureRequest defines single request to save relayer signature. @@ -287,6 +298,38 @@ type TransactionEvidence struct { RelayerAddresses []sdk.AccAddress `json:"relayer_addresses"` } +// DataToTx is data to tx mapping. +type DataToTx[T any] struct { + Evidence T + Tx *sdk.TxResponse +} + +// XRPLToCoreumTracingInfo is XRPL to Coreum tracing info. +type XRPLToCoreumTracingInfo struct { + CoreumTx *sdk.TxResponse + EvidenceToTxs []DataToTx[XRPLToCoreumTransferEvidence] +} + +// CoreumToXRPLTracingInfo is Coreum to XRPL tracing info. +// +//nolint:revive //kept for the better naming convention. +type CoreumToXRPLTracingInfo struct { + XRPLTxHashes []string + CoreumTx sdk.TxResponse + EvidenceToTxs [][]DataToTx[XRPLTransactionResultEvidence] +} + +// SaveEvidenceRequest is save_evidence method request. +type SaveEvidenceRequest struct { + Evidence evidence `json:"evidence"` +} + +// ExecutePayload aggregates execute contract payload. +type ExecutePayload struct { + SaveEvidence *SaveEvidenceRequest `json:"save_evidence,omitempty"` + SendToXRPL *SendToXRPLRequest `json:"send_to_xrpl,omitempty"` +} + // ******************** Internal transport object ******************** type instantiateRequest struct { @@ -321,10 +364,6 @@ type registerXRPLTokenRequest struct { BridgingFee sdkmath.Int `json:"bridging_fee"` } -type saveEvidenceRequest struct { - Evidence evidence `json:"evidence"` -} - type recoverTicketsRequest struct { AccountSequence uint32 `json:"account_sequence"` NumberOfTickets *uint32 `json:"number_of_tickets,omitempty"` @@ -341,11 +380,6 @@ type saveSignatureRequest struct { Signature string `json:"signature"` } -type sendToXRPLRequest struct { - DeliverAmount *sdkmath.Int `json:"deliver_amount,omitempty"` - Recipient string `json:"recipient"` -} - type recoverXRPLTokenRegistrationRequest struct { Issuer string `json:"issuer"` Currency string `json:"currency"` @@ -474,6 +508,7 @@ type ContractClientConfig struct { PageLimit uint32 OutOfGasRetryDelay time.Duration OutOfGasRetryAttempts uint32 + TxsQueryPageLimit uint32 } // DefaultContractClientConfig returns default ContractClient config. @@ -485,16 +520,18 @@ func DefaultContractClientConfig(contractAddress sdk.AccAddress) ContractClientC PageLimit: 50, OutOfGasRetryDelay: 500 * time.Millisecond, OutOfGasRetryAttempts: 5, + TxsQueryPageLimit: 1000, } } // ContractClient is the bridge contract client. type ContractClient struct { - cfg ContractClientConfig - log logger.Logger - clientCtx client.Context - wasmClient wasmtypes.QueryClient - assetftClient assetfttypes.QueryClient + cfg ContractClientConfig + log logger.Logger + clientCtx client.Context + wasmClient wasmtypes.QueryClient + assetftClient assetfttypes.QueryClient + cometServiceClient sdktxtypes.ServiceClient execMu sync.Mutex } @@ -508,8 +545,9 @@ func NewContractClient(cfg ContractClientConfig, log logger.Logger, clientCtx cl WithBroadcastMode(flags.BroadcastSync). WithAwaitTx(true).WithGasPriceAdjustment(cfg.GasPriceAdjustment). WithGasAdjustment(cfg.GasAdjustment), - wasmClient: wasmtypes.NewQueryClient(clientCtx), - assetftClient: assetfttypes.NewQueryClient(clientCtx), + wasmClient: wasmtypes.NewQueryClient(clientCtx), + assetftClient: assetfttypes.NewQueryClient(clientCtx), + cometServiceClient: sdktxtypes.NewServiceClient(clientCtx), execMu: sync.Mutex{}, } @@ -751,13 +789,13 @@ func (c *ContractClient) SendXRPLToCoreumTransferEvidence( sender sdk.AccAddress, evd XRPLToCoreumTransferEvidence, ) (*sdk.TxResponse, error) { - req := saveEvidenceRequest{ + req := SaveEvidenceRequest{ Evidence: evidence{ XRPLToCoreumTransfer: &evd, }, } txRes, err := c.execute(ctx, sender, execRequest{ - Body: map[ExecMethod]saveEvidenceRequest{ + Body: map[ExecMethod]SaveEvidenceRequest{ ExecMethodSaveEvidence: req, }, }) @@ -775,7 +813,7 @@ func (c *ContractClient) SendXRPLTicketsAllocationTransactionResultEvidence( sender sdk.AccAddress, evd XRPLTransactionResultTicketsAllocationEvidence, ) (*sdk.TxResponse, error) { - req := saveEvidenceRequest{ + req := SaveEvidenceRequest{ Evidence: evidence{ XRPLTransactionResult: &xrplTransactionResultEvidence{ XRPLTransactionResultEvidence: evd.XRPLTransactionResultEvidence, @@ -788,7 +826,7 @@ func (c *ContractClient) SendXRPLTicketsAllocationTransactionResultEvidence( }, } txRes, err := c.execute(ctx, sender, execRequest{ - Body: map[ExecMethod]saveEvidenceRequest{ + Body: map[ExecMethod]SaveEvidenceRequest{ ExecMethodSaveEvidence: req, }, }) @@ -805,7 +843,7 @@ func (c *ContractClient) SendXRPLTrustSetTransactionResultEvidence( sender sdk.AccAddress, evd XRPLTransactionResultTrustSetEvidence, ) (*sdk.TxResponse, error) { - req := saveEvidenceRequest{ + req := SaveEvidenceRequest{ Evidence: evidence{ XRPLTransactionResult: &xrplTransactionResultEvidence{ XRPLTransactionResultEvidence: evd.XRPLTransactionResultEvidence, @@ -813,7 +851,7 @@ func (c *ContractClient) SendXRPLTrustSetTransactionResultEvidence( }, } txRes, err := c.execute(ctx, sender, execRequest{ - Body: map[ExecMethod]saveEvidenceRequest{ + Body: map[ExecMethod]SaveEvidenceRequest{ ExecMethodSaveEvidence: req, }, }) @@ -831,7 +869,7 @@ func (c *ContractClient) SendCoreumToXRPLTransferTransactionResultEvidence( sender sdk.AccAddress, evd XRPLTransactionResultCoreumToXRPLTransferEvidence, ) (*sdk.TxResponse, error) { - req := saveEvidenceRequest{ + req := SaveEvidenceRequest{ Evidence: evidence{ XRPLTransactionResult: &xrplTransactionResultEvidence{ XRPLTransactionResultEvidence: evd.XRPLTransactionResultEvidence, @@ -839,7 +877,7 @@ func (c *ContractClient) SendCoreumToXRPLTransferTransactionResultEvidence( }, } txRes, err := c.execute(ctx, sender, execRequest{ - Body: map[ExecMethod]saveEvidenceRequest{ + Body: map[ExecMethod]SaveEvidenceRequest{ ExecMethodSaveEvidence: req, }, }) @@ -857,7 +895,7 @@ func (c *ContractClient) SendKeysRotationTransactionResultEvidence( sender sdk.AccAddress, evd XRPLTransactionResultKeysRotationEvidence, ) (*sdk.TxResponse, error) { - req := saveEvidenceRequest{ + req := SaveEvidenceRequest{ Evidence: evidence{ XRPLTransactionResult: &xrplTransactionResultEvidence{ XRPLTransactionResultEvidence: evd.XRPLTransactionResultEvidence, @@ -865,7 +903,7 @@ func (c *ContractClient) SendKeysRotationTransactionResultEvidence( }, } txRes, err := c.execute(ctx, sender, execRequest{ - Body: map[ExecMethod]saveEvidenceRequest{ + Body: map[ExecMethod]SaveEvidenceRequest{ ExecMethodSaveEvidence: req, }, }) @@ -967,11 +1005,8 @@ func (c *ContractClient) MultiSendToXRPL( execRequests := make([]execRequest, 0, len(requests)) for _, req := range requests { execRequests = append(execRequests, execRequest{ - Body: map[ExecMethod]sendToXRPLRequest{ - ExecSendToXRPL: { - DeliverAmount: req.DeliverAmount, - Recipient: req.Recipient, - }, + Body: map[ExecMethod]SendToXRPLRequest{ + ExecSendToXRPL: req, }, Funds: sdk.NewCoins(req.Amount), }) @@ -1420,6 +1455,140 @@ func (c *ContractClient) GetProhibitedXRPLAddresses(ctx context.Context) ([]stri return response.ProhibitedXRPLAddresses, nil } +// GetXRPLToCoreumTracingInfo returns XRPL to Coreum tracing info. +func (c *ContractClient) GetXRPLToCoreumTracingInfo( + ctx context.Context, + xrplTxHash string, +) (XRPLToCoreumTracingInfo, error) { + txs, err := c.getContractTransactionsByWasmEventAttributes(ctx, + map[string]string{ + eventAttributeAction: eventValueSaveAction, + eventAttributeHash: xrplTxHash, + }, + ) + if err != nil { + return XRPLToCoreumTracingInfo{}, err + } + + xrplToCoreumTracingInfo := XRPLToCoreumTracingInfo{ + EvidenceToTxs: make([]DataToTx[XRPLToCoreumTransferEvidence], 0), + } + for _, tx := range txs { + executePayloads, err := c.decodeExecutePayload(tx) + if err != nil { + return XRPLToCoreumTracingInfo{}, err + } + for i, payload := range executePayloads { + if payload.SaveEvidence == nil || payload.SaveEvidence.Evidence.XRPLToCoreumTransfer == nil { + continue + } + xrplToCoreumTracingInfo.EvidenceToTxs = append( + xrplToCoreumTracingInfo.EvidenceToTxs, + DataToTx[XRPLToCoreumTransferEvidence]{ + Evidence: *payload.SaveEvidence.Evidence.XRPLToCoreumTransfer, + Tx: tx, + }) + if isEventValueEqual(tx.Logs[i].Events, wasmtypes.WasmModuleEventType, eventAttributeThresholdReached, "true") { + xrplToCoreumTracingInfo.CoreumTx = tx + } + } + } + + return xrplToCoreumTracingInfo, nil +} + +// GetCoreumToXRPLTracingInfo returns Coreum to XRPL tracing info. +func (c *ContractClient) GetCoreumToXRPLTracingInfo( + ctx context.Context, + coreumTxHash string, +) (CoreumToXRPLTracingInfo, error) { + txRes, err := c.cometServiceClient.GetTx(ctx, &sdktxtypes.GetTxRequest{ + Hash: coreumTxHash, + }) + if err != nil { + return CoreumToXRPLTracingInfo{}, err + } + if txRes == nil || txRes.TxResponse == nil { + return CoreumToXRPLTracingInfo{}, errors.Errorf("tx with hash %s not found", coreumTxHash) + } + tx := txRes.TxResponse + + executePayloads, err := c.decodeExecutePayload(tx) + if err != nil { + return CoreumToXRPLTracingInfo{}, err + } + if len(executePayloads) == 0 { + return CoreumToXRPLTracingInfo{}, errors.Errorf("the tx is not the WASM tx") + } + + coreumToXRPLTracingInfo := CoreumToXRPLTracingInfo{ + CoreumTx: *tx, + XRPLTxHashes: make([]string, 0), + EvidenceToTxs: make([][]DataToTx[XRPLTransactionResultEvidence], 0), + } + + for _, payload := range executePayloads { + if payload.SendToXRPL == nil { + continue + } + operationIDs, err := c.getSendToXRPLOperationIDs(ctx, *payload.SendToXRPL, tx.Height) + if err != nil { + return CoreumToXRPLTracingInfo{}, err + } + for _, operationID := range operationIDs { + evidenceToTxs, xrplTxHashes, err := c.getXRPLTxsFromSaveTxResultEvidenceForOperation(ctx, operationID) + if err != nil { + return CoreumToXRPLTracingInfo{}, err + } + coreumToXRPLTracingInfo.EvidenceToTxs = append(coreumToXRPLTracingInfo.EvidenceToTxs, evidenceToTxs) + coreumToXRPLTracingInfo.XRPLTxHashes = append(coreumToXRPLTracingInfo.XRPLTxHashes, xrplTxHashes...) + } + } + + return coreumToXRPLTracingInfo, err +} + +func (c *ContractClient) getXRPLTxsFromSaveTxResultEvidenceForOperation( + ctx context.Context, + operationID uint32, +) ([]DataToTx[XRPLTransactionResultEvidence], []string, error) { + txs, err := c.getContractTransactionsByWasmEventAttributes(ctx, + map[string]string{ + eventAttributeAction: eventValueSaveAction, + eventAttributeOperationID: strconv.FormatUint(uint64(operationID), 10), + }, + ) + if err != nil { + return nil, nil, err + } + // find corresponding XRPL txs + evidenceToTxs := make([]DataToTx[XRPLTransactionResultEvidence], 0) + xrplTxHashes := make(map[string]struct{}, 0) + for _, tx := range txs { + executePayloads, err := c.decodeExecutePayload(tx) + if err != nil { + return nil, nil, err + } + for _, payload := range executePayloads { + if payload.SaveEvidence == nil || + payload.SaveEvidence.Evidence.XRPLTransactionResult == nil { + continue + } + evidenceToTxs = append( + evidenceToTxs, + DataToTx[XRPLTransactionResultEvidence]{ + Evidence: payload.SaveEvidence.Evidence.XRPLTransactionResult.XRPLTransactionResultEvidence, + Tx: tx, + }) + xrplTxHashes[payload.SaveEvidence.Evidence.XRPLTransactionResult.TxHash] = struct{}{} + } + } + + return evidenceToTxs, lo.MapToSlice(xrplTxHashes, func(hash string, _ struct{}) string { + return hash + }), nil +} + func (c *ContractClient) getPaginatedXRPLTokens( ctx context.Context, startAfterKey string, @@ -1635,6 +1804,140 @@ func (c *ContractClient) getTxFactory() client.Factory { WithSimulateAndExecute(true) } +func (c *ContractClient) getContractTransactionsByWasmEventAttributes( + ctx context.Context, + attributes map[string]string, +) ([]*sdk.TxResponse, error) { + page := uint64(0) + txResponses := make([]*sdk.TxResponse, 0) + events := []string{ + fmt.Sprintf( + "%s.%s='%s'", + wasmtypes.WasmModuleEventType, + wasmtypes.AttributeKeyContractAddr, + c.GetContractAddress().String(), + ), + } + for key, value := range attributes { + events = append(events, fmt.Sprintf( + "%s.%s='%s'", + wasmtypes.WasmModuleEventType, + key, + value, + )) + } + + attributes[wasmtypes.AttributeKeyContractAddr] = wasmtypes.WasmModuleEventType + for { + txEventsPage, err := c.cometServiceClient.GetTxsEvent(ctx, &sdktxtypes.GetTxsEventRequest{ + Events: events, + OrderBy: sdktxtypes.OrderBy_ORDER_BY_DESC, + Page: page, + Limit: uint64(c.cfg.TxsQueryPageLimit), + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to get contrac txs by events") + } + txResponses = append(txResponses, txEventsPage.TxResponses...) + if len(txEventsPage.TxResponses) < int(c.cfg.TxsQueryPageLimit) { + break + } + page++ + } + + return txResponses, nil +} + +func (c *ContractClient) decodeExecutePayload(txAny *sdk.TxResponse) ([]ExecutePayload, error) { + var tx sdk.Tx + if err := c.clientCtx.Codec().UnpackAny(txAny.Tx, &tx); err != nil { + return nil, errors.Errorf("failed to unpack sdk.Tx, tx:%v", tx) + } + + executePayloads := make([]ExecutePayload, 0) + for _, msg := range tx.GetMsgs() { + executeContractMsg, ok := msg.(*wasmtypes.MsgExecuteContract) + if !ok { + continue + } + payload := executeContractMsg.Msg + var executePayload ExecutePayload + if err := json.Unmarshal(payload, &executePayload); err != nil { + return nil, errors.Wrapf(err, "failed to decode contract payload to map, raw payload:%s, tx:%v", string(payload), tx) + } + executePayloads = append(executePayloads, executePayload) + } + + return executePayloads, nil +} + +func isEventValueEqual( + events sdk.StringEvents, + etype, key, value string, +) bool { + for _, ev := range events { + if ev.Type != etype { + continue + } + for _, attr := range ev.Attributes { + if attr.Key != key { + continue + } + + return attr.Value == value + } + } + return false +} + +func (c *ContractClient) getSendToXRPLOperationIDs( + ctx context.Context, + sendReq SendToXRPLRequest, + txHeight int64, +) ([]uint32, error) { + beforeCtx := WithHeightRequestContext(ctx, txHeight-1) + operationsBefore, err := c.GetPendingOperations(beforeCtx) + if err != nil { + return nil, err + } + + afterCtx := WithHeightRequestContext(ctx, txHeight) + operationsAfter, err := c.GetPendingOperations(afterCtx) + if err != nil { + return nil, err + } + + operationsBeforeMap := lo.SliceToMap(operationsBefore, func(operation Operation) (uint32, Operation) { + return operation.GetOperationID(), operation + }) + + operationIDs := make([]uint32, 0) + for _, operation := range operationsAfter { + if _, ok := operationsBeforeMap[operation.GetOperationID()]; !ok { + if operation.OperationType.CoreumToXRPLTransfer == nil { + continue + } + if operation.OperationType.CoreumToXRPLTransfer.Recipient != sendReq.Recipient { + continue + } + operationIDs = append(operationIDs, operation.GetOperationID()) + } + } + + return operationIDs, nil +} + +// ******************** Context ******************** + +// WithHeightRequestContext adds the height to the context for queries. +func WithHeightRequestContext(ctx context.Context, height int64) context.Context { + return metadata.AppendToOutgoingContext( + ctx, + grpctypes.GRPCBlockHeightHeader, + strconv.FormatInt(height, 10), + ) +} + // ******************** Contract error ******************** // IsNotOwnerError returns true if error is `not owner`. diff --git a/relayer/runner/config.go b/relayer/runner/config.go index 41d02ef0..63c508c1 100644 --- a/relayer/runner/config.go +++ b/relayer/runner/config.go @@ -3,6 +3,7 @@ package runner import ( "context" + "fmt" "io" "os" "path/filepath" @@ -11,7 +12,6 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/pkg/errors" rippledata "github.com/rubblelabs/ripple/data" - "go.uber.org/zap" "gopkg.in/yaml.v3" toolshttp "github.com/CoreumFoundation/coreum-tools/pkg/http" @@ -260,7 +260,13 @@ func setConfigDefaults(ctx context.Context, log logger.Logger, config *Config) { // Set default retry_delay if the value is not set because of an old config version which doesn't contain retry_delay. if config.Processes.RetryDelay == 0 { defaultRetryDelay := DefaultConfig().Processes.RetryDelay - log.Warn(ctx, "retry_delay is not set, using default value", zap.Duration("retryDelay", defaultRetryDelay)) + log.Warn( + ctx, + fmt.Sprintf( + "processes.retry_delay is not set in %s, using default value: %s", + ConfigFileName, defaultRetryDelay, + ), + ) config.Processes.RetryDelay = defaultRetryDelay } } diff --git a/relayer/runner/runner.go b/relayer/runner/runner.go index db40b8d7..1ca9670f 100644 --- a/relayer/runner/runner.go +++ b/relayer/runner/runner.go @@ -287,14 +287,12 @@ func NewComponents( ) } } - contractClientCfg := coreum.ContractClientConfig{ - ContractAddress: contractAddress, - GasAdjustment: cfg.Coreum.Contract.GasAdjustment, - GasPriceAdjustment: sdk.MustNewDecFromStr(fmt.Sprintf("%f", cfg.Coreum.Contract.GasPriceAdjustment)), - PageLimit: cfg.Coreum.Contract.PageLimit, - OutOfGasRetryDelay: cfg.Coreum.Contract.OutOfGasRetryDelay, - OutOfGasRetryAttempts: cfg.Coreum.Contract.OutOfGasRetryAttempts, - } + contractClientCfg := coreum.DefaultContractClientConfig(contractAddress) + contractClientCfg.GasAdjustment = cfg.Coreum.Contract.GasAdjustment + contractClientCfg.GasPriceAdjustment = sdk.MustNewDecFromStr(fmt.Sprintf("%f", cfg.Coreum.Contract.GasPriceAdjustment)) + contractClientCfg.PageLimit = cfg.Coreum.Contract.PageLimit + contractClientCfg.OutOfGasRetryDelay = cfg.Coreum.Contract.OutOfGasRetryDelay + contractClientCfg.OutOfGasRetryAttempts = cfg.Coreum.Contract.OutOfGasRetryAttempts if cfg.Coreum.GRPC.URL != "" { grpcClient, err := getGRPCClientConn(cfg.Coreum.GRPC.URL)