Browse Source

Added feedback from Doron's review

master
Tal Kol 2 years ago
parent
commit
742bca2f1f
  1. 9
      README.md
  2. 38
      build/build.ts
  3. 13
      contracts/imports/constants.fc
  4. 2
      contracts/imports/utils.fc
  5. 40
      contracts/main.fc
  6. 15
      contracts/main.tlb
  7. 18
      contracts/main.ts
  8. 16
      test/counter.spec.ts
  9. 6
      test/helpers.ts

9
README.md

@ -55,9 +55,12 @@ To setup your machine for development, please make sure you have the following:
* Develop * Develop
* FunC contracts are located in `contracts/*.fc` * FunC contracts are located in `contracts/*.fc`
* Standalone root 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` * Shared imports (when breaking code to multiple files) are in `contracts/imports/*.fc`
* This structure assists with build and deployment and assumed by the included scripts * Contract-specific imports that aren't shared are in `contracts/imports/mycontract/*.fc`
* Tests are located in `test/*.spec.ts` * 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 * Build
* In the root repo dir, run in terminal `npm run build` * In the root repo dir, run in terminal `npm run build`

38
build/build.ts

@ -2,13 +2,15 @@
// It assumes that it is running from the repo root, and the directories are organized this way: // It assumes that it is running from the repo root, and the directories are organized this way:
// ./build/ - directory for build artifacts exists // ./build/ - directory for build artifacts exists
// ./contracts/*.fc - root contracts that are deployed separately are here // ./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 fs from "fs";
import path from "path"; import path from "path";
import process from "process"; import process from "process";
import child_process from "child_process"; import child_process from "child_process";
import fg from "fast-glob"; import glob from "fast-glob";
console.log(`=================================================================`); console.log(`=================================================================`);
console.log(`Build script running, let's find some FunC contracts to compile..`); 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); process.exit(1);
} }
const rootContracts = fg.sync(["contracts/*.fc", "contracts/*.func"]); const rootContracts = glob.sync(["contracts/*.fc", "contracts/*.func"]);
for (const rootContract of rootContracts) { for (const rootContract of rootContracts) {
// compile a new root contract // compile a new root contract
console.log(`\n* Found root contract '${rootContract}' - let's compile it:`); console.log(`\n* Found root contract '${rootContract}' - let's compile it:`);
@ -61,9 +63,25 @@ for (const rootContract of rootContracts) {
fs.unlinkSync(cellArtifact); 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 // create a merged fc file with source code from all dependencies
let sourceToCompile = ""; 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) { for (const importFile of importFiles) {
console.log(` - Adding import '${importFile}'`); console.log(` - Adding import '${importFile}'`);
sourceToCompile += `${fs.readFileSync(importFile).toString()}\n`; sourceToCompile += `${fs.readFileSync(importFile).toString()}\n`;
@ -117,3 +135,15 @@ for (const rootContract of rootContracts) {
} }
console.log(``); 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;
}

13
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

2
contracts/imports/utils.fc

@ -1,6 +1,6 @@
() send_grams(slice address, int amount) impure { () send_grams(slice address, int amount) impure {
cell msg = begin_cell() cell msg = begin_cell()
.store_uint (0x18, 6) .store_uint (0x18, 6) ;; bounce
.store_slice(address) ;; 267 bit address .store_slice(address) ;; 267 bit address
.store_grams(amount) .store_grams(amount)
.store_uint(0, 107) ;; 106 zeroes + 0 as an indicator that there is no cell with the data .store_uint(0, 107) ;; 106 zeroes + 0 as an indicator that there is no cell with the data

40
contracts/main.fc

@ -1,23 +1,6 @@
;; =============== Constants ============================= ;; =============== storage =============================
;; Operations ;; storage binary format is defined as TL-B in companion .tlb file
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
(slice, int) load_data() inline { (slice, int) load_data() inline {
var ds = get_data().begin_parse(); var ds = get_data().begin_parse();
@ -34,13 +17,15 @@ int const::min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON
.end_cell()); .end_cell());
} }
;; =============== Messages ============================= ;; =============== messages =============================
;; message binary format is defined as TL-B in companion .tlb file
() op_withdraw(int withdraw_amount, slice owner_address); () 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 ;; 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 int flags = cs~load_uint(4); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
slice sender_address = cs~load_msg_addr(); 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(); var (owner_address, counter) = load_data();
;; handle operations ;; handle operations
int op = in_msg~load_uint(32); int op = in_msg_body~load_uint(32);
int query_id = in_msg~load_uint(64); int query_id = in_msg_body~load_uint(64);
if (op == op::increment()) { if (op == op::increment()) {
save_data(owner_address, counter + 1); 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()) { if (op == op::deposit()) {
;; empty since ton received (msg_value) is added automatically to contract balance
return (); return ();
} }
if (op == op::withdraw()) { if (op == op::withdraw()) {
throw_unless(error::access_denied(), equal_slices(sender_address, owner_address)); 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); op_withdraw(withdraw_amount, owner_address);
return (); return ();
} }
if (op == op::transfer_ownership()) { if (op == op::transfer_ownership()) {
throw_unless(error::access_denied(), equal_slices(sender_address, owner_address)); 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); save_data(new_owner_address, counter);
return (); return ();
} }
@ -89,7 +75,7 @@ int const::min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON
send_grams(owner_address, return_value); send_grams(owner_address, return_value);
} }
;; =============== Getters ============================= ;; =============== getters =============================
int meaning_of_life() method_id { int meaning_of_life() method_id {
return 42; return 42;

15
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

18
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"; import { Cell, beginCell, Address } from "ton";
// returns contract code cell by relying on the build output in the build directory // encode contract storage according to save_data() contract method
export function createCode() { export function data(params: { ownerAddress: Address; counter: number }): Cell {
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 }) {
return beginCell().storeAddress(params.ownerAddress).storeUint(params.counter, 64).endCell(); return beginCell().storeAddress(params.ownerAddress).storeUint(params.counter, 64).endCell();
} }
// message generators for all ops // message encoders for all ops
export function op_increment() { export function increment(): Cell {
return beginCell().storeUint(1, 32).storeUint(0, 64).endCell(); return beginCell().storeUint(0x37491f2f, 32).storeUint(0, 64).endCell();
} }

16
test/counter.spec.ts

@ -3,18 +3,20 @@ import chaiBN from "chai-bn";
import BN from "bn.js"; import BN from "bn.js";
chai.use(chaiBN(BN)); chai.use(chaiBN(BN));
import * as fs from "fs";
import { Cell } from "ton";
import { SmartContract } from "ton-contract-executor"; import { SmartContract } from "ton-contract-executor";
import { createCode, createData, op_increment } from "../contracts/main"; import * as main from "../contracts/main";
import { internalMessage, randomAddress } from "./utils"; import { internalMessage, randomAddress } from "./helpers";
describe("Counter tests", () => { describe("Counter tests", () => {
let contract: SmartContract; let contract: SmartContract;
beforeEach(async () => { beforeEach(async () => {
contract = await SmartContract.fromCell( contract = await SmartContract.fromCell(
createCode(), Cell.fromBoc(fs.readFileSync("build/main.cell"))[0], // code cell from build output
createData({ main.data({
ownerAddress: randomAddress(0, "owner"), ownerAddress: randomAddress("owner"),
counter: 17, counter: 17,
}) })
); );
@ -31,8 +33,8 @@ describe("Counter tests", () => {
const send = await contract.sendInternalMessage( const send = await contract.sendInternalMessage(
internalMessage({ internalMessage({
from: randomAddress(0, "notowner"), from: randomAddress("notowner"),
body: op_increment(), body: main.increment(),
}) })
); );
expect(send.type).to.equal("success"); expect(send.type).to.equal("success");

6
test/utils.ts → test/helpers.ts

@ -4,19 +4,19 @@ import Prando from "prando";
export const zeroAddress = new Address(0, Buffer.alloc(32, 0)); 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 random = new Prando(seed);
const hash = Buffer.alloc(32); const hash = Buffer.alloc(32);
for (let i = 0; i < hash.length; i++) { for (let i = 0; i < hash.length; i++) {
hash[i] = random.nextInt(0, 255); 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 }) { export function internalMessage(params: { from?: Address; to?: Address; value?: BN; bounce?: boolean; body?: Cell }) {
const message = params.body ? new CellMessage(params.body) : undefined; const message = params.body ? new CellMessage(params.body) : undefined;
return new InternalMessage({ return new InternalMessage({
from: params.from ?? randomAddress(0, "seed"), from: params.from ?? randomAddress("seed"),
to: params.to ?? zeroAddress, to: params.to ?? zeroAddress,
value: params.value ?? 0, value: params.value ?? 0,
bounce: params.bounce ?? true, bounce: params.bounce ?? true,
Loading…
Cancel
Save