Skip to content

Commit

Permalink
Relayer: Recover XRPL originated token registration (#53)
Browse files Browse the repository at this point in the history
* Add contract client integration tests
* Add relayer (runners) integration test
* Add non-existing accounts error handling for the xrpl_tx_submitter
  • Loading branch information
dzmitryhil authored Nov 30, 2023
1 parent 9cf7c67 commit 6affe9b
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 9 deletions.
150 changes: 150 additions & 0 deletions integration-tests/coreum/contract_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2186,6 +2186,156 @@ func TestSendFromCoreumToXRPLCoreumOriginatedTokenWithDifferentSendingPrecisionA
}
}

func TestRecoverXRPLTokeRegistration(t *testing.T) {
t.Parallel()

ctx, chains := integrationtests.NewTestingContext(t)

relayers := genRelayers(ctx, t, chains, 2)

notOwner := chains.Coreum.GenAccount()
issueFee := chains.Coreum.QueryAssetFTParams(ctx, t).IssueFee
chains.Coreum.FundAccountWithOptions(ctx, t, notOwner, coreumintegration.BalancesOptions{
Amount: issueFee.Amount.AddRaw(1_000_000),
})

owner, contractClient := integrationtests.DeployAndInstantiateContract(
ctx,
t,
chains,
relayers,
len(relayers),
3,
defaultTrustSetLimitAmount,
xrpl.GenPrivKeyTxSigner().Account().String(),
)

chains.Coreum.FundAccountWithOptions(ctx, t, owner, coreumintegration.BalancesOptions{
Amount: issueFee.Amount.Mul(sdkmath.NewIntFromUint64(1)),
})

issuerAcc := chains.XRPL.GenAccount(ctx, t, 0)
issuer := issuerAcc.String()
currency := "CRN"
sendingPrecision := int32(15)
maxHoldingAmount := sdk.NewIntFromUint64(10000)

// recover tickets to be able to create operations from coreum to XRPL
recoverTickets(ctx, t, contractClient, owner, relayers, 100)

// register from the owner
_, err := contractClient.RegisterXRPLToken(ctx, owner, issuer, currency, sendingPrecision, maxHoldingAmount)
require.NoError(t, err)

registeredXRPLToken, err := contractClient.GetXRPLTokenByIssuerAndCurrency(ctx, issuer, currency)
require.NoError(t, err)

require.Equal(t, coreum.XRPLToken{
Issuer: issuer,
Currency: currency,
CoreumDenom: registeredXRPLToken.CoreumDenom,
SendingPrecision: sendingPrecision,
MaxHoldingAmount: maxHoldingAmount,
State: coreum.TokenStateProcessing,
}, registeredXRPLToken)

// try to recover the token with the unexpected current state
_, err = contractClient.RecoverXRPLTokenRegistration(ctx, owner, issuer, currency)
require.True(t, coreum.IsXRPLTokenNotInactiveError(err), err)

// reject token trust set to be able to recover
pendingOperations, err := contractClient.GetPendingOperations(ctx)
require.NoError(t, err)
require.Len(t, pendingOperations, 1)
operation := pendingOperations[0]
require.NotNil(t, operation.OperationType.TrustSet)

require.Equal(t, coreum.OperationTypeTrustSet{
Issuer: issuer,
Currency: currency,
TrustSetLimitAmount: defaultTrustSetLimitAmount,
}, *operation.OperationType.TrustSet)

rejectedTxEvidenceTrustSet := coreum.XRPLTransactionResultTrustSetEvidence{
XRPLTransactionResultEvidence: coreum.XRPLTransactionResultEvidence{
TxHash: genXRPLTxHash(t),
TicketSequence: &operation.TicketSequence,
TransactionResult: coreum.TransactionResultRejected,
},
Issuer: issuer,
Currency: currency,
}

// send from first relayer
_, err = contractClient.SendXRPLTrustSetTransactionResultEvidence(ctx, relayers[0].CoreumAddress, rejectedTxEvidenceTrustSet)
require.NoError(t, err)
// send from second relayer
_, err = contractClient.SendXRPLTrustSetTransactionResultEvidence(ctx, relayers[1].CoreumAddress, rejectedTxEvidenceTrustSet)
require.NoError(t, err)

// check that we don't have pending operations anymore
pendingOperations, err = contractClient.GetPendingOperations(ctx)
require.NoError(t, err)
require.Empty(t, pendingOperations)

// fetch token to validate status
registeredXRPLToken, err = contractClient.GetXRPLTokenByIssuerAndCurrency(ctx, issuer, currency)
require.NoError(t, err)
require.Equal(t, coreum.TokenStateInactive, registeredXRPLToken.State)

// try to recover from now owner
_, err = contractClient.RecoverXRPLTokenRegistration(ctx, notOwner, issuer, currency)
require.True(t, coreum.IsNotOwnerError(err), err)

// recover from owner
_, err = contractClient.RecoverXRPLTokenRegistration(ctx, owner, issuer, currency)
require.NoError(t, err)

// fetch token to validate status
registeredXRPLToken, err = contractClient.GetXRPLTokenByIssuerAndCurrency(ctx, issuer, currency)
require.NoError(t, err)
require.Equal(t, coreum.TokenStateProcessing, registeredXRPLToken.State)

// check that new operation is present here
pendingOperations, err = contractClient.GetPendingOperations(ctx)
require.NoError(t, err)
require.Len(t, pendingOperations, 1)
operation = pendingOperations[0]
require.NotNil(t, operation.OperationType.TrustSet)

require.Equal(t, coreum.OperationTypeTrustSet{
Issuer: issuer,
Currency: currency,
TrustSetLimitAmount: defaultTrustSetLimitAmount,
}, *operation.OperationType.TrustSet)

acceptedTxEvidenceTrustSet := coreum.XRPLTransactionResultTrustSetEvidence{
XRPLTransactionResultEvidence: coreum.XRPLTransactionResultEvidence{
TxHash: genXRPLTxHash(t),
TicketSequence: &operation.TicketSequence,
TransactionResult: coreum.TransactionResultAccepted,
},
Issuer: issuer,
Currency: currency,
}

// send from first relayer
_, err = contractClient.SendXRPLTrustSetTransactionResultEvidence(ctx, relayers[0].CoreumAddress, acceptedTxEvidenceTrustSet)
require.NoError(t, err)
// send from second relayer
_, err = contractClient.SendXRPLTrustSetTransactionResultEvidence(ctx, relayers[1].CoreumAddress, acceptedTxEvidenceTrustSet)
require.NoError(t, err)

pendingOperations, err = contractClient.GetPendingOperations(ctx)
require.NoError(t, err)
require.Empty(t, pendingOperations)

// fetch token to validate status
registeredXRPLToken, err = contractClient.GetXRPLTokenByIssuerAndCurrency(ctx, issuer, currency)
require.NoError(t, err)
require.Equal(t, coreum.TokenStateEnabled, registeredXRPLToken.State)
}

