diff --git a/README.md b/README.md index c0d1b44..9fab391 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,12 @@ To setup your machine for development, please make sure you have the following: * Develop * FunC contracts are located in `contracts/*.fc` * Standalone root contracts are located in `contracts/*.fc` - * Imported utility code (when breaking code to multiple files) in `contracts/imports/*.fc` - * This structure assists with build and deployment and assumed by the included scripts - * Tests are located in `test/*.spec.ts` + * Shared imports (when breaking code to multiple files) are in `contracts/imports/*.fc` + * Contract-specific imports that aren't shared are in `contracts/imports/mycontract/*.fc` + * Each contract may have optional but recommended auxiliary files: + * [TL-B](https://ton.org/docs/#/overviews/TL-B) file defining the encoding of its data and message ops in `contracts/mycontract.tld` + * TypeScript file that implements the encoding of its data and message ops in `contracts/mycontract.ts` + * Tests in TypeScript are located in `test/*.spec.ts` * Build * In the root repo dir, run in terminal `npm run build` diff --git a/build/build.ts b/build/build.ts index e2150f3..d342fd8 100644 --- a/build/build.ts +++ b/build/build.ts @@ -2,13 +2,15 @@ // It assumes that it is running from the repo root, and the directories are organized this way: // ./build/ - directory for build artifacts exists // ./contracts/*.fc - root contracts that are deployed separately are here -// ./contracts/imports/*.fc - utility code that should be imported as compilation dependency is here +// ./contracts/imports/*.fc - shared utility code that should be imported as compilation dependency is here +// if you need imports that are dedicated to one contract and aren't shared, place them in a directory with the contract name: +// ./contracts/import/mycontract/*.fc import fs from "fs"; import path from "path"; import process from "process"; import child_process from "child_process"; -import fg from "fast-glob"; +import glob from "fast-glob"; console.log(`=================================================================`); console.log(`Build script running, let's find some FunC contracts to compile..`); @@ -33,7 +35,7 @@ if (!fiftVersion.includes(`Fift build information`)) { process.exit(1); } -const rootContracts = fg.sync(["contracts/*.fc", "contracts/*.func"]); +const rootContracts = glob.sync(["contracts/*.fc", "contracts/*.func"]); for (const rootContract of rootContracts) { // compile a new root contract console.log(`\n* Found root contract '${rootContract}' - let's compile it:`); @@ -61,9 +63,25 @@ for (const rootContract of rootContracts) { fs.unlinkSync(cellArtifact); } + // check if we have a tlb file + const tlbFile = `contracts/${contractName}.tlb`; + if (fs.existsSync(tlbFile)) { + console.log(` - TL-B file '${tlbFile}' found, calculating crc32 on all ops..`); + const tlbContent = fs.readFileSync(tlbFile).toString(); + const tlbOpMessages = tlbContent.match(/^(\w+).*=\s*InternalMsgBody$/gm) ?? []; + for (const tlbOpMessage of tlbOpMessages) { + const crc = crc32(tlbOpMessage); + const asQuery = `0x${(crc & 0x7fffffff).toString(16)}`; + const asResponse = `0x${((crc | 0x80000000) >>> 0).toString(16)}`; + console.log(` op '${tlbOpMessage.split(" ")[0]}': '${asQuery}' as query (&0x7fffffff), '${asResponse}' as response (|0x80000000)`); + } + } else { + console.log(` - Warning: TL-B file for contract '${tlbFile}' not found, are your op consts according to standard?`); + } + // create a merged fc file with source code from all dependencies let sourceToCompile = ""; - const importFiles = fg.sync(["contracts/imports/**/*.fc", "contracts/imports/**/*.func"]); + const importFiles = glob.sync([`contracts/imports/*.fc`, `contracts/imports/*.func`, `contracts/imports/${contractName}/*.fc`, `contracts/imports/${contractName}/*.func`]); for (const importFile of importFiles) { console.log(` - Adding import '${importFile}'`); sourceToCompile += `${fs.readFileSync(importFile).toString()}\n`; @@ -117,3 +135,15 @@ for (const rootContract of rootContracts) { } console.log(``); + +// helpers + +function crc32(r: string) { + for (var a, o = [], c = 0; c < 256; c++) { + a = c; + for (var f = 0; f < 8; f++) a = 1 & a ? 3988292384 ^ (a >>> 1) : a >>> 1; + o[c] = a; + } + for (var n = -1, t = 0; t < r.length; t++) n = (n >>> 8) ^ o[255 & (n ^ r.charCodeAt(t))]; + return (-1 ^ n) >>> 0; +} diff --git a/contracts/imports/constants.fc b/contracts/imports/constants.fc new file mode 100644 index 0000000..8491233 --- /dev/null +++ b/contracts/imports/constants.fc @@ -0,0 +1,13 @@ +;; operations (constant values taken from crc32 on op message in the companion .tlb files and appear during build) +int op::increment() asm "0x37491f2f PUSHINT"; +int op::deposit() asm "0x47d54391 PUSHINT"; +int op::withdraw() asm "0x41836980 PUSHINT"; +int op::transfer_ownership() asm "0x2da38aaf PUSHINT"; + +;; errors +int error::access_denied() asm "0xfffffffe PUSHINT"; +int error::unknown_op() asm "0xffffffff PUSHINT"; +int error::insufficient_balance() asm "101 PUSHINT"; + +;; other +int const::min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON \ No newline at end of file diff --git a/contracts/imports/utils.fc b/contracts/imports/utils.fc index 871e76f..230a8f7 100644 --- a/contracts/imports/utils.fc +++ b/contracts/imports/utils.fc @@ -1,6 +1,6 @@ () send_grams(slice address, int amount) impure { cell msg = begin_cell() - .store_uint (0x18, 6) + .store_uint (0x18, 6) ;; bounce .store_slice(address) ;; 267 bit address .store_grams(amount) .store_uint(0, 107) ;; 106 zeroes + 0 as an indicator that there is no cell with the data diff --git a/contracts/main.fc b/contracts/main.fc index 8ba4858..bbe70ee 100644 --- a/contracts/main.fc +++ b/contracts/main.fc @@ -1,23 +1,6 @@ -;; =============== Constants ============================= +;; =============== storage ============================= -;; Operations -int op::increment() asm "1 PUSHINT"; -int op::deposit() asm "2 PUSHINT"; -int op::withdraw() asm "3 PUSHINT"; -int op::transfer_ownership() asm "4 PUSHINT"; - -;; Errors -int error::access_denied() asm "0xfffffffe PUSHINT"; -int error::unknown_op() asm "0xffffffff PUSHINT"; -int error::insufficient_balance() asm "101 PUSHINT"; - -;; Other -int const::min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON - -;; =============== Storage ============================= - -;; Storage TL-B scheme: -;; storage#_ owner_address:MsgAddress counter:uint64 +;; storage binary format is defined as TL-B in companion .tlb file (slice, int) load_data() inline { var ds = get_data().begin_parse(); @@ -34,13 +17,15 @@ int const::min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON .end_cell()); } -;; =============== Messages ============================= +;; =============== messages ============================= + +;; message binary format is defined as TL-B in companion .tlb file () op_withdraw(int withdraw_amount, slice owner_address); -() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure { +() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { ;; parse incoming internal message - slice cs = in_msg_cell.begin_parse(); + slice cs = in_msg.begin_parse(); int flags = cs~load_uint(4); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool slice sender_address = cs~load_msg_addr(); @@ -53,8 +38,8 @@ int const::min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON var (owner_address, counter) = load_data(); ;; handle operations - int op = in_msg~load_uint(32); - int query_id = in_msg~load_uint(64); + int op = in_msg_body~load_uint(32); + int query_id = in_msg_body~load_uint(64); if (op == op::increment()) { save_data(owner_address, counter + 1); @@ -62,19 +47,20 @@ int const::min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON } if (op == op::deposit()) { + ;; empty since ton received (msg_value) is added automatically to contract balance return (); } if (op == op::withdraw()) { throw_unless(error::access_denied(), equal_slices(sender_address, owner_address)); - int withdraw_amount = in_msg~load_uint(256); + int withdraw_amount = in_msg_body~load_coins(); op_withdraw(withdraw_amount, owner_address); return (); } if (op == op::transfer_ownership()) { throw_unless(error::access_denied(), equal_slices(sender_address, owner_address)); - slice new_owner_address = in_msg~load_msg_addr(); + slice new_owner_address = in_msg_body~load_msg_addr(); save_data(new_owner_address, counter); return (); } @@ -89,7 +75,7 @@ int const::min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON send_grams(owner_address, return_value); } -;; =============== Getters ============================= +;; =============== getters ============================= int meaning_of_life() method_id { return 42; diff --git a/contracts/main.tlb b/contracts/main.tlb new file mode 100644 index 0000000..6259e6e --- /dev/null +++ b/contracts/main.tlb @@ -0,0 +1,15 @@ +// base types defined in https://github.com/newton-blockchain/ton/blob/master/crypto/block/block.tlb + +// storage + +storage#_ owner_address:MsgAddress counter:uint64 + +// ops + +increment query_id:uint64 = InternalMsgBody + +deposit query_id:uint64 = InternalMsgBody + +withdraw query_id:uint64 withdraw_amount:Grams = InternalMsgBody + +transfer_ownership query_id:uint64 new_owner_address:MsgAddress = InternalMsgBody diff --git a/contracts/main.ts b/contracts/main.ts index 280c0a0..6410c3e 100644 --- a/contracts/main.ts +++ b/contracts/main.ts @@ -1,20 +1,12 @@ -// this file assists with instantiating the contract (code and data cells) - -import * as fs from "fs"; import { Cell, beginCell, Address } from "ton"; -// returns contract code cell by relying on the build output in the build directory -export function createCode() { - return Cell.fromBoc(fs.readFileSync(__dirname + "/../build/main.cell"))[0]; -} - -// returns contract data cell (storage) according to save_data() contract method -export function createData(params: { ownerAddress: Address; counter: number }) { +// encode contract storage according to save_data() contract method +export function data(params: { ownerAddress: Address; counter: number }): Cell { return beginCell().storeAddress(params.ownerAddress).storeUint(params.counter, 64).endCell(); } -// message generators for all ops +// message encoders for all ops -export function op_increment() { - return beginCell().storeUint(1, 32).storeUint(0, 64).endCell(); +export function increment(): Cell { + return beginCell().storeUint(0x37491f2f, 32).storeUint(0, 64).endCell(); } diff --git a/test/counter.spec.ts b/test/counter.spec.ts index b7d817d..bb0055c 100644 --- a/test/counter.spec.ts +++ b/test/counter.spec.ts @@ -3,18 +3,20 @@ import chaiBN from "chai-bn"; import BN from "bn.js"; chai.use(chaiBN(BN)); +import * as fs from "fs"; +import { Cell } from "ton"; import { SmartContract } from "ton-contract-executor"; -import { createCode, createData, op_increment } from "../contracts/main"; -import { internalMessage, randomAddress } from "./utils"; +import * as main from "../contracts/main"; +import { internalMessage, randomAddress } from "./helpers"; describe("Counter tests", () => { let contract: SmartContract; beforeEach(async () => { contract = await SmartContract.fromCell( - createCode(), - createData({ - ownerAddress: randomAddress(0, "owner"), + Cell.fromBoc(fs.readFileSync("build/main.cell"))[0], // code cell from build output + main.data({ + ownerAddress: randomAddress("owner"), counter: 17, }) ); @@ -31,8 +33,8 @@ describe("Counter tests", () => { const send = await contract.sendInternalMessage( internalMessage({ - from: randomAddress(0, "notowner"), - body: op_increment(), + from: randomAddress("notowner"), + body: main.increment(), }) ); expect(send.type).to.equal("success"); diff --git a/test/utils.ts b/test/helpers.ts similarity index 82% rename from test/utils.ts rename to test/helpers.ts index a9e090b..eb0a749 100644 --- a/test/utils.ts +++ b/test/helpers.ts @@ -4,19 +4,19 @@ import Prando from "prando"; export const zeroAddress = new Address(0, Buffer.alloc(32, 0)); -export function randomAddress(workchain: number, seed: string) { +export function randomAddress(seed: string, workchain?: number) { const random = new Prando(seed); const hash = Buffer.alloc(32); for (let i = 0; i < hash.length; i++) { hash[i] = random.nextInt(0, 255); } - return new Address(workchain, hash); + return new Address(workchain ?? 0, hash); } export function internalMessage(params: { from?: Address; to?: Address; value?: BN; bounce?: boolean; body?: Cell }) { const message = params.body ? new CellMessage(params.body) : undefined; return new InternalMessage({ - from: params.from ?? randomAddress(0, "seed"), + from: params.from ?? randomAddress("seed"), to: params.to ?? zeroAddress, value: params.value ?? 0, bounce: params.bounce ?? true,