3. Sharding: Poll + Voter
Write two contracts, where the parent deploys a child contract for each unique voter address.
Delete contracts/Empty.tolk, wrappers/Empty.gen.tolk, and tests/contract.test.tolk — they are no longer needed.
Write the shared types
Replace contracts/types.tolk with the types for both contracts:
enum Errors {
InvalidOption = 100
AlreadyVoted = 101
NotFromPoll = 102
InvalidMessage = 0xFFFF
}
// ----- Poll storage -----
struct PollStorage {
owner: address
option0: uint32
option1: uint32
}
fun PollStorage.load(): PollStorage {
return PollStorage.fromCell(contract.getData());
}
fun PollStorage.save(self) {
contract.setData(self.toCell());
}
// ----- Voter storage -----
struct VoterStorage {
poll: address
voter: address
hasVoted: bool
}
fun VoterStorage.load(): VoterStorage {
return VoterStorage.fromCell(contract.getData());
}
fun VoterStorage.save(self) {
contract.setData(self.toCell());
}
// ----- Messages -----
struct (0x694d6f6e) Vote {
option: uint8
}Two things to note:
- Each storage struct gets a
.load()/.save()pair — extension methods (theselfparameter is the implicit first argument in.save()). These are the idiomatic way to read and write contract storage in Tolk. struct (0x694d6f6e) Vote— the hex literal is the opcode. The TVM reads the first 32 bits of every message body to pick the right type in amatch. Use any unique 32-bit value that doesn't clash with other messages in the project.
Write Voter
Create contracts/Voter.tolk:
import "types"
type AllowedVoterMessage = Vote
contract Voter {
storage: VoterStorage
incomingMessages: AllowedVoterMessage
}
fun onInternalMessage(in: InMessage) {
val msg = lazy AllowedVoterMessage.fromSlice(in.body);
match (msg) {
Vote => {
var storage = lazy VoterStorage.load();
assert (in.senderAddress == storage.poll) throw Errors.NotFromPoll;
assert (!storage.hasVoted) throw Errors.AlreadyVoted;
storage.hasVoted = true;
storage.save();
}
else => {
assert (in.body.isEmpty()) throw Errors.InvalidMessage;
}
}
}
fun onBouncedMessage(_in: InMessageBounced) {}-
assert (in.senderAddress == storage.poll) throw Errors.NotFromPoll— Voter only accepts votes forwarded by its own Poll contract, blocking anyone from faking a vote directly to a Voter. -
assert (!storage.hasVoted) throw Errors.AlreadyVoted— on a duplicate vote, this throws a TVM exception. The TVM bounces the message back to the sender (Poll). Page 5 adds the bounce handler to Poll. -
The initial
VoterStorage(withhasVoted: false) is baked into the contract's stateInit when Poll deploys it. The first time Voter receives aVotemessage,hasVotedisfalseand the assert passes.
Write Poll
Create contracts/Poll.tolk:
import "@gen/Voter.code"
import "types"
type AllowedPollMessage = Vote
contract Poll {
storage: PollStorage
incomingMessages: AllowedPollMessage
}
fun onInternalMessage(in: InMessage) {
val msg = lazy AllowedPollMessage.fromSlice(in.body);
match (msg) {
Vote => {
var storage = lazy PollStorage.load();
assert (msg.option == 0 || msg.option == 1) throw Errors.InvalidOption;
// Optimistic increment — reverted in onBouncedMessage if Voter rejects.
if (msg.option == 0) {
storage.option0 += 1;
} else {
storage.option1 += 1;
}
storage.save();
// Deploy or message the Voter for this sender.
// If Voter doesn't exist, the stateInit deploys it.
// If it already exists, the stateInit is ignored; only the body is processed.
val voterInit = ContractState {
code: voterCompiledCode(),
data: VoterStorage {
poll: contract.getAddress(),
voter: in.senderAddress,
hasVoted: false,
}.toCell(),
};
val outMsg = createMessage({
bounce: BounceMode.Only256BitsOfBody,
value: ton("0.05"),
dest: { stateInit: voterInit },
body: msg,
});
outMsg.send(SEND_MODE_PAY_FEES_SEPARATELY);
}
else => {
assert (in.body.isEmpty()) throw Errors.InvalidMessage;
}
}
}
fun onBouncedMessage(_in: InMessageBounced) {
// Bounce handling added later in the tutorial.
}
get fun results(): (uint32, uint32) {
val storage = lazy PollStorage.load();
return (storage.option0, storage.option1);
}
get fun owner(): address {
val storage = lazy PollStorage.load();
return storage.owner;
}
get fun calcVoterAddress(voter: address): address {
val init = ContractState {
code: voterCompiledCode(),
data: VoterStorage {
poll: contract.getAddress(),
voter,
hasVoted: false,
}.toCell(),
};
return AutoDeployAddress { stateInit: init }.calculateAddress();
}-
import "@gen/Voter.code"— imports the generated dependency helper. Later, we will register the contract and[contracts.Poll]will describe its dependency viadepends = ["Voter"]inActon.toml,acton buildcompiles Voter first and writesgen/Voter.code.tolk, which exposes avoterCompiledCode()function that returns Voter's compiled code as a cell. -
VoterStorage { poll: ..., voter: in.senderAddress, ... }— thevoterfield makes each sender's stateInit unique. Because a contract's address is derived from its stateInit (code + initial data), there is exactly one possible address for each(poll, voter)pair, and it can also be calculated off-chain. -
dest: { stateInit: voterInit }— instead of a plain address, pass aContractState(code + initial data). Tolk derives the address from the stateInit automatically. -
bounce: BounceMode.Only256BitsOfBody— if the message fails on Voter, the first 256 bits of the body are included in the bounced message returned to Poll. TheVotestruct is 40 bits (32-bit opcode + 8-bit option), which fits entirely within that limit.
Register both contracts in Acton.toml
Replace the [contracts.Empty] section with entries for both Poll and Voter. Keep the rest of the file (including [import-mappings]) as generated by the scaffold:
[contracts.Poll]
src = "contracts/Poll.tolk"
depends = ["Voter"]
[contracts.Voter]
src = "contracts/Voter.tolk"Both contracts must be listed. depends = ["Voter"] tells Acton to compile Voter first and generate gen/Voter.code.tolk, which Poll imports to embed the compiled bytecode.
Build and inspect
acton build$ acton build
Compiling contracts
Finished in 521.83µsacton build --info$ acton build --info
Compiling contracts
Finished in 498.21µs
Artifacts of Voter
Code te6ccgEBAgEASQABFP8A9KQT9Lzy...
Hash 0xB1F40644E18B13E0D2C43D6D318CF1355EF16906313B7A9BE34ED96CA8937156
Artifacts of Poll
Code te6ccgECDQEAAbEAART/APSkE/S88sgL...
Hash 0xC5656741E8F39D3C2B6D36B19267E02C185D22F6AA60636EDCD7577884A39BD6Poll's code BoC is longer than Voter's because it carries Voter's compiled code inside its own bytecode — that is what voterCompiledCode() embeds via the generated gen/Voter.code.tolk.
Generate wrappers
Wrappers provide a typed API for deploying and messaging contracts from tests and scripts:
acton wrapper Poll
acton wrapper VoterThis creates wrappers/Poll.gen.tolk and wrappers/Voter.gen.tolk. Do not edit these files — regenerate them with acton wrapper whenever the contract changes.
The generated Poll wrapper includes:
Poll.fromStorage(storage: PollStorage)— creates a contract instance and calculates its addressPoll.deploy(from, config)— deploys the contractPoll.sendVote(from, option, config)— sends aVotemessagePoll.results()— calls theresultsgetterPoll.owner()— calls theownergetter
Re-run acton wrapper after adding, renaming, or removing messages or getter methods.
Checkpoint
Two contracts compile and wrappers are generated. Poll deploys a unique Voter per sender address. Voter rejects duplicate votes — currently causing a bounce that Poll ignores. That bounce handler is the subject of later pages in the tutorial.
Run acton check and acton fmt before moving on.
Commands introduced: acton build, acton build --info, acton wrapper
Last updated on