Docs
Scripting

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

The --net Flag

Scripts in Acton can run in two modes:

  1. Local mode (default): Uses a local blockchain emulator with test wallets
  2. Broadcast mode (--net <network>): Connects to the real blockchain and uses configured wallets
AspectLocal ModeBroadcast Mode
BlockchainLocal emulatorReal blockchain
WalletsGenerated test walletsConfigured real wallets
GRAM spentNone (emulated)Real GRAM
Transaction speedInstantReal network timing
State persistenceLost after script endsPermanent 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 mainnet

Manually 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:

deploy.tolk
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:

wrappers/Counter.gen.tolk
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 testnet

Deploy to mainnet:

acton script deploy.tolk --net mainnet

In 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 to testing.treasury()
  • With --net: Uses the real wallet configured in wallets.toml or 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:

ModeBehavior
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 testnet

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

FlagsBlockchain StateWalletsUse Case
NoneLocal emulatorTest walletsLocal development
--fork-net testnetReal testnet stateTest walletsQuery real contracts locally
--net testnetReal testnet stateReal walletsDeploy and read back live state on testnet
--net mainnetReal mainnet stateReal walletsMainnet deployment and post-deploy verification
--net testnet --fork-net testnetReal testnet stateReal walletsExplicit 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

On this page