func recoverTickets(
ctx context.Context,
t *testing.T,
Expand Down
74 changes: 74 additions & 0 deletions integration-tests/processes/send_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,80 @@ func TestSendXRPLOriginatedTokensFromXRPLToCoreumWithDifferentAmountAndPartialAm
runnerEnv.AwaitCoreumBalance(ctx, t, chains.Coreum, coreumRecipient, sdk.NewCoin(registeredXRPLHexCurrencyToken.CoreumDenom, integrationtests.ConvertStringWithDecimalsToSDKInt(t, "9.9", XRPLTokenDecimals)))
}

func TestRecoverXRPLOriginatedTokenRegistrationAndSendFromXRPLToCoreumAndBack(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)

// register the XRPL token and await for the tx to be failed
runnerEnv.Chains.Coreum.FundAccountWithOptions(ctx, t, runnerEnv.ContractOwner, coreumintegration.BalancesOptions{
Amount: runnerEnv.Chains.Coreum.QueryAssetFTParams(ctx, t).IssueFee.Amount,
})

// gen account but don't fund it to let the tx to fail since the account won't exist on the XRPL side
xrplIssuerAddress := chains.XRPL.GenEmptyAccount(t)

_, err = runnerEnv.ContractClient.RegisterXRPLToken(ctx, runnerEnv.ContractOwner, xrplIssuerAddress.String(), xrpl.ConvertCurrencyToString(registeredXRPLCurrency), int32(6), integrationtests.ConvertStringWithDecimalsToSDKInt(t, "1", 30))
require.NoError(t, err)
runnerEnv.AwaitNoPendingOperations(ctx, t)
registeredXRPLToken, err := runnerEnv.ContractClient.GetXRPLTokenByIssuerAndCurrency(ctx, xrplIssuerAddress.String(), xrpl.ConvertCurrencyToString(registeredXRPLCurrency))
require.NoError(t, err)
require.Equal(t, coreum.TokenStateInactive, registeredXRPLToken.State)

