Skip to main content

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

SetupUse whenTrade-off
TonConnectBrowser-side. User-driven flows. Production DApps.User must approve each tx
@ton/ton WalletContractServer / backend. Automated relayers, keepers, indexers.You hold the private key
Mnemonic rawLocal scripts, testing, CISame as above — for non-production only

TonConnect (browser-side)

For a DApp where the user signs transactions through their wallet (Tonkeeper, Tonhub, OpenMask, etc.).

src/oracle/connect.ts
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.

src/oracle/relayer.ts
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 .env files committed to repos)
  • Use a TON RPC API key — anonymous endpoints have aggressive rate limits
  • Retry on seqno mismatch (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:

scripts/local-test.ts
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:

poll until status changes
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');
}
poll for matching transactions
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:

predict address
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.

dedupe pattern
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';