import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
Wallets & senders
The SDK builds message bodies — it doesn't send them. You wire the
built body into your existing wallet / transport layer. This page shows how
to do that with the three most common setups: @ton/ton (server-side),
TonConnect (browser-side), and raw mnemonic signing (testing / scripts).
When you need each
| Setup | Use when | Trade-off |
|---|---|---|
| TonConnect | Browser-side. User-driven flows. Production DApps. | User must approve each tx |
@ton/ton WalletContract | Server / backend. Automated relayers, keepers, indexers. | You hold the private key |
| Mnemonic raw | Local scripts, testing, CI | Same as above — for non-production only |
TonConnect (browser-side)
For a DApp where the user signs transactions through their wallet (Tonkeeper, Tonhub, OpenMask, etc.).
import {TonConnectUI} from '@tonconnect/ui';
import {Address, beginCell, toNano} from '@ton/core';
import {buildMeta, buildCreateAssertion, OP_PROPOSE_TRUE, buildBondPayload} from '../omy';
const tonConnect = new TonConnectUI({
manifestUrl: 'https://yourapp.example/tonconnect-manifest.json',
});
// 1. Create an assertion
export async function createAssertion(args: {
factory: Address;
resolver: Address;
questionId: bigint;
bondAmount: bigint;
livenessSeconds: number;
questionText: string;
callbackRecipient: Address;
}) {
const meta = buildMeta({
identifier: args.questionId,
factTimestamp: Math.floor(Date.now() / 1000),
callbackRecipient: args.callbackRecipient,
question: beginCell().storeStringTail(args.questionText).endCell(),
});
const body = buildCreateAssertion({
id: args.questionId,
resolver: args.resolver,
bondAmount: args.bondAmount,
liveness: args.livenessSeconds,
meta,
});
await tonConnect.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 300,
messages: [{
address: args.factory.toString(),
amount: toNano('0.6').toString(), // factory's CREATE_MIN_VALUE
payload: body.toBoc().toString('base64'),
}],
});
}
// 2. Propose an answer (jetton transfer with inline payload)
export async function proposeYes(args: {
proposerJettonWallet: Address;
assertion: Address;
bondPlusFee: bigint;
}) {
// Build TEP-74 AskToTransfer (this is the user's USDT wallet's interface)
const forwardPayload = buildBondPayload(OP_PROPOSE_TRUE);
const askToTransfer = beginCell()
.storeUint(0x0f8a7ea5, 32) // AskToTransfer opcode
.storeUint(0n, 64) // queryId
.storeCoins(args.bondPlusFee)
.storeAddress(args.assertion) // recipient = assertion
.storeAddress(null) // sendExcessesTo
.storeMaybeRef(null) // customPayload
.storeCoins(toNano('0.05')) // forwardTon (must be > 0 for notification)
.storeBit(0) // inline payload follows
.storeSlice(forwardPayload.beginParse())
.endCell();
await tonConnect.sendTransaction({
validUntil: Math.floor(Date.now() / 1000) + 300,
messages: [{
address: args.proposerJettonWallet.toString(),
amount: toNano('0.2').toString(),
payload: askToTransfer.toBoc().toString('base64'),
}],
});
}
:::info TonConnect manifest
You need a tonconnect-manifest.json hosted at a public URL. See the
TonConnect docs
for the schema.
:::
@ton/ton (backend / server-side)
For relayers, keepers, indexers — anywhere you hold the signing key and run unattended.
import {TonClient, WalletContractV4, internal} from '@ton/ton';
import {mnemonicToWalletKey} from '@ton/crypto';
import {Address, beginCell, toNano} from '@ton/core';
import {buildMeta, buildCreateAssertion} from '../omy';
const client = new TonClient({
endpoint: process.env.TON_RPC_ENDPOINT!,
apiKey: process.env.TONCENTER_API_KEY!,
});
const mnemonic = process.env.RELAYER_MNEMONIC!.split(' ');
const keyPair = await mnemonicToWalletKey(mnemonic);
const wallet = WalletContractV4.create({publicKey: keyPair.publicKey, workchain: 0});
const walletContract = client.open(wallet);
export async function createAssertion(args: {
factory: Address;
body: Cell;
}) {
const seqno = await walletContract.getSeqno();
await walletContract.sendTransfer({
seqno,
secretKey: keyPair.secretKey,
messages: [
internal({
to: args.factory,
value: toNano('0.6'),
body: args.body,
bounce: true,
}),
],
});
}
Production hardening:
- Store mnemonics in a secrets manager (AWS Secrets Manager, HashiCorp Vault,
not
.envfiles committed to repos) - Use a TON RPC API key — anonymous endpoints have aggressive rate limits
- Retry on
seqnomismatch (concurrent send race) - Log every send with
validUntil+ estimated tx hash for forensics
Raw mnemonic (testing / scripts)
For local scripts and tests where TonConnect overhead is too much:
import {KeyPair, mnemonicToPrivateKey} from '@ton/crypto';
import {WalletContractV4, internal} from '@ton/ton';
// Hardcoded for testing — NEVER commit a real mnemonic
const TEST_MNEMONIC = 'word1 word2 ... word24';
const keyPair = await mnemonicToPrivateKey(TEST_MNEMONIC.split(' '));
// ... same wallet flow as @ton/ton above ...
How to know your tx landed
TON is asynchronous. sendTransaction returns when the wallet contract has
accepted the request — not when the destination has processed it. You have to
poll.
Two approaches:
import {Address} from '@ton/core';
async function waitForAssertionDeploy(client: TonClient, predicted: Address, timeoutMs = 30_000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const state = await client.getContractState(predicted);
if (state.state === 'active') return;
await new Promise(r => setTimeout(r, 2000));
}
throw new Error('timeout waiting for assertion deploy');
}
async function waitForTx(client: TonClient, address: Address, predicate: (tx: any) => boolean) {
const start = Date.now();
while (Date.now() - start < 30_000) {
const txs = await client.getTransactions(address, {limit: 10});
const match = txs.find(predicate);
if (match) return match;
await new Promise(r => setTimeout(r, 2000));
}
throw new Error('timeout');
}
For production keepers, poll status. For tests, poll tx history (you can match on opcode + sender).
Computing the assertion's predicted address
Before the factory deploys, you can compute the assertion's expected address off-chain — useful for instant linking, indexing, and pre-funding scenarios:
import {Cell, Address, beginCell} from '@ton/core';
// Note: full address derivation requires the contract code cell; for
// production use, see scripts/scenario-create.tolk in the repo for the
// mirror-the-factory pattern.
The reliable way: send the CreateAssertion, wait for AssertionCreated
callback (factory tells you the spawned address). Off-chain prediction works
but requires replicating the factory's cell construction exactly.
Anti-replay on the sender side
If you're building a relayer that auto-resubmits on failure, dedupe your sends by a request ID. TON doesn't guarantee in-order delivery from a single sender, and a retry after timeout could double-spend if the original landed late.
const pendingRequests = new Map<string, Promise<void>>();
export async function sendDedupe(requestId: string, fn: () => Promise<void>) {
let existing = pendingRequests.get(requestId);
if (existing) return existing;
const p = fn().finally(() => pendingRequests.delete(requestId));
pendingRequests.set(requestId, p);
return p;
}
Next
import {DocCards, DocCard} from '@site/src/components/docs/DocCards';