From d5b84f02b97574c45d4a3db3accfb95c95a416a7 Mon Sep 17 00:00:00 2001 From: Dzmitry Hil Date: Mon, 20 Nov 2023 17:03:27 +0300 Subject: [PATCH] Relayer: Sending of XRPL originated toke from the coreum back to the XRPL implementation (#46) * Update client and add integration test to all new functionality * Integrate the relayer submitter Payment (CoremToXRPL) transaction * Integrate relayer observer Payment (CoremToXRPL) confirmation --- contract/src/msg.rs | 1 + .../coreum/contract_client_test.go | 387 +++++++++++++++--- integration-tests/go.mod | 2 +- integration-tests/processes/env_test.go | 40 +- .../send_from_coreum_to_xrpl_test.go | 205 ++++++++++ .../send_from_xrpl_to_coreum_test.go | 42 +- .../processes/ticket_allocation_test.go | 6 +- integration-tests/xrpl.go | 5 + relayer/coreum/contract.go | 89 +++- relayer/processes/amount.go | 8 +- relayer/processes/amount_test.go | 8 +- relayer/processes/model.go | 8 + relayer/processes/model_mocks_test.go | 15 + relayer/processes/operation_tx.go | 41 +- relayer/processes/xrpl_tx_observer.go | 74 ++-- relayer/processes/xrpl_tx_observer_test.go | 87 ++++ relayer/processes/xrpl_tx_submitter.go | 35 +- relayer/processes/xrpl_tx_submitter_test.go | 127 +++++- relayer/xrpl/constatns.go | 3 + 19 files changed, 1045 insertions(+), 138 deletions(-) create mode 100644 integration-tests/processes/send_from_coreum_to_xrpl_test.go diff --git a/contract/src/msg.rs b/contract/src/msg.rs index 98ed64b6..5de4f220 100644 --- a/contract/src/msg.rs +++ b/contract/src/msg.rs @@ -48,6 +48,7 @@ pub enum ExecuteMsg { SaveEvidence { evidence: Evidence, }, + #[serde(rename = "send_to_xrpl")] SendToXRPL { recipient: String, }, diff --git a/integration-tests/coreum/contract_client_test.go b/integration-tests/coreum/contract_client_test.go index bde617ff..7e0b6129 100644 --- a/integration-tests/coreum/contract_client_test.go +++ b/integration-tests/coreum/contract_client_test.go @@ -14,8 +14,8 @@ import ( sdkmath "cosmossdk.io/math" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" sdk "github.com/cosmos/cosmos-sdk/types" + cosmoserrors "github.com/cosmos/cosmos-sdk/types/errors" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/cosmos/gogoproto/proto" rippledata "github.com/rubblelabs/ripple/data" "github.com/samber/lo" "github.com/stretchr/testify/require" @@ -48,7 +48,7 @@ func TestDeployAndInstantiateContract(t *testing.T) { relayers := genRelayers(ctx, t, chains, 1) - bridgeXRPLAddress := chains.XRPL.GenAccount(ctx, t, 0).String() + bridgeXRPLAddress := xrpl.GenPrivKeyTxSigner().Account().String() usedTicketSequenceThreshold := 10 owner, contractClient := integrationtests.DeployAndInstantiateContract( @@ -122,7 +122,6 @@ func TestChangeContractOwnership(t *testing.T) { ctx, chains := integrationtests.NewTestingContext(t) relayers := genRelayers(ctx, t, chains, 1) - usedTicketSequenceThreshold := 10 owner, contractClient := integrationtests.DeployAndInstantiateContract( ctx, @@ -130,9 +129,9 @@ func TestChangeContractOwnership(t *testing.T) { chains, relayers, len(relayers), - usedTicketSequenceThreshold, + 10, defaultTrustSetLimitAmount, - chains.XRPL.GenAccount(ctx, t, 0).String(), + xrpl.GenPrivKeyTxSigner().Account().String(), ) contractOwnership, err := contractClient.GetContractOwnership(ctx) @@ -179,7 +178,6 @@ func TestRegisterCoreumToken(t *testing.T) { XRPLPubKey: relayerXRPLSigner.PubKey().String(), }, } - usedTicketSequenceThreshold := 10 notOwner := chains.Coreum.GenAccount() chains.Coreum.FundAccountWithOptions(ctx, t, notOwner, coreumintegration.BalancesOptions{ @@ -192,9 +190,9 @@ func TestRegisterCoreumToken(t *testing.T) { chains, relayers, len(relayers), - usedTicketSequenceThreshold, + 10, defaultTrustSetLimitAmount, - chains.XRPL.GenAccount(ctx, t, 0).String(), + xrpl.GenPrivKeyTxSigner().Account().String(), ) denom1 := "denom1" @@ -274,7 +272,6 @@ func TestRegisterXRPLToken(t *testing.T) { bankClient := banktypes.NewQueryClient(chains.Coreum.ClientContext) relayers := genRelayers(ctx, t, chains, 2) - usedTicketSequenceThreshold := 3 coreumRecipient := chains.Coreum.GenAccount() notOwner := chains.Coreum.GenAccount() @@ -291,9 +288,9 @@ func TestRegisterXRPLToken(t *testing.T) { chains, relayers, len(relayers), - usedTicketSequenceThreshold, + 3, defaultTrustSetLimitAmount, - chains.XRPL.GenAccount(ctx, t, 0).String(), + xrpl.GenPrivKeyTxSigner().Account().String(), ) // fund owner to cover issuance fees twice @@ -309,7 +306,7 @@ func TestRegisterXRPLToken(t *testing.T) { maxHoldingAmount := sdk.NewIntFromUint64(10000) // recover tickets so that we can create a pending operation to activate the token - allocateInitialTickets(ctx, t, contractClient, owner, relayers) + recoverTickets(ctx, t, contractClient, owner, relayers, 100) // try to register from not owner _, err := contractClient.RegisterXRPLToken(ctx, notOwner, issuer, inactiveCurrency, sendingPrecision, maxHoldingAmount) @@ -462,7 +459,7 @@ func TestRegisterXRPLToken(t *testing.T) { require.Equal(t, amountToSend.String(), balanceRes.Balance.Amount.String()) } -func TestSendFromXRPLToCoreumXRPLOriginToken(t *testing.T) { +func TestSendFromXRPLToCoreumXRPLOriginatedToken(t *testing.T) { t.Parallel() ctx, chains := integrationtests.NewTestingContext(t) @@ -477,22 +474,20 @@ func TestSendFromXRPLToCoreumXRPLOriginToken(t *testing.T) { Amount: sdkmath.NewInt(1_000_000), }) - usedTicketSequenceThreshold := 3 - owner, contractClient := integrationtests.DeployAndInstantiateContract( ctx, t, chains, relayers, len(relayers), - usedTicketSequenceThreshold, + 3, defaultTrustSetLimitAmount, - chains.XRPL.GenAccount(ctx, t, 0).String(), + xrpl.GenPrivKeyTxSigner().Account().String(), ) issueFee := chains.Coreum.QueryAssetFTParams(ctx, t).IssueFee - // fund owner to cover issuance fees twice + // fund owner to cover issuance fees chains.Coreum.FundAccountWithOptions(ctx, t, owner, coreumintegration.BalancesOptions{ - Amount: issueFee.Amount.Mul(sdkmath.NewIntFromUint64(2)), + Amount: issueFee.Amount, }) issuerAcc := chains.XRPL.GenAccount(ctx, t, 0) @@ -502,7 +497,7 @@ func TestSendFromXRPLToCoreumXRPLOriginToken(t *testing.T) { maxHoldingAmount := sdk.NewIntFromUint64(10000) // recover tickets so that we can create a pending operation to activate the token - allocateInitialTickets(ctx, t, contractClient, owner, relayers) + recoverTickets(ctx, t, contractClient, owner, relayers, 100) // register from the owner _, err := contractClient.RegisterXRPLToken(ctx, owner, issuer, currency, sendingPrecision, maxHoldingAmount) @@ -582,7 +577,7 @@ func TestSendFromXRPLToCoreumXRPLOriginToken(t *testing.T) { require.True(t, coreum.IsOperationAlreadyExecutedError(err), err) } -func TestSendFromXRPLToCoreumXRPLOriginTokenWithDifferentSendingPrecision(t *testing.T) { +func TestSendFromXRPLToCoreumXRPLOriginatedTokenWithDifferentSendingPrecision(t *testing.T) { // intentionally not parallel var ( tokenDecimals = int64(15) @@ -595,20 +590,18 @@ func TestSendFromXRPLToCoreumXRPLOriginTokenWithDifferentSendingPrecision(t *tes relayers := genRelayers(ctx, t, chains, 2) coreumRecipient := chains.Coreum.GenAccount() - usedTicketSequenceThreshold := 10 - owner, contractClient := integrationtests.DeployAndInstantiateContract( ctx, t, chains, relayers, len(relayers), - usedTicketSequenceThreshold, + 10, defaultTrustSetLimitAmount, - chains.XRPL.GenAccount(ctx, t, 0).String(), + xrpl.GenPrivKeyTxSigner().Account().String(), ) // register tickets - allocateInitialTickets(ctx, t, contractClient, owner, relayers) + recoverTickets(ctx, t, contractClient, owner, relayers, 100) issueFee := chains.Coreum.QueryAssetFTParams(ctx, t).IssueFee @@ -705,9 +698,14 @@ func TestSendFromXRPLToCoreumXRPLOriginTokenWithDifferentSendingPrecision(t *tes currency := "CRC" // register from the owner - txRes, err := contractClient.RegisterXRPLToken(ctx, owner, issuer, currency, tt.sendingPrecision, tt.maxHoldingAmount) + _, err := contractClient.RegisterXRPLToken(ctx, owner, issuer, currency, tt.sendingPrecision, tt.maxHoldingAmount) require.NoError(t, err) - issuedDenom := findOneIssuedDenomInTxResponse(t, txRes) + registeredXRPLToken, err := contractClient.GetXRPLToken(ctx, issuer, currency) + require.NoError(t, err) + require.NotNil(t, registeredXRPLToken) + + // activate token + activateXRPLToken(ctx, t, contractClient, relayers, issuerAcc.String(), currency) // create an evidence xrplToCoreumTransferEvidence := coreum.XRPLToCoreumTransferEvidence{ @@ -718,9 +716,6 @@ func TestSendFromXRPLToCoreumXRPLOriginTokenWithDifferentSendingPrecision(t *tes Recipient: coreumRecipient, } - // activate token - activateXRPLToken(ctx, t, contractClient, relayers, issuerAcc.String(), currency) - // call from all relayers for _, relayer := range relayers { _, err = contractClient.SendXRPLToCoreumTransferEvidence(ctx, relayer.CoreumAddress, xrplToCoreumTransferEvidence) @@ -737,7 +732,7 @@ func TestSendFromXRPLToCoreumXRPLOriginTokenWithDifferentSendingPrecision(t *tes balanceRes, err := bankClient.Balance(ctx, &banktypes.QueryBalanceRequest{ Address: coreumRecipient.String(), - Denom: issuedDenom, + Denom: registeredXRPLToken.CoreumDenom, }) require.NoError(t, err) require.Equal(t, tt.wantReceivedAmount.String(), balanceRes.Balance.Amount.String()) @@ -764,7 +759,7 @@ func TestRecoverTickets(t *testing.T) { 2, usedTicketSequenceThreshold, defaultTrustSetLimitAmount, - chains.XRPL.GenAccount(ctx, t, 0).String(), + xrpl.GenPrivKeyTxSigner().Account().String(), ) // ********** Ticket allocation / Recovery ********** @@ -1025,39 +1020,304 @@ func TestRecoverTickets(t *testing.T) { require.True(t, coreum.IsStillHaveAvailableTicketsError(err), err) } -func findOneIssuedDenomInTxResponse(t *testing.T, txRes *sdk.TxResponse) string { - t.Helper() +func TestSendFromCoreumToXRPLXRPLOriginatedToken(t *testing.T) { + t.Parallel() - eventIssuedName := proto.MessageName(&assetfttypes.EventIssued{}) - foundDenom := "" - for i := range txRes.Events { - if txRes.Events[i].Type != eventIssuedName { - continue - } - if foundDenom != "" { - require.Failf(t, "found multiple issued denom is the tx response, but expected one", "events:%+v", txRes.Events) - } - eventsTokenIssued, err := event.FindTypedEvents[*assetfttypes.EventIssued](txRes.Events) + ctx, chains := integrationtests.NewTestingContext(t) + + coreumSenderAddress := chains.Coreum.GenAccount() + bankClient := banktypes.NewQueryClient(chains.Coreum.ClientContext) + chains.Coreum.FundAccountWithOptions(ctx, t, coreumSenderAddress, coreumintegration.BalancesOptions{ + Amount: sdkmath.NewInt(1_000_000), + }) + + xrplRecipientAddress := chains.XRPL.GenAccount(ctx, t, 0) + + relayers := genRelayers(ctx, t, chains, 2) + owner, contractClient := integrationtests.DeployAndInstantiateContract( + ctx, + t, + chains, + relayers, + len(relayers), + 3, + defaultTrustSetLimitAmount, + xrpl.GenPrivKeyTxSigner().Account().String(), + ) + issueFee := chains.Coreum.QueryAssetFTParams(ctx, t).IssueFee + chains.Coreum.FundAccountWithOptions(ctx, t, owner, coreumintegration.BalancesOptions{ + Amount: issueFee.Amount, + }) + + issuerAcc := chains.XRPL.GenAccount(ctx, t, 0) + issuer := issuerAcc.String() + currency := "CRN" + sendingPrecision := int32(15) + maxHoldingAmount := sdk.NewIntFromUint64(1_000_000_000) + + // recover tickets so that we can create a pending operation to activate the token + recoverTickets(ctx, t, contractClient, owner, relayers, 5) + + // register new token + _, err := contractClient.RegisterXRPLToken(ctx, owner, issuer, currency, sendingPrecision, maxHoldingAmount) + require.NoError(t, err) + // activate token + registeredXRPLOriginatedToken, err := contractClient.GetXRPLToken(ctx, issuer, currency) + require.NoError(t, err) + require.NotEmpty(t, registeredXRPLOriginatedToken) + activateXRPLToken(ctx, t, contractClient, relayers, issuer, currency) + + amountToSendFromXRPLToCoreum := sdkmath.NewInt(1_000_100) + sendFromXRPLToCoreum(ctx, t, contractClient, relayers, issuer, currency, amountToSendFromXRPLToCoreum, coreumSenderAddress) + // validate that the amount is received + balanceRes, err := bankClient.Balance(ctx, &banktypes.QueryBalanceRequest{ + Address: coreumSenderAddress.String(), + Denom: registeredXRPLOriginatedToken.CoreumDenom, + }) + require.NoError(t, err) + require.Equal(t, amountToSendFromXRPLToCoreum.String(), balanceRes.Balance.Amount.String()) + + amountToSend := sdkmath.NewInt(1_000_000) + + // try to send more than account has + _, err = contractClient.SendToXRPL(ctx, coreumSenderAddress, xrplRecipientAddress.String(), sdk.NewCoin(registeredXRPLOriginatedToken.CoreumDenom, amountToSendFromXRPLToCoreum.AddRaw(1))) + require.ErrorContains(t, err, cosmoserrors.ErrInsufficientFunds.Error()) + + // try to send with invalid recipient + _, err = contractClient.SendToXRPL(ctx, coreumSenderAddress, "invalid", sdk.NewCoin(registeredXRPLOriginatedToken.CoreumDenom, amountToSend)) + require.True(t, coreum.IsInvalidXRPLAddressError(err), err) + + // try to send with not registered token + _, err = contractClient.SendToXRPL(ctx, coreumSenderAddress, xrplRecipientAddress.String(), sdk.NewCoin(chains.Coreum.ChainSettings.Denom, sdk.NewIntFromUint64(1))) + require.True(t, coreum.IsTokenNotRegisteredError(err), err) + + // send valid amount and validate the state + coreumSenderBalanceBeforeRes, err := bankClient.Balance(ctx, &banktypes.QueryBalanceRequest{ + Address: coreumSenderAddress.String(), + Denom: registeredXRPLOriginatedToken.CoreumDenom, + }) + require.NoError(t, err) + _, err = contractClient.SendToXRPL(ctx, coreumSenderAddress, xrplRecipientAddress.String(), sdk.NewCoin(registeredXRPLOriginatedToken.CoreumDenom, amountToSend)) + require.NoError(t, err) + // check the remaining balance + coreumSenderBalanceAfterRes, err := bankClient.Balance(ctx, &banktypes.QueryBalanceRequest{ + Address: coreumSenderAddress.String(), + Denom: registeredXRPLOriginatedToken.CoreumDenom, + }) + require.NoError(t, err) + require.Equal(t, coreumSenderBalanceBeforeRes.Balance.Amount.Sub(amountToSend).String(), coreumSenderBalanceAfterRes.Balance.Amount.String()) + + pendingOperations, err := contractClient.GetPendingOperations(ctx) + require.NoError(t, err) + require.Len(t, pendingOperations, 1) + operation := pendingOperations[0] + operationType := operation.OperationType.CoreumToXRPLTransfer + require.NotNil(t, operationType) + require.NotNil(t, operationType.Issuer, registeredXRPLOriginatedToken.Issuer) + require.NotNil(t, operationType.Currency, registeredXRPLOriginatedToken.Currency) + require.NotNil(t, operationType.Amount, amountToSend) + require.NotNil(t, operationType.Recipient, xrplRecipientAddress.String()) + + acceptedTxEvidence := coreum.XRPLTransactionResultCoreumToXRPLTransferEvidence{ + XRPLTransactionResultEvidence: coreum.XRPLTransactionResultEvidence{ + TxHash: genXRPLTxHash(t), + TicketSequence: &operation.TicketSequence, + TransactionResult: coreum.TransactionResultAccepted, + }, + } + + // send from first relayer + _, err = contractClient.SendCoreumToXRPLTransferTransactionResultEvidence(ctx, relayers[0].CoreumAddress, acceptedTxEvidence) + require.NoError(t, err) + + // send from second relayer + _, err = contractClient.SendCoreumToXRPLTransferTransactionResultEvidence(ctx, relayers[1].CoreumAddress, acceptedTxEvidence) + require.NoError(t, err) + + // check pending operations + pendingOperations, err = contractClient.GetPendingOperations(ctx) + require.NoError(t, err) + require.Empty(t, pendingOperations) + + // use all available tickets + tickets, err := contractClient.GetAvailableTickets(ctx) + require.NoError(t, err) + for i := 0; i < len(tickets)-1; i++ { + _, err = contractClient.SendToXRPL(ctx, coreumSenderAddress, xrplRecipientAddress.String(), sdk.NewCoin(registeredXRPLOriginatedToken.CoreumDenom, sdk.NewIntFromUint64(1))) require.NoError(t, err) - foundDenom = eventsTokenIssued[0].Denom } - if foundDenom == "" { - require.Failf(t, "not found in the issue response", "event: %s ", eventIssuedName) + + // try to use last (protected) ticket + _, err = contractClient.SendToXRPL(ctx, coreumSenderAddress, xrplRecipientAddress.String(), sdk.NewCoin(registeredXRPLOriginatedToken.CoreumDenom, sdk.NewIntFromUint64(1))) + require.True(t, coreum.IsLastTicketReservedError(err)) +} + +func TestSendFromCoreumToXRPLXRPLOriginatedTokenWithDifferentSendingPrecision(t *testing.T) { + // intentionally not parallel + var ( + tokenDecimals = int64(15) + highMaxHoldingAmount = integrationtests.ConvertStringWithDecimalsToSDKInt(t, "1", 30) + ) + + ctx, chains := integrationtests.NewTestingContext(t) + bankClient := banktypes.NewQueryClient(chains.Coreum.ClientContext) + + relayers := genRelayers(ctx, t, chains, 2) + xrplRecipient := xrpl.GenPrivKeyTxSigner().Account() + + owner, contractClient := integrationtests.DeployAndInstantiateContract( + ctx, + t, + chains, + relayers, + len(relayers), + 50, + defaultTrustSetLimitAmount, + xrpl.GenPrivKeyTxSigner().Account().String(), + ) + // register tickets + recoverTickets(ctx, t, contractClient, owner, relayers, 100) + issueFee := chains.Coreum.QueryAssetFTParams(ctx, t).IssueFee + + tests := []struct { + name string + sendingPrecision int32 + sendingAmount sdkmath.Int + maxHoldingAmount sdkmath.Int + wantReceivedAmount sdkmath.Int + wantIsAmountSentIsZeroAfterTruncationError bool + }{ + { + name: "positive_precision_no_truncation", + sendingPrecision: 2, + maxHoldingAmount: highMaxHoldingAmount, + sendingAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "9999999999.15", tokenDecimals), + wantReceivedAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "9999999999.15", tokenDecimals), + }, + { + name: "positive_precision_with_truncation", + sendingPrecision: 2, + maxHoldingAmount: highMaxHoldingAmount, + sendingAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "0.15567", tokenDecimals), + wantReceivedAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "0.15", tokenDecimals), + }, + { + name: "positive_precision_low_amount", + sendingPrecision: 2, + maxHoldingAmount: highMaxHoldingAmount, + sendingAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "0.009999", tokenDecimals), + wantIsAmountSentIsZeroAfterTruncationError: true, + }, + { + name: "zero_precision_no_truncation", + sendingPrecision: 0, + maxHoldingAmount: highMaxHoldingAmount, + sendingAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "9999999999", tokenDecimals), + wantReceivedAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "9999999999", tokenDecimals), + }, + { + name: "zero_precision_with_truncation", + sendingPrecision: 0, + maxHoldingAmount: highMaxHoldingAmount, + sendingAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "1.15567", tokenDecimals), + wantReceivedAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "1", tokenDecimals), + }, + { + name: "zero_precision_low_amount", + sendingPrecision: 0, + maxHoldingAmount: highMaxHoldingAmount, + sendingAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "0.9999", tokenDecimals), + wantIsAmountSentIsZeroAfterTruncationError: true, + }, + { + name: "negative_precision_no_truncation", + sendingPrecision: -2, + maxHoldingAmount: highMaxHoldingAmount, + sendingAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "9999999900", tokenDecimals), + wantReceivedAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "9999999900", tokenDecimals), + }, + { + name: "negative_precision_with_truncation", + sendingPrecision: -2, + maxHoldingAmount: highMaxHoldingAmount, + sendingAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "9999.15567", tokenDecimals), + wantReceivedAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "9900", tokenDecimals), + }, + { + name: "negative_precision_low_amount", + sendingPrecision: -2, + maxHoldingAmount: highMaxHoldingAmount, + sendingAmount: integrationtests.ConvertStringWithDecimalsToSDKInt(t, "99.9999", tokenDecimals), + wantIsAmountSentIsZeroAfterTruncationError: true, + }, } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + // fund owner to cover registration fee twice + chains.Coreum.FundAccountWithOptions(ctx, t, owner, coreumintegration.BalancesOptions{ + Amount: issueFee.Amount, + }) - return foundDenom + issuerAcc := xrpl.GenPrivKeyTxSigner().Account() + issuer := issuerAcc.String() + currency := "CRC" + + // register from the owner + _, err := contractClient.RegisterXRPLToken(ctx, owner, issuer, currency, tt.sendingPrecision, tt.maxHoldingAmount) + require.NoError(t, err) + registeredXRPLToken, err := contractClient.GetXRPLToken(ctx, issuer, currency) + require.NoError(t, err) + require.NotNil(t, registeredXRPLToken) + + // activate token + activateXRPLToken(ctx, t, contractClient, relayers, issuerAcc.String(), currency) + + coreumSenderAddress := chains.Coreum.GenAccount() + // fund coreum sender address to cover fee + chains.Coreum.FundAccountWithOptions(ctx, t, coreumSenderAddress, coreumintegration.BalancesOptions{ + Amount: sdkmath.NewInt(1_000_000), + }) + sendFromXRPLToCoreum(ctx, t, contractClient, relayers, issuer, currency, tt.maxHoldingAmount, coreumSenderAddress) + coreumSenderBalanceRes, err := bankClient.Balance(ctx, &banktypes.QueryBalanceRequest{ + Address: coreumSenderAddress.String(), + Denom: registeredXRPLToken.CoreumDenom, + }) + require.NoError(t, err) + require.Equal(t, tt.maxHoldingAmount.String(), coreumSenderBalanceRes.Balance.Amount.String()) + + _, err = contractClient.SendToXRPL(ctx, coreumSenderAddress, xrplRecipient.String(), sdk.NewCoin(registeredXRPLToken.CoreumDenom, tt.sendingAmount)) + if tt.wantIsAmountSentIsZeroAfterTruncationError { + require.True(t, coreum.IsAmountSentIsZeroAfterTruncationError(err), err) + return + } + require.NoError(t, err) + + pendingOperations, err := contractClient.GetPendingOperations(ctx) + require.NoError(t, err) + found := false + for _, operation := range pendingOperations { + operationType := operation.OperationType.CoreumToXRPLTransfer + if operationType != nil && operationType.Issuer == issuer && operationType.Currency == currency { + found = true + require.Equal(t, tt.wantReceivedAmount.String(), operationType.Amount.String()) + } + } + require.True(t, found) + }) + } } -func allocateInitialTickets( +func recoverTickets( ctx context.Context, t *testing.T, contractClient *coreum.ContractClient, owner sdk.AccAddress, relayers []coreum.Relayer, + numberOfTickets uint32, ) { - numberOfTicketsToInit := uint32(100) bridgeXRPLAccountFirstSeqNumber := uint32(1) - _, err := contractClient.RecoverTickets(ctx, owner, bridgeXRPLAccountFirstSeqNumber, &numberOfTicketsToInit) + _, err := contractClient.RecoverTickets(ctx, owner, bridgeXRPLAccountFirstSeqNumber, &numberOfTickets) require.NoError(t, err) acceptedTxEvidence := coreum.XRPLTransactionResultTicketsAllocationEvidence{ @@ -1066,7 +1326,7 @@ func allocateInitialTickets( AccountSequence: &bridgeXRPLAccountFirstSeqNumber, TransactionResult: coreum.TransactionResultAccepted, }, - Tickets: lo.RepeatBy(int(numberOfTicketsToInit), func(index int) uint32 { + Tickets: lo.RepeatBy(int(numberOfTickets), func(index int) uint32 { return uint32(index + 1) }), } @@ -1093,14 +1353,27 @@ func activateXRPLToken( pendingOperations, err := contractClient.GetPendingOperations(ctx) require.NoError(t, err) - require.Len(t, pendingOperations, 1) - operation := pendingOperations[0] - require.NotNil(t, operation.OperationType.TrustSet) + + var ( + turstSetOperation coreum.Operation + found bool + ) + for _, operation := range pendingOperations { + operationType := operation.OperationType.TrustSet + if operationType != nil && operationType.Issuer == issuer && operationType.Currency == currency { + found = true + turstSetOperation = operation + break + } + } + require.True(t, found) + + require.NotNil(t, turstSetOperation.OperationType.TrustSet) acceptedTxEvidenceTrustSet := coreum.XRPLTransactionResultTrustSetEvidence{ XRPLTransactionResultEvidence: coreum.XRPLTransactionResultEvidence{ TxHash: genXRPLTxHash(t), - TicketSequence: &operation.TicketSequence, + TicketSequence: &turstSetOperation.TicketSequence, TransactionResult: coreum.TransactionResultAccepted, }, Issuer: issuer, diff --git a/integration-tests/go.mod b/integration-tests/go.mod index d379a9d4..a1248819 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -72,7 +72,7 @@ require ( github.com/cosmos/cosmos-sdk v0.47.5 github.com/cosmos/go-bip39 v1.0.0 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect - github.com/cosmos/gogoproto v1.4.10 + github.com/cosmos/gogoproto v1.4.10 // indirect github.com/cosmos/iavl v0.20.0 // indirect github.com/cosmos/ibc-go/v7 v7.2.0 // indirect github.com/cosmos/ics23/go v0.10.0 // indirect diff --git a/integration-tests/processes/env_test.go b/integration-tests/processes/env_test.go index 175f123d..3c26a9f4 100644 --- a/integration-tests/processes/env_test.go +++ b/integration-tests/processes/env_test.go @@ -253,8 +253,8 @@ func (r *RunnerEnv) AllocateTickets( require.Len(t, availableTickets, int(numberOfTicketsToAllocate)) } -// RegisterXRPLTokenAndAwaitTrustSet registers XRPL currency and awaits for the trust set ot be set. -func (r *RunnerEnv) RegisterXRPLTokenAndAwaitTrustSet( +// RegisterXRPLOriginatedToken registers XRPL currency and awaits for the trust set ot be set. +func (r *RunnerEnv) RegisterXRPLOriginatedToken( ctx context.Context, t *testing.T, issuer rippledata.Account, @@ -262,6 +262,9 @@ func (r *RunnerEnv) RegisterXRPLTokenAndAwaitTrustSet( sendingPrecision int32, maxHoldingAmount sdkmath.Int, ) coreum.XRPLToken { + r.Chains.Coreum.FundAccountWithOptions(ctx, t, r.ContractOwner, coreumintegration.BalancesOptions{ + Amount: r.Chains.Coreum.QueryAssetFTParams(ctx, t).IssueFee.Amount, + }) _, err := r.ContractClient.RegisterXRPLToken(ctx, r.ContractOwner, issuer.String(), xrpl.ConvertCurrencyToString(currency), sendingPrecision, maxHoldingAmount) require.NoError(t, err) // await for the trust set @@ -326,6 +329,39 @@ func (r *RunnerEnv) SendXRPLPartialPaymentTx( require.NoError(t, r.Chains.XRPL.AutoFillSignAndSubmitTx(ctx, t, &xrpPaymentTx, senderAcc)) } +func (r *RunnerEnv) EnableXRPLAccountRippling(ctx context.Context, t *testing.T, account rippledata.Account) { + // enable rippling on this account's trust lines. + accountSetTx := rippledata.AccountSet{ + SetFlag: lo.ToPtr(uint32(rippledata.TxDefaultRipple)), + TxBase: rippledata.TxBase{ + TransactionType: rippledata.ACCOUNT_SET, + }, + } + require.NoError(t, r.Chains.XRPL.AutoFillSignAndSubmitTx(ctx, t, &accountSetTx, account)) +} + +func (r *RunnerEnv) SendXRPLMaxTrustSetTx( + ctx context.Context, + t *testing.T, + account rippledata.Account, + issuer rippledata.Account, + currency rippledata.Currency, +) { + value, err := rippledata.NewValue("1000000000000", false) + require.NoError(t, err) + trustSetTx := rippledata.TrustSet{ + LimitAmount: rippledata.Amount{ + Value: value, + Currency: currency, + Issuer: issuer, + }, + TxBase: rippledata.TxBase{ + TransactionType: rippledata.TRUST_SET, + }, + } + require.NoError(t, r.Chains.XRPL.AutoFillSignAndSubmitTx(ctx, t, &trustSetTx, account)) +} + func genCoreumRelayers( ctx context.Context, t *testing.T, diff --git a/integration-tests/processes/send_from_coreum_to_xrpl_test.go b/integration-tests/processes/send_from_coreum_to_xrpl_test.go new file mode 100644 index 00000000..784e4a66 --- /dev/null +++ b/integration-tests/processes/send_from_coreum_to_xrpl_test.go @@ -0,0 +1,205 @@ +//go:build integrationtests +// +build integrationtests + +package processes_test + +import ( + "encoding/hex" + "strings" + "testing" + "time" + + 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/v3/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 TestRegisterXRPLOriginatedTokensSendFromXRPLToCoreumAndBack(t *testing.T) { + t.Parallel() + + ctx, chains := integrationtests.NewTestingContext(t) + + envCfg := DefaultRunnerEnvConfig() + runnerEnv := NewRunnerEnv(ctx, t, envCfg, chains) + runnerEnv.StartAllRunnerProcesses(ctx, t) + runnerEnv.AllocateTickets(ctx, t, uint32(200)) + + coreumSender := chains.Coreum.GenAccount() + chains.Coreum.FundAccountWithOptions(ctx, t, coreumSender, coreumintegration.BalancesOptions{ + Amount: sdkmath.NewIntFromUint64(1_000_000), + }) + t.Logf("Coreum sender: %s", coreumSender.String()) + xrplRecipientAddress := chains.XRPL.GenAccount(ctx, t, 0) + t.Logf("XRPL recipient: %s", xrplRecipientAddress.String()) + + registeredXRPLCurrency, err := rippledata.NewCurrency("RCP") + require.NoError(t, err) + + xrplIssuerAddress := chains.XRPL.GenAccount(ctx, t, 1) + // enable to be able to send to any address + runnerEnv.EnableXRPLAccountRippling(ctx, t, xrplIssuerAddress) + registeredXRPLToken := runnerEnv.RegisterXRPLOriginatedToken(ctx, t, xrplIssuerAddress, registeredXRPLCurrency, int32(6), integrationtests.ConvertStringWithDecimalsToSDKInt(t, "1", 30)) + + valueToSendFromXRPLtoCoreum, err := rippledata.NewValue("1e10", false) + require.NoError(t, err) + amountToSendFromXRPLtoCoreum := rippledata.Amount{ + Value: valueToSendFromXRPLtoCoreum, + Currency: registeredXRPLCurrency, + Issuer: xrplIssuerAddress, + } + memo, err := xrpl.EncodeCoreumRecipientToMemo(coreumSender) + require.NoError(t, err) + + runnerEnv.SendXRPLPaymentTx(ctx, t, xrplIssuerAddress, runnerEnv.bridgeXRPLAddress, amountToSendFromXRPLtoCoreum, memo) + runnerEnv.AwaitCoreumBalance(ctx, t, chains.Coreum, coreumSender, sdk.NewCoin(registeredXRPLToken.CoreumDenom, integrationtests.ConvertStringWithDecimalsToSDKInt(t, valueToSendFromXRPLtoCoreum.String(), XRPLTokenDecimals))) + + // send the full amount in 4 transactions to XRPL + amountToSend := integrationtests.ConvertStringWithDecimalsToSDKInt(t, valueToSendFromXRPLtoCoreum.String(), XRPLTokenDecimals).QuoRaw(4) + + // send 2 transactions without the trust set to be reverted + // TODO(dzmitryhil) update assertion once we add the final tx revert/recovery + _, err = runnerEnv.ContractClient.SendToXRPL(ctx, coreumSender, xrplRecipientAddress.String(), sdk.NewCoin(registeredXRPLToken.CoreumDenom, amountToSend)) + require.NoError(t, err) + _, err = runnerEnv.ContractClient.SendToXRPL(ctx, coreumSender, xrplRecipientAddress.String(), sdk.NewCoin(registeredXRPLToken.CoreumDenom, amountToSend)) + require.NoError(t, err) + runnerEnv.AwaitNoPendingOperations(ctx, t) + + // send TrustSet to be able to receive coins + runnerEnv.SendXRPLMaxTrustSetTx(ctx, t, xrplRecipientAddress, xrplIssuerAddress, registeredXRPLCurrency) + + _, err = runnerEnv.ContractClient.SendToXRPL(ctx, coreumSender, xrplRecipientAddress.String(), sdk.NewCoin(registeredXRPLToken.CoreumDenom, amountToSend)) + require.NoError(t, err) + _, err = runnerEnv.ContractClient.SendToXRPL(ctx, coreumSender, xrplRecipientAddress.String(), sdk.NewCoin(registeredXRPLToken.CoreumDenom, amountToSend)) + require.NoError(t, err) + runnerEnv.AwaitNoPendingOperations(ctx, t) + + balance := runnerEnv.Chains.XRPL.GetAccountBalance(ctx, t, xrplRecipientAddress, xrplIssuerAddress, registeredXRPLCurrency) + require.Equal(t, "5000000000", balance.Value.String()) +} + +func TestSendFromXRPLToCoreumWithMaliciousRelayer(t *testing.T) { + t.Parallel() + + ctx, chains := integrationtests.NewTestingContext(t) + + envCfg := DefaultRunnerEnvConfig() + envCfg.MaliciousRelayerNumber = 1 + runnerEnv := NewRunnerEnv(ctx, t, envCfg, chains) + runnerEnv.StartAllRunnerProcesses(ctx, t) + runnerEnv.AllocateTickets(ctx, t, uint32(200)) + + coreumSender := chains.Coreum.GenAccount() + chains.Coreum.FundAccountWithOptions(ctx, t, coreumSender, coreumintegration.BalancesOptions{ + Amount: sdkmath.NewIntFromUint64(1_000_000), + }) + t.Logf("Coreum sender: %s", coreumSender.String()) + xrplRecipientAddress := chains.XRPL.GenAccount(ctx, t, 0) + t.Logf("XRPL recipient: %s", xrplRecipientAddress.String()) + + currencyHexSymbol := hex.EncodeToString([]byte(strings.Repeat("X", 20))) + registeredXRPLCurrency, err := rippledata.NewCurrency(currencyHexSymbol) + require.NoError(t, err) + + xrplIssuerAddress := chains.XRPL.GenAccount(ctx, t, 1) + // enable to be able to send to any address + runnerEnv.EnableXRPLAccountRippling(ctx, t, xrplIssuerAddress) + registeredXRPLToken := runnerEnv.RegisterXRPLOriginatedToken(ctx, t, xrplIssuerAddress, registeredXRPLCurrency, int32(6), integrationtests.ConvertStringWithDecimalsToSDKInt(t, "1", 30)) + + valueToSendFromXRPLtoCoreum, err := rippledata.NewValue("1e10", false) + require.NoError(t, err) + amountToSendFromXRPLtoCoreum := rippledata.Amount{ + Value: valueToSendFromXRPLtoCoreum, + Currency: registeredXRPLCurrency, + Issuer: xrplIssuerAddress, + } + memo, err := xrpl.EncodeCoreumRecipientToMemo(coreumSender) + require.NoError(t, err) + + runnerEnv.SendXRPLPaymentTx(ctx, t, xrplIssuerAddress, runnerEnv.bridgeXRPLAddress, amountToSendFromXRPLtoCoreum, memo) + runnerEnv.AwaitCoreumBalance(ctx, t, chains.Coreum, coreumSender, sdk.NewCoin(registeredXRPLToken.CoreumDenom, integrationtests.ConvertStringWithDecimalsToSDKInt(t, valueToSendFromXRPLtoCoreum.String(), XRPLTokenDecimals))) + + // send TrustSet to be able to receive coins + runnerEnv.SendXRPLMaxTrustSetTx(ctx, t, xrplRecipientAddress, xrplIssuerAddress, registeredXRPLCurrency) + + amountToSend := integrationtests.ConvertStringWithDecimalsToSDKInt(t, valueToSendFromXRPLtoCoreum.String(), XRPLTokenDecimals).QuoRaw(4) + _, err = runnerEnv.ContractClient.SendToXRPL(ctx, coreumSender, xrplRecipientAddress.String(), sdk.NewCoin(registeredXRPLToken.CoreumDenom, amountToSend)) + require.NoError(t, err) + runnerEnv.AwaitNoPendingOperations(ctx, t) + + balance := runnerEnv.Chains.XRPL.GetAccountBalance(ctx, t, xrplRecipientAddress, xrplIssuerAddress, registeredXRPLCurrency) + require.Equal(t, "2500000000", balance.Value.String()) +} + +func TestSendFromXRPLToCoreumWithTicketsReallocation(t *testing.T) { + t.Parallel() + + ctx, chains := integrationtests.NewTestingContext(t) + + envCfg := DefaultRunnerEnvConfig() + envCfg.UsedTicketSequenceThreshold = 3 + runnerEnv := NewRunnerEnv(ctx, t, envCfg, chains) + runnerEnv.StartAllRunnerProcesses(ctx, t) + runnerEnv.AllocateTickets(ctx, t, uint32(5)) + sendingCount := 10 + + coreumSender := chains.Coreum.GenAccount() + chains.Coreum.FundAccountWithOptions(ctx, t, coreumSender, coreumintegration.BalancesOptions{ + Amount: sdkmath.NewIntFromUint64(1_000_000), + }) + t.Logf("Coreum sender: %s", coreumSender.String()) + xrplRecipientAddress := chains.XRPL.GenAccount(ctx, t, 0) + t.Logf("XRPL recipient: %s", xrplRecipientAddress.String()) + + currencyHexSymbol := hex.EncodeToString([]byte(strings.Repeat("X", 20))) + registeredXRPLCurrency, err := rippledata.NewCurrency(currencyHexSymbol) + require.NoError(t, err) + + xrplIssuerAddress := chains.XRPL.GenAccount(ctx, t, 1) + // enable to be able to send to any address + runnerEnv.EnableXRPLAccountRippling(ctx, t, xrplIssuerAddress) + registeredXRPLToken := runnerEnv.RegisterXRPLOriginatedToken(ctx, t, xrplIssuerAddress, registeredXRPLCurrency, int32(6), integrationtests.ConvertStringWithDecimalsToSDKInt(t, "1", 30)) + + valueToSendFromXRPLtoCoreum, err := rippledata.NewValue("1e10", false) + require.NoError(t, err) + amountToSendFromXRPLtoCoreum := rippledata.Amount{ + Value: valueToSendFromXRPLtoCoreum, + Currency: registeredXRPLCurrency, + Issuer: xrplIssuerAddress, + } + memo, err := xrpl.EncodeCoreumRecipientToMemo(coreumSender) + require.NoError(t, err) + + runnerEnv.SendXRPLPaymentTx(ctx, t, xrplIssuerAddress, runnerEnv.bridgeXRPLAddress, amountToSendFromXRPLtoCoreum, memo) + runnerEnv.AwaitCoreumBalance(ctx, t, chains.Coreum, coreumSender, sdk.NewCoin(registeredXRPLToken.CoreumDenom, integrationtests.ConvertStringWithDecimalsToSDKInt(t, valueToSendFromXRPLtoCoreum.String(), XRPLTokenDecimals))) + + // send TrustSet to be able to receive coins + runnerEnv.SendXRPLMaxTrustSetTx(ctx, t, xrplRecipientAddress, xrplIssuerAddress, registeredXRPLCurrency) + + totalSent := sdkmath.ZeroInt() + amountToSend := integrationtests.ConvertStringWithDecimalsToSDKInt(t, "10", XRPLTokenDecimals) + for i := 0; i < sendingCount; i++ { + for { + _, err = runnerEnv.ContractClient.SendToXRPL(ctx, coreumSender, xrplRecipientAddress.String(), sdk.NewCoin(registeredXRPLToken.CoreumDenom, amountToSend)) + if err == nil { + break + } + if coreum.IsLastTicketReservedError(err) || coreum.IsNoAvailableTicketsError(err) { + t.Logf("No tickets left, waiting for new tickets...") + <-time.After(500 * time.Millisecond) + continue + } + require.NoError(t, err) + } + totalSent = totalSent.Add(amountToSend) + } + runnerEnv.AwaitNoPendingOperations(ctx, t) + + balance := runnerEnv.Chains.XRPL.GetAccountBalance(ctx, t, xrplRecipientAddress, xrplIssuerAddress, registeredXRPLCurrency) + require.Equal(t, totalSent.Quo(sdkmath.NewIntWithDecimal(1, XRPLTokenDecimals)).String(), balance.Value.String()) +} diff --git a/integration-tests/processes/send_from_xrpl_to_coreum_test.go b/integration-tests/processes/send_from_xrpl_to_coreum_test.go index 8c338992..356dd3f1 100644 --- a/integration-tests/processes/send_from_xrpl_to_coreum_test.go +++ b/integration-tests/processes/send_from_xrpl_to_coreum_test.go @@ -18,13 +18,13 @@ import ( "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/xrpl" ) -func TestRegisterXRPLTokensAndSendFromXRPLToCoreum(t *testing.T) { +func TestRegisterXRPLOriginatedTokensAndSendFromXRPLToCoreum(t *testing.T) { t.Parallel() ctx, chains := integrationtests.NewTestingContext(t) - xrplIssuerAcc := chains.XRPL.GenAccount(ctx, t, 100) - t.Logf("XRPL currency issuer account: %s", xrplIssuerAcc) + xrplIssuerAddress := chains.XRPL.GenAccount(ctx, t, 100) + t.Logf("XRPL currency issuer address: %s", xrplIssuerAddress) coreumRecipient := chains.Coreum.GenAccount() t.Logf("Coreum recipient: %s", coreumRecipient.String()) @@ -59,30 +59,28 @@ func TestRegisterXRPLTokensAndSendFromXRPLToCoreum(t *testing.T) { // start relayers runnerEnv.StartAllRunnerProcesses(ctx, t) - - require.NoError(t, err) runnerEnv.AwaitNoPendingOperations(ctx, t) availableTickets, err := runnerEnv.ContractClient.GetAvailableTickets(ctx) require.NoError(t, err) require.Len(t, availableTickets, int(numberOfTicketsToAllocate)) // register XRPL originated token with 3 chars - _, err = runnerEnv.ContractClient.RegisterXRPLToken(ctx, runnerEnv.ContractOwner, xrplIssuerAcc.String(), xrpl.ConvertCurrencyToString(registeredXRPLCurrency), sendingPrecision, maxHoldingAmount) + _, err = runnerEnv.ContractClient.RegisterXRPLToken(ctx, runnerEnv.ContractOwner, xrplIssuerAddress.String(), xrpl.ConvertCurrencyToString(registeredXRPLCurrency), sendingPrecision, maxHoldingAmount) require.NoError(t, err) // register XRPL originated token with 20 chars - _, err = runnerEnv.ContractClient.RegisterXRPLToken(ctx, runnerEnv.ContractOwner, xrplIssuerAcc.String(), xrpl.ConvertCurrencyToString(registeredXRPLHexCurrency), sendingPrecision, maxHoldingAmount) + _, err = runnerEnv.ContractClient.RegisterXRPLToken(ctx, runnerEnv.ContractOwner, xrplIssuerAddress.String(), xrpl.ConvertCurrencyToString(registeredXRPLHexCurrency), sendingPrecision, maxHoldingAmount) require.NoError(t, err) // await for the trust set runnerEnv.AwaitNoPendingOperations(ctx, t) - registeredXRPLToken, err := runnerEnv.ContractClient.GetXRPLToken(ctx, xrplIssuerAcc.String(), xrpl.ConvertCurrencyToString(registeredXRPLCurrency)) + registeredXRPLToken, err := runnerEnv.ContractClient.GetXRPLToken(ctx, xrplIssuerAddress.String(), xrpl.ConvertCurrencyToString(registeredXRPLCurrency)) require.NoError(t, err) require.NotNil(t, registeredXRPLToken) require.Equal(t, coreum.TokenStateEnabled, registeredXRPLToken.State) - registeredXRPLHexCurrencyToken, err := runnerEnv.ContractClient.GetXRPLToken(ctx, xrplIssuerAcc.String(), xrpl.ConvertCurrencyToString(registeredXRPLHexCurrency)) + registeredXRPLHexCurrencyToken, err := runnerEnv.ContractClient.GetXRPLToken(ctx, xrplIssuerAddress.String(), xrpl.ConvertCurrencyToString(registeredXRPLHexCurrency)) require.NoError(t, err) require.NotNil(t, registeredXRPLHexCurrencyToken) require.Equal(t, coreum.TokenStateEnabled, registeredXRPLHexCurrencyToken.State) @@ -92,7 +90,7 @@ func TestRegisterXRPLTokensAndSendFromXRPLToCoreum(t *testing.T) { maxDecimalsRegisterCurrencyAmount := rippledata.Amount{ Value: lowValue, Currency: registeredXRPLCurrency, - Issuer: xrplIssuerAcc, + Issuer: xrplIssuerAddress, } highValue, err := rippledata.NewValue("100000", false) @@ -100,7 +98,7 @@ func TestRegisterXRPLTokensAndSendFromXRPLToCoreum(t *testing.T) { highValueRegisteredCurrencyAmount := rippledata.Amount{ Value: highValue, Currency: registeredXRPLCurrency, - Issuer: xrplIssuerAcc, + Issuer: xrplIssuerAddress, } normalValue, err := rippledata.NewValue("9.9", false) @@ -108,37 +106,37 @@ func TestRegisterXRPLTokensAndSendFromXRPLToCoreum(t *testing.T) { registeredHexCurrencyAmount := rippledata.Amount{ Value: normalValue, Currency: registeredXRPLHexCurrency, - Issuer: xrplIssuerAcc, + Issuer: xrplIssuerAddress, } memo, err := xrpl.EncodeCoreumRecipientToMemo(coreumRecipient) require.NoError(t, err) // incorrect memo - runnerEnv.SendXRPLPaymentTx(ctx, t, xrplIssuerAcc, runnerEnv.bridgeXRPLAddress, maxDecimalsRegisterCurrencyAmount, rippledata.Memo{}) + runnerEnv.SendXRPLPaymentTx(ctx, t, xrplIssuerAddress, runnerEnv.bridgeXRPLAddress, maxDecimalsRegisterCurrencyAmount, rippledata.Memo{}) // send correct transactions // send tx with partial payment - runnerEnv.SendXRPLPartialPaymentTx(ctx, t, xrplIssuerAcc, runnerEnv.bridgeXRPLAddress, highValueRegisteredCurrencyAmount, maxDecimalsRegisterCurrencyAmount, memo) + runnerEnv.SendXRPLPartialPaymentTx(ctx, t, xrplIssuerAddress, runnerEnv.bridgeXRPLAddress, highValueRegisteredCurrencyAmount, maxDecimalsRegisterCurrencyAmount, memo) // send tx with high amount - runnerEnv.SendXRPLPaymentTx(ctx, t, xrplIssuerAcc, runnerEnv.bridgeXRPLAddress, highValueRegisteredCurrencyAmount, memo) + runnerEnv.SendXRPLPaymentTx(ctx, t, xrplIssuerAddress, runnerEnv.bridgeXRPLAddress, highValueRegisteredCurrencyAmount, memo) // send tx with hex currency - runnerEnv.SendXRPLPaymentTx(ctx, t, xrplIssuerAcc, runnerEnv.bridgeXRPLAddress, registeredHexCurrencyAmount, memo) + runnerEnv.SendXRPLPaymentTx(ctx, t, xrplIssuerAddress, runnerEnv.bridgeXRPLAddress, registeredHexCurrencyAmount, memo) runnerEnv.AwaitCoreumBalance(ctx, t, chains.Coreum, coreumRecipient, sdk.NewCoin(registeredXRPLToken.CoreumDenom, integrationtests.ConvertStringWithDecimalsToSDKInt(t, "100001.000001", XRPLTokenDecimals))) runnerEnv.AwaitCoreumBalance(ctx, t, chains.Coreum, coreumRecipient, sdk.NewCoin(registeredXRPLHexCurrencyToken.CoreumDenom, integrationtests.ConvertStringWithDecimalsToSDKInt(t, "9.9", XRPLTokenDecimals))) } -func TestSendFromXRPLToCoreumWithMaliciousRelayer(t *testing.T) { +func TestSendFromXRPLToCoreumXRPLOriginatedTokenWithMaliciousRelayer(t *testing.T) { t.Parallel() ctx, chains := integrationtests.NewTestingContext(t) - xrplCurrencyIssuerAcc := chains.XRPL.GenAccount(ctx, t, 100) - t.Logf("XRPL currency issuer account: %s", xrplCurrencyIssuerAcc) + xrplIssuerAddress := chains.XRPL.GenAccount(ctx, t, 100) + t.Logf("XRPL currency issuer address: %s", xrplIssuerAddress) coreumRecipient := chains.Coreum.GenAccount() t.Logf("Coreum recipient: %s", coreumRecipient.String()) @@ -166,7 +164,7 @@ func TestSendFromXRPLToCoreumWithMaliciousRelayer(t *testing.T) { runnerEnv.AllocateTickets(ctx, t, 200) // register XRPL token - registeredXRPLToken := runnerEnv.RegisterXRPLTokenAndAwaitTrustSet(ctx, t, xrplCurrencyIssuerAcc, registeredXRPLCurrency, sendingPrecision, maxHoldingAmount) + registeredXRPLToken := runnerEnv.RegisterXRPLOriginatedToken(ctx, t, xrplIssuerAddress, registeredXRPLCurrency, sendingPrecision, maxHoldingAmount) // send value, err := rippledata.NewValue("9999999999999.1111", false) @@ -174,11 +172,11 @@ func TestSendFromXRPLToCoreumWithMaliciousRelayer(t *testing.T) { registerCurrencyAmount := rippledata.Amount{ Value: value, Currency: registeredXRPLCurrency, - Issuer: xrplCurrencyIssuerAcc, + Issuer: xrplIssuerAddress, } memo, err := xrpl.EncodeCoreumRecipientToMemo(coreumRecipient) require.NoError(t, err) - runnerEnv.SendXRPLPaymentTx(ctx, t, xrplCurrencyIssuerAcc, runnerEnv.bridgeXRPLAddress, registerCurrencyAmount, memo) + runnerEnv.SendXRPLPaymentTx(ctx, t, xrplIssuerAddress, runnerEnv.bridgeXRPLAddress, registerCurrencyAmount, memo) runnerEnv.AwaitCoreumBalance(ctx, t, chains.Coreum, coreumRecipient, sdk.NewCoin(registeredXRPLToken.CoreumDenom, integrationtests.ConvertStringWithDecimalsToSDKInt(t, "9999999999999.111", XRPLTokenDecimals))) } diff --git a/integration-tests/processes/ticket_allocation_test.go b/integration-tests/processes/ticket_allocation_test.go index 47d08452..0f69cea9 100644 --- a/integration-tests/processes/ticket_allocation_test.go +++ b/integration-tests/processes/ticket_allocation_test.go @@ -14,8 +14,6 @@ import ( integrationtests "github.com/CoreumFoundation/coreumbridge-xrpl/integration-tests" ) -// TODO(dzmitryhil) add the additional test for each operation which might cause the re-allocation - func TestTicketsAllocationRecoveryWithAccountSequence(t *testing.T) { t.Parallel() @@ -207,7 +205,7 @@ func TestTicketsReAllocationByTheXRPLTokenRegistration(t *testing.T) { for i := 0; i < numberOfXRPLTokensToRegister; i++ { registeredXRPLCurrency, err := rippledata.NewCurrency(fmt.Sprintf("CR%d", i)) require.NoError(t, err) - runnerEnv.RegisterXRPLTokenAndAwaitTrustSet(ctx, t, xrplCurrencyIssuerAcc, registeredXRPLCurrency, int32(6), integrationtests.ConvertStringWithDecimalsToSDKInt(t, "1", 30)) + runnerEnv.RegisterXRPLOriginatedToken(ctx, t, xrplCurrencyIssuerAcc, registeredXRPLCurrency, int32(6), integrationtests.ConvertStringWithDecimalsToSDKInt(t, "1", 30)) } runnerEnv.AwaitNoPendingOperations(ctx, t) @@ -221,7 +219,7 @@ func TestTicketsReAllocationByTheXRPLTokenRegistration(t *testing.T) { for i := 0; i < numberOfXRPLTokensToRegister; i++ { registeredXRPLCurrency, err := rippledata.NewCurrency(fmt.Sprintf("DR%d", i)) require.NoError(t, err) - runnerEnv.RegisterXRPLTokenAndAwaitTrustSet(ctx, t, xrplCurrencyIssuerAcc, registeredXRPLCurrency, int32(6), integrationtests.ConvertStringWithDecimalsToSDKInt(t, "1", 30)) + runnerEnv.RegisterXRPLOriginatedToken(ctx, t, xrplCurrencyIssuerAcc, registeredXRPLCurrency, int32(6), integrationtests.ConvertStringWithDecimalsToSDKInt(t, "1", 30)) } runnerEnv.AwaitNoPendingOperations(ctx, t) availableTicketsAfterSecondReallocation, err := runnerEnv.ContractClient.GetAvailableTickets(ctx) diff --git a/integration-tests/xrpl.go b/integration-tests/xrpl.go index 1e89b2ff..281b4b00 100644 --- a/integration-tests/xrpl.go +++ b/integration-tests/xrpl.go @@ -252,6 +252,11 @@ func (c XRPLChain) SubmitTx(ctx context.Context, t *testing.T, tx rippledata.Tra }) } +// GetAccountBalance returns account balance for the provided issuer and currency. +func (c XRPLChain) GetAccountBalance(ctx context.Context, t *testing.T, account, issuer rippledata.Account, currency rippledata.Currency) rippledata.Amount { + return c.GetAccountBalances(ctx, t, account)[fmt.Sprintf("%s/%s", currency.String(), issuer.String())] +} + // GetAccountBalances returns account balances. func (c XRPLChain) GetAccountBalances(ctx context.Context, t *testing.T, acc rippledata.Account) map[string]rippledata.Amount { t.Helper() diff --git a/relayer/coreum/contract.go b/relayer/coreum/contract.go index 83dd65a8..fb3ded25 100644 --- a/relayer/coreum/contract.go +++ b/relayer/coreum/contract.go @@ -34,6 +34,7 @@ const ( ExecMethodSaveEvidence ExecMethod = "save_evidence" ExecMethodRecoverTickets ExecMethod = "recover_tickets" ExecMethodSaveSignature ExecMethod = "save_signature" + ExecSendToXRPL ExecMethod = "send_to_xrpl" ) // TransactionResult is transaction result. @@ -86,6 +87,9 @@ const ( maximumBridgedAmountReachedErrorString = "MaximumBridgedAmountReached" stillHaveAvailableTicketsErrorString = "StillHaveAvailableTickets" xrplTokenNotEnabledErrorString = "XRPLTokenNotEnabled" + invalidXRPLAddressErrorString = "InvalidXRPLAddress" + lastTicketReservedErrorString = "LastTicketReserved" + noAvailableTicketsErrorString = "NoAvailableTickets" ) // Relayer is the relayer information in the contract config. @@ -170,6 +174,11 @@ type XRPLTransactionResultTrustSetEvidence struct { Currency string } +// XRPLTransactionResultCoreumToXRPLTransferEvidence is evidence of the sending from XRPL to coreum. +type XRPLTransactionResultCoreumToXRPLTransferEvidence struct { + XRPLTransactionResultEvidence +} + // Signature is a pair of the relayer provided the signature and signature string. type Signature struct { RelayerCoreumAddress sdk.AccAddress `json:"relayer_coreum_address"` @@ -188,10 +197,19 @@ type OperationTypeTrustSet struct { TrustSetLimitAmount sdkmath.Int `json:"trust_set_limit_amount"` } +// OperationTypeCoreumToXRPLTransfer is coreum to XRPL transfer operation type. +type OperationTypeCoreumToXRPLTransfer struct { + Issuer string `json:"issuer"` + Currency string `json:"currency"` + Amount sdkmath.Int `json:"amount"` + Recipient string `json:"recipient"` +} + // OperationType is operation type. type OperationType struct { - AllocateTickets *OperationTypeAllocateTickets `json:"allocate_tickets,omitempty"` - TrustSet *OperationTypeTrustSet `json:"trust_set,omitempty"` + AllocateTickets *OperationTypeAllocateTickets `json:"allocate_tickets,omitempty"` + TrustSet *OperationTypeTrustSet `json:"trust_set,omitempty"` + CoreumToXRPLTransfer *OperationTypeCoreumToXRPLTransfer `json:"coreum_to_xrpl_transfer,omitempty"` } // Operation is contract operation which should be signed and executed. @@ -256,6 +274,10 @@ type saveSignatureRequest struct { Signature string `json:"signature"` } +type sendToXRPLRequest struct { + Recipient string `json:"recipient"` +} + type xrplTransactionEvidenceTicketsAllocationOperationResult struct { Tickets []uint32 `json:"tickets"` } @@ -265,9 +287,12 @@ type xrplTransactionEvidenceTrustSetOperationResult struct { Currency string `json:"currency"` } +type xrplTransactionEvidenceCoreumToXRPLTransferOperationResult struct{} + type xrplTransactionEvidenceOperationResult struct { - TicketsAllocation *xrplTransactionEvidenceTicketsAllocationOperationResult `json:"tickets_allocation,omitempty"` - TrustSet *xrplTransactionEvidenceTrustSetOperationResult `json:"trust_set,omitempty"` + TicketsAllocation *xrplTransactionEvidenceTicketsAllocationOperationResult `json:"tickets_allocation,omitempty"` + TrustSet *xrplTransactionEvidenceTrustSetOperationResult `json:"trust_set,omitempty"` + CoreumToXRPLTransfer *xrplTransactionEvidenceCoreumToXRPLTransferOperationResult `json:"coreum_to_xrpl_transfer,omitempty"` } type xrplTransactionResultEvidence struct { @@ -589,6 +614,30 @@ func (c *ContractClient) SendXRPLTrustSetTransactionResultEvidence(ctx context.C return txRes, nil } +// SendCoreumToXRPLTransferTransactionResultEvidence sends an Evidence of an accepted or rejected coreum to XRPL transfer transaction. +func (c *ContractClient) SendCoreumToXRPLTransferTransactionResultEvidence(ctx context.Context, sender sdk.AccAddress, evd XRPLTransactionResultCoreumToXRPLTransferEvidence) (*sdk.TxResponse, error) { + req := saveEvidenceRequest{ + Evidence: evidence{ + XRPLTransactionResult: &xrplTransactionResultEvidence{ + XRPLTransactionResultEvidence: evd.XRPLTransactionResultEvidence, + OperationResult: xrplTransactionEvidenceOperationResult{ + CoreumToXRPLTransfer: &xrplTransactionEvidenceCoreumToXRPLTransferOperationResult{}, + }, + }, + }, + } + txRes, err := c.execute(ctx, sender, execRequest{ + Body: map[ExecMethod]saveEvidenceRequest{ + ExecMethodSaveEvidence: req, + }, + }) + if err != nil { + return nil, err + } + + return txRes, nil +} + // RecoverTickets executes `recover_tickets` method. func (c *ContractClient) RecoverTickets(ctx context.Context, sender sdk.AccAddress, accountSequence uint32, numberOfTickets *uint32) (*sdk.TxResponse, error) { txRes, err := c.execute(ctx, sender, execRequest{ @@ -623,6 +672,23 @@ func (c *ContractClient) SaveSignature(ctx context.Context, sender sdk.AccAddres return txRes, nil } +// SendToXRPL executes `send_to_xrpl` method. +func (c *ContractClient) SendToXRPL(ctx context.Context, sender sdk.AccAddress, recipient string, amount sdk.Coin) (*sdk.TxResponse, error) { + txRes, err := c.execute(ctx, sender, execRequest{ + Body: map[ExecMethod]sendToXRPLRequest{ + ExecSendToXRPL: { + Recipient: recipient, + }, + }, + Funds: sdk.NewCoins(amount), + }) + if err != nil { + return nil, err + } + + return txRes, nil +} + // ******************** Query ******************** // GetContractConfig returns contract config. @@ -912,6 +978,21 @@ func IsXRPLTokenNotEnabledError(err error) bool { return isError(err, xrplTokenNotEnabledErrorString) } +// IsInvalidXRPLAddressError returns true if error is `InvalidXRPLAddress`. +func IsInvalidXRPLAddressError(err error) bool { + return isError(err, invalidXRPLAddressErrorString) +} + +// IsLastTicketReservedError returns true if error is `LastTicketReserved`. +func IsLastTicketReservedError(err error) bool { + return isError(err, lastTicketReservedErrorString) +} + +// IsNoAvailableTicketsError returns true if error is `NoAvailableTickets`. +func IsNoAvailableTicketsError(err error) bool { + return isError(err, noAvailableTicketsErrorString) +} + func isError(err error, errorString string) bool { return err != nil && strings.Contains(err.Error(), errorString) } diff --git a/relayer/processes/amount.go b/relayer/processes/amount.go index ae23ca9d..be5f5597 100644 --- a/relayer/processes/amount.go +++ b/relayer/processes/amount.go @@ -22,9 +22,9 @@ const ( XRPCurrency = "XRP" ) -// ConvertXRPLOriginTokenXRPLAmountToCoreumAmount converts the XRPL native token amount from XRPL to coreum amount +// ConvertXRPLOriginatedTokenXRPLAmountToCoreumAmount converts the XRPL native token amount from XRPL to coreum amount // based on the currency type. -func ConvertXRPLOriginTokenXRPLAmountToCoreumAmount(xrplAmount rippledata.Amount) (sdkmath.Int, error) { +func ConvertXRPLOriginatedTokenXRPLAmountToCoreumAmount(xrplAmount rippledata.Amount) (sdkmath.Int, error) { if xrplAmount.Value == nil { return sdkmath.ZeroInt(), nil } @@ -43,9 +43,9 @@ func ConvertXRPLOriginTokenXRPLAmountToCoreumAmount(xrplAmount rippledata.Amount return sdkmath.NewIntFromBigInt(binIntAmount), nil } -// ConvertXRPLOriginTokenCoreumAmountToXRPLAmount converts the XRPL originated token amount from coreum to XRPL amount +// ConvertXRPLOriginatedTokenCoreumAmountToXRPLAmount converts the XRPL originated token amount from coreum to XRPL amount // based on the currency type. -func ConvertXRPLOriginTokenCoreumAmountToXRPLAmount(coreumAmount sdkmath.Int, issuerString, currencyString string) (rippledata.Amount, error) { +func ConvertXRPLOriginatedTokenCoreumAmountToXRPLAmount(coreumAmount sdkmath.Int, issuerString, currencyString string) (rippledata.Amount, error) { if isXRPToken(issuerString, currencyString) { // format with exponent amountString := big.NewFloat(0).SetInt(coreumAmount.BigInt()).Text('g', XRPLAmountPrec) diff --git a/relayer/processes/amount_test.go b/relayer/processes/amount_test.go index 41f30aae..e320c58c 100644 --- a/relayer/processes/amount_test.go +++ b/relayer/processes/amount_test.go @@ -12,7 +12,7 @@ import ( "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/xrpl" ) -func TestConvertXRPLOriginTokenXRPLAmountToCoreumAmount(t *testing.T) { +func TestConvertXRPLOriginatedTokenXRPLAmountToCoreumAmount(t *testing.T) { t.Parallel() var ( @@ -81,7 +81,7 @@ func TestConvertXRPLOriginTokenXRPLAmountToCoreumAmount(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - got, err := processes.ConvertXRPLOriginTokenXRPLAmountToCoreumAmount(tt.xrplAmount) + got, err := processes.ConvertXRPLOriginatedTokenXRPLAmountToCoreumAmount(tt.xrplAmount) if tt.wantErr { require.Error(t, err) } else { @@ -92,7 +92,7 @@ func TestConvertXRPLOriginTokenXRPLAmountToCoreumAmount(t *testing.T) { } } -func TestConvertXRPLOriginTokenCoreumAmountToXRPLAmount(t *testing.T) { +func TestConvertXRPLOriginatedTokenCoreumAmountToXRPLAmount(t *testing.T) { t.Parallel() var ( @@ -176,7 +176,7 @@ func TestConvertXRPLOriginTokenCoreumAmountToXRPLAmount(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - got, err := processes.ConvertXRPLOriginTokenCoreumAmountToXRPLAmount(tt.coreumAmount, tt.issuer, tt.currency) + got, err := processes.ConvertXRPLOriginatedTokenCoreumAmountToXRPLAmount(tt.coreumAmount, tt.issuer, tt.currency) if tt.wantErr { require.Error(t, err) } else { diff --git a/relayer/processes/model.go b/relayer/processes/model.go index b57d0ee0..15442768 100644 --- a/relayer/processes/model.go +++ b/relayer/processes/model.go @@ -18,6 +18,7 @@ type ContractClient interface { SendXRPLToCoreumTransferEvidence(ctx context.Context, sender sdk.AccAddress, evidence coreum.XRPLToCoreumTransferEvidence) (*sdk.TxResponse, error) SendXRPLTicketsAllocationTransactionResultEvidence(ctx context.Context, sender sdk.AccAddress, evidence coreum.XRPLTransactionResultTicketsAllocationEvidence) (*sdk.TxResponse, error) SendXRPLTrustSetTransactionResultEvidence(ctx context.Context, sender sdk.AccAddress, evd coreum.XRPLTransactionResultTrustSetEvidence) (*sdk.TxResponse, error) + SendCoreumToXRPLTransferTransactionResultEvidence(ctx context.Context, sender sdk.AccAddress, evd coreum.XRPLTransactionResultCoreumToXRPLTransferEvidence) (*sdk.TxResponse, error) SaveSignature(ctx context.Context, sender sdk.AccAddress, operationID uint32, signature string) (*sdk.TxResponse, error) GetPendingOperations(ctx context.Context) ([]coreum.Operation, error) GetContractConfig(ctx context.Context) (coreum.ContractConfig, error) @@ -38,3 +39,10 @@ type XRPLRPCClient interface { type XRPLTxSigner interface { MultiSign(tx rippledata.MultiSignable, keyName string) (rippledata.Signer, error) } + +// IsEvidenceErrorCausedByResubmission returns true is error is cause of the re-submitting of the transaction. +func IsEvidenceErrorCausedByResubmission(err error) bool { + return coreum.IsEvidenceAlreadyProvidedError(err) || + coreum.IsOperationAlreadyExecutedError(err) || + coreum.IsPendingOperationNotFoundError(err) +} diff --git a/relayer/processes/model_mocks_test.go b/relayer/processes/model_mocks_test.go index f3502b3e..e4f073de 100644 --- a/relayer/processes/model_mocks_test.go +++ b/relayer/processes/model_mocks_test.go @@ -98,6 +98,21 @@ func (mr *MockContractClientMockRecorder) SaveSignature(arg0, arg1, arg2, arg3 i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSignature", reflect.TypeOf((*MockContractClient)(nil).SaveSignature), arg0, arg1, arg2, arg3) } +// SendCoreumToXRPLTransferTransactionResultEvidence mocks base method. +func (m *MockContractClient) SendCoreumToXRPLTransferTransactionResultEvidence(arg0 context.Context, arg1 types.AccAddress, arg2 coreum.XRPLTransactionResultCoreumToXRPLTransferEvidence) (*types.TxResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendCoreumToXRPLTransferTransactionResultEvidence", arg0, arg1, arg2) + ret0, _ := ret[0].(*types.TxResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SendCoreumToXRPLTransferTransactionResultEvidence indicates an expected call of SendCoreumToXRPLTransferTransactionResultEvidence. +func (mr *MockContractClientMockRecorder) SendCoreumToXRPLTransferTransactionResultEvidence(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCoreumToXRPLTransferTransactionResultEvidence", reflect.TypeOf((*MockContractClient)(nil).SendCoreumToXRPLTransferTransactionResultEvidence), arg0, arg1, arg2) +} + // SendXRPLTicketsAllocationTransactionResultEvidence mocks base method. func (m *MockContractClient) SendXRPLTicketsAllocationTransactionResultEvidence(arg0 context.Context, arg1 types.AccAddress, arg2 coreum.XRPLTransactionResultTicketsAllocationEvidence) (*types.TxResponse, error) { m.ctrl.T.Helper() diff --git a/relayer/processes/operation_tx.go b/relayer/processes/operation_tx.go index 22d7c408..c4aa8043 100644 --- a/relayer/processes/operation_tx.go +++ b/relayer/processes/operation_tx.go @@ -1,6 +1,9 @@ package processes import ( + "fmt" + + "github.com/pkg/errors" rippledata "github.com/rubblelabs/ripple/data" "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/coreum" @@ -35,7 +38,7 @@ func BuildTicketCreateTxForMultiSigning(bridgeXRPLAddress rippledata.Account, op // BuildTrustSetTxForMultiSigning builds TrustSet transaction operation from the contract operation. func BuildTrustSetTxForMultiSigning(bridgeXRPLAddress rippledata.Account, operation coreum.Operation) (*rippledata.TrustSet, error) { trustSetType := operation.OperationType.TrustSet - value, err := ConvertXRPLOriginTokenCoreumAmountToXRPLAmount( + value, err := ConvertXRPLOriginatedTokenCoreumAmountToXRPLAmount( trustSetType.TrustSetLimitAmount, trustSetType.Issuer, trustSetType.Currency, @@ -62,3 +65,39 @@ func BuildTrustSetTxForMultiSigning(bridgeXRPLAddress rippledata.Account, operat return &tx, nil } + +// BuildCoreumToXRPLTransferPaymentTxForMultiSigning builds Payment transaction operation from the contract operation. +func BuildCoreumToXRPLTransferPaymentTxForMultiSigning(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 + } + recipient, err := rippledata.NewAccountFromAddress(coreumToXRPLTransferOperationType.Recipient) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("failed to convert XRPL recipient to rippledata.Account, recipient:%s", coreumToXRPLTransferOperationType.Recipient)) + } + tx := rippledata.Payment{ + Destination: *recipient, + TxBase: rippledata.TxBase{ + Account: bridgeXRPLAddress, + TransactionType: rippledata.PAYMENT, + }, + Amount: value, + } + tx.TicketSequence = &operation.TicketSequence + // important for the multi-signing + tx.TxBase.SigningPubKey = &rippledata.PublicKey{} + + fee, err := GetTxFee(&tx) + if err != nil { + return nil, err + } + tx.TxBase.Fee = fee + + return &tx, nil +} diff --git a/relayer/processes/xrpl_tx_observer.go b/relayer/processes/xrpl_tx_observer.go index a00da145..d4519b8e 100644 --- a/relayer/processes/xrpl_tx_observer.go +++ b/relayer/processes/xrpl_tx_observer.go @@ -114,7 +114,7 @@ func (o *XRPLTxObserver) processIncomingTx(ctx context.Context, tx rippledata.Tr } deliveredXRPLAmount := tx.MetaData.DeliveredAmount - coreumAmount, err := ConvertXRPLOriginTokenXRPLAmountToCoreumAmount(*deliveredXRPLAmount) + coreumAmount, err := ConvertXRPLOriginatedTokenXRPLAmountToCoreumAmount(*deliveredXRPLAmount) if err != nil { return err } @@ -160,6 +160,8 @@ func (o *XRPLTxObserver) processOutgoingTx(ctx context.Context, tx rippledata.Tr return o.sendXRPLTicketsAllocationTransactionResultEvidence(ctx, tx) case rippledata.TRUST_SET.String(): return o.sendXRPLTrustSetTransactionResultEvidence(ctx, tx) + case rippledata.PAYMENT.String(): + return o.sendCoreumToXRPLTransferTransactionResultEvidence(ctx, tx) default: // TODO(dzmitryhil) replace with the error once we integrate all supported types o.log.Warn(ctx, "Found unsupported transaction type", logger.AnyField("tx", tx)) @@ -167,18 +169,10 @@ func (o *XRPLTxObserver) processOutgoingTx(ctx context.Context, tx rippledata.Tr } } -// IsEvidenceErrorCausedByResubmission returns true is error is cause of the re-submitting of the transaction. -func IsEvidenceErrorCausedByResubmission(err error) bool { - return coreum.IsEvidenceAlreadyProvidedError(err) || - coreum.IsOperationAlreadyExecutedError(err) || - coreum.IsPendingOperationNotFoundError(err) -} - func (o *XRPLTxObserver) sendXRPLTicketsAllocationTransactionResultEvidence(ctx context.Context, tx rippledata.TransactionWithMetaData) error { tickets := extractTicketSequencesFromMetaData(tx.MetaData) - txResult := coreum.TransactionResultAccepted - if !tx.MetaData.TransactionResult.Success() { - txResult = coreum.TransactionResultRejected + txResult := getTransactionResult(tx) + if txResult == coreum.TransactionResultRejected { tickets = nil } evidence := coreum.XRPLTransactionResultTicketsAllocationEvidence{ @@ -203,25 +197,11 @@ func (o *XRPLTxObserver) sendXRPLTicketsAllocationTransactionResultEvidence(ctx o.cfg.RelayerAddress, evidence, ) - if err == nil { - if evidence.TransactionResult != coreum.TransactionResultAccepted { - o.log.Warn(ctx, "Transaction was rejected", logger.StringField("txResult", tx.MetaData.TransactionResult.String())) - } - return nil - } - if IsEvidenceErrorCausedByResubmission(err) { - o.log.Debug(ctx, "Received expected send evidence error") - return nil - } - return err + return o.handleEvidenceSubmissionError(ctx, err, tx, evidence.XRPLTransactionResultEvidence) } func (o *XRPLTxObserver) sendXRPLTrustSetTransactionResultEvidence(ctx context.Context, tx rippledata.TransactionWithMetaData) error { - txResult := coreum.TransactionResultAccepted - if !tx.MetaData.TransactionResult.Success() { - txResult = coreum.TransactionResultRejected - } trustSetTx, ok := tx.Transaction.(*rippledata.TrustSet) if !ok { return errors.Errorf("failed to cast tx to TrustSet, data:%+v", tx) @@ -229,7 +209,7 @@ func (o *XRPLTxObserver) sendXRPLTrustSetTransactionResultEvidence(ctx context.C evidence := coreum.XRPLTransactionResultTrustSetEvidence{ XRPLTransactionResultEvidence: coreum.XRPLTransactionResultEvidence{ TxHash: tx.GetHash().String(), - TransactionResult: txResult, + TransactionResult: getTransactionResult(tx), TicketSequence: trustSetTx.TicketSequence, }, Issuer: trustSetTx.LimitAmount.Issuer.String(), @@ -241,6 +221,38 @@ func (o *XRPLTxObserver) sendXRPLTrustSetTransactionResultEvidence(ctx context.C o.cfg.RelayerAddress, evidence, ) + + return o.handleEvidenceSubmissionError(ctx, err, tx, evidence.XRPLTransactionResultEvidence) +} + +func (o *XRPLTxObserver) sendCoreumToXRPLTransferTransactionResultEvidence(ctx context.Context, tx rippledata.TransactionWithMetaData) error { + paymentTx, ok := tx.Transaction.(*rippledata.Payment) + if !ok { + return errors.Errorf("failed to cast tx to Payment, data:%+v", tx) + } + evidence := coreum.XRPLTransactionResultCoreumToXRPLTransferEvidence{ + XRPLTransactionResultEvidence: coreum.XRPLTransactionResultEvidence{ + TxHash: tx.GetHash().String(), + TransactionResult: getTransactionResult(tx), + TicketSequence: paymentTx.TicketSequence, + }, + } + + _, err := o.contractClient.SendCoreumToXRPLTransferTransactionResultEvidence( + ctx, + o.cfg.RelayerAddress, + evidence, + ) + + return o.handleEvidenceSubmissionError(ctx, err, tx, evidence.XRPLTransactionResultEvidence) +} + +func (o *XRPLTxObserver) handleEvidenceSubmissionError( + ctx context.Context, + err error, + tx rippledata.TransactionWithMetaData, + evidence coreum.XRPLTransactionResultEvidence, +) error { if err == nil { if evidence.TransactionResult != coreum.TransactionResultAccepted { o.log.Warn(ctx, "Transaction was rejected", logger.StringField("txResult", tx.MetaData.TransactionResult.String())) @@ -251,10 +263,16 @@ func (o *XRPLTxObserver) sendXRPLTrustSetTransactionResultEvidence(ctx context.C o.log.Debug(ctx, "Received expected send evidence error") return nil } - return err } +func getTransactionResult(tx rippledata.TransactionWithMetaData) coreum.TransactionResult { + if tx.MetaData.TransactionResult.Success() { + return coreum.TransactionResultAccepted + } + return coreum.TransactionResultRejected +} + func extractTicketSequencesFromMetaData(metaData rippledata.MetaData) []uint32 { ticketSequences := make([]uint32, 0) for _, node := range metaData.AffectedNodes { diff --git a/relayer/processes/xrpl_tx_observer_test.go b/relayer/processes/xrpl_tx_observer_test.go index 9f287515..19bdc0af 100644 --- a/relayer/processes/xrpl_tx_observer_test.go +++ b/relayer/processes/xrpl_tx_observer_test.go @@ -20,6 +20,7 @@ func TestXRPLTxObserver_Start(t *testing.T) { t.Parallel() bridgeXRPLAddress := xrpl.GenPrivKeyTxSigner().Account() + recipientXRPLAddress := xrpl.GenPrivKeyTxSigner().Account() issuerAccount := xrpl.GenPrivKeyTxSigner().Account() failTxResult := rippledata.TransactionResult(111) @@ -363,7 +364,93 @@ func TestXRPLTxObserver_Start(t *testing.T) { return contractClientMock }, }, + { + name: "outgoing_payment_tx", + txScannerBuilder: func(ctrl *gomock.Controller, cancel func()) processes.XRPLAccountTxScanner { + xrplAccountTxScannerMock := NewMockXRPLAccountTxScanner(ctrl) + xrplAccountTxScannerMock.EXPECT().ScanTxs(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, ch chan<- rippledata.TransactionWithMetaData) error { + go func() { + ch <- rippledata.TransactionWithMetaData{ + Transaction: &rippledata.Payment{ + TxBase: rippledata.TxBase{ + Account: bridgeXRPLAddress, + TransactionType: rippledata.PAYMENT, + }, + Destination: recipientXRPLAddress, + Amount: xrplCurrencyAmount, + TicketSequence: lo.ToPtr(uint32(11)), + }, + } + cancel() + }() + return nil + }) + + return xrplAccountTxScannerMock + }, + contractClientBuilder: func(ctrl *gomock.Controller) processes.ContractClient { + contractClientMock := NewMockContractClient(ctrl) + contractClientMock.EXPECT().SendCoreumToXRPLTransferTransactionResultEvidence( + gomock.Any(), + relayerAddress, + coreum.XRPLTransactionResultCoreumToXRPLTransferEvidence{ + XRPLTransactionResultEvidence: coreum.XRPLTransactionResultEvidence{ + TxHash: rippledata.Hash256{}.String(), + TicketSequence: lo.ToPtr(uint32(11)), + TransactionResult: coreum.TransactionResultAccepted, + }, + }, + ).Return(nil, nil) + + return contractClientMock + }, + }, + { + name: "outgoing_payment_tx_with_failure", + txScannerBuilder: func(ctrl *gomock.Controller, cancel func()) processes.XRPLAccountTxScanner { + xrplAccountTxScannerMock := NewMockXRPLAccountTxScanner(ctrl) + xrplAccountTxScannerMock.EXPECT().ScanTxs(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, ch chan<- rippledata.TransactionWithMetaData) error { + go func() { + ch <- rippledata.TransactionWithMetaData{ + Transaction: &rippledata.Payment{ + TxBase: rippledata.TxBase{ + Account: bridgeXRPLAddress, + TransactionType: rippledata.PAYMENT, + }, + Destination: recipientXRPLAddress, + Amount: xrplCurrencyAmount, + TicketSequence: lo.ToPtr(uint32(11)), + }, + MetaData: rippledata.MetaData{ + TransactionResult: failTxResult, + }, + } + cancel() + }() + return nil + }) + + return xrplAccountTxScannerMock + }, + contractClientBuilder: func(ctrl *gomock.Controller) processes.ContractClient { + contractClientMock := NewMockContractClient(ctrl) + contractClientMock.EXPECT().SendCoreumToXRPLTransferTransactionResultEvidence( + gomock.Any(), + relayerAddress, + coreum.XRPLTransactionResultCoreumToXRPLTransferEvidence{ + XRPLTransactionResultEvidence: coreum.XRPLTransactionResultEvidence{ + TxHash: rippledata.Hash256{}.String(), + TicketSequence: lo.ToPtr(uint32(11)), + TransactionResult: coreum.TransactionResultRejected, + }, + }, + ).Return(nil, nil) + return contractClientMock + }, + }, { name: "outgoing_not_expected_tx", txScannerBuilder: func(ctrl *gomock.Controller, cancel func()) processes.XRPLAccountTxScanner { diff --git a/relayer/processes/xrpl_tx_submitter.go b/relayer/processes/xrpl_tx_submitter.go index 46068247..9d18f050 100644 --- a/relayer/processes/xrpl_tx_submitter.go +++ b/relayer/processes/xrpl_tx_submitter.go @@ -221,6 +221,12 @@ func (s *XRPLTxSubmitter) signOrSubmitOperation(ctx context.Context, operation c case xrpl.TefNOTicketTxResult, xrpl.TefPastSeqTxResult, xrpl.TerPreSeqTxResult: s.log.Debug(ctx, "Transaction has been already submitted", logger.StringField("txHash", tx.GetHash().String())) return nil + case xrpl.TecPathDryTxResult: + s.log.Info( + ctx, + "The transaction has been sent, but will be reverted since the provided path does not have enough liquidity or the receipt doesn't link by trust lines.", + logger.StringField("txHash", tx.GetHash().String())) + return nil case xrpl.TecInsufficientReserveTxResult: // for that case the tx will be accepted by the node and its rejection will be handled in the observer s.log.Error(ctx, "Insufficient reserve to complete the operation", logger.StringField("txHash", tx.GetHash().String())) @@ -399,13 +405,32 @@ func (s *XRPLTxSubmitter) registerTxSignature(ctx context.Context, operation cor func (s *XRPLTxSubmitter) buildXRPLTxFromOperation(operation coreum.Operation) (MultiSignableTransaction, error) { switch { - case operation.OperationType.AllocateTickets != nil && operation.OperationType.AllocateTickets.Number > 0: + case isAllocateTicketsOperation(operation): return BuildTicketCreateTxForMultiSigning(s.cfg.BridgeAccount, operation) - case operation.OperationType.TrustSet != nil && - operation.OperationType.TrustSet.Issuer != "" && - operation.OperationType.TrustSet.Currency != "": + case isTrustSetOperation(operation): return BuildTrustSetTxForMultiSigning(s.cfg.BridgeAccount, operation) + case isCoreumToXRPLTransfer(operation): + return BuildCoreumToXRPLTransferPaymentTxForMultiSigning(s.cfg.BridgeAccount, operation) default: - return nil, errors.Errorf("failed to process operation, unable to determin operation type, operation:%+v", operation) + return nil, errors.Errorf("failed to process operation, unable to determine operation type, operation:%+v", operation) } } + +func isAllocateTicketsOperation(operation coreum.Operation) bool { + return operation.OperationType.AllocateTickets != nil && + operation.OperationType.AllocateTickets.Number > 0 +} + +func isTrustSetOperation(operation coreum.Operation) bool { + return operation.OperationType.TrustSet != nil && + operation.OperationType.TrustSet.Issuer != "" && + operation.OperationType.TrustSet.Currency != "" +} + +func isCoreumToXRPLTransfer(operation coreum.Operation) bool { + return operation.OperationType.CoreumToXRPLTransfer != nil && + operation.OperationType.CoreumToXRPLTransfer.Issuer != "" && + operation.OperationType.CoreumToXRPLTransfer.Currency != "" && + !operation.OperationType.CoreumToXRPLTransfer.Amount.IsZero() && + operation.OperationType.CoreumToXRPLTransfer.Recipient != "" +} diff --git a/relayer/processes/xrpl_tx_submitter_test.go b/relayer/processes/xrpl_tx_submitter_test.go index 641b01f3..391f48be 100644 --- a/relayer/processes/xrpl_tx_submitter_test.go +++ b/relayer/processes/xrpl_tx_submitter_test.go @@ -136,8 +136,8 @@ func TestXRPLTxSubmitter_Start(t *testing.T) { // ********** TrustSet ********** trustSetOperationWithoutSignatures := coreum.Operation{ - AccountSequence: 1, - Signatures: nil, + TicketSequence: 1, + Signatures: nil, OperationType: coreum.OperationType{ TrustSet: &coreum.OperationTypeTrustSet{ Issuer: xrpl.GenPrivKeyTxSigner().Account().String(), @@ -180,6 +180,54 @@ func TestXRPLTxSubmitter_Start(t *testing.T) { trustSetOperationSigner2, } + // ********** coreumToXRPLTransfer ********** + + coreumToXRPLTransferOperationWithoutSignatures := coreum.Operation{ + TicketSequence: 1, + Signatures: nil, + OperationType: coreum.OperationType{ + CoreumToXRPLTransfer: &coreum.OperationTypeCoreumToXRPLTransfer{ + Issuer: xrpl.GenPrivKeyTxSigner().Account().String(), + Currency: "TRC", + Amount: sdkmath.NewIntFromUint64(123), + Recipient: xrpl.GenPrivKeyTxSigner().Account().String(), + }, + }, + } + + coreumToXRPLTransferOperationWithSignatures := coreumToXRPLTransferOperationWithoutSignatures + coreumToXRPLTransferOperationSigner1 := multiSignCoreumToXRPLTransferOperation( + t, + xrplRelayer1Signer, + bridgeXRPLAddress, + coreumToXRPLTransferOperationWithSignatures, + ) + coreumToXRPLTransferOperationSigner2 := multiSignCoreumToXRPLTransferOperation( + t, + xrplRelayer2Signer, + bridgeXRPLAddress, + coreumToXRPLTransferOperationWithSignatures, + ) + coreumToXRPLTransferOperationWithSignatures.Signatures = []coreum.Signature{ + { + RelayerCoreumAddress: coreumRelayer1Address, + Signature: coreumToXRPLTransferOperationSigner1.Signer.TxnSignature.String(), + }, + { + RelayerCoreumAddress: coreumRelayer2Address, + Signature: coreumToXRPLTransferOperationSigner2.Signer.TxnSignature.String(), + }, + { + RelayerCoreumAddress: coreumRelayer3Address, + // the signature is taken from the first signer, so it is invalid + Signature: coreumToXRPLTransferOperationSigner1.Signer.TxnSignature.String(), + }, + } + coreumToXRPLTransferOperationWithSignaturesSigners := []rippledata.Signer{ + coreumToXRPLTransferOperationSigner1, + coreumToXRPLTransferOperationSigner2, + } + tests := []struct { name string contractClientBuilder func(ctrl *gomock.Controller) processes.ContractClient @@ -195,7 +243,7 @@ func TestXRPLTxSubmitter_Start(t *testing.T) { }, }, { - name: "resister_signature_for_create_ticket_tx", + name: "register_signature_for_create_ticket_tx", contractClientBuilder: func(ctrl *gomock.Controller) processes.ContractClient { contractClientMock := NewMockContractClient(ctrl) contractClientMock.EXPECT().GetPendingOperations(gomock.Any()).Return([]coreum.Operation{allocateTicketOperationWithoutSignatures}, nil) @@ -249,7 +297,7 @@ func TestXRPLTxSubmitter_Start(t *testing.T) { }, }, { - name: "resister_invalid_create_ticket_tx", + name: "register_invalid_create_ticket_tx", contractClientBuilder: func(ctrl *gomock.Controller) processes.ContractClient { contractClientMock := NewMockContractClient(ctrl) contractClientMock.EXPECT().GetPendingOperations(gomock.Any()).Return([]coreum.Operation{allocateTicketOperationWithUnexpectedSequencNumber}, nil) @@ -272,14 +320,14 @@ func TestXRPLTxSubmitter_Start(t *testing.T) { }, }, { - name: "resister_signature_for_trust_set_tx", + name: "register_signature_for_trust_set_tx", contractClientBuilder: func(ctrl *gomock.Controller) processes.ContractClient { contractClientMock := NewMockContractClient(ctrl) contractClientMock.EXPECT().GetPendingOperations(gomock.Any()).Return([]coreum.Operation{trustSetOperationWithoutSignatures}, nil) contractClientMock.EXPECT().GetContractConfig(gomock.Any()).Return(coreum.ContractConfig{ Relayers: contractRelayers, }, nil) - contractClientMock.EXPECT().SaveSignature(gomock.Any(), coreumRelayer1Address, trustSetOperationWithoutSignatures.AccountSequence, trustSetOperationSigner1.Signer.TxnSignature.String()) + contractClientMock.EXPECT().SaveSignature(gomock.Any(), coreumRelayer1Address, trustSetOperationWithoutSignatures.TicketSequence, trustSetOperationSigner1.Signer.TxnSignature.String()) return contractClientMock }, xrplRPCClientBuilder: func(ctrl *gomock.Controller) processes.XRPLRPCClient { @@ -321,6 +369,59 @@ func TestXRPLTxSubmitter_Start(t *testing.T) { return xrpl.SubmitResult{}, nil }) + return xrplRPCClientMock + }, + }, + { + name: "register_signature_for_coreum_to_XRPL_transfer_payment_tx", + contractClientBuilder: func(ctrl *gomock.Controller) processes.ContractClient { + contractClientMock := NewMockContractClient(ctrl) + contractClientMock.EXPECT().GetPendingOperations(gomock.Any()).Return([]coreum.Operation{coreumToXRPLTransferOperationWithoutSignatures}, nil) + contractClientMock.EXPECT().GetContractConfig(gomock.Any()).Return(coreum.ContractConfig{ + Relayers: contractRelayers, + }, nil) + contractClientMock.EXPECT().SaveSignature(gomock.Any(), coreumRelayer1Address, coreumToXRPLTransferOperationWithoutSignatures.TicketSequence, coreumToXRPLTransferOperationSigner1.Signer.TxnSignature.String()) + return contractClientMock + }, + xrplRPCClientBuilder: func(ctrl *gomock.Controller) processes.XRPLRPCClient { + xrplRPCClientMock := NewMockXRPLRPCClient(ctrl) + xrplRPCClientMock.EXPECT().AccountInfo(gomock.Any(), bridgeXRPLAddress).Return(bridgeXRPLSignerAccountWithSigners, nil) + return xrplRPCClientMock + }, + xrplTxSignerBuilder: func(ctrl *gomock.Controller) processes.XRPLTxSigner { + xrplTxSignerMock := NewMockXRPLTxSigner(ctrl) + tx, err := processes.BuildCoreumToXRPLTransferPaymentTxForMultiSigning(bridgeXRPLAddress, coreumToXRPLTransferOperationWithoutSignatures) + require.NoError(t, err) + xrplTxSignerMock.EXPECT().MultiSign(tx, xrplTxSignerKeyName).Return(coreumToXRPLTransferOperationSigner1, nil) + + return xrplTxSignerMock + }, + }, + { + name: "submit_coreum_to_XRPL_transfer_payment_tx_with_filtered_signature", + contractClientBuilder: func(ctrl *gomock.Controller) processes.ContractClient { + contractClientMock := NewMockContractClient(ctrl) + contractClientMock.EXPECT().GetPendingOperations(gomock.Any()).Return([]coreum.Operation{coreumToXRPLTransferOperationWithSignatures}, nil) + contractClientMock.EXPECT().GetContractConfig(gomock.Any()).Return(coreum.ContractConfig{ + Relayers: contractRelayers, + }, nil) + return contractClientMock + }, + xrplRPCClientBuilder: func(ctrl *gomock.Controller) processes.XRPLRPCClient { + xrplRPCClientMock := NewMockXRPLRPCClient(ctrl) + xrplRPCClientMock.EXPECT().AccountInfo(gomock.Any(), bridgeXRPLAddress).Return(bridgeXRPLSignerAccountWithSigners, nil) + expectedTx, err := processes.BuildCoreumToXRPLTransferPaymentTxForMultiSigning(bridgeXRPLAddress, coreumToXRPLTransferOperationWithSignatures) + require.NoError(t, err) + require.NoError(t, rippledata.SetSigners(expectedTx, coreumToXRPLTransferOperationWithSignaturesSigners...)) + xrplRPCClientMock.EXPECT().Submit(gomock.Any(), gomock.Any()).Do(func(ctx context.Context, tx rippledata.Transaction) (xrpl.SubmitResult, error) { + _, expectedTxRaw, err := rippledata.Raw(expectedTx) + require.NoError(t, err) + _, txRaw, err := rippledata.Raw(tx) + require.NoError(t, err) + require.Equal(t, expectedTxRaw, txRaw) + return xrpl.SubmitResult{}, nil + }) + return xrplRPCClientMock }, }, @@ -393,3 +494,17 @@ func multiSignTrustSetOperation( return signer } + +func multiSignCoreumToXRPLTransferOperation( + t *testing.T, + relayerXRPLSigner *xrpl.PrivKeyTxSigner, + bridgeXRPLAcc rippledata.Account, + operation coreum.Operation, +) rippledata.Signer { + tx, err := processes.BuildCoreumToXRPLTransferPaymentTxForMultiSigning(bridgeXRPLAcc, operation) + require.NoError(t, err) + signer, err := relayerXRPLSigner.MultiSign(tx) + require.NoError(t, err) + + return signer +} diff --git a/relayer/xrpl/constatns.go b/relayer/xrpl/constatns.go index d23bd63a..fcda1c66 100644 --- a/relayer/xrpl/constatns.go +++ b/relayer/xrpl/constatns.go @@ -1,6 +1,9 @@ package xrpl const ( + // TecPathDryTxResult defines that provided paths did not have enough liquidity to send anything at all. + // This could mean that the source and destination accounts are not linked by trust lines. + TecPathDryTxResult = "tecPATH_DRY" // TefNOTicketTxResult defines the result which indicates the usage of the passed ticket or not created ticket. TefNOTicketTxResult = "tefNO_TICKET" // TefPastSeqTxResult defines the result which indicates the usage of the sequence in the past.