Skip to main content

Automating Testnet and Futurenet reset data

Overview

Stellar operates two primary testing environments: the Testnet and the Futurenet. These networks allow developers to experiment with Stellar features without risking real assets. Periodically, these networks are reset to ensure they remain clean and manageable.

What is the Testnet and Futurenet reset?

Testnet and Futurenet are reset periodically to the genesis ledger to declutter the network, remove spam, reduce the time needed to catch up on the latest ledger, and help maintain the system. These resets take place approximately quarterly. Resets clear all ledger entries (accounts, trustlines, offers, smart contract data, etc.), transactions, and historical data from Stellar Core, Horizon, and the Stellar RPC, which is why developers should not rely on the persistence of accounts or the state of any balances when using Testnet or Futurenet.

You can check current reset dates here.

Why resets are important

  1. Clean Slate: Regular resets ensure that both Testnet and Futurenet provide a clean environment for testing. This helps in avoiding complications arising from old data or configurations.
  2. Performance: Over time, test environments can accumulate a lot of data, which can slow down performance. Resets help in maintaining optimal performance.
  3. Protocol Updates: Introducing new features or protocol changes often requires a reset to ensure compatibility and stability.
  4. Development Cycles: Aligning with development cycles allows developers to plan their testing phases and ensures they have a reliable environment for their work.

Data automation on Testnet and Futurenet

Automating blockchain state on Stellar's Testnet and Futurenet can streamline development workflows, ensuring that you can consistently test and validate your applications in these environments.

Code walkthrough

Prerequisites:

  • Node.js and npm installed.
  • Stellar SDK for JavaScript and fs installed
  • An understanding of the rudimentary, retry-enabled transaction polling function yeetTx which we outlined in another guide

Code

import {
Networks,
Keypair,
TransactionBuilder,
Operation,
Address,
StrKey,
Contract,
LiquidityPoolAsset,
LiquidityPoolFeeV18,
BASE_FEE,
} from "@stellar/stellar-sdk";
import { Server } from "@stellar/stellar-sdk/rpc";
import fs from "fs";

// const networkRPC = "USE EITHER FUTERNET OR TESTNET RPC"
const networkRPC = "https://soroban-testnet.stellar.org";
// Example
// FOR FUTERENET - https://rpc-futurenet.stellar.org
// FOR TESTNET - https://soroban-testnet.stellar.org
const server = new Server(networkRPC);

// const networkURL = "USE EITHER FUTERNET OR TESTNET URL"
const networkURL = "https://friendbot.stellar.org";
// Example
// FOR FUTURENET - https://friendbot-futurenet.stellar.org
// FOR TESTNET - https://friendbot.stellar.org

const networkPassphrase = Networks.TESTNET; // or Networks.FUTURENET, PUBLIC

// Create an Account
async function createAccount(networkURL, SecretKey) {
if (!SecretKey) {
try {
// Generate a keypair
const pair = Keypair.random();
// Fund the new account using Friendbot
const response = await fetch(
`${networkURL}?addr=${encodeURIComponent(pair.publicKey())}`,
);
const responseJSON = await response.json();
console.log("Account created:", responseJSON);

return pair;
} catch (error) {
console.error("Error creating account:", error);
}
} else {
try {
const pair = Keypair.fromSecret(SecretKey);
console.log("Account Restored:", pair);
return pair;
} catch (error) {
console.error("Error restoring account:", error);
}
}
}

// Issues an Asset
async function issueAsset(issuerKeys, receivingKeys, customAsset) {
try {
// First, the receiving account must trust the asset
const receiver = await server.getAccount(receivingKeys.publicKey());
let transaction = new TransactionBuilder(receiver, {
fee: BASE_FEE,
networkPassphrase,
})
.addOperation(
Operation.changeTrust({
asset: customAsset,
limit: "100000",
}),
)
// setTimeout is required for a transaction
.setTimeout(100)
.build();
transaction.sign(receivingKeys);
const result = await server.sendTransaction(transaction);
await yeetTx(result);
console.log(`Receiver Trusting ${customAsset.code} Asset......`);

// Second, the issuing account actually sends a payment using the asset
const issuer = await server.getAccount(issuerKeys.publicKey());
transaction = new TransactionBuilder(issuer, {
fee: BASE_FEE,
networkPassphrase,
})
.addOperation(
Operation.payment({
destination: receivingKeys.publicKey(),
asset: customAsset,
amount: "1000", // change to desired amount you want to pay
}),
)
// setTimeout is required for a transaction
.setTimeout(100)
.build();
transaction.sign(issuerKeys);
const resultFeeback = await server.sendTransaction(transaction);
await yeetTx(resultFeeback);
console.log(
`Issuer Payment using ${
customAsset.code
} to ${receivingKeys.publicKey()}`,
);
} catch (e) {
console.error("An error occurred while issuing assets:", e);
}
}

