diff --git a/.gitignore b/.gitignore index 372bdf04..1befba17 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ build vendor *.iml -.xrpl-bridge diff --git a/relayer/client/xrpl/examples/examples_test.go b/relayer/client/xrpl/examples/examples_test.go new file mode 100644 index 00000000..b9654b4b --- /dev/null +++ b/relayer/client/xrpl/examples/examples_test.go @@ -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 + }) +} diff --git a/relayer/go.mod b/relayer/go.mod new file mode 100644 index 00000000..29bcc717 --- /dev/null +++ b/relayer/go.mod @@ -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 +) diff --git a/relayer/go.sum b/relayer/go.sum new file mode 100644 index 00000000..192af63a --- /dev/null +++ b/relayer/go.sum @@ -0,0 +1,95 @@ +github.com/CoreumFoundation/coreum-tools v0.4.0 h1:ATmoiaDdcoGx1TqheTekkNEYb3u0fpp1H4+Qnxz3dmg= +github.com/CoreumFoundation/coreum-tools v0.4.0/go.mod h1:VD93vCHkxYaT/RhOesXTFgd/GQDW54tr0BqGi5JU1c0= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bits-and-blooms/bitset v1.2.1 h1:M+/hrU9xlMp7t4TyTDQW97d3tRPVuKFC6zBEK16QnXY= +github.com/bits-and-blooms/bitset v1.2.1/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0 h1:MSskdM4/xJYcFzy0altH/C/xHopifpWzHUi1JeVI34Q= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +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/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= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/juju/loggo v0.0.0-20200526014432-9ce3a2e09b5e h1:FdDd7bdI6cjq5vaoYlK1mfQYfF9sF2VZw8VEZMsl5t8= +github.com/juju/loggo v0.0.0-20200526014432-9ce3a2e09b5e/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208 h1:/WiCm+Vpj87e4QWuWwPD/bNE9kDrWCLvPBHOQNcG2+A= +github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208/go.mod h1:0OChplkvPTZ174D2FYZXg4IB9hbEwyHkD+zT+/eK+Fg= +github.com/juju/testing v0.0.0-20210324180055-18c50b0c2098 h1:yrhek184cGp0IRyHg0uV1khLaorNg6GtDLkry4oNNjE= +github.com/juju/testing v0.0.0-20210324180055-18c50b0c2098/go.mod h1:7lxZW0B50+xdGFkvhAb8bwAGt6IU87JB1H9w4t8MNVM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= +golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=