Interacting with the real blockchain
Learn how to use scripts to deploy contracts and send transactions on testnet and mainnet
Acton scripts can interact with the real TON blockchain, enabling contract deployment, transaction sending, and state querying. By using --net <network>, Tolk scripts can execute real operations using configured wallets.
Prerequisites
Before interacting with the real blockchain, configure a wallet. Follow the wallet management guide to either set up a funded wallet or connect one through TON Connect via passing --tonconnect on the CLI:
acton script <SCRIPT_PATH> --net testnet --tonconnectThe --net Flag
Scripts in Acton can run in two modes:
- Local mode (default): Uses a local blockchain emulator with test wallets
- Broadcast mode (
--net <network>): Connects to the real blockchain and uses configured wallets
| Aspect | Local Mode | Broadcast Mode |
|---|---|---|
| Blockchain | Local emulator | Real blockchain |
| Wallets | Generated test wallets | Configured real wallets |
| GRAM spent | None (emulated) | Real GRAM |
| Transaction speed | Instant | Real network timing |
| State persistence | Lost after script ends | Permanent on blockchain |
Always test the scripts without --net first. The local emulator allows validating the logic before spending real GRAM on testnet or mainnet:
# Test locally without spending real GRAM
acton script deploy.tolk
# Deploy to testnet using real testnet wallet
acton script deploy.tolk --net testnet
# Deploy to mainnet using real mainnet wallet
acton script deploy.tolk --net mainnetManually controlling broadcasting mode
For advanced use cases, control the broadcasting flag using net.enableBroadcast() and net.disableBroadcast():
fun main() {
net.disableBroadcast();
// ...
// This send will be emulated and not sent to the real blockchain
val result = net.send(wallet.address, msg);
result.waitForFirstTransaction();
// Enable broadcasting mode again
net.enableBroadcast();
// ...
}These helpers only toggle the runtime broadcast flag. They do not load configured wallets after startup, so real sends still require launching the script with --net.
Deploying a contract
Here's a complete example of deploying a counter contract to the blockchain:
import "@acton/io"
import "@acton/emulation/network"
import "@acton/emulation/scripts"
import "@acton/build"
import "@contracts/types"
import "@wrappers/Counter.gen"
fun main() {
val deployer = scripts.wallet("deployer");
val counter = Counter.fromStorage({
id: 8,
owner: deployer.address,
counter: 0,
});
val result = counter.deploy(deployer.address, { value: ton("0.05") });
if (result.waitForFirstTransaction() == null) {
return;
}
println("Deployed counter to {}", counter.address);
println("On-chain counter is {}", counter.currentCounter());
}The wrappers/Counter.gen.tolk file provides a convenient wrapper around the counter contract:
import "@acton/build"
import "@acton/emulation/network"
import "@acton/testing/assert"
import "@contracts/types"
struct Counter {
address: address
stateInit: ContractState? = null
}
fun Counter.fromStorage(storage: Storage, toShard: AddressShardingOptions? = null): Counter {
val stateInit = ContractState {
code: build("Counter"),
data: storage.toCell(),
};
val address = AutoDeployAddress { stateInit, toShard }.calculateAddress();
return Counter { address, stateInit };
}
fun Counter.deploy(self, from: address, config: SendParams = {}): SendResultList {
if (self.stateInit == null) {
Assert.fail(
"Cannot deploy a contract created with 'fromAddress' because it lacks state init",
);
}
val msg = createMessage({
bounce: config.bounce,
value: config.value,
dest: {
stateInit: self.stateInit,
},
});
return net.send(from, msg);
}
fun Counter.sendIncreaseCounter(
self,
from: address,
increaseBy: uint32,
config: SendParams = {},
): SendResultList {
val msg = createMessage({
bounce: config.bounce,
value: config.value,
dest: self.address,
body: IncreaseCounter { increaseBy },
});
return net.send(from, msg);
}
fun Counter.currentCounter(self): int {
return net.runGetMethod(self.address, "currentCounter");
}Notice that the wrapper uses the same patterns as in tests, making it easy to reuse testing code in scripts.
Deploy to testnet:
acton script deploy.tolk --net testnetDeploy to mainnet:
acton script deploy.tolk --net mainnetIn broadcast mode, post-deploy getter calls such as counter.currentCounter() automatically read from the selected broadcast network. An explicit --fork-net is not required for the common deploy-and-check flow.
Using wallets in scripts
The scripts.wallet() function returns a wallet struct with an address field:
import "@acton/emulation/scripts"
fun main() {
val deployer = scripts.wallet("deployer");
println("Wallet address: {}", deployer.address);
}Behavior:
- Without
--net: Creates a local test wallet similar totesting.treasury() - With
--net: Uses the real wallet configured inwallets.tomlor global wallets
This dual behavior allows testing scripts locally before running them on the real blockchain.
Sending messages
Use net.send() to send messages from a wallet address:
import "@acton/emulation/scripts"
fun main() {
val deployer = scripts.wallet("deployer");
val msg = createMessage({
bounce: true,
value: ton("0.1"),
dest: someContractAddress,
body: SomeMessage { param: 42 },
});
net.send(deployer.address, msg);
println("Message sent!");
}Be extremely careful with --net. While local tests allow arbitrary big values, sending messages with large values to the real blockchain will spend real GRAM.
Recommendation: Use smaller values like ton("0.05") or ton("0.1") for most operations.
Waiting for transactions
The SendResultList.waitForFirstTransaction() polls until the first transaction from the result list is confirmed, while SendResultList.waitForTrace() waits for the complete descendant trace:
import "@acton/emulation/scripts"
fun main() {
val deployer = scripts.wallet("deployer");
// Deploy counter
val counter = Counter.fromStorage({
id: 8,
owner: deployer.address,
counter: 0,
});
val deployResult = counter.deploy(deployer.address, { value: ton("0.05") });
deployResult.waitForFirstTransaction(); // Wait for deployment
println("Counter deployed at {}", counter.address);
// Send increase message
val increaseResult = counter.sendIncreaseCounter(deployer.address, 5, { value: ton("0.05") });
increaseResult.waitForTrace(); // Wait for the full message chain
println("Counter increased!");
}Check the waiting result:
fun main() {
// ...
val result = net.send(deployer.address, msg);
val tx = result.waitForFirstTransaction();
if (tx != null) {
println("Transaction confirmed!");
} else {
println("Transaction not found after max attempts");
}
}Behavior in different modes
Both wait helpers automatically adapt to the execution mode:
| Mode | Behavior |
|---|---|
| Local (default) | Returns the already processed local transaction or trace immediately |
--net <network> | Polls the network until the transaction or trace is found |
Check the current mode using net.isBroadcasting():
if (net.isBroadcasting()) {
println("Running on real network");
} else {
println("Running in emulation mode");
}Querying blockchain state
Scripts can read contract state using net.runGetMethod():
fun main() {
val counterAddress = address("EQDt7LL...");
val currentValue = net.runGetMethod<int>(counterAddress, "currentCounter");
println("Counter value: {}", currentValue);
}Local vs remote state
By default, net.runGetMethod() queries the local emulator state when the script runs without --net. To query real deployed contracts without broadcasting, use the --fork-net flag:
# Query a contract on testnet
acton script query.tolk --fork-net testnet
# Query a contract on mainnet
acton script query.tolk --fork-net mainnet
# With a TonCenter API key to avoid rate limiting
TONCENTER_TESTNET_API_KEY=YOUR_API_KEY acton script query.tolk --fork-net testnetWhen --fork-net is enabled, Acton automatically fetches account state from
the real blockchain and caches it locally for the script execution. This allows
to:
- Query real contract state
- Test interactions with deployed contracts
- Verify contract behavior against live data
If the script also passes --fork-block-number, fetched account states are
persisted under
build/cache/<network>/<seqno>/<workchain>_<address-hash>.json. Later script
runs with the same fork network, block number, and address reuse that file
before calling the remote API. Use --no-fork-cache to force fresh remote
account reads for a run, or --clear-cache to remove the persisted entries with
the rest of build/cache.
When the script runs with --net, the selected network already becomes the default remote read network. This means deploy scripts can send a transaction, wait for it, and immediately call net.runGetMethod() or wrapper getters against the same chain without passing --fork-net.
See the fork testing guide for more.
Combining with --net
--fork-net and --net can be combined, but in broadcast mode they must point to the same network.
| Flags | Blockchain State | Wallets | Use Case |
|---|---|---|---|
| None | Local emulator | Test wallets | Local development |
--fork-net testnet | Real testnet state | Test wallets | Query real contracts locally |
--net testnet | Real testnet state | Real wallets | Deploy and read back live state on testnet |
--net mainnet | Real mainnet state | Real wallets | Mainnet deployment and post-deploy verification |
--net testnet --fork-net testnet | Real testnet state | Real wallets | Explicit same-network fork setup, optionally with block pinning |
Passing different --net and --fork-net values in broadcast mode is rejected to avoid reading from one network while sending to another.
See also
Last updated on