# How to interact with WalletTg (https://docs.ton.org/llms/contracts/standard/wallets/telegram/interact/content.md)



Build and submit single transfers, batches, and gasless relayed requests for a [WalletTg account](https://docs.ton.org/llms/contracts/standard/wallets/telegram/overview/content.md).

## Prerequisites [#prerequisites]

* [Node.js](https://nodejs.org/en/download) 22 or later LTS version.
* The example project from the [integration guide](https://docs.ton.org/llms/contracts/standard/wallets/telegram/integrate/content.md).

## Build signed requests [#build-signed-requests]

The `signedSend()` function in the following script can accept 1-255 outgoing messages and select the [single-message layout](https://docs.ton.org/llms/contracts/standard/wallets/telegram/messages/content.md) or [batch layout](https://docs.ton.org/llms/contracts/standard/wallets/telegram/messages/content.md).

```ts title="requests.ts"
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 })))
    .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`](https://docs.ton.org/llms/foundations/messages/modes/content.md): pay transfer fees separately and ignore eligible send errors. It checks expiry against the local clock, while the contract uses `blockchain.now()`.

## Relay gasless requests [#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](https://docs.ton.org/llms/contracts/standard/wallets/gasless/content.md):

1. Read and validate the current account with `readWallet`.
2. Build the approved outgoing messages and sign with `channel: 'internal'`.
3. 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.
4. 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.
5. [Verify the result](#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.

## Submit external requests [#submit-external-requests]

In the example project, install the client:

```bash
npm install --save-exact @ton/ton@16.3.0
```

For an active mainnet account, pass a configured [`TonClient`](https://github.com/ton-org/ton/blob/master/src/client/TonClient.ts) and its network identifier to this function. Call it only after user approval and successful emulation of the approved messages against current network state.

This application integration excerpt builds the complete external-in envelope and submits its BoC.

```ts
// Not runnable: requires a configured client and approved transfer arguments.
import type { MessageRelaxed } from '@ton/core';
import type { TonClient } from '@ton/ton';
import { readWallet, type WalletIdentity } from './wallet-tg.ts';
import { signedSend, externalEnvelope } from './requests.ts';

export async function submitApprovedTransfer({
  client, network, identity, messages, validUntil,
}: {
  client: TonClient;
  network: string;
  identity: WalletIdentity;
  messages: MessageRelaxed[];
  validUntil: number;
}) {
  const provider = client.provider(identity.address, null);
  const { seqno } = await readWallet(provider, identity, network);
  const body = signedSend(identity, { seqno, validUntil, messages });
  const envelope = externalEnvelope(identity, body);
  await client.sendFile(envelope.toBoc());
  return { seqno, messageHash: envelope.hash() };
}
```

`network` must come from trusted provider configuration, not wallet input. Use `mainnet` for Telegram accounts. Read state and getters from the same block when the provider supports it.

## Verify the result [#verify-the-result]

* Allow enough time for user confirmation when setting `validUntil`. Serialize pending requests per wallet: do not sign two requests with the same `seqno`.

* For the complete confirmation, 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 `seqno` before retrying. [Failure behavior](https://docs.ton.org/llms/contracts/standard/wallets/telegram/messages/content.md) determines whether the request was consumed.
