Skip to content

Instantly share code, notes, and snippets.

@marcelosalloum
Last active March 18, 2025 14:54
Show Gist options
  • Select an option

  • Save marcelosalloum/9c62c434248b2b3051ed3f2ec3a7fdd1 to your computer and use it in GitHub Desktop.

Select an option

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.
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