Skip to content

Commit

Permalink
feat: separate compilables (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
pixelplex authored Jul 2, 2024
1 parent 9380020 commit 31fdb57
Show file tree
Hide file tree
Showing 21 changed files with 228 additions and 31 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<CONTRACT>.compile.ts` - [example](/example/wrappers/Counter.compile.ts)
1. You need a compilation script in `compilables/<CONTRACT>.compile.ts` - [example](/example/compilables/Counter.compile.ts)
2. Run interactive: &nbsp;&nbsp; `npx blueprint build` &nbsp; or &nbsp; `yarn blueprint build`
3. Non-interactive: &nbsp; `npx/yarn blueprint build <CONTRACT>` &nbsp; OR build all contracts &nbsp; `yarn blueprint build --all`
* Example: `yarn blueprint build counter`
Expand Down
1 change: 1 addition & 0 deletions example/blueprint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const config = { separateCompilables: true };
File renamed without changes.
32 changes: 12 additions & 20 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Runner> = {
create,
Expand Down Expand Up @@ -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 = {
Expand Down
8 changes: 6 additions & 2 deletions src/cli/create.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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') {
Expand Down
19 changes: 16 additions & 3 deletions src/compile/compile.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<CompilerConfig> {
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`);
Expand Down
1 change: 1 addition & 0 deletions src/config/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import { Plugin } from './Plugin';
export interface Config {
plugins?: Plugin[];
network?: 'mainnet' | 'testnet' | CustomNetwork;
separateCompilables?: boolean;
}
22 changes: 22 additions & 0 deletions src/config/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Config } from './Config';
import { BLUEPRINT_CONFIG } from '../paths';

let config: Config | undefined;

export async function getConfig(): Promise<Config | undefined> {
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;
}
}
16 changes: 11 additions & 5 deletions src/network/createNetworkProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -129,7 +131,10 @@ class WrappedContractProvider implements ContractProvider {
}

open<T extends Contract>(contract: T): OpenedContract<T> {
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<Transaction[]> {
Expand Down Expand Up @@ -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);
}

Expand Down
3 changes: 3 additions & 0 deletions src/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ 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);
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');
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{{name}}.compile.ts
import { CompilerConfig } from '@ton/blueprint';

export const compile: CompilerConfig = {
lang: 'func',
targets: ['{{contractPath}}'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{{name}}.compile.ts
import { CompilerConfig } from '@ton/blueprint';

export const compile: CompilerConfig = {
lang: 'tact',
target: '{{contractPath}}',
options: {
debug: true,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{name}}.ts
export * from '../build/{{name}}/tact_{{name}}';
4 changes: 4 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './object.utils';
export * from './timer.utils';
export * from './ton.utils';
export * from './selection.utils';
13 changes: 13 additions & 0 deletions src/utils/object.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function oneOrZeroOf<T extends { [k: string]: boolean | undefined }>(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;
}
79 changes: 79 additions & 0 deletions src/utils/selection.utils.ts
Original file line number Diff line number Diff line change
@@ -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<File[]> => {
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),

Check failure on line 13 in src/utils/selection.utils.ts

View workflow job for this annotation

GitHub Actions / Publish

Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
name: file.slice(0, file.length - COMPILE_END.length),
}));
};

export const findScripts = async (): Promise<File[]> => {
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,
};
}
5 changes: 5 additions & 0 deletions src/utils/timer.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function sleep(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
30 changes: 30 additions & 0 deletions src/utils/ton.utils.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
}

0 comments on commit 31fdb57

Please sign in to comment.