Last active
March 18, 2025 14:54
-
-
Save marcelosalloum/9c62c434248b2b3051ed3f2ec3a7fdd1 to your computer and use it in GitHub Desktop.
Test file that validates the maximum number of operations possible in a Stellar account rotation transaction.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package integrationtests | |
| import ( | |
| "fmt" | |
| "testing" | |
| "github.com/stellar/go/clients/horizonclient" | |
| "github.com/stellar/go/keypair" | |
| "github.com/stellar/go/network" | |
| "github.com/stellar/go/txnbuild" | |
| "github.com/stretchr/testify/require" | |
| ) | |
| const ( | |
| numAssets = 32 | |
| networkPassphrase = network.TestNetworkPassphrase | |
| ) | |
| var hClient = horizonclient.DefaultTestNetClient | |
| // TestAccountRotationWithMultipleTrustlines performs a full account rotation with multiple trustlines. It sets up | |
| // initial accounts, performs the rotation, and verifies the results. It's used to validate the practical limits of | |
| // Stellar transactions when rotating accounts with multiple trustlines. | |
| func TestAccountRotationWithMultipleTrustlines(t *testing.T) { | |
| // Skip if not running integration tests | |
| if testing.Short() { | |
| t.Skip("skipping integration test") | |
| } | |
| // Create issuer and old account keypairs | |
| issuerKP := keypair.MustRandom() | |
| _, err := hClient.Fund(issuerKP.Address()) | |
| require.NoError(t, err) | |
| oldAccKP := keypair.MustRandom() | |
| _, err = hClient.Fund(oldAccKP.Address()) | |
| require.NoError(t, err) | |
| t.Logf("issuerKP.Address(): %s", issuerKP.Address()) | |
| t.Logf("oldAccKP.Address(): %s", oldAccKP.Address()) | |
| setupInitialAccounts(t, hClient, issuerKP, oldAccKP) | |
| rotateAccount(t, hClient, oldAccKP, issuerKP) | |
| } | |
| // Test file that validates the maximum number of operations possible in a Stellar account rotation transaction. It: | |
| // setupInitialAccounts does the following: | |
| // 1. Creates an issuer account that will create assets | |
| // 2. Creates an old account that will be rotated | |
| // 3. Creates 32 trustlines between old account and issuer | |
| func setupInitialAccounts(t *testing.T, hClient *horizonclient.Client, issuerKP *keypair.Full, oldAccKP *keypair.Full) { | |
| t.Helper() | |
| oldAcc, err := hClient.AccountDetail(horizonclient.AccountRequest{AccountID: oldAccKP.Address()}) | |
| require.NoError(t, err) | |
| // Create assets and establish trustlines | |
| assets := make([]txnbuild.CreditAsset, numAssets) | |
| txOps := []txnbuild.Operation{} | |
| for i := range numAssets { | |
| // Generate random asset code | |
| assetCode := fmt.Sprintf("ASSET%d", i) | |
| assets[i] = txnbuild.CreditAsset{ | |
| Code: assetCode, | |
| Issuer: issuerKP.Address(), | |
| } | |
| // Create trustline | |
| txOps = append(txOps, | |
| &txnbuild.ChangeTrust{ | |
| Line: txnbuild.ChangeTrustAssetWrapper{ | |
| Asset: &assets[i], | |
| }, | |
| // SourceAccount: oldAcc.AccountID, // can be omitted because it's the same as tx.SourceAccount | |
| // Limit: "1000", | |
| }, | |
| &txnbuild.Payment{ | |
| Destination: oldAccKP.Address(), | |
| Amount: "10", | |
| Asset: &assets[i], | |
| SourceAccount: issuerKP.Address(), | |
| }, | |
| ) | |
| } | |
| tx, err := txnbuild.NewTransaction( | |
| txnbuild.TransactionParams{ | |
| SourceAccount: &oldAcc, | |
| IncrementSequenceNum: true, | |
| Operations: txOps, | |
| BaseFee: 30 * txnbuild.MinBaseFee, | |
| Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(300)}, | |
| }, | |
| ) | |
| require.NoError(t, err) | |
| tx, err = tx.Sign(networkPassphrase, issuerKP, oldAccKP) | |
| require.NoError(t, err) | |
| _, err = hClient.SubmitTransaction(tx) | |
| require.NoError(t, err) | |
| } | |
| // rotateAccount performs account rotation through the following steps: | |
| // 1. Creates a new account | |
| // 2. Migrates all assets (including native XLM) to the new account | |
| // 3. Closes all trustlines in the old account | |
| // 4. Merges the old account into the new one | |
| // 5. Uses fee bump transactions to handle the high number of operations (32 assets = 98 operations) | |
| func rotateAccount(t *testing.T, hClient *horizonclient.Client, oldAccKP, issuerKP *keypair.Full) { | |
| t.Helper() | |
| // Create new account for rotation | |
| newAccKP, err := keypair.Random() | |
| require.NoError(t, err) | |
| t.Logf("newAccKP.Address(): %s", newAccKP.Address()) | |
| // 1. Create new account | |
| amount := float64(numAssets)/2 + 1 | |
| txOps := []txnbuild.Operation{ | |
| &txnbuild.CreateAccount{ | |
| Destination: newAccKP.Address(), | |
| Amount: fmt.Sprintf("%f", amount), | |
| }, | |
| } | |
| // 2.1 Get old account balances | |
| oldAcc, err := hClient.AccountDetail(horizonclient.AccountRequest{AccountID: oldAccKP.Address()}) | |
| require.NoError(t, err) | |
| balances := oldAcc.Balances | |
| // 2.2 Migrate assets to new account | |
| for _, b := range balances { | |
| if b.Asset.Type == "native" { | |
| continue | |
| } | |
| asset := txnbuild.CreditAsset{Code: b.Asset.Code, Issuer: b.Asset.Issuer} | |
| txOps = append(txOps, | |
| // 2.2.1 Create trustlines in newAccount | |
| &txnbuild.ChangeTrust{ | |
| Line: txnbuild.ChangeTrustAssetWrapper{Asset: &asset}, | |
| Limit: "", // empty means no limit | |
| SourceAccount: newAccKP.Address(), | |
| }, | |
| // 2.2.2 Send payments to newAccount | |
| &txnbuild.Payment{ | |
| Destination: newAccKP.Address(), | |
| Amount: b.Balance, | |
| Asset: &asset, | |
| // SourceAccount: oldAccKP.Address(), // can be omitted because it's the same as tx.SourceAccount | |
| }, | |
| // 2.2.3 Close trustlines in old account | |
| &txnbuild.ChangeTrust{ | |
| Line: txnbuild.ChangeTrustAssetWrapper{Asset: &asset}, | |
| Limit: "0", // 0 means to remove the trustline | |
| // SourceAccount: oldAccKP.Address(), // can be omitted because it's the same as tx.SourceAccount | |
| }, | |
| ) | |
| } | |
| // 3 Merge old account | |
| mergeOp := txnbuild.AccountMerge{ | |
| Destination: newAccKP.Address(), | |
| // SourceAccount: oldAccKP.Address(), // can be omitted because it's the same as tx.SourceAccount | |
| } | |
| txOps = append(txOps, &mergeOp) | |
| // Create and submit the rotation transaction | |
| tx, err := txnbuild.NewTransaction( | |
| txnbuild.TransactionParams{ | |
| SourceAccount: &oldAcc, | |
| IncrementSequenceNum: true, | |
| Operations: txOps, | |
| BaseFee: 30 * txnbuild.MinBaseFee, | |
| Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(300)}, | |
| }, | |
| ) | |
| require.NoError(t, err) | |
| tx, err = tx.Sign(networkPassphrase, oldAccKP, newAccKP) | |
| require.NoError(t, err) | |
| feeBumpTx, err := txnbuild.NewFeeBumpTransaction( | |
| txnbuild.FeeBumpTransactionParams{ | |
| Inner: tx, | |
| FeeAccount: issuerKP.Address(), | |
| BaseFee: 60 * txnbuild.MinBaseFee, | |
| }, | |
| ) | |
| require.NoError(t, err) | |
| feeBumpTx, err = feeBumpTx.Sign(networkPassphrase, issuerKP) | |
| require.NoError(t, err) | |
| _, err = hClient.SubmitFeeBumpTransaction(feeBumpTx) | |
| require.NoError(t, err) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment