diff --git a/CHANGELOG.md b/CHANGELOG.md index e9d6b02..b38c992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.44.0] - 2026-03-02 + +- Support tetra via `tetra` network option + ## [0.43.0] - 2026-01-19 ### Added diff --git a/README.md b/README.md index ab0e921..b3982e1 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,7 @@ Start by adding the following environment variables to your `.env` file: **Optional variables:** * **`WALLET_ID`**: The wallet ID (can be used with versions below `v5r1`). * **`SUBWALLET_NUMBER`**: The subwallet number used to build the wallet ID (can be used with `v5r1` wallets). +* **`WALLET_NETWORK_ID`**: Network ID used to build the wallet ID (v5 wallets). Defaults: `-3` testnet, `-239` mainnet and other networks (tetra, custom). Once your environment is set up, you can use the mnemonic wallet for deployment with the appropriate configuration. diff --git a/package.json b/package.json index f253109..7d51333 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ton/blueprint", - "version": "0.43.0", + "version": "0.44.0", "description": "Framework for development of TON smart contracts", "main": "dist/index.js", "bin": "./dist/cli/cli.js", diff --git a/src/cli/constants.ts b/src/cli/constants.ts index 51764f3..0189b4a 100644 --- a/src/cli/constants.ts +++ b/src/cli/constants.ts @@ -127,8 +127,7 @@ ${chalk.cyan('--custom')} [api-endpoint] - use a custom API ${chalk.cyan('--custom-version')} - API version (v2, v4) ${chalk.cyan('--custom-key')} - API key (v2 only) ${chalk.cyan('--custom-type')} - network type (custom, mainnet, testnet) -${chalk.cyan('--custom-domain')} - network domain (for custom l2 domain) -${chalk.cyan('--custom-network-id')} - network global ID (for custom network) +${chalk.cyan('--custom-global-id')} - network global ID (for custom l2 domain) ${chalk.cyan('--tonconnect')}, ${chalk.cyan('--deeplink')}, ${chalk.cyan('--mnemonic')} - deployer options ${chalk.cyan('--tonscan')}, ${chalk.cyan('--tonviewer')}, ${chalk.cyan('--toncx')}, ${chalk.cyan('--dton')} - explorer (default: tonviewer) ${chalk.gray('[...args]')} (array of strings, optional) - Arguments passed directly to the script. @@ -166,7 +165,7 @@ ${chalk.bold('SEE ALSO')} Verifies a deployed contract on ${chalk.underline('https://verifier.ton.org')}. ${chalk.bold('Flags:')} -${chalk.cyan('--mainnet')}, ${chalk.cyan('--testnet')} - selects network +${chalk.cyan('--mainnet')}, ${chalk.cyan('--testnet')}, ${chalk.cyan('--tetra')} - selects network ${chalk.cyan('--verifier')} - specifies the verifier ID to use (default: ${chalk.cyan('verifier.ton.org')}) ${chalk.cyan('--list-verifiers')} - lists all available verifiers for the selected network (or both networks if none selected) ${chalk.cyan('--compiler-version')} - specifies the exact compiler version to use (e.g. ${chalk.cyan('0.4.4-newops.1')}). Note: this does not change the underlying compiler itself. diff --git a/src/config/Config.ts b/src/config/Config.ts index ec50c2c..03018d8 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -33,9 +33,6 @@ export interface Config { */ network?: 'mainnet' | 'testnet' | 'tetra' | CustomNetwork; - domain?: number; - networkId?: number; - /** * If true, keeps compilable files (`*.compile.ts`) in a separate directory `compilables`. * When false or unset, compilables are stored in `wrappers` directory. diff --git a/src/config/CustomNetwork.ts b/src/config/CustomNetwork.ts index 8d539d2..caaeb51 100644 --- a/src/config/CustomNetwork.ts +++ b/src/config/CustomNetwork.ts @@ -6,4 +6,5 @@ export type CustomNetwork = { version?: NetworkVersion; key?: string; type?: Network; + globalId?: number; }; diff --git a/src/network/Network.ts b/src/network/Network.ts index d4a800f..31f6ae3 100644 --- a/src/network/Network.ts +++ b/src/network/Network.ts @@ -1 +1,3 @@ -export type Network = 'mainnet' | 'testnet' | 'tetra' | 'custom'; +import { AVAILABLE_NETWORKS } from './constants'; + +export type Network = (typeof AVAILABLE_NETWORKS)[number]; diff --git a/src/network/constants.ts b/src/network/constants.ts new file mode 100644 index 0000000..1d3f8c0 --- /dev/null +++ b/src/network/constants.ts @@ -0,0 +1,10 @@ +export const AVAILABLE_NETWORKS = ['mainnet', 'testnet', 'tetra', 'custom'] as const satisfies string[]; + +export const MAINNET_NETWORK_GLOBAL_ID = -239; +export const TESTNET_NETWORK_GLOBAL_ID = -3; +const TETRA_NETWORK_GLOBAL_ID = 662387; + +export const TETRA_DOMAIN = { + type: 'l2', + globalId: TETRA_NETWORK_GLOBAL_ID, +} as const; diff --git a/src/network/createNetworkProvider.ts b/src/network/createNetworkProvider.ts index 673ef62..cf717bf 100644 --- a/src/network/createNetworkProvider.ts +++ b/src/network/createNetworkProvider.ts @@ -42,6 +42,7 @@ import { CustomNetwork } from '../config/CustomNetwork'; import { Network } from './Network'; import { WalletVersion } from './send/wallets'; import { Explorer } from './Explorer'; +import { AVAILABLE_NETWORKS } from './constants'; const INITIAL_DELAY = 400; const MAX_ATTEMPTS = 4; @@ -55,8 +56,7 @@ export const argSpec = { '--custom-type': String, '--custom-version': String, '--custom-key': String, - '--custom-domain': Number, - '--custom-network-id': Number, + '--custom-global-id': Number, '--compiler-version': String, @@ -403,13 +403,7 @@ function getOptionalNumberEnv(envName: string) { return value; } -async function createMnemonicProvider( - client: BlueprintTonClient, - network: Network, - ui: UIProvider, - domain?: number, - networkId?: number, -) { +async function createMnemonicProvider(client: BlueprintTonClient, network: Network, ui: UIProvider, globalId?: number) { const mnemonic = process.env.WALLET_MNEMONIC ?? ''; const walletVersion = process.env.WALLET_VERSION ?? ''; if (mnemonic.length === 0 || walletVersion.length === 0) { @@ -419,6 +413,7 @@ async function createMnemonicProvider( } const walletId = getOptionalNumberEnv('WALLET_ID'); const subwalletNumber = getOptionalNumberEnv('SUBWALLET_NUMBER'); + const walletNetworkId = getOptionalNumberEnv('WALLET_NETWORK_ID'); const keyPair = await mnemonicToPrivateKey(mnemonic.split(' ')); return new MnemonicProvider({ @@ -429,8 +424,8 @@ async function createMnemonicProvider( walletId, subwalletNumber, network, - domain, - networkId, + globalId, + networkId: walletNetworkId, }); } @@ -493,11 +488,7 @@ class NetworkProviderBuilder { return typeof this.config.network === 'string' ? this.config.network : 'custom'; } - network = await this.ui.choose( - 'Which network do you want to use?', - ['mainnet', 'testnet', 'tetra', 'custom'], - (c) => c, - ); + network = await this.ui.choose('Which network do you want to use?', AVAILABLE_NETWORKS, (c) => c); if (network === 'custom') { const defaultCustomEndpoint = 'http://localhost:8081/'; this.args['--custom'] = ( @@ -566,15 +557,14 @@ class NetworkProviderBuilder { this.config?.manifestUrl, ); break; - case 'mnemonic': - provider = await createMnemonicProvider( - client, - network, - this.ui, - this.config?.domain, - this.config?.networkId, - ); + case 'mnemonic': { + let globalId: number | undefined = undefined; + if (typeof this.config?.network === 'object') { + globalId = this.config.network.globalId; + } + provider = await createMnemonicProvider(client, network, this.ui, globalId); break; + } default: throw new Error('Unknown deploy option'); } @@ -584,7 +574,6 @@ class NetworkProviderBuilder { async build(): Promise { let network = await this.chooseNetwork(); - const explorer = this.chooseExplorer(); if ( network !== 'custom' && @@ -612,21 +601,14 @@ class NetworkProviderBuilder { if (inputType !== undefined) { type = inputType as any; // checks come later } + const globalId = this.args['--custom-global-id']; configNetwork = { endpoint: this.args['--custom'], version, key: this.args['--custom-key'], + globalId, type, }; - - if (this.config && this.args['--custom-domain']) { - const customDomain = this.args['--custom-domain']; - this.config.domain = customDomain; - } - - if (this.config && this.args['--custom-network-id']) { - this.config.networkId = this.args['--custom-network-id']; - } } if (configNetwork === undefined) { throw new Error('Custom network is (somehow) undefined'); @@ -657,8 +639,8 @@ class NetworkProviderBuilder { } if (configNetwork.type !== undefined) { - const ct = configNetwork.type.toLowerCase(); - if (!['mainnet', 'testnet', 'custom', 'tetra'].includes(ct)) { + const ct = configNetwork.type.toLowerCase() as Network; + if (!AVAILABLE_NETWORKS.includes(ct)) { throw new Error('Unknown network type: ' + ct); } network = ct as Network; @@ -719,6 +701,7 @@ class NetworkProviderBuilder { const sender = new SendProviderSender(sendProvider); + const explorer = network === 'tetra' ? 'tonviewer' : this.chooseExplorer(); return new NetworkProviderImpl(tc, sender, network, explorer, this.ui); } } diff --git a/src/network/send/MnemonicProvider.ts b/src/network/send/MnemonicProvider.ts index fef8a1a..fba1228 100644 --- a/src/network/send/MnemonicProvider.ts +++ b/src/network/send/MnemonicProvider.ts @@ -9,6 +9,7 @@ import { openContract, OpenedContract, SendMode, + SignatureDomain, StateInit, } from '@ton/core'; import { KeyPair, keyPairFromSecretKey } from '@ton/crypto'; @@ -18,7 +19,8 @@ import { UIProvider } from '../../ui/UIProvider'; import { BlueprintTonClient } from '../NetworkProvider'; import { Network } from '../Network'; import { wallets, WalletVersion } from './wallets'; -import { getW5NetworkGlobalId, TETRA_DOMAIN } from '../../utils/network.utils'; +import { getW5NetworkGlobalId } from '../utils'; +import { TETRA_DOMAIN } from '../constants'; interface WalletInstance extends Contract { getSeqno(provider: ContractProvider): Promise; @@ -44,7 +46,7 @@ type MnemonicProviderParams = { client: BlueprintTonClient; ui: UIProvider; network: Network; - domain?: number; + globalId?: number; networkId?: number; }; @@ -79,12 +81,21 @@ export class MnemonicProvider implements SendProvider { this.ui = params.ui; } + private getDomain(params: MnemonicProviderParams): SignatureDomain | undefined { + if (params.globalId !== undefined) { + return { type: 'l2' as const, globalId: params.globalId }; + } + if (params.network === 'tetra') { + return TETRA_DOMAIN; + } + return undefined; + } + private createWallet(params: MnemonicProviderParams, kp: KeyPair): WalletInstance { - const networkDomain = params.network === 'tetra' ? TETRA_DOMAIN : undefined; - const domain = params.domain ? { type: 'l2' as const, globalId: params.domain } : networkDomain; - const networkGlobalId = params.networkId ?? getW5NetworkGlobalId(params.network); + const domain = this.getDomain(params); if (params.version === 'v5r1') { + const networkGlobalId = params.networkId ?? getW5NetworkGlobalId(params.network); return wallets[params.version].create({ publicKey: kp.publicKey, walletId: { diff --git a/src/utils/network.utils.ts b/src/network/utils.ts similarity index 50% rename from src/utils/network.utils.ts rename to src/network/utils.ts index c95af3e..ff56ba6 100644 --- a/src/utils/network.utils.ts +++ b/src/network/utils.ts @@ -1,13 +1,5 @@ -import { Network } from '../network/Network'; - -const MAINNET_NETWORK_GLOBAL_ID = -239; -const TESTNET_NETWORK_GLOBAL_ID = -3; -const TETRA_NETWORK_GLOBAL_ID = 662387; - -export const TETRA_DOMAIN = { - type: 'l2', - globalId: TETRA_NETWORK_GLOBAL_ID, -} as const; +import { Network } from './Network'; +import { MAINNET_NETWORK_GLOBAL_ID, TESTNET_NETWORK_GLOBAL_ID } from './constants'; export function getW5NetworkGlobalId(network: Network): number { switch (network) { @@ -18,5 +10,5 @@ export function getW5NetworkGlobalId(network: Network): number { case 'tetra': return MAINNET_NETWORK_GLOBAL_ID; } - return TESTNET_NETWORK_GLOBAL_ID; + return MAINNET_NETWORK_GLOBAL_ID; } diff --git a/src/utils/ton.utils.ts b/src/utils/ton.utils.ts index 28c6803..289456b 100644 --- a/src/utils/ton.utils.ts +++ b/src/utils/ton.utils.ts @@ -31,6 +31,13 @@ export const tonDeepLink = ( stateInit ? '&init=' + stateInit.toBoc().toString('base64url') : '' }`; +function getNetworkPrefix(network: string) { + if (network === 'testnet' || network === 'tetra') { + return `${network}.`; + } + return ''; +} + /** * Generates a link to view a TON address in a selected blockchain explorer. * @@ -38,7 +45,7 @@ export const tonDeepLink = ( * dynamically adds the testnet prefix when needed. * * @param {string} address - The TON address to view in explorer. - * @param {string} network - The target network, either 'mainnet' or 'testnet'. + * @param {string} network - The target network. * @param {string} explorer - The desired explorer. Supported values: 'tonscan', 'tonviewer', 'toncx', 'dton'. * @returns {string} A full URL pointing to the address in the selected explorer. * @@ -46,8 +53,9 @@ export const tonDeepLink = ( * const link = getExplorerLink("EQC...", "testnet", "tonscan"); * // "https://testnet.tonscan.org/address/EQC..." */ + export function getExplorerLink(address: string, network: string, explorer: Explorer) { - const networkPrefix = network === 'testnet' ? 'testnet.' : ''; + const networkPrefix = getNetworkPrefix(network); switch (explorer) { case 'tonscan':