// create the account on the XRPL and send some XRP on top to cover fees
runnerEnv.Chains.XRPL.CreateAccount(ctx, t, xrplIssuerAddress, 1)
// recover from owner
_, err = runnerEnv.ContractClient.RecoverXRPLTokenRegistration(ctx, runnerEnv.ContractOwner, xrplIssuerAddress.String(), registeredXRPLCurrency.String())
require.NoError(t, err)
runnerEnv.AwaitNoPendingOperations(ctx, t)
// now the token is enabled
registeredXRPLToken, err = runnerEnv.ContractClient.GetXRPLTokenByIssuerAndCurrency(ctx, xrplIssuerAddress.String(), xrpl.ConvertCurrencyToString(registeredXRPLCurrency))
require.NoError(t, err)
require.Equal(t, coreum.TokenStateEnabled, registeredXRPLToken.State)

// enable to be able to send to any address
runnerEnv.EnableXRPLAccountRippling(ctx, t, xrplIssuerAddress)

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)

_, err = runnerEnv.ContractClient.SendToXRPL(ctx, coreumSender, xrplRecipientAddress.String(), sdk.NewCoin(registeredXRPLToken.CoreumDenom, integrationtests.ConvertStringWithDecimalsToSDKInt(t, valueToSendFromXRPLtoCoreum.String(), XRPLTokenDecimals)))
require.NoError(t, err)
runnerEnv.AwaitNoPendingOperations(ctx, t)

balance := runnerEnv.Chains.XRPL.GetAccountBalance(ctx, t, xrplRecipientAddress, xrplIssuerAddress, registeredXRPLCurrency)
require.Equal(t, valueToSendFromXRPLtoCoreum.String(), balance.Value.String())
}

