Skip to content

Commit

Permalink
Add XRPL payment transaction examples for normal and multi-signing ac…
Browse files Browse the repository at this point in the history
…counts. (#2)
  • Loading branch information
dzmitryhil committed Sep 6, 2023
1 parent 3b06467 commit 692c28d
Show file tree
Hide file tree
Showing 4 changed files with 413 additions and 1 deletion.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@
build
vendor
*.iml
.xrpl-bridge
291 changes: 291 additions & 0 deletions relayer/client/xrpl/examples/examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
//go:build examples
// +build examples

package examples_test

import (
"context"
"fmt"
"testing"
"time"

"github.com/CoreumFoundation/coreum-tools/pkg/retry"
"github.com/pkg/errors"
ripplecrypto "github.com/rubblelabs/ripple/crypto"
rippledata "github.com/rubblelabs/ripple/data"
ripplewebsockets "github.com/rubblelabs/ripple/websockets"
"github.com/samber/lo"
"github.com/stretchr/testify/require"
)

var (
xrpCurrency = "XRP"

testnetHost = "wss://s.altnet.rippletest.net:51233/"
ecdsaKeyType = rippledata.ECDSA

seedPhrase1 = "ssWU9edn2TGCByJAa6CXbAAsCkNzQ" // 0 key : rwPpi6BnAxvvEu75m8GtGtFRDFvMAUuiG3
seedPhrase2 = "ss9D9iMVnq78mKPGwhHGxc4x8wJqY" // 0 key : rhFXgxqXMChyath7CkCHc2J8jJxPu8JftS
seedPhrase3 = "ssr6XnquehSA89CndWo98dGYJBLtK" // 0 key : rQBLm9DqSQS6Z3ARvGm8JDcuXmH3zrWBXC
seedPhrase4 = "shVSrqJcPstHAzbSJJqZ2yuWuWH4Y" // 0 key : rfXoPtE851hbUaFCgLioXAecCLAJngbod2
)

func TestXRPAndIssuedTokensPayment(t *testing.T) {
remote, err := ripplewebsockets.NewRemote(testnetHost)
defer remote.Close()

issuerSeed, err := rippledata.NewSeedFromAddress(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)

recipientSeed, err := rippledata.NewSeedFromAddress(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)

// 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,
Amount: *xrpAmount,
TxBase: rippledata.TxBase{
TransactionType: rippledata.PAYMENT,
},
}

require.NoError(t, signAndSubmitTx(t, remote, &xrpPaymentTx, issuerAccount, issuerKey, issuerKeySeq))

// allow the FOO coin issued by the issuer to be received by the recipient
const fooCurrencyCode = "FOO"
fooCurrency, err := rippledata.NewCurrency(fooCurrencyCode)
require.NoError(t, err)
fooCurrencyTrustsetValue, err := rippledata.NewValue("10000000000000000", false)
require.NoError(t, err)
fooCurrencyTrustsetTx := rippledata.TrustSet{
LimitAmount: rippledata.Amount{
Value: fooCurrencyTrustsetValue,
Currency: fooCurrency,
Issuer: issuerAccount,
},
TxBase: rippledata.TxBase{
TransactionType: rippledata.TRUST_SET,
},
}
require.NoError(t, signAndSubmitTx(t, remote, &fooCurrencyTrustsetTx, recipientAccount, recipientKey, recipientKeySeq))

// send/issue the FOO token
fooAmount, err := rippledata.NewValue("100000", false)
require.NoError(t, err)
fooPaymentTx := rippledata.Payment{
Destination: recipientAccount,
Amount: rippledata.Amount{
Value: fooAmount,
Currency: fooCurrency,
Issuer: issuerAccount,
},
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))
}

func TestMultisigPayment(t *testing.T) {
remote, err := ripplewebsockets.NewRemote(testnetHost)
defer remote.Close()

multisigSeed, err := rippledata.NewSeedFromAddress(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)

signer1Seed, err := rippledata.NewSeedFromAddress(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)

signer2Seed, err := rippledata.NewSeedFromAddress(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)

signer3Seed, err := rippledata.NewSeedFromAddress(seedPhrase4)
require.NoError(t, err)
signer3KeySeq := lo.ToPtr(uint32(0))
signer3Account := signer3Seed.AccountId(ecdsaKeyType, signer3KeySeq)
t.Logf("Signer3 account: %s", signer3Account)

signerListSetTx := rippledata.SignerListSet{
SignerQuorum: 2, // weighted threshold
SignerEntries: []rippledata.SignerEntry{
{
SignerEntry: rippledata.SignerEntryItem{
Account: &signer1Account,
SignerWeight: lo.ToPtr(uint16(1)),
},
},
{
SignerEntry: rippledata.SignerEntryItem{
Account: &signer2Account,
SignerWeight: lo.ToPtr(uint16(1)),
},
},
{
SignerEntry: rippledata.SignerEntryItem{
Account: &signer3Account,
SignerWeight: lo.ToPtr(uint16(1)),
},
},
},
TxBase: rippledata.TxBase{
TransactionType: rippledata.SIGNER_LIST_SET,
},
}
require.NoError(t, signAndSubmitTx(t, remote, &signerListSetTx, multisigAccount, multisigKey, multisigKeySeq))
t.Logf("The signers set is updated")

// prepare transaction to be signed
xrpAmount, err := rippledata.NewAmount("100000") // 0.1 XRP tokens
require.NoError(t, err)

// 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{}

return xrpPaymentTx
}

