How to send messages to WalletTg
Build and submit owner-signed requests to a WalletTg account. Each request instructs the wallet to send a single message or a batch of messages.
Mainnet transfers are irreversible. Test in a local emulator, then confirm the recipient, amount, and fees before signing.
Prerequisites
- Node.js 22 or later LTS version.
- The example project from the integration guide.
- An active mainnet WalletTg account and its current signing key.
Build signed requests
The signedSend() function in the following script can accept 1-255 outgoing messages and select the single-message layout or batch layout.
import { beginCell, external, storeMessage, storeMessageRelaxed, type Cell, type MessageRelaxed } from '@ton/core';
import { sign } from '@ton/crypto';
import type { WalletIdentity } from './wallet-tg.ts';
function packMessages(messages: Cell[]) {
let next: Cell | undefined;
for (let end = messages.length; end > 0;) {
const start = Math.max(0, end - (next ? 3 : 4));
const chunk = beginCell().storeMaybeRef(next);
for (const message of messages.slice(start, end)) {
chunk.storeUint(3, 8).storeRef(message);
}
next = chunk.endCell();
end = start;
}
return beginCell().storeUint(messages.length, 8).storeMaybeRef(next).endCell();
}
export function signedSend(
identity: WalletIdentity,
{ seqno, validUntil, messages, channel = 'external' }: {
seqno: number;
validUntil: number;
messages: MessageRelaxed[];
channel?: 'external' | 'internal';
},
) {
for (const value of [seqno, validUntil]) {
if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) {
throw new Error('Header fields must be uint32');
}
}
if (validUntil <= Math.floor(Date.now() / 1000)) throw new Error('Request has expired');
if (!Array.isArray(messages) || messages.length < 1 || messages.length > 255) {
throw new Error('Expected 1-255 messages');
}
if (channel !== 'external' && channel !== 'internal') throw new Error('Invalid channel');
const cells = messages.map((message) =>
beginCell().store(storeMessageRelaxed(message)).endCell());
const opcode = (cells.length === 1 ? 0x63896E74 : 0x73896E74)
+ (channel === 'external' ? 1 : 0);
const request = beginCell()
.storeUint(opcode, 32)
.storeUint(identity.subwalletId, 32)
.storeUint(validUntil, 32)
.storeUint(seqno, 32);
// Mode 3: pay transfer fees separately and ignore eligible send errors.
if (cells.length === 1) request.storeUint(3, 8).storeRef(cells[0]);
else request.storeSlice(packMessages(cells).beginParse());
const payload = request.endCell();
return beginCell()
.storeBuffer(sign(payload.hash(), identity.signer.secretKey))
.storeSlice(payload.beginParse())
.endCell();
}
export function externalEnvelope(identity: WalletIdentity, body: Cell) {
return beginCell()
.store(storeMessage(external({ to: identity.address, body }), { forceRef: true }))
.endCell();
}Set channel to external for direct submission or internal for relaying. Changing the channel requires a new signature.
The script uses send mode 3: pay transfer fees separately and ignore eligible send errors. It checks expiry against the local clock, while the contract uses blockchain.now().
Send external messages to the contract
In the example project, install an additional dependency:
npm install --save-exact @ton/[email protected]Use a TonClient from @ton/ton and the validated identity from the integration guide. Replace <MAINNET_RPC_URL> with a trusted mainnet JSON-RPC endpoint and <RECIPIENT_ADDRESS> with the recipient wallet address.
Obtain user approval and emulate the 0.01 GRAM transfer before submission.
// Not runnable: supply identity and replace the RPC and recipient placeholders.
import { internal, toNano } from '@ton/core';
import { TonClient } from '@ton/ton';
import { readWallet } from './wallet-tg.ts';
import { signedSend, externalEnvelope } from './requests.ts';
const client = new TonClient({ endpoint: '<MAINNET_RPC_URL>' });
// NOTE: For a batch transfer, add more messages to this array:
const messages = [
internal({
to: '<RECIPIENT_ADDRESS>',
value: toNano('0.01'),
bounce: false,
body: 'WalletTg transfer',
}),
];
const validUntil = Math.floor(Date.now() / 1000) + 300;
const provider = client.provider(identity.address, null);
const { seqno } = await readWallet(provider, identity, 'mainnet');
const body = signedSend(identity, { seqno, validUntil, messages });
const envelope = externalEnvelope(identity, body);
await client.sendFile(envelope.toBoc());
const messageHash = envelope.hash();Use messageHash, a normalized external message hash, to track the transaction.
Relay gasless requests
Gasless submission lets a relayer submit an owner-signed internal request and attach Gram for processing, as described in the gasless transactions guide:
- Read and validate the current account with
readWallet. - Build the approved outgoing messages and sign with
channel: 'internal'. - Emulate the complete transaction chain with the wallet's current balance and intended incoming value. Attach enough Gram for compute and any outgoing values and fees the existing balance cannot cover. If the wallet has no balance, the relayer must attach the full budget. For live accounts, use their current state, network configuration, and shared implementation.
- Give the signed body and destination wallet address to the relayer. The relayer sends that body unchanged in an internal message with the emulated attached Gram amount.
- Verify the result.
The sender and incoming value are outside the signature. Anyone can relay the request without changing its signed outgoing messages. Internal requests do not call ACCEPT; too little incoming Gram can prevent execution even when the wallet has its own balance.
WalletTg provides the signed internal entrypoint. A wallet app must arrange sponsorship or reimbursement with a relayer. The contract supplies neither a relayer service nor an automatic jetton fee payment. The relayer must check what it sponsors before sending Gram.
Verify the result
-
Allow enough time for user confirmation when setting
validUntil. Serialize pending requests per wallet: do not sign two requests with the sameseqno. -
Track the wallet transaction, check its compute and action results, and inspect the outgoing messages and recipient transactions.
-
After failure or an unknown outcome, read the current key and
seqnobefore retrying. Failure behavior determines whether the request was consumed.