Инициализация кошелька (StateInit) — деплой при первой транзакции

Поймёшь StateInit: как из code+data считается адрес, почему кошелёк uninit до первого сообщения и как задеплоить Wallet v4 на testnet.

StateInit задаёт адрес до деплоя

StateInit — структура с code и data контракта. Адрес = (workchain, hash(StateInit)). Кошелёк можно вычислить и показать в UI до появления on-chain. Пока в сеть не ушло сообщение с этим StateInit (или эквивалентный деплой), аккаунт часто в статусе uninit / nonexist, даже если на адрес уже кинули TON (non-bounceable).

Первая успешная транзакция с state_init активирует контракт: код и данные записываются в состояние шарда.


Деплой Wallet v4R2 на testnet

typescript
// @ton/ton ^15.1.0, @ton/crypto ^3.3.0, @ton/core ^0.59.0
import { mnemonicToPrivateKey } from "@ton/crypto";
import { TonClient, WalletContractV4, internal, toNano } from "@ton/ton";
import { SendMode } from "@ton/core";

const client = new TonClient({
  endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC",
  // apiKey: process.env.TONCENTER_API_KEY,
});

// Только testnet-мнемоника из env — не хардкодь mainnet seed
const words = process.env.TESTNET_MNEMONIC!.split(" ");
const keyPair = await mnemonicToPrivateKey(words);

const wallet = WalletContractV4.create({
  workchain: 0,
  publicKey: keyPair.publicKey,
});
const opened = client.open(wallet);

const state = await client.getContractState(wallet.address);
console.log(state.state); // uninitialized | active | ...

if (state.state !== "active") {
  // Пополни адрес non-bounceable с faucet, затем:
  const seqno = await opened.getSeqno(); // 0 для нового кошелька
  await opened.sendTransfer({
    seqno,
    secretKey: keyPair.secretKey,
    messages: [
      internal({
        to: wallet.address,
        value: toNano("0.01"),
        body: "deploy",
      }),
    ],
    sendMode: SendMode.PAY_GAS_SEPARATELY | SendMode.IGNORE_ERRORS,
  });
  // SDK/кошелёк приложит StateInit к первому external при необходимости
}

console.log(
  wallet.address.toString({ bounceable: false, testOnly: true }),
);

Пополнение до деплоя — на non-bounceable адрес (UQ... / test-only аналог). Bounceable перевод на неинициализированный кошелёк может вернуться отправителю.

Как это работает

  1. Считаешь адрес из WalletContractV*.create → это хеш будущего StateInit.
  2. Кладешь testnet TON на non-bounceable адрес (faucet).
  3. Шлёшь первое подписанное external; в транзакции участвует state_init.
  4. Статус становится active; дальше работают обычные переводы по seqno без повторной передачи полного code (он уже в состоянии).

Частые ошибки

Ждёшь баланс на bounceable EQ... для нового кошелька → сумма может отскочить. Для первого депозита используй non-bounceable.

Сменил publicKey / версию контракта после раздачи адреса → StateInit другой, адрес другой; старый останется пустым или с «зависшими» монетами без кода.

Считаешь, что «импорт seed» сам деплоит контракт → импорт только вычисляет адрес; on-chain активация — при первой исходящей (или явном деплое).


Что дальше

Материалы gramdocs.tech носят образовательный характер и не являются финансовой, юридической или инвестиционной рекомендацией. Работа с блокчейном TON и токеном Gram связана с рисками потери средств. Правовая информация