signedXrpPaymentTx1 := buildXrpPaymentTx()
require.NoError(t, rippledata.MultiSign(&signedXrpPaymentTx1, signer1Key, signer1KeySeq, signer1Account))

signedXrpPaymentTx2 := buildXrpPaymentTx()
require.NoError(t, rippledata.MultiSign(&signedXrpPaymentTx2, signer2Key, signer2KeySeq, signer2Account))

xrpPaymentTx := buildXrpPaymentTx()
require.NoError(t, rippledata.SetSigners(&xrpPaymentTx, []rippledata.Signer{
{
Signer: rippledata.SignerItem{
Account: signer1Account,
TxnSignature: signedXrpPaymentTx1.TxnSignature,
SigningPubKey: signedXrpPaymentTx1.SigningPubKey,
},
},
{
Signer: rippledata.SignerItem{
Account: signer2Account,
TxnSignature: signedXrpPaymentTx2.TxnSignature,
SigningPubKey: signedXrpPaymentTx2.SigningPubKey,
},
},
}...))

t.Logf("Recipinet account balance before: %s", getAccountBalance(t, remote, xrpPaymentTx.Destination))
require.NoError(t, submitTx(t, remote, &xrpPaymentTx))
t.Logf("Recipinet account balance after: %s", getAccountBalance(t, remote, xrpPaymentTx.Destination))
}

func getAccountBalance(t *testing.T, remote *ripplewebsockets.Remote, acc rippledata.Account) map[string]rippledata.Amount {
amounts := make(map[string]rippledata.Amount, 0)

accInfo, err := remote.AccountInfo(acc)
require.NoError(t, err)
amounts[xrpCurrency] = rippledata.Amount{
Value: accInfo.AccountData.Balance,
}
// none xrp amounts
accLines, err := remote.AccountLines(acc, "closed")
require.NoError(t, err)

for _, line := range accLines.Lines {
amounts[fmt.Sprintf("%s/%s", line.Currency.String(), line.Account.String())] = rippledata.Amount{
Value: &line.Balance.Value,
Currency: line.Currency,
Issuer: line.Account,
}
}

return amounts
}

func signAndSubmitTx(
t *testing.T,
remote *ripplewebsockets.Remote,
tx rippledata.Transaction,
sender rippledata.Account,
key ripplecrypto.Key,
keySeq *uint32,
) error {
t.Helper()

autoFillTx(t, remote, tx, sender)
require.NoError(t, rippledata.Sign(tx, key, keySeq))

return submitTx(t, remote, tx)
}

func autoFillTx(t *testing.T, remote *ripplewebsockets.Remote, tx rippledata.Transaction, sender rippledata.Account) {
t.Helper()

accInfo, err := remote.AccountInfo(sender)
require.NoError(t, err)
// update base settings
base := tx.GetBase()
fee, err := rippledata.NewValue("100", true)
require.NoError(t, err)
base.Fee = *fee
base.Account = sender
base.Sequence = *accInfo.AccountData.Sequence
}

func submitTx(t *testing.T, remote *ripplewebsockets.Remote, tx rippledata.Transaction) error {
t.Helper()

// submit the transaction
res, err := remote.Submit(tx)
if err != nil {
return err
}
if !res.EngineResult.Success() {
return errors.Errorf("the tx submition is failed, %+v", res)
}

retryCtx, retryCtxCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer retryCtxCancel()

t.Logf("Transaction is submitted waitig for hash:%s", tx.GetHash())
return retry.Do(retryCtx, 250*time.Millisecond, func() error {
txRes, err := remote.Tx(*tx.GetHash())
if err != nil {
return retry.Retryable(err)
}

if !txRes.Validated {
return retry.Retryable(errors.Errorf("transaction is not validated"))
}

return nil
})
}
27 changes: 27 additions & 0 deletions relayer/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module github.com/CoreumFoundation/xrpl-bridge-v2/relayer

go 1.21.0

// TODO remove once PR with the changes is accepped
replace github.com/rubblelabs/ripple => github.com/dzmitryhil/rubblelabs-ripple v0.0.0-20230905094753-c6551b3863cd

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/samber/lo v1.38.1
github.com/stretchr/testify v1.8.4
)

require (
github.com/bits-and-blooms/bitset v1.2.1 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit 692c28d

Please sign in to comment.