diff --git a/CHANGELOG.md b/CHANGELOG.md index e6cecfa..7dca0de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for scripts in subdirectories, for example `scripts/counter/deploy.ts` - Added the ability to specify test files in `blueprint test` command, for example `blueprint test Counter` +### Changed + +- Separated compilables and wrappers + ### Fixed - Fixed `code overflow` error when generating QR code for ton:// link diff --git a/README.md b/README.md index a91fc02..2b13105 100644 --- a/README.md +++ b/README.md @@ -75,13 +75,14 @@ Blueprint is an all-in-one development environment designed to enhance the proce * `wrappers/` - TypeScript interface classes for all contracts (implementing `Contract` from [@ton/core](https://www.npmjs.com/package/@ton/core)) * include message [de]serialization primitives, getter wrappers and compilation functions * used by the test suite and client code to interact with the contracts from TypeScript +* `compilables/` - Compilations scripts for contracts * `tests/` - TypeScript test suite for all contracts (relying on [Sandbox](https://github.com/ton-org/sandbox) for in-process tests) * `scripts/` - Deployment scripts to mainnet/testnet and other scripts interacting with live contracts * `build/` - Compilation artifacts created here after running a build command ### Building contracts -1. You need a compilation script in `wrappers/.compile.ts` - [example](/example/wrappers/Counter.compile.ts) +1. You need a compilation script in `compilables/.compile.ts` - [example](/example/compilables/Counter.compile.ts) 2. Run interactive:    `npx blueprint build`   or   `yarn blueprint build` 3. Non-interactive:   `npx/yarn blueprint build `   OR build all contracts   `yarn blueprint build --all` * Example: `yarn blueprint build counter` diff --git a/example/blueprint.config.ts b/example/blueprint.config.ts new file mode 100644 index 0000000..937ab42 --- /dev/null +++ b/example/blueprint.config.ts @@ -0,0 +1 @@ +export const config = { separateCompilables: true }; diff --git a/example/wrappers/Counter.compile.ts b/example/compilables/Counter.compile.ts similarity index 100% rename from example/wrappers/Counter.compile.ts rename to example/compilables/Counter.compile.ts diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 8bf7511..7d66830 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -13,8 +13,7 @@ import { convert } from './convert'; import { additionalHelpMessages, help } from './help'; import { InquirerUIProvider } from '../ui/InquirerUIProvider'; import { argSpec, Runner, RunnerContext } from './Runner'; -import path from 'path'; -import { Config } from '../config/Config'; +import { getConfig } from '../config/utils'; const runners: Record = { create, @@ -43,28 +42,21 @@ async function main() { const runnerContext: RunnerContext = {}; + const config = await getConfig(); + try { - const configModule = await import(path.join(process.cwd(), 'blueprint.config.ts')); - - try { - if ('config' in configModule && typeof configModule.config === 'object') { - const config: Config = configModule.config; - runnerContext.config = config; - - for (const plugin of config.plugins ?? []) { - for (const runner of plugin.runners()) { - effectiveRunners[runner.name] = runner.runner; - additionalHelpMessages[runner.name] = runner.help; - } - } + runnerContext.config = config; + + for (const plugin of config?.plugins ?? []) { + for (const runner of plugin.runners()) { + effectiveRunners[runner.name] = runner.runner; + additionalHelpMessages[runner.name] = runner.help; } - } catch (e) { - // if plugin.runners() throws - console.error('Could not load one or more plugins'); - console.error(e); } } catch (e) { - // no config + // if plugin.runners() throws + console.error('Could not load one or more plugins'); + console.error(e); } effectiveRunners = { diff --git a/src/cli/create.ts b/src/cli/create.ts index b2ddac4..b77197a 100644 --- a/src/cli/create.ts +++ b/src/cli/create.ts @@ -1,11 +1,12 @@ import { Args, Runner } from './Runner'; -import { open, mkdir, readdir, lstat, readFile } from 'fs/promises'; +import { lstat, mkdir, open, readdir, readFile } from 'fs/promises'; import path from 'path'; import { executeTemplate, TEMPLATES_DIR } from '../template'; import { selectOption } from '../utils'; import arg from 'arg'; import { UIProvider } from '../ui/UIProvider'; import { buildOne } from '../build'; +import { getConfig } from '../config/utils'; import { helpArgs, helpMessages } from './constants'; function toSnakeCase(v: string): string { @@ -106,8 +107,11 @@ export const create: Runner = async (args: Args, ui: UIProvider) => { contractPath: 'contracts/' + snakeName + '.' + (lang === 'func' ? 'fc' : 'tact'), }; - await createFiles(path.join(TEMPLATES_DIR, lang, 'common'), process.cwd(), replaces); + const config = await getConfig(); + const commonPath = config?.separateCompilables ? 'common' : 'not-separated-common'; + + await createFiles(path.join(TEMPLATES_DIR, lang, commonPath), process.cwd(), replaces); await createFiles(path.join(TEMPLATES_DIR, lang, template), process.cwd(), replaces); if (lang === 'tact') { diff --git a/src/compile/compile.ts b/src/compile/compile.ts index d5c0fd4..bbcdb75 100644 --- a/src/compile/compile.ts +++ b/src/compile/compile.ts @@ -1,19 +1,32 @@ import { compileFunc, - compilerVersion, CompilerConfig as FuncCompilerConfig, + compilerVersion, SourcesArray, } from '@ton-community/func-js'; import { existsSync, readFileSync } from 'fs'; import path from 'path'; import { Cell } from '@ton/core'; -import { TACT_ROOT_CONFIG, BUILD_DIR, WRAPPERS_DIR } from '../paths'; +import { BUILD_DIR, COMPILABLES_DIR, TACT_ROOT_CONFIG, WRAPPERS_DIR } from '../paths'; import { CompilerConfig, TactCompilerConfig } from './CompilerConfig'; import * as Tact from '@tact-lang/compiler'; import { OverwritableVirtualFileSystem } from './OverwritableVirtualFileSystem'; +import { getConfig } from '../config/utils'; + +export async function getCompilablesDirectory(): Promise { + const config = await getConfig(); + if (config?.separateCompilables) { + return COMPILABLES_DIR; + } + + return WRAPPERS_DIR; +} + +export const COMPILE_END = '.compile.ts'; async function getCompilerConfigForContract(name: string): Promise { - const mod = await import(path.join(WRAPPERS_DIR, name + '.compile.ts')); + const compilablesDirectory = await getCompilablesDirectory(); + const mod = await import(path.join(compilablesDirectory, name + COMPILE_END)); if (typeof mod.compile !== 'object') { throw new Error(`Object 'compile' is missing`); diff --git a/src/config/Config.ts b/src/config/Config.ts index 4c2fff6..eb4a504 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -4,4 +4,5 @@ import { Plugin } from './Plugin'; export interface Config { plugins?: Plugin[]; network?: 'mainnet' | 'testnet' | CustomNetwork; + separateCompilables?: boolean; } diff --git a/src/config/utils.ts b/src/config/utils.ts new file mode 100644 index 0000000..f6dac3d --- /dev/null +++ b/src/config/utils.ts @@ -0,0 +1,22 @@ +import { Config } from './Config'; +import { BLUEPRINT_CONFIG } from '../paths'; + +let config: Config | undefined; + +export async function getConfig(): Promise { + if (config) { + return config; + } + + try { + const configModule = await import(BLUEPRINT_CONFIG); + if (!('config' in configModule) || typeof configModule.config !== 'object') { + return undefined; + } + config = configModule.config; + + return config; + } catch { + return undefined; + } +} diff --git a/src/network/createNetworkProvider.ts b/src/network/createNetworkProvider.ts index d374e52..7e8d545 100644 --- a/src/network/createNetworkProvider.ts +++ b/src/network/createNetworkProvider.ts @@ -13,8 +13,10 @@ import { OpenedContract, Sender, SenderArguments, - SendMode, StateInit, - toNano, Transaction, + SendMode, + StateInit, + toNano, + Transaction, TupleItem, } from '@ton/core'; import { TonClient, TonClient4 } from '@ton/ton'; @@ -55,7 +57,7 @@ type Network = 'mainnet' | 'testnet' | 'custom'; type Explorer = 'tonscan' | 'tonviewer' | 'toncx' | 'dton'; -type ContractProviderFactory = (params: { address: Address, init?: StateInit | null }) => ContractProvider; +type ContractProviderFactory = (params: { address: Address; init?: StateInit | null }) => ContractProvider; class SendProviderSender implements Sender { #provider: SendProvider; @@ -129,7 +131,10 @@ class WrappedContractProvider implements ContractProvider { } open(contract: T): OpenedContract { - return openContract(contract, (params) => new WrappedContractProvider(params.address, this.#factory, params.init)); + return openContract( + contract, + (params) => new WrappedContractProvider(params.address, this.#factory, params.init), + ); } getTransactions(address: Address, lt: bigint, hash: Buffer, limit?: number): Promise { @@ -169,7 +174,8 @@ class NetworkProviderImpl implements NetworkProvider { } provider(address: Address, init?: StateInit | null): ContractProvider { - const factory = (params: { address: Address, init?: StateInit | null }) => this.#tc.provider(params.address, params.init); + const factory = (params: { address: Address; init?: StateInit | null }) => + this.#tc.provider(params.address, params.init); return new WrappedContractProvider(address, factory, init); } diff --git a/src/paths.ts b/src/paths.ts index 5b25d44..0daec71 100644 --- a/src/paths.ts +++ b/src/paths.ts @@ -2,11 +2,13 @@ import path from 'path'; export const CONTRACTS = 'contracts'; export const TESTS = 'tests'; +export const COMPILABLES = 'compilables'; export const WRAPPERS = 'wrappers'; export const SCRIPTS = 'scripts'; export const TEMP = 'temp'; export const BUILD = 'build'; +export const COMPILABLES_DIR = path.join(process.cwd(), COMPILABLES); export const WRAPPERS_DIR = path.join(process.cwd(), WRAPPERS); export const SCRIPTS_DIR = path.join(process.cwd(), SCRIPTS); export const BUILD_DIR = path.join(process.cwd(), BUILD); @@ -14,4 +16,5 @@ export const TEMP_DIR = path.join(process.cwd(), TEMP); export const CONTRACTS_DIR = path.join(process.cwd(), CONTRACTS); export const TESTS_DIR = path.join(process.cwd(), TESTS); +export const BLUEPRINT_CONFIG = path.join(process.cwd(), 'blueprint.config.ts'); export const TACT_ROOT_CONFIG = path.join(process.cwd(), 'tact.config.json'); diff --git a/src/templates/func/common/wrappers/compile.ts.template b/src/templates/func/common/compilables/compile.ts.template similarity index 100% rename from src/templates/func/common/wrappers/compile.ts.template rename to src/templates/func/common/compilables/compile.ts.template diff --git a/src/templates/func/not-separated-common/wrappers/compile.ts.template b/src/templates/func/not-separated-common/wrappers/compile.ts.template new file mode 100644 index 0000000..a989127 --- /dev/null +++ b/src/templates/func/not-separated-common/wrappers/compile.ts.template @@ -0,0 +1,7 @@ +{{name}}.compile.ts +import { CompilerConfig } from '@ton/blueprint'; + +export const compile: CompilerConfig = { + lang: 'func', + targets: ['{{contractPath}}'], +}; diff --git a/src/templates/tact/common/wrappers/compile.ts.template b/src/templates/tact/common/compilables/compile.ts.template similarity index 100% rename from src/templates/tact/common/wrappers/compile.ts.template rename to src/templates/tact/common/compilables/compile.ts.template diff --git a/src/templates/tact/not-separated-common/wrappers/compile.ts.template b/src/templates/tact/not-separated-common/wrappers/compile.ts.template new file mode 100644 index 0000000..b4be49b --- /dev/null +++ b/src/templates/tact/not-separated-common/wrappers/compile.ts.template @@ -0,0 +1,10 @@ +{{name}}.compile.ts +import { CompilerConfig } from '@ton/blueprint'; + +export const compile: CompilerConfig = { + lang: 'tact', + target: '{{contractPath}}', + options: { + debug: true, + }, +}; diff --git a/src/templates/tact/not-separated-common/wrappers/wrapper.ts.template b/src/templates/tact/not-separated-common/wrappers/wrapper.ts.template new file mode 100644 index 0000000..ae09d61 --- /dev/null +++ b/src/templates/tact/not-separated-common/wrappers/wrapper.ts.template @@ -0,0 +1,2 @@ +{{name}}.ts +export * from '../build/{{name}}/tact_{{name}}'; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..470ce97 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,4 @@ +export * from './object.utils'; +export * from './timer.utils'; +export * from './ton.utils'; +export * from './selection.utils'; diff --git a/src/utils/object.utils.ts b/src/utils/object.utils.ts new file mode 100644 index 0000000..a170a56 --- /dev/null +++ b/src/utils/object.utils.ts @@ -0,0 +1,13 @@ +export function oneOrZeroOf(options: T): keyof T | undefined { + let opt: keyof T | undefined = undefined; + for (const k in options) { + if (options[k]) { + if (opt === undefined) { + opt = k; + } else { + throw new Error(`Please pick only one of the options: ${Object.keys(options).join(', ')}`); + } + } + } + return opt; +} diff --git a/src/utils/selection.utils.ts b/src/utils/selection.utils.ts new file mode 100644 index 0000000..4267f65 --- /dev/null +++ b/src/utils/selection.utils.ts @@ -0,0 +1,79 @@ +import path from 'path'; +import fs from 'fs/promises'; +import { UIProvider } from '../ui/UIProvider'; +import { SCRIPTS_DIR } from '../paths'; +import { COMPILE_END, getCompilablesDirectory } from '../compile/compile'; +import { File } from '../types/file'; + +export const findCompiles = async (directory?: string): Promise => { + directory ??= await getCompilablesDirectory(); + const files = await fs.readdir(directory); + const compilables = files.filter((file) => file.endsWith(COMPILE_END)); + return compilables.map((file) => ({ + path: path.join(directory, file), + name: file.slice(0, file.length - COMPILE_END.length), + })); +}; + +export const findScripts = async (): Promise => { + const dirents = await fs.readdir(SCRIPTS_DIR, { recursive: true, withFileTypes: true }); + const scripts = dirents.filter((dirent) => dirent.isFile() && dirent.name.endsWith('.ts')); + + return scripts + .map((script) => ({ + name: path.join(script.path.slice(SCRIPTS_DIR.length), script.name), + path: path.join(SCRIPTS_DIR, script.path, script.name), + })) + .sort((a, b) => (a.name >= b.name ? 1 : -1)); +}; + +export async function selectOption( + options: { name: string; value: string }[], + opts: { + ui: UIProvider; + msg: string; + hint?: string; + }, +) { + if (opts.hint) { + const found = options.find((o) => o.value === opts.hint); + if (found === undefined) { + throw new Error(`Could not find option '${opts.hint}'`); + } + return found; + } else { + return await opts.ui.choose(opts.msg, options, (o) => o.name); + } +} + +export async function selectFile( + files: File[], + opts: { + ui: UIProvider; + hint?: string; + import?: boolean; + }, +) { + let selected: File; + + if (opts.hint) { + const found = files.find((f) => f.name.toLowerCase() === opts.hint?.toLowerCase()); + if (found === undefined) { + throw new Error(`Could not find file with name '${opts.hint}'`); + } + selected = found; + opts.ui.write(`Using file: ${selected.name}`); + } else { + if (files.length === 1) { + selected = files[0]; + opts.ui.write(`Using file: ${selected.name}`); + } else { + selected = await opts.ui.choose('Choose file to use', files, (f) => f.name); + } + } + + return { + ...selected, + module: opts.import !== false ? await import(selected.path) : undefined, + }; +} diff --git a/src/utils/timer.utils.ts b/src/utils/timer.utils.ts new file mode 100644 index 0000000..2c81c93 --- /dev/null +++ b/src/utils/timer.utils.ts @@ -0,0 +1,5 @@ +export function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/src/utils/ton.utils.ts b/src/utils/ton.utils.ts new file mode 100644 index 0000000..dbc86e3 --- /dev/null +++ b/src/utils/ton.utils.ts @@ -0,0 +1,30 @@ +import { Address, Cell } from '@ton/core'; + +export const tonDeepLink = (address: Address, amount: bigint, body?: Cell, stateInit?: Cell) => + `ton://transfer/${address.toString({ + urlSafe: true, + bounceable: true, + })}?amount=${amount.toString()}${body ? '&bin=' + body.toBoc().toString('base64url') : ''}${ + stateInit ? '&init=' + stateInit.toBoc().toString('base64url') : '' + }`; + +export function getExplorerLink(address: string, network: string, explorer: string) { + const networkPrefix = network === 'testnet' ? 'testnet.' : ''; + + switch (explorer) { + case 'tonscan': + return `https://${networkPrefix}tonscan.org/address/${address}`; + + case 'tonviewer': + return `https://${networkPrefix}tonviewer.com/${address}`; + + case 'toncx': + return `https://${networkPrefix}ton.cx/address/${address}`; + + case 'dton': + return `https://${networkPrefix}dton.io/a/${address}`; + + default: + return `https://${networkPrefix}tonscan.org/address/${address}`; + } +}