//Create Liquidity Pool
async function createLiquidityPool(accountKeypair, nativeAsset, customAsset) {
try {
const account = await server.getAccount(accountKeypair.publicKey());

// Create the liquidity pool
const poolIdAsset = new LiquidityPoolAsset(
nativeAsset,
customAsset,
LiquidityPoolFeeV18,
);
const poolId = poolIdAsset.toString().split(":")[1]; // To Get the Pool ID

const transaction = new TransactionBuilder(account, {
fee: BASE_FEE,
networkPassphrase: networkPassPhrase,
})
.addOperation(
Operation.changeTrust({
asset: poolIdAsset,
limit: "100000", // Set an appropriate limit
}),
)
.addOperation(
Operation.liquidityPoolDeposit({
liquidityPoolId: poolId,
maxAmountA: "1000", // Amount of asset A to deposit
maxAmountB: "500", // Amount of asset B to deposit
minPrice: "0.5", // Minimum price ratio
maxPrice: "2.0", // Maximum price ratio
}),
)
.setTimeout(30)
.build();
transaction.sign(accountKeypair);
const resultFeeback = await server.sendTransaction(transaction);

await yeetTx(resultFeeback);
console.log(
`Creating Liquidity Pool for ${nativeAsset.code} and ${customAsset.code}`,
);
} catch (error) {
console.error("Error creating liquidity pool:", error);
throw error;
}
}
//Deploy and Invoke Contract
async function deployAndInvokeContract(deployer, contractWasmFilePath) {
try {
// Step 1: Upload WASM
const bytecode = fs.readFileSync(contractWasmFilePath);
const account = await server.getAccount(deployer.publicKey());

const uploadTransaction = new TransactionBuilder(account, {
fee: BASE_FEE,
networkPassphrase,
})
.addOperation(Operation.uploadContractWasm({ wasm: bytecode }))
.setTimeout(30)
.build();

const uploadTx = await server.prepareTransaction(uploadTransaction);
uploadTx.sign(deployer);

console.log("Submitting WASM upload transaction...");
let uploadResponse = await server.sendTransaction(uploadTx);
const result = await yeetTx(uploadResponse, `WASM upload transaction...`);
const wasmHash = result.returnValue._value;

const deployerAddress = new Address(deployer.publicKey());

// Deploy the Contract
const createContractTransaction = new TransactionBuilder(account, {
fee: BASE_FEE,
networkPassphrase,
})
.addOperation(
Operation.createCustomContract({
address: deployerAddress,
wasmHash: wasmHash,
}),
)
.setTimeout(30)
.build();

const createContractTx = await server.prepareTransaction(
createContractTransaction,
);
createContractTx.sign(deployer);
let createContractResponse = await server.sendTransaction(createContractTx);
const returnContractResponse = await yeetTx(createContractResponse);
console.log(`Contract Deployed...`);
const contractBuffer = returnContractResponse.returnValue?._value?._value;
const contractId = StrKey.encodeContract(contractBuffer);
const contract = new Contract(contractId);

// Invoke Contract
const invokeContractTransaction = new TransactionBuilder(account, {
fee: BASE_FEE,
networkPassphrase,
})
.addOperation(
contract.call("hello", nativeToScVal("World", { type: "symbol" })),
)
.setTimeout(30)
.build();

const invokeContractTx = await server.prepareTransaction(
invokeContractTransaction,
);
invokeContractTx.sign(deployer);
let invokeContractResponse = await server.sendTransaction(invokeContractTx);
const returnInvokeContractResponse = await yeetTx(invokeContractResponse);
console.log(`Invoke Contract.`);

const returnValues = scValToNative(returnInvokeContractResponse).filter(
Boolean,
);

return { contractId, returnValues };
} catch (error) {
console.error("Error in contract deployment and invocation:", error);
throw error;
}
}

async function automateSetup() {
try {
//Check Network Status
console.log("Checking network health...");
const health = await server.getHealth();
console.log("Network health:", health);

// Flexible Account Configuration
const secretkey =
"SBGGNMUPVF2SDN4KZOJQFVFX7VDR4Q4NK3FMEFRQC64D3UBMFELKF5GC"; // This is an example of a user's secret key

const accountOne = await createAccount(networkURL, secretkey);
const accountTwo = await createAccount(networkURL);

console.log("Issuing an Asset...");
// Issue assets to these accounts
const customAsset = new Asset("Boya", accountOne.publicKey());
await issueAsset(accountOne, accountTwo, customAsset);

// Create liquidity pool
console.log("Creating liquidity pool...");
const nativeAsset = Asset.native();
await createLiquidityPool(accountTwo, nativeAsset, customAsset);

// Deploy a contract
console.log("Deploying contract...");
// Ensure you have the contract Wasm file compiled and saved in the specified path.
// Adjust this path as necessary
const contractWasmFilePath =
"./target/wasm32-unknown-unknown/release/hello_world.wasm"; // This is Wasmfile for an Hello World Contract.
const ContractData = await deployAndInvokeContract(
accountOne,
contractWasmFilePath,
);

console.log("Contract ID:", ContractData.contractId);
console.log("Return Values:", ContractData.returnValues.join(", "));
} catch (error) {
console.error("An error occurred:", error);
}
}

automateSetup();

The code defines several asynchronous functions:

  1. createAccount(networkURL):

Generates a new Stellar keypair (public and secret keys). Uses FriendBot to fund the new account on the test network. Returns the created keypair.

  1. issueAsset(issuerKeys, receivingKeys, customAsset):

Sets up a trust line for the receiving account to accept the custom asset, Issues the custom asset from the issuer account to the receiving account.

  1. createLiquidityPool(accountKeypair, nativeAsset, customAsset):

Creates a liquidity pool asset, Sets up a trust line for the pool and Deposits initial liquidity into the pool.

  1. deployAndInvokeContract(deployer, contractWasmFilePath):

Uploads the contract's webAssembly (wasm) code, creates and deploys the contract on the network, invokes the contract function and returns the contract ID and function return values

  1. automateSetup():

Initializes the Stellar server connection, Creates two accounts, Issues a custom asset, Creates a liquidity pool, Deploys a smart contract and returns the contract ID and function values.

Helper functions

sleep(ms): A utility function to introduce delays in asynchronous operations.

yeetTx(feedback, message): provides more detailed logging of transaction results.

Conclusion

Automating the setup of data on the Stellar Testnet and Futurenet can significantly enhance your development workflow, ensuring that you can quickly return to testing after a network reset. By following the above steps and using the provided code samples, you can streamline your processes and maintain consistency across resets.