Generating wrappers
Learn how to automatically generate wrappers for contracts
Integration tests often need utility wrapper files to interact with smart contracts: send messages and call get-methods. The acton wrapper command generates those wrappers from contract ABI.
For precompiled .boc contracts, add a types interface file first. See Precompiled BoC contracts.
Generate a wrapper
Run acton wrapper with the contract name from Acton.toml:
acton wrapper CounterThis will generate a Tolk wrapper file, e.g., wrappers/Counter.gen.tolk.
Bootstrap tests with --test
New contract tests repeat the same boilerplate: imports, deploy, and basic checks. Pass --test to emit a runnable test stub file in addition to the wrapper:
acton wrapper Counter --testThis generates tests/Counter.test.tolk with a setupTest() helper and an example test case:
import "wrappers/Counter.gen"
import "@acton/emulation/testing"
// ... other imports
/// Initializes the test environment, creating a fresh instance of the contract.
/// Returns the contract wrapper and two treasury accounts (`deployer` and `notDeployer`).
fun setupTest(): (Counter, Treasury, Treasury) {
// Create a treasury account for deployment (typically the owner)
val deployer = testing.treasury("deployer");
// Create another treasury account for testing interactions from other users
val notDeployer = testing.treasury("notDeployer");
// Initialize and deploy the contract with default values
val contract = Counter.fromStorage({
id: 0,
owner: deployer.address,
counter: 0,
});
val res = contract.deploy(deployer.address, { value: ton("1") });
expect(res).toHaveSuccessfulDeploy({ to: contract.address });
return (contract, deployer, notDeployer);
}
/// Example test case demonstrating the basic flow
get fun `test basic flow`() {
val (contract, deployer, notDeployer) = setupTest();
// TODO: Implement your test logic here
// Example:
// val res = contract.sendMsg(deployer.address, ...);
// expect(res).toHaveTransaction({ ... });
}Requirements and caveats
Contract header is required
Wrapper generation uses the ABI emitted by the Tolk compiler. The contract header is the source of truth for:
- storage, via
storage: ...; - incoming message wrappers, via
incomingMessages: ....
If incomingMessages is not declared in the contract header, send{MessageName} methods will not be generated. If storage is missing, the wrapper falls back to an empty fromStorage() initializer instead of a typed storage-based one.
contract Counter {
storage: Storage
incomingMessages: AllowedMessage
}
struct Storage {
counter: uint32
}
struct (0x7e8764ef) Increase {
amount: int32
}
type AllowedMessage = Increase
fun onInternalMessage(in: InMessage) {
val msg = lazy AllowedMessage.fromSlice(in.body);
match (msg) {
Increase => { ... }
}
}Types in the contract file or imports
Storage and message types may live either in the contract file itself or in imported files. Both layouts are supported.
struct (0x7e8764ef) Increase {
amount: int32
}
type AllowedMessage = Increaseimport "types"
contract Counter {
storage: Storage
incomingMessages: AllowedMessage
}
struct Storage {
counter: uint32
}
fun onInternalMessage(in: InMessage) {
val msg = lazy AllowedMessage.fromSlice(in.body);
match (msg) {
Increase => { ... }
}
}Register the contract in Acton.toml
The command takes a contract name, not a file path. The contract must appear under [contracts.<Name>] in Acton.toml:
[contracts.Counter]
display-name = "Counter"
src = "contracts/Counter.tolk"Use the generated wrapper
The generated wrapper provides a struct representing the contract and helper methods to deploy and interact with it.
Initialize and deploy
The wrapper provides static fromStorage() method to create the initial state and a deploy method to put it on the chain.
import "@wrappers/Counter.gen"
// NOTE: Always import types used by the storage struct!
import "@contracts/types"
import "@acton/emulation/testing"
get fun `test example`() {
// 1. Create contract instance from storage data
val contract = Counter.fromStorage({
counter: 0,
});
// 2. Deploy it
val deployer = testing.treasury("deployer");
contract.deploy(deployer.address, { value: ton("1") });
}Send messages
For every message type listed in contract { incomingMessages: ... }, the wrapper generates a corresponding send{MessageName} method.
For instance, if the counter contract handles the Increase message, the method signature would look like this:
// Generated method signature:
fun Counter.sendIncrease(self, from: address, amount: int, config: ...): SendResultListUsage examples:
// Send the "Increase" message
contract.sendIncrease(deployer.address, 10);
// With custom value and bounce flag
contract.sendIncrease(deployer.address, 10, {
value: ton("0.5"),
bounce: true
});Call get methods
Get-method helpers are also generated. They return typed values directly:
val current = contract.currentCounter();
expect(current).toEqual(10);Command-line options
Customize output paths:
# Generate wrapper in a specific file
acton wrapper Counter --output tests/my_wrapper.tolk
# Generate wrapper into a specific directory
acton wrapper Counter --output-dir tests/generated
# Generate a test file alongside the wrapper
acton wrapper Counter --test --test-output tests/my.test.tolk
# Generate a test file into a specific directory
acton wrapper Counter --test --test-output-dir tests/generatedCustomize project defaults in Acton.toml:
[wrappers.tolk]
output-dir = "tests/generated-wrappers"
generate-test = true
test-output-dir = "tests/generated-tests"
[wrappers.typescript]
output-dir = "./wrappers-ts"With this configuration:
acton wrapper Counterwould writetests/generated-wrappers/Counter.gen.tolkandtests/generated-tests/Counter.test.tolkacton wrapper Counter --tswould write towrappers-ts/Counter.gen.ts(by default)
See the acton wrapper command reference for more details on CLI options.
Last updated on