diff --git a/relayer/client/xrpl/examples/examples_test.go b/relayer/client/xrpl/examples/examples_test.go index b9654b4b..e78bc5e1 100644 --- a/relayer/client/xrpl/examples/examples_test.go +++ b/relayer/client/xrpl/examples/examples_test.go @@ -6,6 +6,7 @@ package examples_test import ( "context" "fmt" + "math/rand" "testing" "time" @@ -24,42 +25,97 @@ var ( testnetHost = "wss://s.altnet.rippletest.net:51233/" ecdsaKeyType = rippledata.ECDSA - seedPhrase1 = "ssWU9edn2TGCByJAa6CXbAAsCkNzQ" // 0 key : rwPpi6BnAxvvEu75m8GtGtFRDFvMAUuiG3 + seedPhrase1 = "sneZuwbLynqsZQtRuDa7yt6maJbuR" // 0 key : rwPpi6BnAxvvEu75m8GtGtFRDFvMAUuiG3 seedPhrase2 = "ss9D9iMVnq78mKPGwhHGxc4x8wJqY" // 0 key : rhFXgxqXMChyath7CkCHc2J8jJxPu8JftS seedPhrase3 = "ssr6XnquehSA89CndWo98dGYJBLtK" // 0 key : rQBLm9DqSQS6Z3ARvGm8JDcuXmH3zrWBXC seedPhrase4 = "shVSrqJcPstHAzbSJJqZ2yuWuWH4Y" // 0 key : rfXoPtE851hbUaFCgLioXAecCLAJngbod2 ) +// ********** Wallet ********** + +type Wallet struct { + Key ripplecrypto.Key + Sequence *uint32 + Account rippledata.Account +} + +func NewWalletFromSeedPhrase(seedPhrase string) (Wallet, error) { + seed, err := rippledata.NewSeedFromAddress(seedPhrase) + if err != nil { + return Wallet{}, err + } + + key := seed.Key(ecdsaKeyType) + seq := lo.ToPtr(uint32(0)) + account := seed.AccountId(ecdsaKeyType, seq) + + return Wallet{ + Key: key, + Sequence: seq, + Account: account, + }, nil +} + +func GenWallet() (Wallet, error) { + const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + b := make([]byte, 10) + for i := range b { + b[i] = letterBytes[rand.Intn(len(letterBytes))] + } + + familySeed, err := ripplecrypto.GenerateFamilySeed(string(b)) + if err != nil { + panic(err) + } + seed, err := rippledata.NewSeedFromAddress(familySeed.String()) + if err != nil { + panic(err) + } + + return NewWalletFromSeedPhrase(seed.String()) +} + +func (w Wallet) MultiSign(tx rippledata.MultiSignable) (rippledata.Signer, error) { + if err := rippledata.MultiSign(tx, w.Key, w.Sequence, w.Account); err != nil { + return rippledata.Signer{}, err + } + + return rippledata.Signer{ + Signer: rippledata.SignerItem{ + Account: w.Account, + TxnSignature: tx.GetSignature(), + SigningPubKey: tx.GetPublicKey(), + }, + }, nil +} + +// ********** Tests ********** + func TestXRPAndIssuedTokensPayment(t *testing.T) { remote, err := ripplewebsockets.NewRemote(testnetHost) + require.NoError(t, err) defer remote.Close() - issuerSeed, err := rippledata.NewSeedFromAddress(seedPhrase1) + issuerWallet, err := NewWalletFromSeedPhrase(seedPhrase1) require.NoError(t, err) - issuerKey := issuerSeed.Key(ecdsaKeyType) - issuerKeySeq := lo.ToPtr(uint32(0)) - issuerAccount := issuerSeed.AccountId(ecdsaKeyType, issuerKeySeq) - t.Logf("Issuer account: %s", issuerAccount) + t.Logf("Issuer account: %s", issuerWallet.Account) - recipientSeed, err := rippledata.NewSeedFromAddress(seedPhrase2) + recipientWallet, err := NewWalletFromSeedPhrase(seedPhrase2) require.NoError(t, err) - recipientKey := recipientSeed.Key(ecdsaKeyType) - recipientKeySeq := lo.ToPtr(uint32(0)) - recipientAccount := recipientSeed.AccountId(ecdsaKeyType, recipientKeySeq) - t.Logf("Recipient account: %s", recipientAccount) + t.Logf("Recipient account: %s", recipientWallet.Account) // send XRP coins from issuer to recipient (if account is new you need to send 10 XRP to activate it) xrpAmount, err := rippledata.NewAmount("100000") // 0.1 XRP tokens require.NoError(t, err) xrpPaymentTx := rippledata.Payment{ - Destination: recipientAccount, + Destination: recipientWallet.Account, Amount: *xrpAmount, TxBase: rippledata.TxBase{ TransactionType: rippledata.PAYMENT, }, } - require.NoError(t, signAndSubmitTx(t, remote, &xrpPaymentTx, issuerAccount, issuerKey, issuerKeySeq)) + require.NoError(t, autoFillSignAndSubmitTx(t, remote, &xrpPaymentTx, issuerWallet)) // allow the FOO coin issued by the issuer to be received by the recipient const fooCurrencyCode = "FOO" @@ -71,82 +127,72 @@ func TestXRPAndIssuedTokensPayment(t *testing.T) { LimitAmount: rippledata.Amount{ Value: fooCurrencyTrustsetValue, Currency: fooCurrency, - Issuer: issuerAccount, + Issuer: issuerWallet.Account, }, TxBase: rippledata.TxBase{ TransactionType: rippledata.TRUST_SET, }, } - require.NoError(t, signAndSubmitTx(t, remote, &fooCurrencyTrustsetTx, recipientAccount, recipientKey, recipientKeySeq)) + require.NoError(t, autoFillSignAndSubmitTx(t, remote, &fooCurrencyTrustsetTx, recipientWallet)) // send/issue the FOO token fooAmount, err := rippledata.NewValue("100000", false) require.NoError(t, err) fooPaymentTx := rippledata.Payment{ - Destination: recipientAccount, + Destination: recipientWallet.Account, Amount: rippledata.Amount{ Value: fooAmount, Currency: fooCurrency, - Issuer: issuerAccount, + Issuer: issuerWallet.Account, }, TxBase: rippledata.TxBase{ TransactionType: rippledata.PAYMENT, }, } - t.Logf("Recipinet account balance before: %s", getAccountBalance(t, remote, recipientAccount)) - require.NoError(t, signAndSubmitTx(t, remote, &fooPaymentTx, issuerAccount, issuerKey, issuerKeySeq)) - t.Logf("Recipinet account balance after: %s", getAccountBalance(t, remote, recipientAccount)) + t.Logf("Recipinet account balance before: %s", getAccountBalance(t, remote, recipientWallet.Account)) + require.NoError(t, autoFillSignAndSubmitTx(t, remote, &fooPaymentTx, issuerWallet)) + t.Logf("Recipinet account balance after: %s", getAccountBalance(t, remote, recipientWallet.Account)) } func TestMultisigPayment(t *testing.T) { remote, err := ripplewebsockets.NewRemote(testnetHost) + require.NoError(t, err) defer remote.Close() - multisigSeed, err := rippledata.NewSeedFromAddress(seedPhrase1) + multisigWallet, err := NewWalletFromSeedPhrase(seedPhrase1) require.NoError(t, err) - multisigKey := multisigSeed.Key(ecdsaKeyType) - multisigKeySeq := lo.ToPtr(uint32(0)) - multisigAccount := multisigSeed.AccountId(ecdsaKeyType, multisigKeySeq) - t.Logf("Multisig account: %s", multisigAccount) + t.Logf("Multisig account: %s", multisigWallet.Account) - signer1Seed, err := rippledata.NewSeedFromAddress(seedPhrase2) + wallet1, err := NewWalletFromSeedPhrase(seedPhrase2) require.NoError(t, err) - signer1Key := signer1Seed.Key(ecdsaKeyType) - signer1KeySeq := lo.ToPtr(uint32(0)) - signer1Account := signer1Seed.AccountId(ecdsaKeyType, signer1KeySeq) - t.Logf("Signer1 account: %s", signer1Account) + t.Logf("Wallet1 account: %s", wallet1.Account) - signer2Seed, err := rippledata.NewSeedFromAddress(seedPhrase3) + wallet2, err := NewWalletFromSeedPhrase(seedPhrase3) require.NoError(t, err) - signer2Key := signer2Seed.Key(ecdsaKeyType) - signer2KeySeq := lo.ToPtr(uint32(0)) - signer2Account := signer2Seed.AccountId(ecdsaKeyType, signer2KeySeq) - t.Logf("Signer2 account: %s", signer2Account) + t.Logf("Wallet2 account: %s", wallet2.Account) - signer3Seed, err := rippledata.NewSeedFromAddress(seedPhrase4) + wallet3, err := NewWalletFromSeedPhrase(seedPhrase4) require.NoError(t, err) - signer3KeySeq := lo.ToPtr(uint32(0)) - signer3Account := signer3Seed.AccountId(ecdsaKeyType, signer3KeySeq) - t.Logf("Signer3 account: %s", signer3Account) + t.Logf("Wallet3 account: %s", wallet3.Account) signerListSetTx := rippledata.SignerListSet{ SignerQuorum: 2, // weighted threshold SignerEntries: []rippledata.SignerEntry{ { SignerEntry: rippledata.SignerEntryItem{ - Account: &signer1Account, + Account: &wallet1.Account, SignerWeight: lo.ToPtr(uint16(1)), }, }, { SignerEntry: rippledata.SignerEntryItem{ - Account: &signer2Account, + Account: &wallet2.Account, SignerWeight: lo.ToPtr(uint16(1)), }, }, { SignerEntry: rippledata.SignerEntryItem{ - Account: &signer3Account, + Account: &wallet3.Account, SignerWeight: lo.ToPtr(uint16(1)), }, }, @@ -155,51 +201,372 @@ func TestMultisigPayment(t *testing.T) { TransactionType: rippledata.SIGNER_LIST_SET, }, } - require.NoError(t, signAndSubmitTx(t, remote, &signerListSetTx, multisigAccount, multisigKey, multisigKeySeq)) + require.NoError(t, autoFillSignAndSubmitTx(t, remote, &signerListSetTx, multisigWallet)) t.Logf("The signers set is updated") - // prepare transaction to be signed + xrplPaymentTx := buildXrpPaymentTxForMultiSigning(t, remote, multisigWallet.Account, wallet1.Account) + signer1, err := wallet1.MultiSign(&xrplPaymentTx) + require.NoError(t, err) + + xrplPaymentTx = buildXrpPaymentTxForMultiSigning(t, remote, multisigWallet.Account, wallet1.Account) + signer2, err := wallet2.MultiSign(&xrplPaymentTx) + require.NoError(t, err) + + xrplPaymentTx = buildXrpPaymentTxForMultiSigning(t, remote, multisigWallet.Account, wallet1.Account) + signer3, err := wallet3.MultiSign(&xrplPaymentTx) + require.NoError(t, err) + + xrpPaymentTxTwoSigners := buildXrpPaymentTxForMultiSigning(t, remote, multisigWallet.Account, wallet1.Account) + require.NoError(t, rippledata.SetSigners(&xrpPaymentTxTwoSigners, []rippledata.Signer{ + signer1, + signer2, + }...)) + + xrpPaymentTxThreeSigners := buildXrpPaymentTxForMultiSigning(t, remote, multisigWallet.Account, wallet1.Account) + require.NoError(t, rippledata.SetSigners(&xrpPaymentTxThreeSigners, []rippledata.Signer{ + signer1, + signer2, + signer3, + }...)) + + // compare hashes + t.Logf("TwoSignersHash/ThreeSignersHash: %s/%s", xrpPaymentTxTwoSigners.Hash, xrpPaymentTxThreeSigners.Hash) + require.NotEqual(t, xrpPaymentTxTwoSigners.Hash.String(), xrpPaymentTxThreeSigners.Hash.String()) + + t.Logf("Recipinet account balance before: %s", getAccountBalance(t, remote, xrpPaymentTxTwoSigners.Destination)) + require.NoError(t, submitTx(t, remote, &xrpPaymentTxTwoSigners)) + t.Logf("Recipinet account balance after: %s", getAccountBalance(t, remote, xrpPaymentTxTwoSigners.Destination)) + + // try to submit with three signers (the transaction won't be accepted) + require.ErrorContains(t, submitTx(t, remote, &xrpPaymentTxThreeSigners), "This sequence number has already passed") +} + +func TestCreateAndUseTicketForPaymentAndTicketsCreation(t *testing.T) { + remote, err := ripplewebsockets.NewRemote(testnetHost) + require.NoError(t, err) + defer remote.Close() + + senderWallet, err := NewWalletFromSeedPhrase(seedPhrase1) + require.NoError(t, err) + t.Logf("Sender account: %s", senderWallet.Account) + + recipientWallet, err := NewWalletFromSeedPhrase(seedPhrase2) + require.NoError(t, err) + t.Logf("Recipient account: %s", recipientWallet.Account) + + ticketsToCreate := 1 + createTicketsTx := rippledata.TicketCreate{ + TicketCount: lo.ToPtr(uint32(ticketsToCreate)), + TxBase: rippledata.TxBase{ + TransactionType: rippledata.TICKET_CREATE, + }, + } + require.NoError(t, autoFillSignAndSubmitTx(t, remote, &createTicketsTx, senderWallet)) + txRes, err := remote.Tx(*createTicketsTx.GetHash()) + require.NoError(t, err) + + createdTickets := extractTicketsFromMeta(txRes) + require.Len(t, createdTickets, ticketsToCreate) + + // create tickets with ticket + ticketsToCreate = 2 + createTicketsTx = rippledata.TicketCreate{ + TicketCount: lo.ToPtr(uint32(ticketsToCreate)), + TxBase: rippledata.TxBase{ + TransactionType: rippledata.TICKET_CREATE, + }, + } + autoFillTx(t, remote, &createTicketsTx, senderWallet.Account) + // reset sequence and add ticket + createTicketsTx.TxBase.Sequence = 0 + createTicketsTx.TicketSequence = createdTickets[0].TicketSequence + require.NoError(t, signAndSubmitTx(t, remote, &createTicketsTx, senderWallet)) + + txRes, err = remote.Tx(*createTicketsTx.GetHash()) + require.NoError(t, err) + + createdTickets = extractTicketsFromMeta(txRes) + require.Len(t, createdTickets, ticketsToCreate) + + // send XRP coins from sender to recipient with ticket xrpAmount, err := rippledata.NewAmount("100000") // 0.1 XRP tokens require.NoError(t, err) + xrpPaymentTx := rippledata.Payment{ + Destination: recipientWallet.Account, + Amount: *xrpAmount, + TxBase: rippledata.TxBase{ + TransactionType: rippledata.PAYMENT, + }, + } + autoFillTx(t, remote, &xrpPaymentTx, senderWallet.Account) + // reset sequence and add ticket + xrpPaymentTx.TxBase.Sequence = 0 + xrpPaymentTx.TicketSequence = createdTickets[0].TicketSequence + + t.Logf("Recipinet account balance before: %s", getAccountBalance(t, remote, recipientWallet.Account)) + require.NoError(t, signAndSubmitTx(t, remote, &xrpPaymentTx, senderWallet)) + t.Logf("Recipinet account balance after: %s", getAccountBalance(t, remote, recipientWallet.Account)) + + // try to use tickets for the transactions without the trust-line + const newFooCurrencyCode = "NFO" + fooCurrency, err := rippledata.NewCurrency(newFooCurrencyCode) + require.NoError(t, err) + // send/issue the FOO token + fooAmount, err := rippledata.NewValue("100000", false) + require.NoError(t, err) + ticketForFailingTx := createdTickets[1].TicketSequence + fooPaymentTx := rippledata.Payment{ + Destination: recipientWallet.Account, + Amount: rippledata.Amount{ + Value: fooAmount, + Currency: fooCurrency, + Issuer: senderWallet.Account, + }, + TxBase: rippledata.TxBase{ + TransactionType: rippledata.PAYMENT, + }, + } + autoFillTx(t, remote, &fooPaymentTx, senderWallet.Account) + // reset sequence and add ticket + fooPaymentTx.TxBase.Sequence = 0 + fooPaymentTx.TicketSequence = ticketForFailingTx + // there is no trust set so the tx should fail and use the ticket + require.ErrorContains(t, signAndSubmitTx(t, remote, &fooPaymentTx, senderWallet), "Path could not send partial amount") + + // try to reuse the ticket for the success tx + xrpPaymentTx = rippledata.Payment{ + Destination: recipientWallet.Account, + Amount: *xrpAmount, + TxBase: rippledata.TxBase{ + TransactionType: rippledata.PAYMENT, + }, + } + autoFillTx(t, remote, &xrpPaymentTx, senderWallet.Account) + // reset sequence and add ticket + xrpPaymentTx.TxBase.Sequence = 0 + xrpPaymentTx.TicketSequence = ticketForFailingTx + // the ticket is used in prev failed transaction so can't be used here + require.ErrorContains(t, signAndSubmitTx(t, remote, &fooPaymentTx, senderWallet), "Ticket is not in ledger") +} - // build payment tx using function to prevent signing function mutations - buildXrpPaymentTx := func() rippledata.Payment { - xrpPaymentTx := rippledata.Payment{ - Destination: signer1Account, - Amount: *xrpAmount, - TxBase: rippledata.TxBase{ - TransactionType: rippledata.PAYMENT, - }, - } - autoFillTx(t, remote, &xrpPaymentTx, multisigAccount) - // important for the multi-signing - xrpPaymentTx.TxBase.SigningPubKey = &rippledata.PublicKey{} +func TestCreateAndUseTicketForTicketsCreationWithMultisigning(t *testing.T) { + remote, err := ripplewebsockets.NewRemote(testnetHost) + require.NoError(t, err) + defer remote.Close() - return xrpPaymentTx + multisigWallet, err := NewWalletFromSeedPhrase(seedPhrase1) + require.NoError(t, err) + t.Logf("Multisig account: %s", multisigWallet.Account) + + wallet1, err := NewWalletFromSeedPhrase(seedPhrase2) + require.NoError(t, err) + t.Logf("Wallet1 account: %s", wallet1.Account) + + signerListSetTx := rippledata.SignerListSet{ + SignerQuorum: 1, // weighted threshold + SignerEntries: []rippledata.SignerEntry{ + { + SignerEntry: rippledata.SignerEntryItem{ + Account: &wallet1.Account, + SignerWeight: lo.ToPtr(uint16(1)), + }, + }, + }, + TxBase: rippledata.TxBase{ + TransactionType: rippledata.SIGNER_LIST_SET, + }, } + require.NoError(t, autoFillSignAndSubmitTx(t, remote, &signerListSetTx, multisigWallet)) + t.Logf("The signers set is updated") - signedXrpPaymentTx1 := buildXrpPaymentTx() - require.NoError(t, rippledata.MultiSign(&signedXrpPaymentTx1, signer1Key, signer1KeySeq, signer1Account)) + ticketsToCreate := uint32(1) + createTicketsTx := buildCreateTicketsTxForMultiSigning(t, remote, ticketsToCreate, nil, multisigWallet.Account) + signer1, err := wallet1.MultiSign(&createTicketsTx) + require.NoError(t, err) - signedXrpPaymentTx2 := buildXrpPaymentTx() - require.NoError(t, rippledata.MultiSign(&signedXrpPaymentTx2, signer2Key, signer2KeySeq, signer2Account)) + createTicketsTx = buildCreateTicketsTxForMultiSigning(t, remote, ticketsToCreate, nil, multisigWallet.Account) + require.NoError(t, rippledata.SetSigners(&createTicketsTx, []rippledata.Signer{ + signer1, + }...)) - xrpPaymentTx := buildXrpPaymentTx() - require.NoError(t, rippledata.SetSigners(&xrpPaymentTx, []rippledata.Signer{ - { - Signer: rippledata.SignerItem{ - Account: signer1Account, - TxnSignature: signedXrpPaymentTx1.TxnSignature, - SigningPubKey: signedXrpPaymentTx1.SigningPubKey, + require.NoError(t, submitTx(t, remote, &createTicketsTx)) + + txRes, err := remote.Tx(*createTicketsTx.GetHash()) + require.NoError(t, err) + + createdTickets := extractTicketsFromMeta(txRes) + require.Len(t, createdTickets, int(ticketsToCreate)) + + createTicketsTx = buildCreateTicketsTxForMultiSigning(t, remote, ticketsToCreate, createdTickets[0].TicketSequence, multisigWallet.Account) + signer1, err = wallet1.MultiSign(&createTicketsTx) + require.NoError(t, err) + + createTicketsTx = buildCreateTicketsTxForMultiSigning(t, remote, ticketsToCreate, createdTickets[0].TicketSequence, multisigWallet.Account) + require.NoError(t, rippledata.SetSigners(&createTicketsTx, []rippledata.Signer{ + signer1, + }...)) + + require.NoError(t, submitTx(t, remote, &createTicketsTx)) + + txRes, err = remote.Tx(*createTicketsTx.GetHash()) + require.NoError(t, err) + + createdTickets = extractTicketsFromMeta(txRes) + require.Len(t, createdTickets, int(ticketsToCreate)) +} + +func TestCreateAndUseTicketForMultisigningKeysRotation(t *testing.T) { + remote, err := ripplewebsockets.NewRemote(testnetHost) + require.NoError(t, err) + defer remote.Close() + + multisigWallet, err := NewWalletFromSeedPhrase(seedPhrase1) + require.NoError(t, err) + t.Logf("Multisig account: %s", multisigWallet.Account) + + wallet1, err := NewWalletFromSeedPhrase(seedPhrase2) + require.NoError(t, err) + t.Logf("Wallet1 account: %s", wallet1.Account) + + wallet2, err := NewWalletFromSeedPhrase(seedPhrase3) + require.NoError(t, err) + t.Logf("Wallet2 account: %s", wallet2.Account) + + signerListSetTx := rippledata.SignerListSet{ + SignerQuorum: 1, // weighted threshold + SignerEntries: []rippledata.SignerEntry{ + { + SignerEntry: rippledata.SignerEntryItem{ + Account: &wallet1.Account, + SignerWeight: lo.ToPtr(uint16(1)), + }, }, }, - { - Signer: rippledata.SignerItem{ - Account: signer2Account, - TxnSignature: signedXrpPaymentTx2.TxnSignature, - SigningPubKey: signedXrpPaymentTx2.SigningPubKey, + TxBase: rippledata.TxBase{ + TransactionType: rippledata.SIGNER_LIST_SET, + }, + } + require.NoError(t, autoFillSignAndSubmitTx(t, remote, &signerListSetTx, multisigWallet)) + + ticketsToCreate := uint32(2) + + createTicketsTx := buildCreateTicketsTxForMultiSigning(t, remote, ticketsToCreate, nil, multisigWallet.Account) + signer1, err := wallet1.MultiSign(&createTicketsTx) + require.NoError(t, err) + + createTicketsTx = buildCreateTicketsTxForMultiSigning(t, remote, ticketsToCreate, nil, multisigWallet.Account) + require.NoError(t, rippledata.SetSigners(&createTicketsTx, []rippledata.Signer{ + signer1, + }...)) + require.NoError(t, submitTx(t, remote, &createTicketsTx)) + + txRes, err := remote.Tx(*createTicketsTx.GetHash()) + require.NoError(t, err) + + createdTickets := extractTicketsFromMeta(txRes) + require.Len(t, createdTickets, int(ticketsToCreate)) + + updateSignerListSetTx := buildUpdateSignerListSetTxForMultiSigning(t, remote, wallet2.Account, createdTickets[0].TicketSequence, multisigWallet.Account) + signer1, err = wallet1.MultiSign(&updateSignerListSetTx) + require.NoError(t, err) + + updateSignerListSetTx = buildUpdateSignerListSetTxForMultiSigning(t, remote, wallet2.Account, createdTickets[0].TicketSequence, multisigWallet.Account) + require.NoError(t, rippledata.SetSigners(&updateSignerListSetTx, []rippledata.Signer{ + signer1, + }...)) + require.NoError(t, submitTx(t, remote, &updateSignerListSetTx)) + + // try to sign and send with previous signer + restoreSignerListSetTx := buildUpdateSignerListSetTxForMultiSigning(t, remote, wallet1.Account, createdTickets[1].TicketSequence, multisigWallet.Account) + signer1, err = wallet1.MultiSign(&restoreSignerListSetTx) + require.NoError(t, err) + + restoreSignerListSetTx = buildUpdateSignerListSetTxForMultiSigning(t, remote, wallet1.Account, createdTickets[1].TicketSequence, multisigWallet.Account) + require.NoError(t, rippledata.SetSigners(&restoreSignerListSetTx, []rippledata.Signer{ + signer1, + }...)) + require.ErrorContains(t, submitTx(t, remote, &restoreSignerListSetTx), "A signature is provided for a non-signer") + + // build and send with correct signer + restoreSignerListSetTx = buildUpdateSignerListSetTxForMultiSigning(t, remote, wallet1.Account, createdTickets[1].TicketSequence, multisigWallet.Account) + signer2, err := wallet2.MultiSign(&restoreSignerListSetTx) + require.NoError(t, err) + + restoreSignerListSetTx = buildUpdateSignerListSetTxForMultiSigning(t, remote, wallet1.Account, createdTickets[1].TicketSequence, multisigWallet.Account) + require.NoError(t, rippledata.SetSigners(&restoreSignerListSetTx, []rippledata.Signer{ + signer2, + }...)) + require.NoError(t, submitTx(t, remote, &restoreSignerListSetTx)) +} + +func TestMultisigWithMasterKeyRemoval(t *testing.T) { + remote, err := ripplewebsockets.NewRemote(testnetHost) + require.NoError(t, err) + defer remote.Close() + + multisigWalletToDisable, err := GenWallet() + require.NoError(t, err) + t.Logf("Multisig account: %s", multisigWalletToDisable.Account) + fundAccount(t, remote, multisigWalletToDisable.Account, "20000000") + + wallet1, err := NewWalletFromSeedPhrase(seedPhrase2) + require.NoError(t, err) + t.Logf("Wallet1 account: %s", wallet1.Account) + + wallet2, err := NewWalletFromSeedPhrase(seedPhrase3) + require.NoError(t, err) + t.Logf("Wallet2 account: %s", wallet2.Account) + + signerListSetTx := rippledata.SignerListSet{ + SignerQuorum: 2, // weighted threshold + SignerEntries: []rippledata.SignerEntry{ + { + SignerEntry: rippledata.SignerEntryItem{ + Account: &wallet1.Account, + SignerWeight: lo.ToPtr(uint16(1)), + }, }, + { + SignerEntry: rippledata.SignerEntryItem{ + Account: &wallet2.Account, + SignerWeight: lo.ToPtr(uint16(1)), + }, + }, + }, + TxBase: rippledata.TxBase{ + TransactionType: rippledata.SIGNER_LIST_SET, }, + } + require.NoError(t, autoFillSignAndSubmitTx(t, remote, &signerListSetTx, multisigWalletToDisable)) + t.Logf("The signers set is updated") + + // disable master key now to be able to use multi-signing only + disableMasterKeyTx := rippledata.AccountSet{ + TxBase: rippledata.TxBase{ + Account: multisigWalletToDisable.Account, + TransactionType: rippledata.ACCOUNT_SET, + }, + SetFlag: lo.ToPtr(uint32(4)), + } + require.NoError(t, autoFillSignAndSubmitTx(t, remote, &disableMasterKeyTx, multisigWalletToDisable)) + t.Logf("The master key is disabled") + + // try to update signers one more time + require.ErrorContains(t, autoFillSignAndSubmitTx(t, remote, &signerListSetTx, multisigWalletToDisable), "Master key is disabled") + + // now use multi-signing for the account + xrpPaymentTx := buildXrpPaymentTxForMultiSigning(t, remote, multisigWalletToDisable.Account, wallet1.Account) + signer1, err := wallet1.MultiSign(&xrpPaymentTx) + require.NoError(t, err) + + xrpPaymentTx = buildXrpPaymentTxForMultiSigning(t, remote, multisigWalletToDisable.Account, wallet1.Account) + signer2, err := wallet2.MultiSign(&xrpPaymentTx) + require.NoError(t, err) + + xrpPaymentTx = buildXrpPaymentTxForMultiSigning(t, remote, multisigWalletToDisable.Account, wallet1.Account) + require.NoError(t, rippledata.SetSigners(&xrpPaymentTx, []rippledata.Signer{ + signer1, + signer2, }...)) t.Logf("Recipinet account balance before: %s", getAccountBalance(t, remote, xrpPaymentTx.Destination)) @@ -230,19 +597,27 @@ func getAccountBalance(t *testing.T, remote *ripplewebsockets.Remote, acc ripple return amounts } -func signAndSubmitTx( +func autoFillSignAndSubmitTx( t *testing.T, remote *ripplewebsockets.Remote, tx rippledata.Transaction, - sender rippledata.Account, - key ripplecrypto.Key, - keySeq *uint32, + wallet Wallet, ) error { t.Helper() - autoFillTx(t, remote, tx, sender) - require.NoError(t, rippledata.Sign(tx, key, keySeq)) + autoFillTx(t, remote, tx, wallet.Account) + return signAndSubmitTx(t, remote, tx, wallet) +} +func signAndSubmitTx( + t *testing.T, + remote *ripplewebsockets.Remote, + tx rippledata.Transaction, + wallet Wallet, +) error { + t.Helper() + + require.NoError(t, rippledata.Sign(tx, wallet.Key, wallet.Sequence)) return submitTx(t, remote, tx) } @@ -263,6 +638,7 @@ func autoFillTx(t *testing.T, remote *ripplewebsockets.Remote, tx rippledata.Tra func submitTx(t *testing.T, remote *ripplewebsockets.Remote, tx rippledata.Transaction) error { t.Helper() + t.Logf("Submitting transaction, hash:%s", tx.GetHash()) // submit the transaction res, err := remote.Submit(tx) if err != nil { @@ -289,3 +665,129 @@ func submitTx(t *testing.T, remote *ripplewebsockets.Remote, tx rippledata.Trans return nil }) } + +func extractTicketsFromMeta(txRes *ripplewebsockets.TxResult) []*rippledata.Ticket { + createdTickets := make([]*rippledata.Ticket, 0) + for _, node := range txRes.MetaData.AffectedNodes { + createdNode := node.CreatedNode + if createdNode == nil { + continue + } + newFields := createdNode.NewFields + if newFields == nil { + continue + } + if rippledata.TICKET.String() != newFields.GetType() { + continue + } + ticket, ok := newFields.(*rippledata.Ticket) + if !ok { + continue + } + createdTickets = append(createdTickets, ticket) + } + + return createdTickets +} + +func fundAccount(t *testing.T, remote *ripplewebsockets.Remote, acc rippledata.Account, amount string) { + t.Helper() + + xrpAmount, err := rippledata.NewAmount(amount) + require.NoError(t, err) + fundXrpTx := rippledata.Payment{ + Destination: acc, + Amount: *xrpAmount, + TxBase: rippledata.TxBase{ + TransactionType: rippledata.PAYMENT, + }, + } + + wallet, err := NewWalletFromSeedPhrase(seedPhrase1) + require.NoError(t, err) + t.Logf("Funding account: %s", wallet.Account) + require.NoError(t, autoFillSignAndSubmitTx(t, remote, &fundXrpTx, wallet)) + t.Logf("The account %s is funded", acc) +} + +func buildXrpPaymentTxForMultiSigning( + t *testing.T, + remote *ripplewebsockets.Remote, + from, to rippledata.Account, +) rippledata.Payment { + t.Helper() + + xrpAmount, err := rippledata.NewAmount("100000") // 0.1 XRP tokens + require.NoError(t, err) + + tx := rippledata.Payment{ + Destination: to, + Amount: *xrpAmount, + TxBase: rippledata.TxBase{ + TransactionType: rippledata.PAYMENT, + }, + } + autoFillTx(t, remote, &tx, from) + // important for the multi-signing + tx.TxBase.SigningPubKey = &rippledata.PublicKey{} + + return tx +} + +func buildCreateTicketsTxForMultiSigning( + t *testing.T, + remote *ripplewebsockets.Remote, + ticketsToCreate uint32, + ticketSeq *uint32, + from rippledata.Account, +) rippledata.TicketCreate { + tx := rippledata.TicketCreate{ + TicketCount: lo.ToPtr(uint32(ticketsToCreate)), + TxBase: rippledata.TxBase{ + TransactionType: rippledata.TICKET_CREATE, + }, + } + autoFillTx(t, remote, &tx, from) + + if ticketSeq != nil { + tx.Sequence = 0 + tx.TicketSequence = ticketSeq + } + // important for the multi-signing + tx.TxBase.SigningPubKey = &rippledata.PublicKey{} + + return tx +} + +func buildUpdateSignerListSetTxForMultiSigning( + t *testing.T, + remote *ripplewebsockets.Remote, + signerAcc rippledata.Account, + ticketSeq *uint32, + from rippledata.Account, +) rippledata.SignerListSet { + tx := rippledata.SignerListSet{ + SignerQuorum: 1, // weighted threshold + SignerEntries: []rippledata.SignerEntry{ + { + SignerEntry: rippledata.SignerEntryItem{ + Account: &signerAcc, + SignerWeight: lo.ToPtr(uint16(1)), + }, + }, + }, + TxBase: rippledata.TxBase{ + TransactionType: rippledata.SIGNER_LIST_SET, + }, + } + autoFillTx(t, remote, &tx, from) + // important for the multi-signing + tx.TxBase.SigningPubKey = &rippledata.PublicKey{} + + if ticketSeq != nil { + tx.Sequence = 0 + tx.TicketSequence = ticketSeq + } + + return tx +} diff --git a/relayer/go.mod b/relayer/go.mod index 29bcc717..c1bf1c02 100644 --- a/relayer/go.mod +++ b/relayer/go.mod @@ -1,14 +1,13 @@ module github.com/CoreumFoundation/xrpl-bridge-v2/relayer -go 1.21.0 +go 1.21 -// TODO remove once PR with the changes is accepped -replace github.com/rubblelabs/ripple => github.com/dzmitryhil/rubblelabs-ripple v0.0.0-20230905094753-c6551b3863cd +replace github.com/rubblelabs/ripple => github.com/dzmitryhil/rubblelabs-ripple v0.0.0-20230908134241-1d6176a8f47b require ( github.com/CoreumFoundation/coreum-tools v0.4.0 github.com/pkg/errors v0.9.1 - github.com/rubblelabs/ripple v0.0.0-20221111074737-85936e0db3a0 + github.com/rubblelabs/ripple v0.0.0-20230907071013-ff021a3b5408 github.com/samber/lo v1.38.1 github.com/stretchr/testify v1.8.4 ) diff --git a/relayer/go.sum b/relayer/go.sum index 192af63a..284595d2 100644 --- a/relayer/go.sum +++ b/relayer/go.sum @@ -14,8 +14,8 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/dzmitryhil/rubblelabs-ripple v0.0.0-20230905094753-c6551b3863cd h1:5QpvwZjH55RvKOhDZmrYqyqg04+XmRRZ9alnE1A+Mso= -github.com/dzmitryhil/rubblelabs-ripple v0.0.0-20230905094753-c6551b3863cd/go.mod h1:fMkR1lFpPmqtrRLsnAT86pDLUlOBqcfot815LgiAqjQ= +github.com/dzmitryhil/rubblelabs-ripple v0.0.0-20230908134241-1d6176a8f47b h1:5yT390/Or/Puqc4EMPXP7c48qPVeUNicMdIOJMNqeqI= +github.com/dzmitryhil/rubblelabs-ripple v0.0.0-20230908134241-1d6176a8f47b/go.mod h1:fMkR1lFpPmqtrRLsnAT86pDLUlOBqcfot815LgiAqjQ= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=