func TestSendCoreumOriginatedTokenFromCoreumToXRPLAndBackWithDifferentAmountsAndPartialAmount(t *testing.T) {
t.Parallel()

Expand Down
19 changes: 17 additions & 2 deletions integration-tests/xrpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ func (c XRPLChain) RPCClient() *xrpl.RPCClient {
func (c XRPLChain) GenAccount(ctx context.Context, t *testing.T, amount float64) rippledata.Account {
t.Helper()

acc := c.GenEmptyAccount(t)
c.CreateAccount(ctx, t, acc, amount)

return acc
}

// GenEmptyAccount generates the signer but doesn't activate it.
func (c XRPLChain) GenEmptyAccount(t *testing.T) rippledata.Account {
t.Helper()

const signerKeyName = "signer"
kr := createInMemoryKeyring()
_, mnemonic, err := kr.NewMnemonic(
Expand All @@ -118,11 +128,16 @@ func (c XRPLChain) GenAccount(ctx context.Context, t *testing.T, amount float64)
)
require.NoError(t, err)

c.FundAccount(ctx, t, acc, amount+xrplReserveToActivateAccount)

return acc
}

// CreateAccount funds the provided account with the amount/reserve to activate the account.
func (c XRPLChain) CreateAccount(ctx context.Context, t *testing.T, acc rippledata.Account, amount float64) {
t.Helper()
// amount to activate the account and some tokens on top
c.FundAccount(ctx, t, acc, amount+xrplReserveToActivateAccount)
}

// GetSignerKeyring returns signer keyring.
func (c XRPLChain) GetSignerKeyring() keyring.Keyring {
return c.signer.GetKeyring()
Expand Down
42 changes: 35 additions & 7 deletions relayer/coreum/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ type ExecMethod string

// ExecMethods.
const (
ExecMethodUpdateOwnership ExecMethod = "update_ownership"
ExecMethodRegisterCoreumToken ExecMethod = "register_coreum_token"
ExecMethodRegisterXRPLToken ExecMethod = "register_xrpl_token"
ExecMethodSaveEvidence ExecMethod = "save_evidence"
ExecMethodRecoverTickets ExecMethod = "recover_tickets"
ExecMethodSaveSignature ExecMethod = "save_signature"
ExecSendToXRPL ExecMethod = "send_to_xrpl"
ExecMethodUpdateOwnership ExecMethod = "update_ownership"
ExecMethodRegisterCoreumToken ExecMethod = "register_coreum_token"
ExecMethodRegisterXRPLToken ExecMethod = "register_xrpl_token"
ExecMethodSaveEvidence ExecMethod = "save_evidence"
ExecMethodRecoverTickets ExecMethod = "recover_tickets"
ExecMethodSaveSignature ExecMethod = "save_signature"
ExecSendToXRPL ExecMethod = "send_to_xrpl"
ExecRecoveryXRPLTokenRegistration ExecMethod = "recover_xrpl_token_registration"
)

// TransactionResult is transaction result.
Expand Down Expand Up @@ -262,6 +263,11 @@ type sendToXRPLRequest struct {
Recipient string `json:"recipient"`
}

type recoverXRPLTokenRegistrationRequest struct {
Issuer string `json:"issuer"`
Currency string `json:"currency"`
}

type xrplTransactionEvidenceTicketsAllocationOperationResult struct {
Tickets []uint32 `json:"tickets"`
}
Expand Down Expand Up @@ -673,6 +679,23 @@ func (c *ContractClient) SendToXRPL(ctx context.Context, sender sdk.AccAddress,
return txRes, nil
}

// RecoverXRPLTokenRegistration executes `recover_xrpl_token_registration` method.
func (c *ContractClient) RecoverXRPLTokenRegistration(ctx context.Context, sender sdk.AccAddress, issuer, currency string) (*sdk.TxResponse, error) {
txRes, err := c.execute(ctx, sender, execRequest{
Body: map[ExecMethod]recoverXRPLTokenRegistrationRequest{
ExecRecoveryXRPLTokenRegistration: {
Issuer: issuer,
Currency: currency,
},
},
})
if err != nil {
return nil, err
}

return txRes, nil
}

// ******************** Query ********************

// GetContractConfig returns contract config.
Expand Down Expand Up @@ -1008,6 +1031,11 @@ func IsNoAvailableTicketsError(err error) bool {
return isError(err, "NoAvailableTickets")
}

// IsXRPLTokenNotInactiveError returns true if error is `XRPLTokenNotInactive`.
func IsXRPLTokenNotInactiveError(err error) bool {
return isError(err, "XRPLTokenNotInactive")
}

// ******************** Asset FT errors ********************

// IsAssetFTStateError returns true if the error is caused by enabled asset FT features.
Expand Down
6 changes: 6 additions & 0 deletions relayer/processes/xrpl_tx_submitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ func (s *XRPLTxSubmitter) signOrSubmitOperation(ctx context.Context, operation c
"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.TecNoDstTxResult:
s.log.Info(
ctx,
"The transaction has been sent, but will be reverted since account used in the transaction doesn't exist.",
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()))
Expand Down
3 changes: 3 additions & 0 deletions relayer/xrpl/constatns.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ 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"
// TecNoDstTxResult defines that provided the account on the receiving end of the transaction does not exist.
// This includes Payment and TrustSet transaction types. (It could be created if it received enough XRP.)
TecNoDstTxResult = "tecNO_DST"
// TefNOTicketTxResult defines the result which indicates the usage of the passed ticket or not created ticket.
TefNOTicketTxResult = "tefNO_TICKET"
// TefPastSeqTxResult defines that the usage of the sequence in the past.
Expand Down

0 comments on commit 6affe9b

Please sign in to comment.