Browse Source

Init

dev/signature-instant-buy
Lev 2 years ago
commit
f5c545d9ba
  1. 21
      LICENSE
  2. 99
      README.md
  3. 4
      build/.gitignore
  4. 181
      build/_build.ts
  5. 161
      build/_deploy.ts
  6. 30
      build/_setup.ts
  7. 31
      build/main.deploy.ts
  8. 13
      contracts/imports/constants.fc
  9. 112
      contracts/imports/dns-utils.fc
  10. 16
      contracts/imports/op-codes.fc
  11. 6
      contracts/imports/params.fc
  12. 80
      contracts/imports/root-dns.fc
  13. 215
      contracts/imports/stdlib.fc
  14. 9
      contracts/imports/utils.fc
  15. 155
      contracts/main.fc
  16. 32
      contracts/main.ts
  17. 142
      contracts/nft-collection.fc
  18. 322
      contracts/nft-item.fc
  19. 66
      contracts/utils.ts
  20. 3448
      package-lock.json
  21. 51
      package.json
  22. 63
      test/helpers.ts
  23. 56
      test/ownership.spec.ts
  24. 15
      tsconfig.json

21
LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 DeFi.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

99
README.md

@ -0,0 +1,99 @@
# TON Starter Template - Contracts
> Starter template for a new TON project - FunC contracts, JS tests, compilation and deployment scripts
## Overview
This project is part of a set of 3 typical repositories needed for a blockchain dapp running on TON blockchain:
* Smart contracts in FunC that are deployed on-chain (this repo)
* Web frontend for interacting with the dapp from a web browser (coming soon)
* Telegram bot for interacting with the [dapp from inside Telegram messenger](https://github.com/ton-defi-org/tonstarter-twa)
## What does this repo contain?
* `contracts/*.fc` - Smart contracts for TON blockchain written in [FunC](https://ton.org/docs/#/func) language
* `test/*.spec.ts` - Test suite for the contracts in TypeScript running on [Mocha](https://mochajs.org/) test runner
* `build/_build.ts` - Build script to compile the FunC code to [Fift](https://ton-blockchain.github.io/docs/fiftbase.pdf) and [TVM](https://ton-blockchain.github.io/docs/tvm.pdf) opcodes
* `build/_deploy.ts` - Deploy script to deploy the compiled code to TON mainnet (or testnet)
* `build/_setup.ts` - Setup script to install build dependencies (used primarily for Glitch.com support)
There is no one official way to develop smart contracts for TON. Every developer has their own best practices. This setup is definitely opinionated and some developers may not appreciate the choices made. Nevertheless, we stand by every choice made here and believe that this is the optimal setup to develop fully tested contracts in the most seamless way possible.
Some of the opinionated choices made here include:
* Cross platform support - allow developers to work on Mac M1, Mac Intel, Windows or Linux
* Strong belief in tests - contracts often manage money - they must be developed under high scrutiny
* Clear and documented code to help users audit the contracts sources and understand what they do
* Reliance on modern TypeScript to develop clean and typed scripts and tests in a modern framework
* Reliance on TypeScript for deployment instead of working with `fift` CLI tools - it's simply easier
* Tests are executed in JavaScript with TVM in web-assembly - a great balance of speed and convenience
* Following of the TON contract [best practices](https://ton.org/docs/#/howto/smart-contract-guidelines) appearing in the official docs
## Dependencies and requirements
To setup your local machine for development, please make sure you have the following:
* A modern version of Node.js (version 16.15.0 or later)
* Installation instructions can be found [here](https://nodejs.org/)
* Run in terminal `node -v` to verify your installation, the project was tested on `v17.3.0`
* The `func` CLI tool (FunC compiler)
* Installation instructions can be found [here](https://github.com/ton-defi-org/ton-binaries)
* Run in terminal `func -V` to verify your installation
* The `fift` CLI tool
* Installation instructions can be found [here](https://github.com/ton-defi-org/ton-binaries)
* Don't forget to set the `FIFTPATH` env variable as part of the installation above
* Run in terminal `fift -V` and `fift` to verify your installation
* A decent IDE with FunC and TypeScript support
* We recommend using [Visual Studio Code](https://code.visualstudio.com/) with the [FunC plugin](https://marketplace.visualstudio.com/items?itemName=tonwhales.func-vscode) installed
Once your local machine is ready, install the project:
* Git clone the repo locally and rename the directory to your own project name
* In the root repo dir, run in terminal `npm install`
### or.. work 100% online instead
Alternatively, you can ignore the above requirements and develop right inside a web browser with an online IDE and *zero* setup. Simply open this repo inside [Glitch](https://glitch.com/) without installing anything:
* Create your new Glitch workspace by opening [this link](https://glitch.com/edit/#!/remix/clone-from-repo?&REPO_URL=https%3A%2F%2Fgithub.com%2Fton-defi-org%2Ftonstarter-contracts.git) in your browser
* Wait about 60 seconds until installation completes <br>(click the "LOGS" button on the bottom of the IDE to see progress)
* Edit your contract files and tests in the online IDE
* To run terminal commands like `npm run build` click the "TERMINAL" button on the bottom of the online IDE
* Working online is slow! run on a local machine if you want a much faster experience
## Development instructions
* Write code
* FunC contracts are located in `contracts/*.fc`
* Standalone root contracts are located in `contracts/*.fc`
* 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`
* Compilation errors will appear on screen
* Resulting build artifacts include:
* `mycontract.fif` - Fift file result of compilation (not very useful by itself)
* `mycontract.compiled.json` - the binary code cell of the compiled contract (for deployment). Saved in a hex format within a json file to support webapp imports
* Test
* In the root repo dir, run in terminal `npm run test`
* Don't forget to build (or rebuild) before running tests
* Tests are running inside Node.js by running TVM in web-assembly using `ton-contract-executor`
* Deploy
* Make sure all contracts are built and your setup is ready to deploy:
* Each contract to deploy should have a script `build/mycontract.deploy.ts` to return its init data cell
* The deployment wallet is configured in `.env` (created automatically if not exists), with contents:<br>
`DEPLOYER_MNEMONIC="mad nation chief flavor ..."` (24 secret words)
* To deploy to mainnet (production), run in terminal `npm run deploy`
* To deploy to testnet instead (where TON coins are free), run `npm run deploy:testnet`
* Follow the on-screen instructions of the deploy script
# License
MIT

4
build/.gitignore vendored

@ -0,0 +1,4 @@
*.fif
*.fc
*.cell
*.compiled.json

181
build/_build.ts

@ -0,0 +1,181 @@
// This is a simple generic build script in TypeScript that should work for most projects without modification
// The script 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 - 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 glob from "fast-glob";
import { Cell } from "ton";
import semver from "semver";
async function main() {
console.log("=================================================================");
console.log("Build script running, let's find some FunC contracts to compile..");
// if we have an explicit bin directory, use the executables there (needed for glitch.com)
if (fs.existsSync("bin")) {
process.env.PATH = path.join(__dirname, "..", "bin") + path.delimiter + process.env.PATH;
process.env.FIFTPATH = path.join(__dirname, "..", "bin", "fiftlib");
}
// make sure func compiler is available
const minSupportFunc = "0.2.0";
try {
const funcVersion = child_process
.execSync("func -V")
.toString()
.match(/semantic version: v([0-9.]+)/)?.[1];
if (!semver.gte(semver.coerce(funcVersion) ?? "", minSupportFunc)) throw new Error("Nonexistent version or outdated");
} catch (e) {
console.log(`\nFATAL ERROR: 'func' with version >= ${minSupportFunc} executable is not found, is it installed and in path?`);
process.exit(1);
}
// make sure fift cli is available
let fiftVersion = "";
try {
fiftVersion = child_process.execSync("fift -V").toString();
} catch (e) {}
if (!fiftVersion.includes("Fift build information")) {
console.log("\nFATAL ERROR: 'fift' executable is not found, is it installed and in path?");
process.exit(1);
}
// go over all the root contracts in the contracts directory
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:`);
const contractName = path.parse(rootContract).name;
// delete existing build artifacts
const fiftArtifact = `build/${contractName}.fif`;
if (fs.existsSync(fiftArtifact)) {
console.log(` - Deleting old build artifact '${fiftArtifact}'`);
fs.unlinkSync(fiftArtifact);
}
const mergedFuncArtifact = `build/${contractName}.merged.fc`;
if (fs.existsSync(mergedFuncArtifact)) {
console.log(` - Deleting old build artifact '${mergedFuncArtifact}'`);
fs.unlinkSync(mergedFuncArtifact);
}
const fiftCellArtifact = `build/${contractName}.cell.fif`;
if (fs.existsSync(fiftCellArtifact)) {
console.log(` - Deleting old build artifact '${fiftCellArtifact}'`);
fs.unlinkSync(fiftCellArtifact);
}
const cellArtifact = `build/${contractName}.cell`;
if (fs.existsSync(cellArtifact)) {
console.log(` - Deleting old build artifact '${cellArtifact}'`);
fs.unlinkSync(cellArtifact);
}
const hexArtifact = `build/${contractName}.compiled.json`;
if (fs.existsSync(hexArtifact)) {
console.log(` - Deleting old build artifact '${hexArtifact}'`);
fs.unlinkSync(hexArtifact);
}
// 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?`);
}
// run the func compiler to create a fif file
console.log(` - Trying to compile '${rootContract}' with 'func' compiler..`);
let buildErrors: string;
try {
buildErrors = child_process.execSync(`func -APS -o build/${contractName}.fif ${rootContract} 2>&1 1>node_modules/.tmpfunc`).toString();
} catch (e) {
buildErrors = e.stdout.toString();
}
if (buildErrors.length > 0) {
console.log(" - OH NO! Compilation Errors! The compiler output was:");
console.log(`\n${buildErrors}`);
process.exit(1);
} else {
console.log(" - Compilation successful!");
}
// make sure fif build artifact was created
if (!fs.existsSync(fiftArtifact)) {
console.log(` - For some reason '${fiftArtifact}' was not created!`);
process.exit(1);
} else {
console.log(` - Build artifact created '${fiftArtifact}'`);
}
// create a temp cell.fif that will generate the cell
let fiftCellSource = '"Asm.fif" include\n';
fiftCellSource += `${fs.readFileSync(fiftArtifact).toString()}\n`;
fiftCellSource += `boc>B "${cellArtifact}" B>file`;
fs.writeFileSync(fiftCellArtifact, fiftCellSource);
// run fift cli to create the cell
try {
child_process.execSync(`fift ${fiftCellArtifact}`);
} catch (e) {
console.log("FATAL ERROR: 'fift' executable failed, is FIFTPATH env variable defined?");
process.exit(1);
}
// Remove intermediary
fs.unlinkSync(fiftCellArtifact);
// make sure cell build artifact was created
if (!fs.existsSync(cellArtifact)) {
console.log(` - For some reason, intermediary file '${cellArtifact}' was not created!`);
process.exit(1);
}
fs.writeFileSync(
hexArtifact,
JSON.stringify({
hex: Cell.fromBoc(fs.readFileSync(cellArtifact))[0].toBoc().toString("hex"),
})
);
// Remove intermediary
fs.unlinkSync(cellArtifact);
// make sure hex artifact was created
if (!fs.existsSync(hexArtifact)) {
console.log(` - For some reason '${hexArtifact}' was not created!`);
process.exit(1);
} else {
console.log(` - Build artifact created '${hexArtifact}'`);
}
}
console.log("");
}
main();
// helpers
function crc32(r: string) {
for (var a, o = [], c = 0; c < 256; c++) {
a = c;
for (let 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;
}

161
build/_deploy.ts

@ -0,0 +1,161 @@
// This is a simple generic deploy script in TypeScript that should work for most projects without modification
// Every contract you want to deploy should have a mycontract.deploy.ts script that returns its init data
// The script assumes that it is running from the repo root, and the directories are organized this way:
// ./build/ - directory for build artifacts (mycontract.compiled.json) and deploy init data scripts (mycontract.deploy.ts)
// ./.env - config file with DEPLOYER_MNEMONIC - secret mnemonic of deploying wallet (will be created if not found)
import axios from "axios";
import axiosThrottle from "axios-request-throttle";
axiosThrottle.use(axios, { requestsPerSecond: 0.5 }); // required since toncenter jsonRPC limits to 1 req/sec without API key
import dotenv from "dotenv";
dotenv.config();
import fs from "fs";
import path from "path";
import glob from "fast-glob";
import { Address, Cell, CellMessage, CommonMessageInfo, fromNano, InternalMessage, StateInit, toNano } from "ton";
import { TonClient, WalletContract, WalletV3R2Source, contractAddress, SendMode } from "ton";
import { mnemonicNew, mnemonicToWalletKey } from "ton-crypto";
async function main() {
console.log(`=================================================================`);
console.log(`Deploy script running, let's find some contracts to deploy..`);
const isTestnet = process.env.TESTNET || process.env.npm_lifecycle_event == "deploy:testnet";
// check input arguments (given through environment variables)
if (isTestnet) {
console.log(`\n* We are working with 'testnet' (https://t.me/testgiver_ton_bot will give you test TON)`);
} else {
console.log(`\n* We are working with 'mainnet'`);
}
// initialize globals
const client = new TonClient({ endpoint: `https://${isTestnet ? "testnet." : ""}toncenter.com/api/v2/jsonRPC` });
const deployerWalletType = "org.ton.wallets.v3.r2"; // also see WalletV3R2Source class used below
const newContractFunding = toNano(0.02); // this will be (almost in full) the balance of a new deployed contract and allow it to pay rent
const workchain = 0; // normally 0, only special contracts should be deployed to masterchain (-1)
// make sure we have a wallet mnemonic to deploy from (or create one if not found)
const deployConfigEnv = ".env";
let deployerMnemonic;
if (!fs.existsSync(deployConfigEnv) || !process.env.DEPLOYER_MNEMONIC) {
console.log(`\n* Config file '${deployConfigEnv}' not found, creating a new wallet for deploy..`);
deployerMnemonic = (await mnemonicNew(24)).join(" ");
const deployWalletEnvContent = `DEPLOYER_WALLET=${deployerWalletType}\nDEPLOYER_MNEMONIC="${deployerMnemonic}"\n`;
fs.writeFileSync(deployConfigEnv, deployWalletEnvContent);
console.log(` - Created new wallet in '${deployConfigEnv}' - keep this file secret!`);
} else {
console.log(`\n* Config file '${deployConfigEnv}' found and will be used for deployment!`);
deployerMnemonic = process.env.DEPLOYER_MNEMONIC;
}
// open the wallet and make sure it has enough TON
const walletKey = await mnemonicToWalletKey(deployerMnemonic.split(" "));
const walletContract = WalletContract.create(client, WalletV3R2Source.create({ publicKey: walletKey.publicKey, workchain }));
console.log(` - Wallet address used to deploy from is: ${walletContract.address.toFriendly()}`);
const walletBalance = await client.getBalance(walletContract.address);
if (walletBalance.lt(toNano(0.2))) {
console.log(` - ERROR: Wallet has less than 0.2 TON for gas (${fromNano(walletBalance)} TON), please send some TON for gas first`);
process.exit(1);
} else {
console.log(` - Wallet balance is ${fromNano(walletBalance)} TON, which will be used for gas`);
}
// go over all the contracts we have deploy scripts for
const rootContracts = glob.sync(["build/*.deploy.ts"]);
for (const rootContract of rootContracts) {
// deploy a new root contract
console.log(`\n* Found root contract '${rootContract} - let's deploy it':`);
const contractName = path.parse(path.parse(rootContract).name).name;
// prepare the init data cell
const deployInitScript = require(__dirname + "/../" + rootContract);
if (typeof deployInitScript.initData !== "function") {
console.log(` - ERROR: '${rootContract}' does not have 'initData()' function`);
process.exit(1);
}
const initDataCell = deployInitScript.initData() as Cell;
// prepare the init message
if (typeof deployInitScript.initMessage !== "function") {
console.log(` - ERROR: '${rootContract}' does not have 'initMessage()' function`);
process.exit(1);
}
const initMessageCell = deployInitScript.initMessage() as Cell | null;
// prepare the init code cell
const hexArtifact = `build/${contractName}.compiled.json`;
if (!fs.existsSync(hexArtifact)) {
console.log(` - ERROR: '${hexArtifact}' not found, did you build?`);
process.exit(1);
}
const initCodeCell = Cell.fromBoc(JSON.parse(fs.readFileSync(hexArtifact).toString()).hex)[0];
// make sure the contract was not already deployed
const newContractAddress = contractAddress({ workchain, initialData: initDataCell, initialCode: initCodeCell });
console.log(` - Based on your init code+data, your new contract address is: ${newContractAddress.toFriendly()}`);
if (await client.isContractDeployed(newContractAddress)) {
console.log(` - Looks like the contract is already deployed in this address, skipping deployment`);
await performPostDeploymentTest(rootContract, deployInitScript, walletContract, walletKey.secretKey, newContractAddress);
continue;
}
// deploy by sending an internal message to the deploying wallet
console.log(` - Let's deploy the contract on-chain..`);
const seqno = await walletContract.getSeqNo();
const transfer = walletContract.createTransfer({
secretKey: walletKey.secretKey,
seqno: seqno,
sendMode: SendMode.PAY_GAS_SEPARATLY + SendMode.IGNORE_ERRORS,
order: new InternalMessage({
to: newContractAddress,
value: newContractFunding,
bounce: false,
body: new CommonMessageInfo({
stateInit: new StateInit({ data: initDataCell, code: initCodeCell }),
body: initMessageCell !== null ? new CellMessage(initMessageCell) : null,
}),
}),
});
await client.sendExternalMessage(walletContract, transfer);
console.log(` - Deploy transaction sent successfully`);
// make sure that the contract was deployed
console.log(` - Block explorer link: https://${process.env.TESTNET ? "test." : ""}tonwhales.com/explorer/address/${newContractAddress.toFriendly()}`);
console.log(` - Waiting up to 20 seconds to check if the contract was actually deployed..`);
for (let attempt = 0; attempt < 10; attempt++) {
await sleep(2000);
const seqnoAfter = await walletContract.getSeqNo();
if (seqnoAfter > seqno) break;
}
if (await client.isContractDeployed(newContractAddress)) {
console.log(` - SUCCESS! Contract deployed successfully to address: ${newContractAddress.toFriendly()}`);
const contractBalance = await client.getBalance(newContractAddress);
console.log(` - New contract balance is now ${fromNano(contractBalance)} TON, make sure it has enough to pay rent`);
await performPostDeploymentTest(rootContract, deployInitScript, walletContract, walletKey.secretKey, newContractAddress);
} else {
console.log(` - FAILURE! Contract address still looks uninitialized: ${newContractAddress.toFriendly()}`);
}
}
console.log(``);
}
main();
// helpers
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function performPostDeploymentTest(rootContract: string, deployInitScript: any, walletContract: WalletContract, secretKey: Buffer, newContractAddress: Address) {
if (typeof deployInitScript.postDeployTest !== "function") {
console.log(` - Not running a post deployment test, '${rootContract}' does not have 'postDeployTest()' function`);
return;
}
console.log(` - Running a post deployment test:`);
await deployInitScript.postDeployTest(walletContract, secretKey, newContractAddress);
}

30
build/_setup.ts

@ -0,0 +1,30 @@
// This is a simple setup script in TypeScript that should work for most projects without modification
// The purpose of this script is to install build dependencies (tools like "func" and "fift") automatically
// We rely on this script for example to support Glitch.com (online IDE) and have it working in one click
import fs from "fs";
import child_process from "child_process";
// package ton-compiler brings its own func and fift executables which interfere with the system ones
try {
fs.unlinkSync(__dirname + "/../node_modules/.bin/func");
fs.unlinkSync(__dirname + "/../node_modules/.bin/fift");
} catch (e) {}
try {
fs.unlinkSync(__dirname + "/../node_modules/.bin/func.cmd");
fs.unlinkSync(__dirname + "/../node_modules/.bin/fift.cmd");
} catch (e) {}
// check if we're running on glitch.com (glitch is running Ubuntu 16)
if (fs.existsSync("/app/.glitchdotcom.json")) {
// make sure we're installed once
if (!fs.existsSync("/app/bin")) {
child_process.execSync(`mkdir bin`);
child_process.execSync(`wget https://github.com/ton-defi-org/ton-binaries/releases/download/ubuntu-16/fift -P ./bin`);
child_process.execSync(`chmod +x ./bin/fift`);
child_process.execSync(`wget https://github.com/ton-defi-org/ton-binaries/releases/download/ubuntu-16/func -P ./bin`);
child_process.execSync(`chmod +x ./bin/func`);
child_process.execSync(`wget https://github.com/ton-defi-org/ton-binaries/releases/download/fiftlib/fiftlib.zip -P ./bin`);
child_process.execSync(`unzip ./bin/fiftlib.zip -d ./bin/fiftlib`);
}
}

31
build/main.deploy.ts

@ -0,0 +1,31 @@
import * as main from "../contracts/main";
import { Address, toNano, TupleSlice, WalletContract } from "ton";
import { sendInternalMessageWithWallet } from "../test/helpers";
// return the init Cell of the contract storage (according to load_data() contract method)
export function initData() {
return main.data({
ownerAddress: Address.parseFriendly("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N").address,
counter: 10,
});
}
// return the op that should be sent to the contract on deployment, can be "null" to send an empty message
export function initMessage() {
return main.increment();
}
// optional end-to-end sanity test for the actual on-chain contract to see it is actually working on-chain
export async function postDeployTest(walletContract: WalletContract, secretKey: Buffer, contractAddress: Address) {
const call = await walletContract.client.callGetMethod(contractAddress, "counter");
const counter = new TupleSlice(call.stack).readBigNumber();
console.log(` # Getter 'counter' = ${counter.toString()}`);
const message = main.increment();
await sendInternalMessageWithWallet({ walletContract, secretKey, to: contractAddress, value: toNano(0.02), body: message });
console.log(` # Sent 'increment' op message`);
const call2 = await walletContract.client.callGetMethod(contractAddress, "counter");
const counter2 = new TupleSlice(call2.stack).readBigNumber();
console.log(` # Getter 'counter' = ${counter2.toString()}`);
}

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)
const op::increment = 0x37491f2f;
const op::deposit = 0x47d54391;
const op::withdraw = 0x41836980;
const op::transfer_ownership = 0x2da38aaf;
;; errors
const error::unknown_op = 101;
const error::access_denied = 102;
const error::insufficient_balance = 103;
;; other
const const::min_tons_for_storage = 10000000; ;; 0.01 TON

112
contracts/imports/dns-utils.fc

@ -0,0 +1,112 @@
#include "stdlib.fc";
const int one_month = 2592000; ;; 1 month in seconds = 60 * 60 * 24 * 30
const int one_year = 31622400; ;; 1 year in seconds = 60 * 60 * 24 * 366
const int auction_start_time = 1659171600; ;; GMT: Monday, 30 July 2022 г., 09:00:00
const int one_ton = 1000000000;
const int dns_next_resolver_prefix = 0xba93; ;; dns_next_resolver prefix - https://github.com/ton-blockchain/ton/blob/7e3df93ca2ab336716a230fceb1726d81bac0a06/crypto/block/block.tlb#L819
const int dns_config_id = 80; ;; dns black list config param; in testnet -80
const int op::fill_up = 0x370fec51;
const int op::outbid_notification = 0x557cea20;
const int op::change_dns_record = 0x4eb1f0f9;
const int op::process_governance_decision = 0x44beae41;
const int op::dns_balance_release = 0x4ed14b65;
int mod(int x, int y) asm "MOD";
slice zero_address() {
return begin_cell().store_uint(0, 2).end_cell().begin_parse();
}
;; "ton\0test\0" -> "ton"
int get_top_domain_bits(slice domain) {
int i = 0;
int need_break = 0;
do {
int char = domain~load_uint(8); ;; we do not check domain.length because it MUST contains \0 character
need_break = char == 0;
if (~ need_break) {
i += 8;
}
} until (need_break);
throw_if(201, i == 0); ;; starts with \0
return i;
}
slice read_domain_from_comment(slice in_msg_body) {
int need_break = 0;
builder result = begin_cell();
do {
result = result.store_slice(in_msg_body~load_bits(in_msg_body.slice_bits()));
int refs_len = in_msg_body.slice_refs();
need_break = refs_len == 0;
if (~ need_break) {
throw_unless(202, refs_len == 1);
in_msg_body = in_msg_body~load_ref().begin_parse();
}
} until (need_break);
return result.end_cell().begin_parse();
}
int check_domain_string(slice domain) {
int i = 0;
int len = slice_bits(domain);
int need_break = 0;
do {
need_break = i == len;
if (~ need_break) {
int char = domain~load_uint(8);
;; we can do it because additional UTF-8 character's octets >= 128 -- https://www.ietf.org/rfc/rfc3629.txt
int is_hyphen = (char == 45);
int valid_char = (is_hyphen & (i > 0) & (i < len - 8)) | ((char >= 48) & (char <= 57)) | ((char >= 97) & (char <= 122)); ;; '-' or 0-9 or a-z
need_break = ~ valid_char;
if (~ need_break) {
i += 8;
}
}
} until (need_break);
return i == len;
}
(int, int) get_min_price_config(int domain_char_count) {
if (domain_char_count == 4) {
return (1000, 100);
}
if (domain_char_count == 5) {
return (500, 50);
}
if (domain_char_count == 6) {
return (400, 40);
}
if (domain_char_count == 7) {
return (300, 30);
}
if (domain_char_count == 8) {
return (200, 20);
}
if (domain_char_count == 9) {
return (100, 10);
}
if (domain_char_count == 10) {
return (50, 5);
}
return (10, 1);
}
int get_min_price(int domain_bits_length, int now_time) {
(int start_min_price, int end_min_price) = get_min_price_config(domain_bits_length / 8);
start_min_price *= one_ton;
end_min_price *= one_ton;
int seconds = now_time - auction_start_time;
int months = seconds / one_month;
if (months > 21) {
return end_min_price;
}
repeat (months) {
start_min_price = start_min_price * 90 / 100;
}
return start_min_price;
}

16
contracts/imports/op-codes.fc

@ -0,0 +1,16 @@
int op::transfer() asm "0x5fcc3d14 PUSHINT";
int op::ownership_assigned() asm "0x05138d91 PUSHINT";
int op::excesses() asm "0xd53276db PUSHINT";
int op::get_static_data() asm "0x2fcb26a2 PUSHINT";
int op::report_static_data() asm "0x8b771735 PUSHINT";
int op::get_royalty_params() asm "0x693d3950 PUSHINT";
int op::report_royalty_params() asm "0xa8cb00ad PUSHINT";
;; NFTEditable
int op::edit_content() asm "0x1a0b9d51 PUSHINT";
int op::transfer_editorship() asm "0x1c04412a PUSHINT";
int op::editorship_assigned() asm "0x511a4463 PUSHINT";
;; Collection
int op::new_nft() asm "0x1a039a51 PUSHINT";
int op::fill_up_balance() asm "0x2fa39a10 PUSHINT";

6
contracts/imports/params.fc

@ -0,0 +1,6 @@
int workchain() asm "0 PUSHINT";
() force_chain(slice addr) impure {
(int wc, _) = parse_std_addr(addr);
throw_unless(333, wc == workchain());
}

80
contracts/imports/root-dns.fc

@ -0,0 +1,80 @@
;; Root DNS resolver 2.0 in masterchain
;; Added support for ".t.me" domain zone (https://t.me/tonblockchain/167), in addition to ".ton" domain zone.
;; Added redirect from short "www.ton" to "foundation.ton" domain
;; compiled by FunC https://github.com/ton-blockchain/ton/tree/20758d6bdd0c1327091287e8a620f660d1a9f4da
(slice, slice, slice) load_data() inline {
slice ds = get_data().begin_parse();
return (
ds~load_msg_addr(), ;; address of ".ton" dns resolver smart contract in basechain
ds~load_msg_addr(), ;; address of ".t.me" dns resolver smart contract in basechain
ds~load_msg_addr() ;; address of "www.ton" dns resolver smart contract in basechain
);
}
(int, cell) dnsresolve(slice subdomain, int category) method_id {
throw_unless(70, mod(slice_bits(subdomain), 8) == 0);
int starts_with_zero_byte = subdomain.preload_int(8) == 0;
int subdomain_len = slice_bits(subdomain);
if (starts_with_zero_byte & (subdomain_len == 8)) { ;; "." requested
return (8, null()); ;; resolved but no dns-records
}
if (starts_with_zero_byte) {
subdomain~load_uint(8);
}
(slice ton_address, slice t_me_address, slice ton_www_address) = load_data();
slice ton_www_domain = begin_cell().store_slice("ton").store_uint(0, 8).store_slice("www").store_uint(0, 8).end_cell().begin_parse();
if (subdomain_len >= 8 * 8) {
if (equal_slices(subdomain.preload_bits(8 * 8), ton_www_domain)) {
cell result = begin_cell()
.store_uint(dns_next_resolver_prefix, 16)
.store_slice(ton_www_address)
.end_cell();
return (7 * 8 + (starts_with_zero_byte ? 8 : 0), result);
}
}
slice ton_domain = begin_cell().store_slice("ton").store_uint(0, 8).end_cell().begin_parse();
if (subdomain_len >= 4 * 8) {
if (equal_slices(subdomain.preload_bits(4 * 8), ton_domain)) {
cell result = begin_cell()
.store_uint(dns_next_resolver_prefix, 16)
.store_slice(ton_address)
.end_cell();
return (3 * 8 + (starts_with_zero_byte ? 8 : 0), result);
}
}
slice t_me_domain = begin_cell().store_slice("me").store_uint(0, 8).store_slice("t").store_uint(0, 8).end_cell().begin_parse();
if (subdomain_len >= 5 * 8) {
if (equal_slices(subdomain.preload_bits(5 * 8), t_me_domain)) {
cell result = begin_cell()
.store_uint(dns_next_resolver_prefix, 16)
.store_slice(t_me_address)
.end_cell();
return (4 * 8 + (starts_with_zero_byte ? 8 : 0), result);
}
}
return (0, null()); ;; domain cannot be resolved
}
() recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure {
}

215
contracts/imports/stdlib.fc

@ -0,0 +1,215 @@
;; Standard library for funC
;;
forall X -> tuple cons(X head, tuple tail) asm "CONS";
forall X -> (X, tuple) uncons(tuple list) asm "UNCONS";
forall X -> (tuple, X) list_next(tuple list) asm( -> 1 0) "UNCONS";
forall X -> X car(tuple list) asm "CAR";
tuple cdr(tuple list) asm "CDR";
tuple empty_tuple() asm "NIL";
forall X -> tuple tpush(tuple t, X value) asm "TPUSH";
forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH";
forall X -> [X] single(X x) asm "SINGLE";
forall X -> X unsingle([X] t) asm "UNSINGLE";
forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR";
forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR";
forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE";
forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE";
forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE";
forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE";
forall X -> X first(tuple t) asm "FIRST";
forall X -> X second(tuple t) asm "SECOND";
forall X -> X third(tuple t) asm "THIRD";
forall X -> X fourth(tuple t) asm "3 INDEX";
forall X, Y -> X pair_first([X, Y] p) asm "FIRST";
forall X, Y -> Y pair_second([X, Y] p) asm "SECOND";
forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST";
forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND";
forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD";
forall X -> X null() asm "PUSHNULL";
forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP";
int now() asm "NOW";
slice my_address() asm "MYADDR";
[int, cell] get_balance() asm "BALANCE";
int cur_lt() asm "LTIME";
int block_lt() asm "BLOCKLT";
int cell_hash(cell c) asm "HASHCU";
int slice_hash(slice s) asm "HASHSU";
int string_hash(slice s) asm "SHA256U";
int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU";
int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS";
(int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE";
(int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE";
(int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT";
(int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT";
;; () throw_if(int excno, int cond) impure asm "THROWARGIF";
() dump_stack() impure asm "DUMPSTK";
cell get_data() asm "c4 PUSH";
() set_data(cell c) impure asm "c4 POP";
cont get_c3() impure asm "c3 PUSH";
() set_c3(cont c) impure asm "c3 POP";
cont bless(slice s) impure asm "BLESS";
() accept_message() impure asm "ACCEPT";
() set_gas_limit(int limit) impure asm "SETGASLIMIT";
() commit() impure asm "COMMIT";
() buy_gas(int gram) impure asm "BUYGAS";
int min(int x, int y) asm "MIN";
int max(int x, int y) asm "MAX";
(int, int) minmax(int x, int y) asm "MINMAX";
int abs(int x) asm "ABS";
slice begin_parse(cell c) asm "CTOS";
() end_parse(slice s) impure asm "ENDS";
(slice, cell) load_ref(slice s) asm( -> 1 0) "LDREF";
cell preload_ref(slice s) asm "PLDREF";
;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX";
;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX";
;; int preload_int(slice s, int len) asm "PLDIX";
;; int preload_uint(slice s, int len) asm "PLDUX";
;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX";
;; slice preload_bits(slice s, int len) asm "PLDSLICEX";
(slice, int) load_grams(slice s) asm( -> 1 0) "LDGRAMS";
slice skip_bits(slice s, int len) asm "SDSKIPFIRST";
(slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST";
slice first_bits(slice s, int len) asm "SDCUTFIRST";
slice skip_last_bits(slice s, int len) asm "SDSKIPLAST";
(slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST";
slice slice_last(slice s, int len) asm "SDCUTLAST";
(slice, cell) load_dict(slice s) asm( -> 1 0) "LDDICT";
cell preload_dict(slice s) asm "PLDDICT";
slice skip_dict(slice s) asm "SKIPDICT";
(slice, cell) load_maybe_ref(slice s) asm( -> 1 0) "LDOPTREF";
cell preload_maybe_ref(slice s) asm "PLDOPTREF";
builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF";
int cell_depth(cell c) asm "CDEPTH";
int slice_refs(slice s) asm "SREFS";
int slice_bits(slice s) asm "SBITS";
(int, int) slice_bits_refs(slice s) asm "SBITREFS";
int slice_empty?(slice s) asm "SEMPTY";
int slice_data_empty?(slice s) asm "SDEMPTY";
int slice_refs_empty?(slice s) asm "SREMPTY";
int slice_depth(slice s) asm "SDEPTH";
int builder_refs(builder b) asm "BREFS";
int builder_bits(builder b) asm "BBITS";
int builder_depth(builder b) asm "BDEPTH";
builder begin_cell() asm "NEWC";
cell end_cell(builder b) asm "ENDC";
builder store_ref(builder b, cell c) asm(c b) "STREF";
;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX";
;; builder store_int(builder b, int x, int len) asm(x b len) "STIX";
builder store_slice(builder b, slice s) asm "STSLICER";
builder store_grams(builder b, int x) asm "STGRAMS";
builder store_dict(builder b, cell c) asm(c b) "STDICT";
(slice, slice) load_msg_addr(slice s) asm( -> 1 0) "LDMSGADDR";
tuple parse_addr(slice s) asm "PARSEMSGADDR";
(int, int) parse_std_addr(slice s) asm "REWRITESTDADDR";
(int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR";
cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF";
(cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF";
cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF";
(cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF";
cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF";
(cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF";
(cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF";
(cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF";
(cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF";
(cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL";
(cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL";
(slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT";
(slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT";
(cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT";
(cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT";
(cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT";
(cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT";
cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
(cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET";
cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET";
(cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET";
cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET";
(cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET";
(cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD";
(cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE";
(cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD";
(cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE";
cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB";
(cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB";
cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB";
(cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB";
cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB";
(cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB";
(cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB";
(cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB";
(cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB";
(cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB";
(cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2";
(cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2";
(cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2";
(cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2";
(cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2";
(cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2";
(cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2";
(cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2";
(cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2";
(cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2";
(cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2";
(cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2";
(int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2";
(int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2";
(int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2";
(int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2";
(int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2";
(int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2";
(int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2";
(int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2";
(int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2";
(int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2";
(int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2";
(int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2";
(int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2";
(int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2";
(int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2";
(int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2";
cell new_dict() asm "NEWDICT";
int dict_empty?(cell c) asm "DICTEMPTY";
(slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2";
(cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET";
(cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL";
cell config_param(int x) asm "CONFIGOPTPARAM";
int cell_null?(cell c) asm "ISNULL";
() raw_reserve(int amount, int mode) impure asm "RAWRESERVE";
() raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX";
() send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG";
() set_code(cell new_code) impure asm "SETCODE";
int random() impure asm "RANDU256";
int rand(int range) impure asm "RAND";
int get_seed() impure asm "RANDSEED";
int set_seed() impure asm "SETRAND";
() randomize(int x) impure asm "ADDRAND";
() randomize_lt() impure asm "LTIME" "ADDRAND";
builder store_coins(builder b, int x) asm "STVARUINT16";
(slice, int) load_coins(slice s) asm( -> 1 0) "LDVARUINT16";
int equal_slices (slice a, slice b) asm "SDEQ";
int builder_null?(builder b) asm "ISNULL";
builder store_builder(builder to, builder from) asm "STBR";

9
contracts/imports/utils.fc

@ -0,0 +1,9 @@
() send_grams(slice address, int amount) impure {
cell msg = begin_cell()
.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
.end_cell();
send_raw_message(msg, 3); ;; mode, 2 for ignoring errors, 1 for sender pays fees, 64 for returning inbound message value
}

155
contracts/main.fc

@ -0,0 +1,155 @@
#pragma version >=0.2.0;
#include "imports/stdlib.fc";
#include "imports/constants.fc";
#include "imports/utils.fc";
#include "imports/dns-utils.fc";
#include "imports/op-codes.fc";
#include "imports/params.fc";
int min_tons_for_storage() asm "1000000000 PUSHINT"; ;; 1 TON
;; =============== storage =============================
;;
;; Storage
;;
;; content:^Cell
;; nft_item_code:^Cell
;; uint256 index
;; MsgAddressInt collection_address
;; MsgAddressInt owner_address
;; cell domain - e.g contains "alice" (without ending \0) for "alice.ton" domain
;; cell auction - auction info
;; int last_fill_up_time
(cell, cell, int, int, slice, slice, cell, cell, int) load_data() {
slice ds = get_data().begin_parse();
cell content = ds~load_ref(); ;; content
cell code = ds~load_ref(); ;; code
var (index, collection_address) = (ds~load_uint(256), ds~load_msg_addr());
if (ds.slice_bits() > 0) {
return (content, code, -1, index, collection_address, ds~load_msg_addr(), ds~load_ref(), ds~load_dict(), ds~load_uint(64));
} else {
return (content, code, 0, index, collection_address, null(), null(), null(), 0); ;; nft not initialized yet
}
}
() store_data(cell collection_content, cell nft_item_code, int index, slice collection_address, slice owner_address, cell domain, cell auction, int last_fill_up_time) impure {
set_data(
begin_cell()
.store_ref(collection_content)
.store_ref(nft_item_code)
.store_uint(index, 256)
.store_slice(collection_address)
.store_slice(owner_address)
.store_ref(domain)
.store_dict(auction)
.store_uint(last_fill_up_time, 64)
.end_cell()
);
}
() send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline {
var msg = begin_cell()
.store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(op, 32)
.store_uint(query_id, 64);
if (~ builder_null?(payload)) {
msg = msg.store_builder(payload);
}
send_raw_message(msg.end_cell(), send_mode);
}
() transfer_ownership(int my_balance, cell collection_content, cell nft_item_code, int index, slice collection_address, slice owner_address, slice sender_address, int query_id, slice in_msg_body, int fwd_fees, cell domain, cell auction) impure inline {
slice new_owner_address = in_msg_body~load_msg_addr();
force_chain(new_owner_address);
slice response_destination = in_msg_body~load_msg_addr();
in_msg_body~load_int(1); ;; this nft don't use custom_payload
int forward_amount = in_msg_body~load_coins();
int rest_amount = my_balance - min_tons_for_storage();
if (forward_amount) {
rest_amount -= (forward_amount + fwd_fees);
}
int need_response = response_destination.preload_uint(2) != 0; ;; if NOT addr_none: 00
if (need_response) {
rest_amount -= fwd_fees;
}
throw_unless(402, rest_amount >= 0); ;; base nft spends fixed amount of gas, will not check for response
if (forward_amount) {
send_msg(new_owner_address, forward_amount, op::ownership_assigned(), query_id, begin_cell().store_slice(owner_address).store_slice(in_msg_body), 1); ;; paying fees, revert on errors
}
if (need_response) {
force_chain(response_destination);
send_msg(response_destination, rest_amount, op::excesses(), query_id, null(), 1); ;; paying fees, revert on errors
}
store_data(collection_content, nft_item_code, index, collection_address, new_owner_address, domain, auction, now());
}
() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {
}
;;
;; GET Methods
;;
(int, int, slice, slice, cell) get_nft_data() method_id {
(cell content, cell item_code, int init?, int index, slice collection_address, slice owner_address, cell domain, cell auction, int last_fill_up_time) = load_data();
return (init?, index, collection_address, owner_address, content);
}
slice get_editor() method_id {
(cell coll_content, cell item_code, int init?, int index, slice collection_address, slice owner_address, cell domain, cell auction, int last_fill_up_time) = load_data();
return owner_address;
}
slice get_domain() method_id {
(cell coll_content, cell item_code, int init?, int index, slice collection_address, slice owner_address, cell domain, cell auction, int last_fill_up_time) = load_data();
return domain.begin_parse();
}
;; (slice, int, int) get_auction_info() method_id {
;; (cell coll_content, cell item_code, int init?, int index, slice collection_address, slice owner_address, cell content, cell domain, cell auction, int last_fill_up_time) = load_data();
;; return unpack_auction(auction);
;; }
int get_last_fill_up_time() method_id {
(cell coll_content, cell item_code, int init?, int index, slice collection_address, slice owner_address, cell domain, cell auction, int last_fill_up_time) = load_data();
return last_fill_up_time;
}
(int, cell) dnsresolve(slice subdomain, int category) method_id {
int subdomain_bits = slice_bits(subdomain);
throw_unless(70, mod(subdomain_bits, 8) == 0);
(cell content, cell item_code, int init?, int index, slice collection_address, slice owner_address, cell my_domain_cell, cell auction, int last_fill_up_time) = load_data();
slice cs = content.begin_parse();
throw_unless(412, cs~load_uint(8) == 0); ;; data onchain tag
cell keyvalue_map = cs~load_dict();
int starts_with_zero_byte = subdomain.preload_int(8) == 0;
throw_unless(413, starts_with_zero_byte);
if (subdomain_bits > 8) { ;; more than "." requested
category = "dns_next_resolver"H;
}
if (category == 0) { ;; all categories are requested
return (8, keyvalue_map);
}
(cell value, int found) = keyvalue_map.udict_get_ref?(256, category);
return (8, value);
}

32
contracts/main.ts

@ -0,0 +1,32 @@
import BN from "bn.js";
import { Cell, beginCell, Address } from "ton";
import {encodeOffChainContent, makeSnakeCell} from "./utils";
// encode contract storage according to save_data() contract method
// collection_content:^Cell
// nft_item_code:^Cell
// uint256 index
// MsgAddressInt collection_address
// MsgAddressInt owner_address
// cell domain - e.g contains "alice" (without ending \0) for "alice.ton" domain
// cell auction - auction info
// int last_fill_up_time
export function data(params: { ownerAddress: Address; collectionAddress: Address, code: Cell, domain: String }): Cell {
return beginCell()
.storeRef(encodeOffChainContent("https://agorata.io/collection.json")) // https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md
// For code: https://github.com/getgems-io/nft-contracts/blob/main/packages/contracts/sources/nft-auction/build.sh
.storeRef(params.code)
.storeUint8(0)
.storeAddress(params.collectionAddress)
.storeAddress(params.ownerAddress)
.storeRef(makeSnakeCell(Buffer.from(params.domain)))
.storeRef(beginCell().endCell())
.storeUint(0, 64).endCell();
}
// message encoders for all ops (see contracts/imports/constants.fc for consts)
export function transferOwnership(params: { newOwnerAddress: Address }): Cell {
return beginCell().storeUint(0x2da38aaf, 32).storeUint(0, 64).storeAddress(params.newOwnerAddress).endCell();
}

142
contracts/nft-collection.fc

@ -0,0 +1,142 @@
;; DNS resolver smart contract (implements NFT Collection interface)
#include "imports/dns-utils.fc";
#include "imports/op-codes.fc";
#include "imports/params.fc";
;; storage scheme
;; storage#_ collection_content:^Cell
;; nft_item_code:^Cell
;; = Storage;
(cell, cell) load_data() inline {
var ds = get_data().begin_parse();
return (
ds~load_ref(), ;; content
ds~load_ref() ;; nft_item_code
);
}
() save_data(cell content, cell nft_item_code) impure inline {
set_data(begin_cell()
.store_ref(content)
.store_ref(nft_item_code)
.end_cell());
}
cell calculate_nft_item_state_init(int item_index, cell nft_item_code) {
cell data = begin_cell().store_uint(item_index, 256).store_slice(my_address()).end_cell();
return begin_cell().store_uint(0, 2).store_dict(nft_item_code).store_dict(data).store_uint(0, 1).end_cell();
}
slice calculate_nft_item_address(int wc, cell state_init) {
return begin_cell()
.store_uint(4, 3)
.store_int(wc, 8)
.store_uint(cell_hash(state_init), 256)
.end_cell()
.begin_parse();
}
() deploy_nft_item(int item_index, cell nft_item_code, cell nft_content) impure {
cell state_init = calculate_nft_item_state_init(item_index, nft_item_code);
slice nft_address = calculate_nft_item_address(workchain(), state_init);
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(nft_address)
.store_coins(0)
.store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1)
.store_ref(state_init)
.store_ref(nft_content);
send_raw_message(msg.end_cell(), 64); ;; carry all the remaining value of the inbound message, fee deducted from amount
}
() recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure {
if (in_msg_body.slice_empty?()) { ;; bounce back empty messages
throw(0xffff);
}
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
if (flags & 1) { ;; ignore all bounced messages
return ();
}
slice sender_address = cs~load_msg_addr();
int op = in_msg_body~load_uint(32);
var (content, nft_item_code) = load_data();
if (op == 0) { ;; deploy new nft
int now_time = now();
throw_unless(199, now_time > auction_start_time); ;; start of auction
slice domain = read_domain_from_comment(in_msg_body);
int len = slice_bits(domain);
throw_unless(200, len > 3 * 8); ;; minimum 4 characters
throw_unless(201, len <= 126 * 8); ;; maxmimum 126 characters
throw_unless(202, mod(len, 8) == 0);
throw_unless(203, check_domain_string(domain));
int min_price = get_min_price(len, now_time);
throw_unless(204, msg_value >= min_price);
int item_index = slice_hash(domain);
cell config_cell = config_param(dns_config_id);
if (~ cell_null?(config_cell)) {
slice config_cs = config_cell.begin_parse();
cell config = config_cs~load_dict();
(slice config_value, int found) = config.udict_get?(256, item_index);
throw_if(205, found);
}
cell nft_content = begin_cell()
.store_slice(sender_address)
.store_ref(begin_cell().store_slice(domain).end_cell())
.end_cell();
deploy_nft_item(item_index, nft_item_code, nft_content);
return ();
}
if (op == op::fill_up) { ;; just fill-up balance
return ();
}
throw(0xffff);
}
;; Get methods
(int, cell, slice) get_collection_data() method_id {
var (content, nft_item_code) = load_data();
return (-1, content, zero_address());
}
slice get_nft_address_by_index(int index) method_id {
var (content, nft_item_code) = load_data();
cell state_init = calculate_nft_item_state_init(index, nft_item_code);
return calculate_nft_item_address(workchain(), state_init);
}
cell get_nft_content(int index, cell individual_nft_content) method_id {
return individual_nft_content;
}
(int, cell) dnsresolve(slice subdomain, int category) method_id {
throw_unless(70, mod(slice_bits(subdomain), 8) == 0);
int starts_with_zero_byte = subdomain.preload_int(8) == 0;
if (starts_with_zero_byte & (slice_bits(subdomain) == 8)) { ;; "." requested
return (8, null()); ;; resolved but no dns-records
}
if (starts_with_zero_byte) {
subdomain~load_uint(8);
}
int top_subdomain_bits = get_top_domain_bits(subdomain);
slice top_subdomain = subdomain~load_bits(top_subdomain_bits);
int item_index = slice_hash(top_subdomain);
cell result = begin_cell()
.store_uint(dns_next_resolver_prefix, 16)
.store_slice(get_nft_address_by_index(item_index))
.end_cell();
return (top_subdomain_bits + (starts_with_zero_byte ? 8 : 0), result);
}

322
contracts/nft-item.fc

@ -0,0 +1,322 @@
;; Domain smart contract (implement NFT item interface)
#include "imports/dns-utils.fc";
#include "imports/op-codes.fc";
#include "imports/params.fc";
int min_tons_for_storage() asm "1000000000 PUSHINT"; ;; 1 TON
const auction_start_duration = 604800; ;; 1 week = 60 * 60 * 24 * 7; in testnet 5 min
const auction_end_duration = 3600; ;; 1 hour = 60 * 60; in testnet 1 min
const auction_prolongation = 3600; ;; 1 hour = 60 * 60; in testnet 1 min
;; MsgAddressInt max_bid_address
;; Coins max_bid_amount
;; int auction_end_time
(slice, int, int) unpack_auction(cell auction) {
if (cell_null?(auction)) {
return (null(), 0, 0);
} else {
slice ds = auction.begin_parse();
return (ds~load_msg_addr(), ds~load_coins(), ds~load_uint(64));
}
}
cell pack_auction(slice max_bid_address, int max_bid_amount, int auction_end_time) {
return begin_cell()
.store_slice(max_bid_address)
.store_coins(max_bid_amount)
.store_uint(auction_end_time, 64)
.end_cell();
}
;;
;; Storage
;;
;; uint256 index
;; MsgAddressInt collection_address
;; MsgAddressInt owner_address
;; cell content
;; cell domain - e.g contains "alice" (without ending \0) for "alice.ton" domain
;; cell auction - auction info
;; int last_fill_up_time
(int, int, slice, slice, cell, cell, cell, int) load_data() {
slice ds = get_data().begin_parse();
var (index, collection_address) = (ds~load_uint(256), ds~load_msg_addr());
if (ds.slice_bits() > 0) {
return (-1, index, collection_address, ds~load_msg_addr(), ds~load_ref(), ds~load_ref(), ds~load_dict(), ds~load_uint(64));
} else {
return (0, index, collection_address, null(), null(), null(), null(), 0); ;; nft not initialized yet
}
}
() store_data(int index, slice collection_address, slice owner_address, cell content, cell domain, cell auction, int last_fill_up_time) impure {
set_data(
begin_cell()
.store_uint(index, 256)
.store_slice(collection_address)
.store_slice(owner_address)
.store_ref(content)
.store_ref(domain)
.store_dict(auction)
.store_uint(last_fill_up_time, 64)
.end_cell()
);
}
() send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline {
var msg = begin_cell()
.store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(op, 32)
.store_uint(query_id, 64);
if (~ builder_null?(payload)) {
msg = msg.store_builder(payload);
}
send_raw_message(msg.end_cell(), send_mode);
}
() transfer_ownership(int my_balance, int index, slice collection_address, slice owner_address, cell content, slice sender_address, int query_id, slice in_msg_body, int fwd_fees, cell domain, cell auction) impure inline {
slice new_owner_address = in_msg_body~load_msg_addr();
force_chain(new_owner_address);
slice response_destination = in_msg_body~load_msg_addr();
in_msg_body~load_int(1); ;; this nft don't use custom_payload
int forward_amount = in_msg_body~load_coins();
int rest_amount = my_balance - min_tons_for_storage();
if (forward_amount) {
rest_amount -= (forward_amount + fwd_fees);
}
int need_response = response_destination.preload_uint(2) != 0; ;; if NOT addr_none: 00
if (need_response) {
rest_amount -= fwd_fees;
}
throw_unless(402, rest_amount >= 0); ;; base nft spends fixed amount of gas, will not check for response
if (forward_amount) {
send_msg(new_owner_address, forward_amount, op::ownership_assigned(), query_id, begin_cell().store_slice(owner_address).store_slice(in_msg_body), 1); ;; paying fees, revert on errors
}
if (need_response) {
force_chain(response_destination);
send_msg(response_destination, rest_amount, op::excesses(), query_id, null(), 1); ;; paying fees, revert on errors
}
store_data(index, collection_address, new_owner_address, content, domain, auction, now());
}
() recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure {
int my_balance = pair_first(get_balance());
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
if (flags & 1) { ;; ignore all bounced messages
return ();
}
slice sender_address = cs~load_msg_addr();
cs~load_msg_addr(); ;; skip dst
cs~load_coins(); ;; skip value
cs~skip_bits(1); ;; skip extracurrency collection
cs~load_coins(); ;; skip ihr_fee
int fwd_fee = cs~load_coins(); ;; we use message fwd_fee for estimation of forward_payload costs
(int init?, int index, slice collection_address, slice owner_address, cell content, cell domain, cell auction, int last_fill_up_time) = load_data();
if (~ init?) {
throw_unless(405, equal_slices(collection_address, sender_address));
slice from_address = in_msg_body~load_msg_addr();
cell domain = in_msg_body~load_ref();
cell content = begin_cell().store_uint(0, 8).store_dict(new_dict()).end_cell();
int seconds = now() - auction_start_time;
int months = seconds / one_month;
if (months > 12) {
months = 12;
}
int duration = auction_start_duration - (auction_start_duration - auction_end_duration) * months / 12;
int auction_end_time = now() + duration;
store_data(index, collection_address, zero_address(), content, domain, pack_auction(from_address, msg_value, auction_end_time), now());
return ();
}
if (init? & equal_slices(collection_address, sender_address)) {
slice from_address = in_msg_body~load_msg_addr();
send_msg(from_address, 0, 0, cur_lt(), null(), 64); ;; carry all the remaining value of the inbound message
return ();
}
int op = in_msg_body.slice_empty?() ? 0 : in_msg_body~load_uint(32);
(slice max_bid_address, int max_bid_amount, int auction_end_time) = unpack_auction(auction);
int auction_complete = now() > auction_end_time;
if (op == 0) {
if (auction_complete) {
throw_unless(406, equal_slices(sender_address, owner_address)); ;; only owner can fill-up balance, prevent coins lost right after the auction
;; if owner send bid right after auction he can restore it by transfer resonse message
store_data(index, collection_address, owner_address, content, domain, auction, now());
} else {
throw_unless(407, msg_value >= muldiv(max_bid_amount, 105, 100)); ;; 5% greater then previous bid
int amount_to_send = (max_bid_amount > my_balance - min_tons_for_storage()) ? (my_balance - min_tons_for_storage()) : max_bid_amount;
if (amount_to_send > 0) {
send_msg(max_bid_address, amount_to_send, op::outbid_notification, cur_lt(), null(), 1); ;; pay transfer fees separately
}
max_bid_amount = msg_value;
max_bid_address = sender_address;
int delta_time = auction_prolongation - (auction_end_time - now());
if (delta_time > 0) {
auction_end_time += delta_time;
}
store_data(index, collection_address, owner_address, content, domain, pack_auction(max_bid_address, max_bid_amount, auction_end_time), now());
}
return ();
}
int query_id = in_msg_body~load_uint(64);
if ((auction_complete) & (~ cell_null?(auction))) { ;; take domain after auction
int balance_without_msg = my_balance - msg_value;
int amount_to_send = (max_bid_amount > balance_without_msg - min_tons_for_storage()) ? (balance_without_msg - min_tons_for_storage()) : max_bid_amount;
if (amount_to_send > 0) {
send_msg(collection_address, amount_to_send, op::fill_up, query_id, null(), 2); ;; ignore errors
my_balance -= amount_to_send;
}
owner_address = max_bid_address;
auction = null();
store_data(index, collection_address, owner_address, content, domain, auction, last_fill_up_time);
}
if (op == op::transfer()) {
throw_unless(401, equal_slices(sender_address, owner_address));
transfer_ownership(my_balance, index, collection_address, owner_address, content, sender_address, query_id, in_msg_body, fwd_fee, domain, auction);
return ();
}
if (op == op::edit_content()) { ;; owner can change content and dns records
throw_unless(410, equal_slices(sender_address, owner_address));
store_data(index, collection_address, owner_address, in_msg_body~load_ref(), domain, auction, now());
return ();
}
if (op == op::change_dns_record) { ;; change dns record
throw_unless(411, equal_slices(sender_address, owner_address));
int key = in_msg_body~load_uint(256);
int has_value = in_msg_body.slice_refs() > 0;
slice cs = content.begin_parse();
throw_unless(412, cs~load_uint(8) == 0); ;; data onchain tag
cell keyvalue_map = cs~load_dict();
if (has_value) {
cell value = in_msg_body~load_ref();
keyvalue_map~udict_set_ref(256, key, value);
} else {
keyvalue_map~udict_delete?(256, key);
}
content = begin_cell().store_uint(0, 8).store_dict(keyvalue_map).end_cell();
store_data(index, collection_address, owner_address, content, domain, auction, now());
return ();
}
if (op == op::process_governance_decision) { ;; governance
throw_unless(413, cell_null?(auction));
slice cs = config_param(dns_config_id).begin_parse();
cell config = cs~load_dict();
(slice config_value, int found) = config.udict_get?(256, index);
throw_unless(415, found);
int config_op = config_value~load_uint(8);
throw_unless(416, (config_op == 0) | (config_op == 1));
if (config_op == 0) { ;; transfer
transfer_ownership(my_balance, index, collection_address, owner_address, content, sender_address, query_id, config_value, fwd_fee, domain, auction);
}
if (config_op == 1) { ;; destroy
send_msg(collection_address, 0, op::fill_up, query_id, null(), 128 + 32); ;; carry all the remaining balance + destroy
}
return ();
}
if (op == op::dns_balance_release) { ;; release domain
throw_unless(414, (now() - last_fill_up_time > one_year) & (cell_null?(auction)));
int min_price = get_min_price(domain.begin_parse().slice_bits(), now());
throw_unless(407, msg_value >= min_price);
int balance_without_msg = my_balance - msg_value;
int amount_to_send = balance_without_msg - min_tons_for_storage();
if (amount_to_send > 0) {
send_msg(owner_address, amount_to_send, op::dns_balance_release, query_id, null(), 2); ;; ignore errors
}
max_bid_amount = msg_value;
max_bid_address = sender_address;
auction_end_time = now() + auction_start_duration; ;; always 1 week
owner_address = zero_address();
auction = pack_auction(max_bid_address, max_bid_amount, auction_end_time);
store_data(index, collection_address, owner_address, content, domain, auction, now());
return ();
}
if (op == op::get_static_data()) {
send_msg(sender_address, 0, op::report_static_data(), query_id, begin_cell().store_uint(index, 256).store_slice(collection_address), 64); ;; carry all the remaining value of the inbound message
return ();
}
throw(0xffff);
}
;;
;; GET Methods
;;
(int, int, slice, slice, cell) get_nft_data() method_id {
(int init?, int index, slice collection_address, slice owner_address, cell content, cell domain, cell auction, int last_fill_up_time) = load_data();
return (init?, index, collection_address, owner_address, content);
}
slice get_editor() method_id {
(int init?, int index, slice collection_address, slice owner_address, cell content, cell domain, cell auction, int last_fill_up_time) = load_data();
return owner_address;
}
slice get_domain() method_id {
(int init?, int index, slice collection_address, slice owner_address, cell content, cell domain, cell auction, int last_fill_up_time) = load_data();
return domain.begin_parse();
}
(slice, int, int) get_auction_info() method_id {
(int init?, int index, slice collection_address, slice owner_address, cell content, cell domain, cell auction, int last_fill_up_time) = load_data();
return unpack_auction(auction);
}
int get_last_fill_up_time() method_id {
(int init?, int index, slice collection_address, slice owner_address, cell content, cell domain, cell auction, int last_fill_up_time) = load_data();
return last_fill_up_time;
}
(int, cell) dnsresolve(slice subdomain, int category) method_id {
int subdomain_bits = slice_bits(subdomain);
throw_unless(70, mod(subdomain_bits, 8) == 0);
(int init?, int index, slice collection_address, slice owner_address, cell content, cell my_domain_cell, cell auction, int last_fill_up_time) = load_data();
slice cs = content.begin_parse();
throw_unless(412, cs~load_uint(8) == 0); ;; data onchain tag
cell keyvalue_map = cs~load_dict();
int starts_with_zero_byte = subdomain.preload_int(8) == 0;
throw_unless(413, starts_with_zero_byte);
if (subdomain_bits > 8) { ;; more than "." requested
category = "dns_next_resolver"H;
}
if (category == 0) { ;; all categories are requested
return (8, keyvalue_map);
}
(cell value, int found) = keyvalue_map.udict_get_ref?(256, category);
return (8, value);
}

66
contracts/utils.ts

@ -0,0 +1,66 @@
import {Cell} from "ton";
// from https://github.com/getgems-io/nft-contracts/blob/main/packages/nft-content/nftContent.ts
const OFF_CHAIN_CONTENT_PREFIX = 0x01
export function flattenSnakeCell(cell: Cell) {
let c: Cell|null = cell
let res = Buffer.alloc(0)
while (c) {
let cs = c.beginParse()
let data = cs.readRemainingBytes()
res = Buffer.concat([res, data])
c = c.refs[0]
}
return res
}
function bufferToChunks(buff: Buffer, chunkSize: number) {
let chunks: Buffer[] = []
while (buff.byteLength > 0) {
chunks.push(buff.slice(0, chunkSize))
buff = buff.slice(chunkSize)
}
return chunks
}
export function makeSnakeCell(data: Buffer) {
let chunks = bufferToChunks(data, 127)
let rootCell = new Cell()
let curCell = rootCell
for (let i = 0; i < chunks.length; i++) {
let chunk = chunks[i]
curCell.bits.writeBuffer(chunk)
if (chunks[i+1]) {
let nextCell = new Cell()
curCell.refs.push(nextCell)
curCell = nextCell
}
}
return rootCell
}
export function encodeOffChainContent(content: string) {
let data = Buffer.from(content)
let offChainPrefix = Buffer.from([OFF_CHAIN_CONTENT_PREFIX])
data = Buffer.concat([offChainPrefix, data])
return makeSnakeCell(data)
}
export function decodeOffChainContent(content: Cell) {
let data = flattenSnakeCell(content)
let prefix = data[0]
if (prefix !== OFF_CHAIN_CONTENT_PREFIX) {
throw new Error(`Unknown content prefix: ${prefix.toString(16)}`)
}
return data.slice(1).toString()
}

3448
package-lock.json generated

File diff suppressed because it is too large Load Diff

51
package.json

@ -0,0 +1,51 @@
{
"name": "agorata-contracts",
"description": "",
"version": "0.0.0",
"license": "MIT",
"author": "",
"scripts": {
"prettier": "npx prettier --write '{test,contracts,build}/**/*.{ts,js,json}'",
"test": "node --no-experimental-fetch node_modules/mocha/bin/mocha --exit test/**/*.spec.ts",
"build": "ts-node ./build/_build.ts",
"deploy": "ts-node ./build/_deploy.ts",
"deploy:testnet": "ts-node ./build/_deploy.ts",
"postinstall": "ts-node ./build/_setup.ts"
},
"devDependencies": {
"@swc/core": "^1.2.177",
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"@types/semver": "^7.3.9",
"axios-request-throttle": "^1.0.0",
"chai": "^4.3.4",
"chai-bn": "^0.3.1",
"dotenv": "^16.0.0",
"fast-glob": "^3.2.11",
"mocha": "^9.1.3",
"prando": "^6.0.1",
"prettier": "^2.6.2",
"ton": "^12.1.3",
"ton-contract-executor": "^0.4.8",
"ton-crypto": "^3.1.0",
"ts-node": "^10.4.0",
"typescript": "^4.5.4"
},
"prettier": {
"printWidth": 180
},
"mocha": {
"require": [
"chai",
"ts-node/register"
],
"timeout": 20000
},
"engines": {
"node": ">=16.15.0"
},
"dependencies": {
"semver": "^7.3.7"
}
}

63
test/helpers.ts

@ -0,0 +1,63 @@
import BN from "bn.js";
import { Address, Cell, CellMessage, InternalMessage, CommonMessageInfo, WalletContract, SendMode, Wallet } from "ton";
import { SmartContract } from "ton-contract-executor";
import Prando from "prando";
export const zeroAddress = new Address(0, Buffer.alloc(32, 0));
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 ?? 0, hash);
}
// used with ton-contract-executor (unit tests) to sendInternalMessage easily
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("sender"),
to: params.to ?? zeroAddress,
value: params.value ?? 0,
bounce: params.bounce ?? true,
body: new CommonMessageInfo({ body: message }),
});
}
// temp fix until ton-contract-executor (unit tests) remembers c7 value between calls
export function setBalance(contract: SmartContract, balance: BN) {
contract.setC7Config({
balance: balance.toNumber(),
});
}
// helper for end-to-end on-chain tests (normally post deploy) to allow sending InternalMessages to contracts using a wallet
export async function sendInternalMessageWithWallet(params: { walletContract: WalletContract; secretKey: Buffer; to: Address; value: BN; bounce?: boolean; body?: Cell }) {
const message = params.body ? new CellMessage(params.body) : undefined;
const seqno = await params.walletContract.getSeqNo();
const transfer = params.walletContract.createTransfer({
secretKey: params.secretKey,
seqno: seqno,
sendMode: SendMode.PAY_GAS_SEPARATLY + SendMode.IGNORE_ERRORS,
order: new InternalMessage({
to: params.to,
value: params.value,
bounce: params.bounce ?? false,
body: new CommonMessageInfo({
body: message,
}),
}),
});
await params.walletContract.client.sendExternalMessage(params.walletContract, transfer);
for (let attempt = 0; attempt < 10; attempt++) {
await sleep(2000);
const seqnoAfter = await params.walletContract.getSeqNo();
if (seqnoAfter > seqno) return;
}
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

56
test/ownership.spec.ts

@ -0,0 +1,56 @@
import chai, { expect } from "chai";
import chaiBN from "chai-bn";
import BN from "bn.js";
chai.use(chaiBN(BN));
import { Cell, Slice } from "ton";
import { SmartContract } from "ton-contract-executor";
import * as main from "../contracts/main";
import { internalMessage, randomAddress } from "./helpers";
import { hex } from "../build/main.compiled.json";
describe("Transfer ownership tests", () => {
let contract: SmartContract;
beforeEach(async () => {
contract = await SmartContract.fromCell(
Cell.fromBoc(hex)[0], // code cell from build output
main.data({
ownerAddress: randomAddress("owner"),
code: Cell.fromBoc(hex)[0],
collectionAddress: randomAddress("collection"),
domain: "alice",
})
);
});
// it("should allow the owner to change owners", async () => {
// const send = await contract.sendInternalMessage(
// internalMessage({
// from: randomAddress("owner"),
// body: main.transferOwnership({ newOwnerAddress: randomAddress("newowner") }),
// })
// );
// expect(send.type).to.equal("success");
//
// const call = await contract.invokeGetMethod("owner_address", []);
// const address = (call.result[0] as Slice).readAddress();
// expect(address?.equals(randomAddress("newowner"))).to.equal(true);
// });
//
// it("should prevent others from changing owners", async () => {
// const send = await contract.sendInternalMessage(
// internalMessage({
// from: randomAddress("notowner"),
// body: main.transferOwnership({ newOwnerAddress: randomAddress("newowner") }),
// })
// );
// expect(send.type).to.equal("failed");
// expect(send.exit_code).to.equal(102); // access_denied in contracts/imports/constants.fc
//
// const call = await contract.invokeGetMethod("owner_address", []);
// const address = (call.result[0] as Slice).readAddress();
// expect(address?.equals(randomAddress("owner"))).to.equal(true);
// });
});

15
tsconfig.json

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"ts-node": {
"transpileOnly": true,
"transpiler": "ts-node/transpilers/swc"
}
}
Loading…
Cancel
Save