Skip to content
TONresistor edited this page Apr 11, 2026 · 1 revision

Wallet

Tonnet Browser includes a built-in W5 (v5r1) wallet for TON. Keys are encrypted on disk via the OS keychain, and the secret key is zeroed from memory after each signing operation.

Key Storage

File Location

<userData>/wallet-key.dat

Where <userData> is the Electron user data directory:

  • Linux: ~/.config/<app>
  • macOS: ~/Library/Application Support/<app>
  • Windows: %APPDATA%\<app>

On-Disk Formats

The wallet file uses one of three formats, detected automatically on read:

Format Detection Description
Unencrypted legacy File is exactly 32 bytes Raw Ed25519 seed, no encryption
Encrypted hex seed Starts with SENC marker, plaintext is hex string safeStorage-encrypted 32-byte seed
Encrypted JSON mnemonic Starts with SENC marker, plaintext is JSON safeStorage-encrypted 24-word mnemonic (current format)

Encryption

Uses Electron safeStorage API:

  • macOS: system Keychain
  • Windows: DPAPI (bound to user account)
  • Linux: gnome-keyring, kwallet, or basic_text fallback

The SENC (4-byte ASCII) marker is prepended to all encrypted data on disk.

When the Linux backend is basic_text (no keyring daemon), the wallet still works but weakEncryption: true is set in the wallet state. Auto-migration of legacy seeds is suppressed on this backend.

Auto-Migration

On every read, if the file is an unencrypted 32-byte seed and a proper keychain backend is available, the data is immediately re-encrypted in place (format 1 to format 2). No migration runs from hex seed to JSON mnemonic.

Wallet Lifecycle

Create

  1. Generates a 24-word mnemonic via mnemonicNew(24) from @ton/crypto
  2. Derives Ed25519 keypair via mnemonicToPrivateKey(mnemonic)
  3. Stores { type: 'mnemonic', mnemonic } as encrypted JSON
  4. Creates WalletContractV5R1 with workchain 0
  5. Mnemonic array is zeroed after storage (fill('') + length = 0)

Import

  1. Validates the phrase with mnemonicValidate(words)
  2. Wipes in-memory keys (fill(0))
  3. Deletes existing wallet file
  4. Writes new mnemonic encrypted
  5. Zeros the input words array
  6. Derives new WalletContractV5R1

Export Mnemonic

Returns the 24-word mnemonic from the encrypted file. Only works for JSON mnemonic format (format 3). Legacy seed-only wallets cannot export a mnemonic.

Delete

  1. Unsubscribes account and transaction feeds
  2. Zeroes secretKey and publicKey buffers with fill(0)
  3. Cancels auto-lock timer
  4. Deletes wallet file from disk (ignores if already absent)

Auto-Lock

Default: 5 minutes. Configurable from 0 (disabled) to 1440 minutes.

On expiry:

  • cachedSecretKey is zeroed with fill(0) and set to null
  • Public key remains in memory (address and state queries still work)
  • Next signing operation triggers re-decryption from disk

The timer resets on every successful key load or generation.

Sign-then-Wipe

After every BOC signing operation, the secret key is immediately zeroed and cleared from memory. The secret key exists in memory only for the duration of a single signing call. The next transaction requires re-decryption from disk.

Send Flow

  1. Seqno sync: fetches on-chain seqno via wallet.getSeqno. Uses max(local, onChain) to prevent replay. Undeployed contracts default to seqno 0.
  2. BOC build: concurrent sends are serialized through a sign lock. For seqno 0 (first transaction), the contract init code is included and validity is extended to 1 hour. Otherwise validity is 300 seconds.
  3. Broadcast: sends via lite.sendAndWatch with 120-second confirmation window. On timeout, falls back to lite.sendMessage (fire-and-forget).
  4. Local seqno is incremented immediately after signing, before broadcast confirms.

Receive Flow

Two real-time subscriptions via the bridge:

  • subscribe.accountState: pushes balance, last transaction, and seqno changes
  • subscribe.transactions: pushes new transaction objects

Both subscriptions are re-established automatically after WebSocket reconnect.

Balance Warmup

On wallet load, getBalance() is retried up to 10 times with exponential backoff (500ms to 5s). This warms the shard-specific ADNL path, since the bridge readiness probe only validates the masterchain liteserver. If all attempts fail, balance defaults to 0 and startup proceeds.

Clone this wiki locally