├── deploy ├── deploy.md ├── export-hex.ts ├── upgrader.ts ├── deploy.ts ├── e2e.ts ├── deploy-utils.ts └── collect.ts ├── utils ├── print-hex.fif ├── funcToB64.ts └── deploy-utils.ts ├── .prettierrc ├── babel.config.js ├── test ├── imports │ ├── params.fc │ ├── op-codes.fc │ ├── jetton-utils.fc │ ├── store.fc │ └── stdlib.fc ├── jetton-minter.fc ├── jetton-wallet.fc └── bus.spec.ts ├── contracts ├── imports │ ├── params.fc │ ├── op-codes-amm.fc │ ├── op-codes.fc │ ├── jetton-utils.fc │ ├── store.fc │ ├── amm-minter-utils.fc │ └── stdlib.fc ├── amm-minter.tlb ├── amm-wallet.fc └── amm-minter.fc ├── .gitignore ├── src ├── ops.ts ├── amm-utils.ts ├── wallet.ts ├── utils.ts ├── jetton-wallet.ts ├── amm-wallet.ts ├── jetton-minter.ts └── amm-minter.ts ├── package.json ├── LICENSE ├── Readme.md └── tsconfig.json /deploy/deploy.md: -------------------------------------------------------------------------------- 1 | # Deploy -------------------------------------------------------------------------------- /utils/print-hex.fif: -------------------------------------------------------------------------------- 1 | 2 | "../build/tmp.fif" include 3 | 2 boc+>B dup Bx. -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "bracketSpacing": true, 5 | "printWidth": 140, 6 | "singleQuote": false, 7 | "semi": true 8 | } 9 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript' 5 | ] 6 | }; -------------------------------------------------------------------------------- /test/imports/params.fc: -------------------------------------------------------------------------------- 1 | int workchain() asm "0 PUSHINT"; 2 | 3 | () force_chain(slice addr) impure { 4 | (int wc, _) = parse_std_addr(addr); 5 | throw_unless(333, wc == workchain()); 6 | } -------------------------------------------------------------------------------- /contracts/imports/params.fc: -------------------------------------------------------------------------------- 1 | int workchain() asm "0 PUSHINT"; 2 | 3 | () force_chain(slice addr) impure { 4 | (int wc, _) = parse_std_addr(addr); 5 | throw_unless(333, wc == workchain()); 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | *.boc 4 | *.pk 5 | *.addr 6 | .vscode 7 | func 8 | fift 9 | lite-client 10 | 11 | launch 12 | conifg 13 | .idea 14 | deployed-html 15 | amm-minter-w.fc 16 | 17 | deployed.conifg.json -------------------------------------------------------------------------------- /contracts/imports/op-codes-amm.fc: -------------------------------------------------------------------------------- 1 | ;; s TODO OPPS should be more unique or future on chain indexing 2 | const OP_ADD_LIQUIDITY = 22; 3 | const OP_REMOVE_LIOUIDTY = 23; 4 | const OP_SWAP_TOKEN = 24; 5 | const OP_SWAP_TON = 25; 6 | const OP_CODE_UPGRADE = 26; 7 | 8 | -------------------------------------------------------------------------------- /contracts/amm-minter.tlb: -------------------------------------------------------------------------------- 1 | transfer query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress 2 | response_destination:MsgAddress custom_payload:(Maybe ^Cell) 3 | forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) 4 | = InternalMsgBody; 5 | 6 | 7 | internal_transfer query_id:uint64 amount:(VarUInteger 16) from:MsgAddress 8 | response_address:MsgAddress 9 | forward_ton_amount:(VarUInteger 16) 10 | forward_payload:(Either Cell ^Cell) 11 | = InternalMsgBody; -------------------------------------------------------------------------------- /deploy/export-hex.ts: -------------------------------------------------------------------------------- 1 | import { compileFuncToB64 } from "../utils/funcToB64"; 2 | import { writeFileSync } from "fs"; 3 | import { execSync } from "child_process"; 4 | 5 | function main() { 6 | let getCommitHash = execSync(`git log --format="%H" -n 1`).toString().trim(); 7 | const ammMinterCodeB64: string = compileFuncToB64(["contracts/amm-minter.fc"]); 8 | writeFileSync(`./build/amm-minter.build.json`, `{ "hex":"${ammMinterCodeB64}", "commitHash":"${getCommitHash}" }`); 9 | const ammWalletCode: string = compileFuncToB64(["contracts/amm-wallet.fc"]); 10 | writeFileSync(`./build/amm-wallet.build.json`, `{ "hex":"${ammWalletCode}", "commitHash":"${getCommitHash}" }`); 11 | } 12 | 13 | main(); -------------------------------------------------------------------------------- /test/imports/op-codes.fc: -------------------------------------------------------------------------------- 1 | const OP_TRANSFER = 0xf8a7ea5; 2 | const OP_TRANSFER_NOTIFICATION = 0x7362d09c; 3 | const OP_INTERNAL_TRANSFER = 0x178d4519; 4 | const OP_EXCESSES = 0xd53276db; ;;excesses 5 | const OP_BURN = 0x595f07bc; 6 | const OP_BURN_NOTIFICAITON = 0x7bdd97de; 7 | 8 | ;; Minter 9 | const OP_MINT = 21; 10 | 11 | const ERROR::WRONG_JETTON_SENDER_ADDRESS = 74; 12 | const ERROR::INSFUCIENT_FUNDS = 600; 13 | const ERROR::ZERO_RESERVES = 601; 14 | const ERROR::NULL_JETTON_WALLET = 602; 15 | const ERROR::MSG_VALUE_INSUFFICIENT = 603; 16 | const ERROR::WRONG_JETTON_WALLET = 604; 17 | const ERROR::SWAP_TON_NO_GAS = 605; 18 | const ERROR::QUOTE_BAD_AMOUNT = 606; 19 | const ERROR::QUOTE_BAD_RESERVES = 607; 20 | -------------------------------------------------------------------------------- /contracts/imports/op-codes.fc: -------------------------------------------------------------------------------- 1 | const OP_TRANSFER = 0xf8a7ea5; 2 | const OP_TRANSFER_NOTIFICATION = 0x7362d09c; 3 | const OP_INTERNAL_TRANSFER = 0x178d4519; 4 | const OP_EXCESSES = 0xd53276db; ;;excesses 5 | const OP_BURN = 0x595f07bc; 6 | const OP_BURN_NOTIFICAITON = 0x7bdd97de; 7 | 8 | ;; Minter 9 | const OP_MINT = 21; 10 | 11 | const ERROR::WRONG_JETTON_SENDER_ADDRESS = 74; 12 | const ERROR::INSFUCIENT_FUNDS = 600; 13 | const ERROR::ZERO_RESERVES = 601; 14 | const ERROR::NULL_JETTON_WALLET = 602; 15 | const ERROR::MSG_VALUE_INSUFFICIENT = 603; 16 | const ERROR::WRONG_JETTON_WALLET = 604; 17 | const ERROR::SWAP_TON_NO_GAS = 605; 18 | const ERROR::QUOTE_BAD_AMOUNT = 606; 19 | const ERROR::QUOTE_BAD_RESERVES = 607; 20 | -------------------------------------------------------------------------------- /src/ops.ts: -------------------------------------------------------------------------------- 1 | export enum OPS { 2 | Transfer = 0xf8a7ea5, 3 | Transfer_notification = 0x7362d09c, 4 | Internal_transfer = 0x178d4519, 5 | Excesses = 0xd53276db, 6 | Burn = 0x595f07bc, 7 | Burn_notification = 0x7bdd97de, 8 | ADD_LIQUIDITY = 22, 9 | REMOVE_LIQUIDITY = 23, 10 | SWAP_TOKEN = 24, 11 | SWAP_TON = 25, 12 | MINT = 21, 13 | UPGRADE = 26, 14 | } 15 | 16 | export enum ERROR_CODES { 17 | MinAmountOutIsInsufficient = 601, 18 | ADD_LIQUIDITY_INSUFFICIENT_BALANCE = 103, 19 | ADD_LIQUIDITY_WRONG_JETTON_SENDER = 604, 20 | WRONG_JETTON_SENDER_ADDRESS = 74, 21 | INSFUCIENT_FUNDS = 600, 22 | ZERO_RESERVES = 601, 23 | NULL_JETTON_WALLET = 602, 24 | MSG_VALUE_INSUFFICIENT = 603, 25 | WRONG_JETTON_WALLET = 604, 26 | SWAP_TON_NO_GAS = 605, 27 | QUOTE_BAD_AMOUNT = 606, 28 | QUOTE_BAD_RESERVES = 607, 29 | } 30 | -------------------------------------------------------------------------------- /utils/funcToB64.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | 3 | export function compileFuncToB64(funcFiles: string[]): string { 4 | const funcPath = process.env.FUNC_PATH || "func"; 5 | try { 6 | execSync(`${funcPath} -o build/tmp.fif -SPA ${funcFiles.join(" ")}`); 7 | } catch (e: any) { 8 | if (e.message.indexOf("error: `#include` is not a type identifier") > -1) { 9 | console.log(` 10 | =============================================================================== 11 | Please update your func compiler to support the latest func features 12 | to set custom path to your func compiler please set the env variable "export FUNC_PATH=/usr/local/bin/func" 13 | =============================================================================== 14 | `); 15 | process.exit(1); 16 | } else { 17 | console.log(e.message); 18 | } 19 | } 20 | 21 | const stdOut = execSync(`fift -s utils/print-hex.fif`).toString(); 22 | return stdOut.trim(); 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tonswap", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:tonswap/dex.git", 6 | "scripts": { 7 | "build": "./scripts/build.sh", 8 | "test": "npx jest test/amm-minter.spec.ts", 9 | "test-bus": "npx jest test/bus.spec.ts", 10 | "e2e": "ts-node deploy/e2e.ts", 11 | "deploy": "ts-node deploy/deploy.ts", 12 | "build:web": "ts-node deploy/export-hex.ts" 13 | }, 14 | "dependencies": { 15 | "@babel/preset-env": "^7.16.11", 16 | "@babel/preset-typescript": "^7.16.7", 17 | "axios-request-throttle": "^1.0.0", 18 | "bn.js": "^5.2.0", 19 | "jest": "^27.5.1", 20 | "ton": "^12.0.1", 21 | "ton-contract-executor": "^0.5.2", 22 | "ton-tvm-bus": "^0.0.8", 23 | "tonweb": "^0.0.38", 24 | "tonweb-mnemonic": "^1.0.1", 25 | "typescript": "^4.5.5" 26 | }, 27 | "keywords": [], 28 | "author": "", 29 | "license": "ISC", 30 | "devDependencies": { 31 | "@types/bn.js": "^5.1.0", 32 | "@types/jest": "^27.4.0", 33 | "ts-node": "^10.7.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mitsuru Kudo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/amm-utils.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js"; 2 | import { Address, Cell, CellMessage, CommonMessageInfo, InternalMessage } from "ton"; 3 | import { OutAction } from "ton-contract-executor"; 4 | import { OPS } from "./ops"; 5 | 6 | export function actionToMessage(from: Address, action: OutAction | undefined, messageValue = new BN(1000000000), bounce = true) { 7 | //@ts-ignore 8 | const sendMessageAction = action as SendMsgOutAction; 9 | 10 | let msg = new CommonMessageInfo({ body: new CellMessage(sendMessageAction.message?.body) }); 11 | return new InternalMessage({ 12 | to: sendMessageAction.message?.info.dest, 13 | from, 14 | value: messageValue, 15 | bounce, 16 | body: msg, 17 | }); 18 | } 19 | 20 | export function swapTokenCell(minAmountOut: BN) { 21 | let extra_data = new Cell(); 22 | extra_data.bits.writeUint(OPS.SWAP_TOKEN, 32); 23 | extra_data.bits.writeUint(minAmountOut, 32); // minAmountOut 24 | return extra_data; 25 | } 26 | 27 | export function addLiquidityCell() { 28 | let extra_data = new Cell(); 29 | extra_data.bits.writeUint(OPS.ADD_LIQUIDITY, 32); 30 | extra_data.bits.writeUint(new BN(5), 32); // slippage 31 | return extra_data; 32 | } 33 | -------------------------------------------------------------------------------- /test/imports/jetton-utils.fc: -------------------------------------------------------------------------------- 1 | cell pack_jetton_wallet_data(int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { 2 | return begin_cell() 3 | .store_coins(balance) 4 | .store_slice(owner_address) 5 | .store_slice(jetton_master_address) 6 | .store_ref(jetton_wallet_code) 7 | .end_cell(); 8 | } 9 | 10 | cell calculate_jetton_wallet_state_init(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { 11 | return begin_cell() 12 | .store_uint(0, 2) 13 | .store_dict(jetton_wallet_code) 14 | .store_dict(pack_jetton_wallet_data(0, owner_address, jetton_master_address, jetton_wallet_code)) 15 | .store_uint(0, 1) 16 | .end_cell(); 17 | } 18 | 19 | slice calculate_jetton_wallet_address(cell state_init) inline { 20 | return begin_cell().store_uint(4, 3) 21 | .store_int(workchain(), 8) 22 | .store_uint(cell_hash(state_init), 256) 23 | .end_cell() 24 | .begin_parse(); 25 | } 26 | 27 | slice calculate_user_jetton_wallet_address(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { 28 | return calculate_jetton_wallet_address(calculate_jetton_wallet_state_init(owner_address, jetton_master_address, jetton_wallet_code)); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /contracts/imports/jetton-utils.fc: -------------------------------------------------------------------------------- 1 | cell pack_jetton_wallet_data(int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { 2 | return begin_cell() 3 | .store_coins(balance) 4 | .store_slice(owner_address) 5 | .store_slice(jetton_master_address) 6 | .store_ref(jetton_wallet_code) 7 | .end_cell(); 8 | } 9 | 10 | cell calculate_jetton_wallet_state_init(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { 11 | return begin_cell() 12 | .store_uint(0, 2) 13 | .store_dict(jetton_wallet_code) 14 | .store_dict(pack_jetton_wallet_data(0, owner_address, jetton_master_address, jetton_wallet_code)) 15 | .store_uint(0, 1) 16 | .end_cell(); 17 | } 18 | 19 | slice calculate_jetton_wallet_address(cell state_init) inline { 20 | return begin_cell().store_uint(4, 3) 21 | .store_int(workchain(), 8) 22 | .store_uint(cell_hash(state_init), 256) 23 | .end_cell() 24 | .begin_parse(); 25 | } 26 | 27 | slice calculate_user_jetton_wallet_address(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { 28 | return calculate_jetton_wallet_address(calculate_jetton_wallet_state_init(owner_address, jetton_master_address, jetton_wallet_code)); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /test/imports/store.fc: -------------------------------------------------------------------------------- 1 | ;; storage#_ total_supply:Coins token_wallet_address:MsgAddress ton_reserves:Coins 2 | ;; token_reserves:Coins content:^Cell jetton_wallet_code:^Cell = Storage; 3 | 4 | global int store::total_supply; 5 | global slice store::token_wallet_address; 6 | global int store::ton_reserves; 7 | global int store::token_reserves; 8 | global slice store::admin; 9 | global cell store::content; 10 | global cell store::jetton_wallet_code; 11 | 12 | () load_data() impure { 13 | slice ds = get_data().begin_parse(); 14 | store::total_supply = ds~load_coins(); ;; total_supply 15 | store::token_wallet_address = ds~load_msg_addr(); ;; token_wallet_address 16 | store::ton_reserves = ds~load_coins(); ;; ton_reserves 17 | store::token_reserves = ds~load_coins(); ;; token_reserves 18 | store::admin = ds~load_msg_addr(); ;; admin 19 | store::content = ds~load_ref(); ;; content (uri to json) 20 | store::jetton_wallet_code = ds~load_ref(); ;; jetton_wallet_code 21 | } 22 | 23 | () save_data() impure inline_ref { 24 | 25 | set_data(begin_cell() 26 | .store_coins(store::total_supply) 27 | .store_slice(store::token_wallet_address) 28 | .store_coins(store::ton_reserves) 29 | .store_coins(store::token_reserves) 30 | .store_slice(store::admin) 31 | .store_ref(store::content) 32 | .store_ref(store::jetton_wallet_code) 33 | .end_cell() 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /contracts/imports/store.fc: -------------------------------------------------------------------------------- 1 | ;; storage#_ total_supply:Coins token_wallet_address:MsgAddress ton_reserves:Coins 2 | ;; token_reserves:Coins content:^Cell jetton_wallet_code:^Cell = Storage; 3 | 4 | global int store::total_supply; 5 | global slice store::token_wallet_address; 6 | global int store::ton_reserves; 7 | global int store::token_reserves; 8 | global slice store::admin; 9 | global cell store::content; 10 | global cell store::jetton_wallet_code; 11 | 12 | () load_data() impure { 13 | slice ds = get_data().begin_parse(); 14 | store::total_supply = ds~load_coins(); ;; total_supply 15 | store::token_wallet_address = ds~load_msg_addr(); ;; token_wallet_address 16 | store::ton_reserves = ds~load_coins(); ;; ton_reserves 17 | store::token_reserves = ds~load_coins(); ;; token_reserves 18 | store::admin = ds~load_msg_addr(); ;; admin 19 | store::content = ds~load_ref(); ;; content (uri to json) 20 | store::jetton_wallet_code = ds~load_ref(); ;; jetton_wallet_code 21 | } 22 | 23 | () save_data() impure inline_ref { 24 | 25 | set_data(begin_cell() 26 | .store_coins(store::total_supply) 27 | .store_slice(store::token_wallet_address) 28 | .store_coins(store::ton_reserves) 29 | .store_coins(store::token_reserves) 30 | .store_slice(store::admin) 31 | .store_ref(store::content) 32 | .store_ref(store::jetton_wallet_code) 33 | .end_cell() 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/wallet.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js"; 2 | import { Address, beginCell, Cell, contractAddress, InternalMessage, toNano, WalletV1R2Source } from "ton"; 3 | import { SmartContract } from "ton-contract-executor"; 4 | import { TvmBus, iTvmBusContract } from "ton-tvm-bus"; 5 | 6 | const walletV3Code = Cell.fromBoc( 7 | "B5EE9C724101010100710000DEFF0020DD2082014C97BA218201339CBAB19F71B0ED44D0D31FD31F31D70BFFE304E0A4F2608308D71820D31FD31FD31FF82313BBF263ED44D0D31FD31FD3FFD15132BAF2A15144BAF2A204F901541055F910F2A3F8009320D74A96D307D402FB00E8D101A4C8CB1FCB1FCBFFC9ED5410BD6DAD" 8 | )[0]; 9 | 10 | export class Wallet implements iTvmBusContract { 11 | contract: SmartContract; 12 | address: Address; 13 | 14 | private constructor(contract: SmartContract, myAddress: Address, tvmBus: TvmBus, balance: BN) { 15 | this.contract = contract; 16 | this.address = myAddress; 17 | tvmBus.registerContract(this); 18 | contract.setC7Config({ 19 | balance, 20 | myself: myAddress, 21 | }); 22 | } 23 | 24 | async sendInternalMessage(message: InternalMessage) { 25 | //@ts-ignore 26 | return this.contract.sendInternalMessage(message); 27 | } 28 | 29 | static async Create(tvmBus: TvmBus, balance = toNano(10), publicKey = new BN(0), walletId = 0) { 30 | const dataCell = beginCell().storeUint(0, 32).storeUint(walletId, 32).storeBuffer(publicKey.toBuffer()).endCell(); 31 | const contract = await SmartContract.fromCell(walletV3Code, dataCell, { 32 | getMethodsMutate: true, 33 | }); 34 | const myAddress = contractAddress({ workchain: 0, initialCode: walletV3Code, initialData: dataCell }); 35 | return new Wallet(contract, myAddress, tvmBus, balance); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /deploy/upgrader.ts: -------------------------------------------------------------------------------- 1 | import { Address, beginCell, Cell, toNano, TonClient } from "ton"; 2 | import { AmmMinterBase } from "../src/amm-minter"; 3 | import { initDeployKey, initWallet, sendTransaction, sleep, waitForSeqno } from "../utils/deploy-utils"; 4 | 5 | const client = new TonClient({ 6 | endpoint: "https://scalable-api.tonwhales.com/jsonRPC", 7 | }); 8 | 9 | const OP_CODE_UPGRADE = 26; 10 | 11 | async function setWallet() { 12 | const walletKey = await initDeployKey(); 13 | let { wallet } = await initWallet(client, walletKey.publicKey); 14 | return { 15 | wallet, 16 | walletKey 17 | } 18 | } 19 | 20 | async function getUpgradeMessage() { 21 | 22 | const ammMinterRPC = new AmmMinterBase(); 23 | const codeB64 = ammMinterRPC.compileCodeToCell(); 24 | const upgradeMessage = beginCell().storeUint(OP_CODE_UPGRADE, 32).storeUint(1, 64).storeRef(codeB64[0]).endCell(); 25 | return upgradeMessage; 26 | } 27 | 28 | async function getCodeHash(address: Address, client: TonClient) { 29 | const state = await client.getContractState(address); 30 | let code = Cell.fromBoc(state.code!)[0] 31 | return code.hash().toString("base64") 32 | } 33 | 34 | 35 | async function upgradeAmm(address: Address) { 36 | console.log(`> Starting Upgraded Flow for ${address.toFriendly()} code hash: ${(await getCodeHash(address, client))}`); 37 | 38 | const { wallet , walletKey } = await setWallet() 39 | const upgradeMessage = await getUpgradeMessage(); 40 | await sendTransaction(client, wallet, address, toNano(0.1), walletKey.secretKey, upgradeMessage, true, 3) 41 | await sleep(20 * 1000); 42 | console.log(`> Finnish Upgraded Flow for ${address.toFriendly()} code hash: ${(await getCodeHash(address, client))}`); 43 | } 44 | 45 | async function main() { 46 | upgradeAmm(Address.parse("EQAG57v8WL4U188JmkFJZd5VIbqoFfwx3trefJbfJ5pD1JAP")) 47 | } 48 | 49 | (async ()=> { 50 | await main() 51 | })() -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Tonswap - FunC Smart Contracts 2 | 3 | ## TonSwap is live on TON mainnet. 4 | 5 | TonSwap is live on TON mainnet. TonSwap is in Beta, use at your own risk 6 | 7 | ## Overview 8 | 9 | TonSwap is a DEX/AMM using the Uniswap V2 curve (a financial model shared by many popular AMMs like PancakeSwap, QuickSwap, Sushi) to create liquidity pairs and allow traders to swap tokens and liquidity providers to supply liquidity and earn rewards. 10 | 11 | The project depends on an Jetton standard implementation for [Jetton](https://github.com/ton-blockchain/token-contract/tree/jettons/ft). 12 | 13 | ## Develop 14 | 15 | run `npm install` 16 | 17 | ### Compile contract and run tests 18 | 19 | This project depends on the executables **fift**, **func** . You can build them from [source](https://ton.org/docs/#/howto/getting-started), or you can download the [pre compiled binaries](https://github.com/ton-defi-org/ton-binaries/releases). 20 | 21 | ### Func Compiler 22 | 23 | This project is using the latest func features such as `#include` and `const` so please use the latest func compiler , 24 | If you want to set an explicit func path you may use the FUNC_PATH environment variable `export FUNC_PATH=/usr/local/bin/func` 25 | 26 | ### Run tests 27 | 28 | the project uses [ton-contract-executor](https://github.com/tonwhales/ton-contract-executor) package to run Jest based tests. 29 | Use `npm run test` to execute the test suite. 30 | 31 | ### Run TVM-BUS tests 32 | 33 | run `npm run test-bus` 34 | this tests are different then the tests in `/test/amm-minter.spec.ts`, 35 | Because this tests are using [ton-tvm-bus](https://github.com/ton-defi-org/ton-tvm-bus) , each tests starts in a single message, and the message passing between contracts is done automatically, messages with statInit are auto deployed , messages find their receiver automatically unlike in the first test suite. 36 | 37 | 38 | ### Run end to end test on Mainnet or Testnet 39 | 40 | `npm run e2e` (this process will generate a deploy wallet during it`s execution) 41 | 42 | ### build for web 43 | 44 | `npm run build:web` - this process will generate json files with hex value for the contract, both for `amm-minter.fc` and `amm-wallet.fc`; 45 | -------------------------------------------------------------------------------- /test/jetton-minter.fc: -------------------------------------------------------------------------------- 1 | #include "imports/stdlib.fc"; 2 | #include "imports/params.fc"; 3 | #include "imports/op-codes.fc"; 4 | #include "imports/jetton-utils.fc"; 5 | 6 | 7 | 8 | (int, slice, cell, cell) load_data() inline { 9 | slice ds = get_data().begin_parse(); 10 | return ( 11 | ds~load_coins(), ;; total_supply 12 | ds~load_msg_addr(), ;; admin_address 13 | ds~load_ref(), ;; content 14 | ds~load_ref() ;; jetton_wallet_code 15 | ); 16 | } 17 | 18 | () save_data(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) impure inline { 19 | set_data(begin_cell() 20 | .store_coins(total_supply) 21 | .store_slice(admin_address) 22 | .store_ref(content) 23 | .store_ref(jetton_wallet_code) 24 | .end_cell() 25 | ); 26 | } 27 | 28 | () mint_tokens(slice to_address, cell jetton_wallet_code, int amount, cell master_msg) impure { 29 | 30 | cell state_init = calculate_jetton_wallet_state_init(to_address, my_address(), jetton_wallet_code); 31 | slice to_wallet_address = calculate_jetton_wallet_address(state_init); 32 | var msg = begin_cell() 33 | .store_uint(0x18, 6) 34 | .store_slice(to_wallet_address) 35 | .store_coins(amount) 36 | .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) 37 | .store_ref(state_init) 38 | .store_ref(master_msg); 39 | send_raw_message(msg.end_cell(), 64); ;; pay transfer fees separately, revert on errors 40 | } 41 | 42 | () recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure { 43 | if (in_msg_body.slice_empty?()) { ;; ignore empty messages 44 | return (); 45 | } 46 | slice cs = in_msg_full.begin_parse(); 47 | int flags = cs~load_uint(4); 48 | 49 | if (flags & 1) { ;; ignore all bounced messages 50 | return (); 51 | } 52 | slice sender_address = cs~load_msg_addr(); 53 | 54 | int op = in_msg_body~load_uint(32); 55 | int query_id = in_msg_body~load_uint(64); 56 | 57 | (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); 58 | 59 | if (op == OP_MINT) { 60 | throw_unless(73, equal_slices(sender_address, admin_address)); 61 | slice to_address = in_msg_body~load_msg_addr(); 62 | int amount = in_msg_body~load_coins(); 63 | cell master_msg = in_msg_body~load_ref(); 64 | slice master_msg_cs = master_msg.begin_parse(); 65 | master_msg_cs~skip_bits(32 + 64); ;; op + query_id 66 | int jetton_amount = master_msg_cs~load_coins(); 67 | mint_tokens(to_address, jetton_wallet_code, amount, master_msg); 68 | save_data(total_supply + jetton_amount, admin_address, content, jetton_wallet_code); 69 | return (); 70 | } 71 | 72 | if (op == OP_BURN_NOTIFICAITON) { 73 | int jetton_amount = in_msg_body~load_coins(); 74 | slice from_address = in_msg_body~load_msg_addr(); 75 | throw_unless(74, 76 | equal_slices(calculate_user_jetton_wallet_address(from_address, my_address(), jetton_wallet_code), sender_address) 77 | ); 78 | save_data(total_supply - jetton_amount, admin_address, content, jetton_wallet_code); 79 | slice response_address = in_msg_body~load_msg_addr(); 80 | if (response_address.preload_uint(2) != 0) { 81 | var msg = begin_cell() 82 | .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000 83 | .store_slice(response_address) 84 | .store_coins(0) 85 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 86 | .store_uint(OP_EXCESSES, 32) 87 | .store_uint(query_id, 64); 88 | send_raw_message(msg.end_cell(), 2 + 64); 89 | } 90 | return (); 91 | } 92 | 93 | throw(0xffff); 94 | } 95 | 96 | (int, int, slice, cell, cell, int) get_jetton_data() method_id { 97 | var [balance, _] = get_balance(); 98 | (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); 99 | return (total_supply, -1, admin_address, content, jetton_wallet_code, balance); 100 | } 101 | 102 | slice get_wallet_address(slice owner_address) method_id { 103 | (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); 104 | return calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code); 105 | } 106 | -------------------------------------------------------------------------------- /deploy/deploy.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js"; 2 | import { TonClient, Address, toNano } from "ton"; 3 | import { JettonMinter } from "../src/jetton-minter"; 4 | 5 | import { 6 | initDeployKey, 7 | initWallet, 8 | printBalances, 9 | printDeployerBalances, 10 | sleep, 11 | addLiquidity, 12 | BLOCK_TIME, 13 | deployAmmMinter, 14 | deployJettonMinter, 15 | mintJetton, 16 | saveAddress, 17 | removeLiquidity, 18 | } from "./deploy-utils"; 19 | 20 | const client = new TonClient({ 21 | // endpoint: "https://testnet.tonhubapi.com/jsonRPC", 22 | // endpoint: "https://sandbox.tonhubapi.com/jsonRPC", 23 | endpoint: "https://scalable-api.tonwhales.com/jsonRPC", 24 | // endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC", 25 | }); 26 | 27 | async function deployPool(ammContentUri: string) { 28 | const walletKey = await initDeployKey(); 29 | let { wallet: deployWallet } = await initWallet(client, walletKey.publicKey); 30 | saveAddress("Deployer", deployWallet.address); 31 | const ammMinter = await deployAmmMinter(client, deployWallet, walletKey.secretKey, ammContentUri); 32 | } 33 | /** 34 | * this procedure 35 | * deploy's a jetton contract 36 | * mint some jetton's on behalf of the deployer 37 | * deploys an amm contract 38 | * adds liquidity to the amm 39 | */ 40 | async function deployJettonAmmLiquidity(jettonContentUri: string, ammContentUri: string) { 41 | const walletKey = await initDeployKey(); 42 | let { wallet: deployWallet } = await initWallet(client, walletKey.publicKey); 43 | saveAddress("Deployer", deployWallet.address); 44 | 45 | const jettonMinter = await deployJettonMinter(client, deployWallet, walletKey.secretKey, jettonContentUri); 46 | await mintJetton(client, jettonMinter.address, deployWallet, walletKey.secretKey); 47 | 48 | const deployerUSDCAddress = (await JettonMinter.GetWalletAddress(client, jettonMinter.address, deployWallet.address)) as Address; 49 | saveAddress("DeployerUSDC", deployerUSDCAddress); 50 | printDeployerBalances(client, deployWallet.address, deployerUSDCAddress); 51 | 52 | const ammMinter = await deployAmmMinter(client, deployWallet, walletKey.secretKey, ammContentUri); 53 | console.log(` 54 | Jetton Minter address :${jettonMinter.address.toFriendly()} 55 | Amm Minter address :${ammMinter.address.toFriendly()} 56 | `); 57 | 58 | await addLiquidity(client, ammMinter, deployWallet, deployerUSDCAddress as Address, walletKey.secretKey, 1, toNano(10)); 59 | await sleep(BLOCK_TIME * 2); 60 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 61 | } 62 | 63 | 64 | async function mintJettons(jettonContentUri: string, mintAmount = 100, recipient?: Address) { 65 | const walletKey = await initDeployKey(); 66 | let { wallet: deployWallet } = await initWallet(client, walletKey.publicKey); 67 | saveAddress("Deployer", deployWallet.address); 68 | 69 | const jettonMinter = await deployJettonMinter(client, deployWallet, walletKey.secretKey, jettonContentUri); 70 | await mintJetton(client, jettonMinter.address, deployWallet, walletKey.secretKey, mintAmount, recipient || deployWallet.address); 71 | } 72 | 73 | 74 | async function deployerRemoveLiquidity(jettonContentUri: string, ammContentUri: string) { 75 | const walletKey = await initDeployKey(); 76 | let { wallet: deployWallet } = await initWallet(client, walletKey.publicKey); 77 | saveAddress("Deployer", deployWallet.address); 78 | 79 | const ammMinter = await deployAmmMinter(client, deployWallet, walletKey.secretKey, ammContentUri); 80 | await removeLiquidity(client, ammMinter, deployWallet, walletKey.secretKey, 64); 81 | } 82 | 83 | (async () => { 84 | if (process.env.npm_lifecycle_event == "deploy") { 85 | await deployPool(process.env.POOL_CONTENT_URI || "https://api.jsonbin.io/xxx/62a0436b05f31f68b3b97ac4"); 86 | return; 87 | } 88 | // await deployJettonAmmLiquidity( 89 | // "https://api.jsonbin.io/b/x628f1df905f31f77b3a7c5d0-usdt", 90 | // "https://api.jsonbin.io/b/x628f1df905f31f77b3a7c5d1-usdt" 91 | // ); 92 | 93 | 94 | // await mintJettons("https://api.jsonbin.io/b/x628f1df905f31f77b3a7c5d0-shib", 50, Address.parse("EQDjhy1Ig-S0vKCWwd3XZRKODGx0RJyhqW37ZDMl-pgv8iBr")); 95 | 96 | 97 | // await deployerRemoveLiquidity( 98 | // "https://api.jsonbin.io/b/x628f1df905f31f77b3a7c5d0", 99 | // "https://api.jsonbin.io/b/x628f1df905f31f77b3a7c5d1" 100 | // ); 101 | })(); 102 | 103 | // await deployJettonAmmLiquidity( 104 | // "https://api.jsonbin.io/b/x628f1df905f31f77b3a7c5d0-shib", 105 | // "https://api.jsonbin.io/b/x628f1df905f31f77b3a7c5d1-shib" 106 | // ); 107 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js"; 2 | import { Address, Cell, Slice } from "ton"; 3 | // @ts-ignore 4 | import { ExecutionResult } from "ton-contract-executor"; 5 | 6 | const decimals = new BN("1000000000"); 7 | 8 | export function parseJettonTransfer(msg: Cell) { 9 | let slice = msg.beginParse(); 10 | var op = slice.readUint(32); 11 | var query = slice.readUint(64); 12 | var amount = slice.readCoins(); 13 | var to = slice.readAddress(); 14 | 15 | return { 16 | op: op.toString(10), 17 | query: query.toString(10), 18 | to, 19 | amount, 20 | }; 21 | } 22 | 23 | export function toUnixTime(timeInMS: number) { 24 | return Math.round(timeInMS / 1000); 25 | } 26 | 27 | export function sliceToString(s: Slice) { 28 | let data = s.readRemaining(); 29 | return data.buffer.slice(0, Math.ceil(data.cursor / 8)).toString(); 30 | } 31 | 32 | export function cellToString(s: Cell) { 33 | let data = s.beginParse().readRemaining(); 34 | return data.buffer.slice(0, Math.ceil(data.cursor / 8)).toString(); 35 | } 36 | 37 | export function base64StrToCell(str: string): Cell[] { 38 | let buf = Buffer.from(str, "base64"); 39 | return Cell.fromBoc(buf); 40 | } 41 | 42 | export function addressToSlice264(a: Address) { 43 | let c = new Cell(); 44 | c.bits.writeAddress(a); 45 | const s = c.beginParse(); 46 | const _anyCast = s.readUint(3); 47 | const addr = s.readUint(264); 48 | return addr; 49 | } 50 | 51 | export function sliceToAddress267(s: Slice) { 52 | const _anyCast = new BN(s.readUint(3)); //ignore anycast bits 53 | return sliceToAddress(s); 54 | } 55 | 56 | export function sliceToAddress(s: Slice, isAnyCastAddress = false) { 57 | if (isAnyCastAddress) { 58 | s.skip(3); 59 | } 60 | const wc = new BN(s.readUint(8)); 61 | const addr = s.readUint(256); 62 | const address = new Address(wc.toNumber(), addr.toBuffer()); 63 | return address; 64 | } 65 | 66 | export function toDecimals(num: number | string) { 67 | return new BN(num).mul(decimals); 68 | } 69 | 70 | export function fromDecimals(num: BN) { 71 | const numStr = num.toString(); 72 | const dotIndex = numStr.length - 9; 73 | const formattedStr = numStr.substring(0, dotIndex) + "." + numStr.substring(dotIndex, numStr.length); 74 | return formattedStr; 75 | } 76 | 77 | export function stripBoc(bocStr: string) { 78 | //console.log(`parsing boc ${bocStr}`); 79 | return bocStr.substr(2, bocStr.length - 4); 80 | } 81 | 82 | export function parseInternalMessageResponse(result: ExecutionResult) { 83 | // @ts-ignore 84 | let res = result as SuccessfulExecutionResult; 85 | //console.log(res); 86 | return { 87 | ...res, 88 | returnValue: res.result[1] as BN, 89 | logs: filterLogs(res.logs), 90 | }; 91 | } 92 | 93 | export function filterLogs(logs: string) { 94 | const arr = logs.split("\n"); 95 | // console.log(arr.length); 96 | 97 | let filtered = arr.filter((it) => { 98 | return it.indexOf("#DEBUG#") !== -1 || it.indexOf("error") !== -1; 99 | }); 100 | const beautified = filtered.map((it, i) => { 101 | const tabIndex = it.indexOf("\t"); 102 | return `${i + 1}. ${it.substring(tabIndex + 1, it.length)}`; 103 | }); 104 | 105 | return beautified; 106 | } 107 | 108 | export function writeString(cell: Cell, str: string) { 109 | for (let i = 0; i < str.length; i++) { 110 | cell.bits.writeUint8(str.charCodeAt(i)); 111 | } 112 | } 113 | 114 | /// Ton Web impl for bytes to base64 115 | export function bytesToBase64(bytes: any) { 116 | let result = ""; 117 | let i; 118 | const l = bytes.length; 119 | for (i = 2; i < l; i += 3) { 120 | result += base64abc[bytes[i - 2] >> 2]; 121 | result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)]; 122 | result += base64abc[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)]; 123 | result += base64abc[bytes[i] & 0x3f]; 124 | } 125 | if (i === l + 1) { 126 | // 1 octet missing 127 | result += base64abc[bytes[i - 2] >> 2]; 128 | result += base64abc[(bytes[i - 2] & 0x03) << 4]; 129 | result += "=="; 130 | } 131 | if (i === l) { 132 | // 2 octets missing 133 | result += base64abc[bytes[i - 2] >> 2]; 134 | result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)]; 135 | result += base64abc[(bytes[i - 1] & 0x0f) << 2]; 136 | result += "="; 137 | } 138 | return result; 139 | } 140 | 141 | const base64abc = (() => { 142 | const abc = []; 143 | const A = "A".charCodeAt(0); 144 | const a = "a".charCodeAt(0); 145 | const n = "0".charCodeAt(0); 146 | for (let i = 0; i < 26; ++i) { 147 | abc.push(String.fromCharCode(A + i)); 148 | } 149 | for (let i = 0; i < 26; ++i) { 150 | abc.push(String.fromCharCode(a + i)); 151 | } 152 | for (let i = 0; i < 10; ++i) { 153 | abc.push(String.fromCharCode(n + i)); 154 | } 155 | abc.push("+"); 156 | abc.push("/"); 157 | return abc; 158 | })(); 159 | -------------------------------------------------------------------------------- /contracts/imports/amm-minter-utils.fc: -------------------------------------------------------------------------------- 1 | const TON_TOKEN_SWAP_FEE = 20000000; ;; 0.02 TON; 2 | const AMM_GAS_CONSUMPTION = 10000000; ;; 0.01 TON 3 | const JETTON_GAS_CONSUMPTION = 10000000; ;; 0.01 TON 4 | const JETTON_MIN_TON_FOR_STORAGE = 10000000; ;; 0.01 TON 5 | const MIN_TON_STORAGE = 200000000; ;; 0.2 TON 6 | 7 | ;; ============= Using babylonian method for Math.sqrt 8 | int square_root(int number) { 9 | if (number < 3) { 10 | return 1; 11 | } 12 | int lo = 0; 13 | int hi = number; 14 | while (lo <= hi) { 15 | var mid = (lo + hi) / 2; 16 | if (mid * mid > number) { 17 | hi = mid - 1; 18 | } else { 19 | lo = mid + 1; 20 | } 21 | } 22 | return hi; 23 | } 24 | 25 | (int) jetton_transfer_fee(int fwd_fee) inline { 26 | ;; 2X Fwd_fee + 1 gas-computation for current 27 | ;; + 1 gas-computation for reciver 28 | return 2 * fwd_fee + (2 * JETTON_GAS_CONSUMPTION + JETTON_MIN_TON_FOR_STORAGE); 29 | } 30 | 31 | () send_grams(slice address, int amount) impure { 32 | cell msg = begin_cell() 33 | .store_uint(0x18, 6) 34 | .store_slice(address) 35 | .store_coins(amount) 36 | .store_uint(0, 107) ;; 106 zeroes + 0 as an indicator that there is no cell with the data. 37 | .end_cell(); 38 | send_raw_message(msg, 1 + 2); ;; mode, 2 for ignoring errors, 1 for sender pays fees, 64 for returning inbound message value 39 | } 40 | 41 | 42 | 43 | ;;transfer query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress 44 | ;; response_destination:MsgAddress custom_payload:(Maybe ^Cell) 45 | ;; forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) 46 | ;; = InternalMsgBody; 47 | 48 | () transfer_token(int query_id, slice to, int jetton_amount, int ton_value) impure { 49 | 50 | var jetton_transfer_message = begin_cell() 51 | .store_uint(OP_TRANSFER, 32) 52 | .store_uint(query_id, 64) 53 | .store_coins(jetton_amount) 54 | .store_slice(to) 55 | .store_slice(to) 56 | .store_coins(0) 57 | .store_uint(1, 2) ;; https://github.com/ton-blockchain/token-contract/blob/2c13d3ef61ca4288293ad65bf0cfeaed83879b93/ft/jetton-wallet.fc#L60 58 | .end_cell(); 59 | 60 | var msg = begin_cell() 61 | .store_uint(0x18, 6) 62 | .store_slice(store::token_wallet_address) 63 | .store_coins(ton_value) ;; gas 64 | .store_uint(4, 1 + 4 + 4 + 64 + 32) 65 | .store_uint(0, 1) ;; state-init null 66 | .store_uint(1, 1); ;; message body as REF 67 | 68 | msg = msg.store_ref(jetton_transfer_message); 69 | 70 | send_raw_message(msg.end_cell(), 2 + 1); ;; revert on errors 71 | } 72 | 73 | int get_amount_out(int amountIn, int reserveIn, int reserveOut) method_id { 74 | int amountInWithFee = amountIn * 997; ;; lp fees 0.3% 75 | int numerator = amountInWithFee * reserveOut; 76 | int denominator = reserveIn * 1000 + amountInWithFee; 77 | return numerator / denominator; 78 | } 79 | 80 | int get_amount_in(int amountOut, int reserveIn, int reserveOut) method_id { 81 | int numerator = reserveIn * amountOut * 1000; 82 | int denominator = (reserveOut - amountOut) * 997; 83 | return ( numerator / denominator) + 1; 84 | } 85 | 86 | () swap_tokens( 87 | int min_amount_out, 88 | int in_amount, 89 | int is_ton_src, 90 | slice sender, 91 | int query_id, 92 | int msg_value, 93 | int fwd_fee) impure { 94 | 95 | 96 | int src_resvers = is_ton_src == true ? store::ton_reserves : store::token_reserves; 97 | int trgt_resvers = is_ton_src == true ? store::token_reserves : store::ton_reserves; 98 | int amount_out = get_amount_out(in_amount, src_resvers, trgt_resvers); 99 | 100 | ;; Slippage is to low or insufficient amount, sending back the funds to sender minus gas 101 | if ((amount_out < min_amount_out) | (amount_out > trgt_resvers)) { 102 | if is_ton_src == true { 103 | send_grams(sender, in_amount); 104 | } else { 105 | transfer_token(query_id, sender, in_amount, msg_value); 106 | } 107 | return (); 108 | } 109 | 110 | ;; swap ton->token 111 | if (is_ton_src == true) { 112 | store::ton_reserves += in_amount; 113 | store::token_reserves -= amount_out; 114 | 115 | int transfer_token_gas = msg_value - ( in_amount + AMM_GAS_CONSUMPTION + fwd_fee * 2); 116 | int jetton_fee = jetton_transfer_fee(fwd_fee); 117 | throw_unless(ERROR::SWAP_TON_NO_GAS, transfer_token_gas > jetton_fee); 118 | transfer_token(query_id, sender, amount_out, transfer_token_gas); 119 | } else { 120 | store::ton_reserves -= amount_out; 121 | store::token_reserves += in_amount; 122 | send_grams(sender, amount_out); 123 | } 124 | 125 | return (); 126 | } 127 | 128 | () revert_add_liquidity(int ton_liquidity, 129 | int token_liquidity, 130 | slice jetton_sender, 131 | int query_id, 132 | int msg_value, 133 | int fwd_fee) impure { 134 | 135 | send_grams(jetton_sender, ton_liquidity); 136 | int send_token_gas = msg_value - ( ton_liquidity + jetton_transfer_fee(fwd_fee) + fwd_fee ); 137 | transfer_token(query_id, jetton_sender, token_liquidity, send_token_gas); 138 | } 139 | 140 | (int) calculate_new_lp(int ton_amount,int token_amount) impure { 141 | 142 | int new_liquidity = 0; 143 | int MINIMUM_LIQUIDITY = 1000; 144 | if (store::token_reserves == 0) { 145 | ;; calc the user share Sushi (Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);) 146 | new_liquidity = square_root(ton_amount * token_amount) / MINIMUM_LIQUIDITY; 147 | } else { 148 | ;; liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); 149 | int ton_share = (ton_amount * store::total_supply) / store::ton_reserves; 150 | int token_share = (token_amount * store::total_supply) / store::token_reserves; 151 | new_liquidity = min(ton_share, token_share); 152 | } 153 | 154 | store::token_reserves += token_amount; 155 | store::ton_reserves += ton_amount; 156 | store::total_supply += new_liquidity; 157 | 158 | return new_liquidity; 159 | } 160 | 161 | ;; uniswap v2 162 | (int) quote(int amount_a, int reserve_a, int reserve_b) impure { 163 | throw_unless(ERROR::QUOTE_BAD_AMOUNT, amount_a > 0); 164 | throw_unless(ERROR::QUOTE_BAD_RESERVES, (reserve_a > 0) & (reserve_b > 0)); 165 | return muldiv(amount_a, reserve_b, reserve_a); 166 | } -------------------------------------------------------------------------------- /src/jetton-wallet.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { SmartContract, SuccessfulExecutionResult, FailedExecutionResult, parseActionsList, OutAction } from "ton-contract-executor"; 3 | import { Address, Cell, CellMessage, InternalMessage, Slice, CommonMessageInfo, TonClient, toNano, beginCell } from "ton"; 4 | import BN from "bn.js"; 5 | import { toUnixTime, parseInternalMessageResponse, filterLogs, sliceToAddress, writeString } from "./utils"; 6 | import { OPS } from "./ops"; 7 | import { bytesToAddress } from "../utils/deploy-utils"; 8 | const ZERO_ADDRESS = Address.parse("EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c"); 9 | export declare type ExecutionResult = FailedExecutionResult | SuccessfulExecutionResult; 10 | 11 | import { iTvmBusContract, TvmBus } from "ton-tvm-bus"; 12 | import { compileFuncToB64 } from "../utils/funcToB64"; 13 | 14 | type UsdcTransferNextOp = OPS.ADD_LIQUIDITY | OPS.SWAP_TOKEN; 15 | 16 | export class JettonWallet implements iTvmBusContract { 17 | public initMessageResult: { logs?: string; actionsList?: OutAction[] } = {}; 18 | public address = ZERO_ADDRESS; 19 | public initMessageResultRaw?: ExecutionResult; 20 | 21 | private constructor(public readonly contract: SmartContract) { } 22 | 23 | static getCodeCell(): Cell[] { 24 | const jettonWalletCodeB64: string = compileFuncToB64(["test/jetton-wallet.fc"]); 25 | return Cell.fromBoc(jettonWalletCodeB64); 26 | } 27 | 28 | async getData() { 29 | let res = await this.contract.invokeGetMethod("get_wallet_data", []); 30 | const balance = res.result[0] as BN; 31 | const owner = res.result[1] as Slice; 32 | const jettonMaster = res.result[2] as Slice; 33 | const code = res.result[3] as Cell; 34 | 35 | return { 36 | balance, 37 | owner, 38 | jettonMaster: sliceToAddress(jettonMaster), 39 | code, 40 | }; 41 | } 42 | //BUS implementation 43 | sendInternalMessage(message: InternalMessage) { 44 | return this.contract.sendInternalMessage(message); 45 | } 46 | 47 | async sendInternalMessage2(message: InternalMessage) { 48 | const res = await this.contract.sendInternalMessage(message); 49 | return parseInternalMessageResponse(res); 50 | } 51 | 52 | // transfer#f8a7ea5 query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress 53 | // response_destination:MsgAddress custom_payload:(Maybe ^Cell) 54 | // forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) 55 | // = InternalMsgBody; 56 | 57 | static TransferOverloaded( 58 | to: Address, 59 | jettonAmount: BN, 60 | responseDestination: Address, 61 | forwardTonAmount: BN = new BN(0), 62 | overloadOp: UsdcTransferNextOp, 63 | overloadValue: BN, 64 | tonLiquidity?: BN 65 | ) { 66 | let messageBody = new Cell(); 67 | messageBody.bits.writeUint(OPS.Transfer, 32); // action 68 | messageBody.bits.writeUint(1, 64); // query-id 69 | messageBody.bits.writeCoins(jettonAmount); 70 | messageBody.bits.writeAddress(to); 71 | messageBody.bits.writeAddress(responseDestination); 72 | messageBody.bits.writeBit(false); // null custom_payload 73 | messageBody.bits.writeCoins(forwardTonAmount); 74 | messageBody.bits.writeBit(false); // forward_payload in this slice, not separate messageBody 75 | messageBody.bits.writeUint(new BN(overloadOp), 32); 76 | if (overloadOp == OPS.ADD_LIQUIDITY && tonLiquidity) { 77 | messageBody.bits.writeUint(overloadValue, 32); // slippage 78 | messageBody.bits.writeCoins(tonLiquidity); 79 | } else if (overloadOp == OPS.SWAP_TOKEN) { 80 | messageBody.bits.writeCoins(overloadValue); // min amount out 81 | } 82 | return messageBody; 83 | } 84 | 85 | async transfer( 86 | from: Address, 87 | to: Address, 88 | amount: BN, 89 | responseDestination: Address, 90 | customPayload: Cell | undefined, 91 | forwardTonAmount: BN = new BN(0), 92 | overloadOp: UsdcTransferNextOp, 93 | overloadValue: BN, 94 | tonLiquidity?: BN 95 | ) { 96 | const messageBody = JettonWallet.TransferOverloaded( 97 | to, 98 | amount, 99 | responseDestination, 100 | forwardTonAmount, 101 | overloadOp, 102 | overloadValue, 103 | tonLiquidity 104 | ); 105 | 106 | let res = await this.contract.sendInternalMessage( 107 | new InternalMessage({ 108 | from: from, 109 | to: to, 110 | value: forwardTonAmount.add(toNano("0.08")), // TODO 111 | bounce: false, 112 | body: new CommonMessageInfo({ body: new CellMessage(messageBody) }), 113 | }) 114 | ); 115 | 116 | let successResult = res as SuccessfulExecutionResult; 117 | 118 | return { 119 | ...res, 120 | returnValue: res.result[1] as BN, 121 | logs: filterLogs(res.logs), 122 | }; 123 | } 124 | 125 | setUnixTime(time: number) { 126 | this.contract.setUnixTime(time); 127 | } 128 | 129 | static async GetData(client: TonClient, jettonWallet: Address) { 130 | let res = await client.callGetMethod(jettonWallet, "get_wallet_data", []); 131 | 132 | const balance = BigInt(res.stack[0][1]); 133 | const owner = bytesToAddress(res.stack[1][1].bytes); 134 | const jettonMaster = bytesToAddress(res.stack[2][1].bytes); 135 | 136 | return { 137 | balance, 138 | owner, 139 | jettonMaster, 140 | }; 141 | } 142 | 143 | static async createFromMessage(code: Cell, data: Cell, initMessage: InternalMessage, tvmBus?: TvmBus): Promise { 144 | const jettonWallet = await SmartContract.fromCell(code, data, { getMethodsMutate: true, debug: true }); 145 | 146 | const contract = new JettonWallet(jettonWallet); 147 | contract.setUnixTime(toUnixTime(Date.now())); 148 | 149 | const initRes = await jettonWallet.sendInternalMessage(initMessage); 150 | let successResult = initRes as SuccessfulExecutionResult; 151 | const initMessageResponse = { 152 | ...successResult, 153 | logs: filterLogs(successResult.logs), 154 | }; 155 | // @ts-ignore 156 | contract.initMessageResult = initMessageResponse; 157 | contract.initMessageResultRaw = initRes; 158 | contract.address = initMessage.to; 159 | if (tvmBus) { 160 | tvmBus.registerContract(contract as iTvmBusContract); 161 | } 162 | 163 | return contract; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": false, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 45 | 46 | /* Module Resolution Options */ 47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | }, 72 | 73 | "include": ["src/**/*"], 74 | "exclude": ["**/__mocks__/*"], 75 | } -------------------------------------------------------------------------------- /utils/deploy-utils.ts: -------------------------------------------------------------------------------- 1 | import { mnemonicNew, mnemonicToWalletKey } from "ton-crypto"; 2 | import * as fs from "fs"; 3 | import { Address, Cell, CellMessage, CommonMessageInfo, fromNano, InternalMessage, TonClient, WalletContract, WalletV3R2Source } from "ton"; 4 | import { AmmMinterRPC } from "../src/amm-minter"; 5 | import { JettonWallet } from "../src/jetton-wallet"; 6 | import BN from "bn.js"; 7 | const BOC_MAX_LENGTH = 48; 8 | 9 | export async function initDeployKey() { 10 | const deployConfigJson = `./build/deploy.config.json`; 11 | const deployerWalletType = "org.ton.wallets.v3.r2"; 12 | let deployerMnemonic; 13 | if (!fs.existsSync(deployConfigJson)) { 14 | console.log(`\n* Config file '${deployConfigJson}' not found, creating a new wallet for deploy..`); 15 | deployerMnemonic = (await mnemonicNew(24)).join(" "); 16 | const deployWalletJsonContent = { 17 | created: new Date().toISOString(), 18 | deployerWalletType, 19 | deployerMnemonic, 20 | }; 21 | fs.writeFileSync(deployConfigJson, JSON.stringify(deployWalletJsonContent, null, 2)); 22 | console.log(` - Created new wallet in '${deployConfigJson}' - keep this file secret!`); 23 | } else { 24 | console.log(`\n* Config file '${deployConfigJson}' found and will be used for deployment!`); 25 | const deployConfigJsonContentRaw = fs.readFileSync(deployConfigJson, "utf-8"); 26 | const deployConfigJsonContent = JSON.parse(deployConfigJsonContentRaw); 27 | if (!deployConfigJsonContent.deployerMnemonic) { 28 | console.log(` - ERROR: '${deployConfigJson}' does not have the key 'deployerMnemonic'`); 29 | process.exit(1); 30 | } 31 | deployerMnemonic = deployConfigJsonContent.deployerMnemonic; 32 | } 33 | return mnemonicToWalletKey(deployerMnemonic.split(" ")); 34 | } 35 | 36 | export function bytesToAddress(bufferB64: string) { 37 | const buff = Buffer.from(bufferB64, "base64"); 38 | let c2 = Cell.fromBoc(buff); 39 | return c2[0].beginParse().readAddress() as Address; 40 | } 41 | 42 | export function sleep(time: number) { 43 | return new Promise((resolve) => { 44 | console.log(`💤 ${time / 1000}s ...`); 45 | 46 | setTimeout(resolve, time); 47 | }); 48 | } 49 | export async function printDeployerBalances(client: TonClient, deployer: Address, deployerUSDCAddress: Address) { 50 | const usdcData = await JettonWallet.GetData(client, deployerUSDCAddress); 51 | const ton = await client.getBalance(deployer); 52 | console.log(``); 53 | console.log(`⛏ Deployer Balance: ${fromNano(ton)}💎 | ${fromNano(usdcData.balance.toString())}$ USDC `); 54 | console.log(``); 55 | } 56 | 57 | export async function printBalances(client: TonClient, ammMinter: AmmMinterRPC, deployer: Address, deployerUSDCAddress?: Address) { 58 | try { 59 | const data = await ammMinter.getJettonData(); 60 | const balance = await client.getBalance(ammMinter.address); 61 | console.log(`-----==== AmmMinter ====----- `); 62 | console.log(`[${ammMinter.address.toFriendly()}] 63 | 💎 balance : ${fromNano(balance)} ${fromNano(balance.sub(hexToBn(data.tonReserves)))} 64 | 💰 totalSupply : ${hexToBn(data.totalSupply)} (${bnFmt(hexToBn(data.totalSupply))}) 65 | 💰 tonReserves : ${hexToBn(data.tonReserves)} (${bnFmt(hexToBn(data.tonReserves))}) 66 | 💰 tokenReserves: ${hexToBn(data.tokenReserves)} (${bnFmt(hexToBn(data.tokenReserves))}) 67 | 📪 JettonWallet : ${data.jettonWalletAddress.toFriendly()} 68 | `); 69 | // await printDeployerBalances(client, deployer, deployerUSDCAddress); 70 | console.log(`-----==== ***** ====----- 71 | `); 72 | } catch (e) {} 73 | } 74 | 75 | export function hexToBn(num: string) { 76 | return new BN(BigInt(num).toString()); 77 | } 78 | 79 | export function bnFmt(num: BN | BigInt) { 80 | let str = num.toString(); 81 | return `${BigInt(str) / BigInt(1e9)}.${BigInt(str) % BigInt(1e9)} `; 82 | } 83 | 84 | export function hexFromNano(num: string) { 85 | const res = BigInt(num) / BigInt(100000000); 86 | return res.toString(); 87 | } 88 | 89 | export function printAddresses(addressBook: { [key: string]: string }, network: "sandbox." | "test." | "" = "") { 90 | console.log(``); //br 91 | let lsSnippet = ``; 92 | for (var key in addressBook) { 93 | const address = key; 94 | console.log(`${addressBook[key]} : https://${network}tonwhales.com/explorer/address/${key}`); 95 | const ellipsisAddress = `${address.substring(0, 6)}...${address.substring(address.length - 7, address.length - 1)}`; 96 | lsSnippet += `localStorage["${key}"]="${addressBook[key]}";`; 97 | lsSnippet += `localStorage["${ellipsisAddress}"]="${addressBook[key]}";`; 98 | } 99 | console.log(``); 100 | console.log(lsSnippet); 101 | console.log(``); 102 | } 103 | 104 | export async function initWallet(client: TonClient, publicKey: Buffer, workchain = 0) { 105 | const wallet = await WalletContract.create(client, WalletV3R2Source.create({ publicKey: publicKey, workchain })); 106 | const walletBalance = await client.getBalance(wallet.address); 107 | if (parseFloat(fromNano(walletBalance)) < 1) { 108 | throw `Insufficient Deployer [${wallet.address.toFriendly()}] funds ${fromNano(walletBalance)}`; 109 | } 110 | console.log( 111 | `Init wallet ${wallet.address.toFriendly()} | 112 | balance: ${fromNano(await client.getBalance(wallet.address))} | seqno: ${await wallet.getSeqNo()} 113 | ` 114 | ); 115 | 116 | return { wallet, walletBalance }; 117 | } 118 | 119 | export async function waitForSeqno(walletContract: WalletContract, seqno: number) { 120 | const seqnoStepInterval = 3000; 121 | console.log(`⏳ waiting for seqno to update (${seqno})`); 122 | for (var attempt = 0; attempt < 10; attempt++) { 123 | await sleep(seqnoStepInterval); 124 | const seqnoAfter = await walletContract.getSeqNo(); 125 | if (seqnoAfter > seqno) break; 126 | } 127 | console.log(`⌛️ seqno update after ${((attempt + 1) * seqnoStepInterval) / 1000}s`); 128 | } 129 | 130 | export async function waitForContractToBeDeployed(client: TonClient, deployedContract: Address) { 131 | const seqnoStepInterval = 2500; 132 | console.log(`⏳ waiting for contract to be deployed at [${deployedContract.toFriendly()}]`); 133 | for (var attempt = 0; attempt < 10; attempt++) { 134 | await sleep(seqnoStepInterval); 135 | if (await client.isContractDeployed(deployedContract)) { 136 | break; 137 | } 138 | } 139 | console.log(`⌛️ waited for contract deployment ${((attempt + 1) * seqnoStepInterval) / 1000}s`); 140 | } 141 | 142 | export async function sendTransaction( 143 | client: TonClient, 144 | walletContract: WalletContract, 145 | receivingContract: Address, 146 | value: BN, 147 | privateKey: Buffer, 148 | messageBody: Cell, 149 | bounce = false, 150 | sendMode = 3 151 | ) { 152 | const seqno = await walletContract.getSeqNo(); 153 | const bocStr = await messageBody.toString(); 154 | const boc = bocStr.substring(0, bocStr.length < BOC_MAX_LENGTH ? bocStr.length : BOC_MAX_LENGTH).toString(); 155 | console.log(`🧱 send Transaction to ${receivingContract.toFriendly()} value:${fromNano(value)}💎 [boc:${boc}]`); 156 | 157 | const transfer = await walletContract.createTransfer({ 158 | secretKey: privateKey, 159 | seqno: seqno, 160 | sendMode: sendMode, 161 | order: new InternalMessage({ 162 | to: receivingContract, 163 | value: value, 164 | bounce, 165 | body: new CommonMessageInfo({ 166 | body: new CellMessage(messageBody), 167 | }), 168 | }), 169 | }); 170 | console.log(`🚀 sending transaction to contract [seqno:${seqno}]`); 171 | 172 | client.sendExternalMessage(walletContract, transfer); 173 | 174 | await waitForSeqno(walletContract, seqno); 175 | } 176 | -------------------------------------------------------------------------------- /src/amm-wallet.ts: -------------------------------------------------------------------------------- 1 | import { SmartContract } from "ton-contract-executor"; 2 | import { Address, Cell, CellMessage, InternalMessage, Slice, CommonMessageInfo, ExternalMessage, TonClient, toNano } from "ton"; 3 | import BN from "bn.js"; 4 | import { toUnixTime, toDecimals, parseInternalMessageResponse } from "./utils"; 5 | import { OPS } from "./ops"; 6 | import { compileFuncToB64 } from "../utils/funcToB64"; 7 | import { bytesToAddress } from "../utils/deploy-utils"; 8 | import { printChain, iTvmBusContract, ExecutionResult, iDeployableContract } from "ton-tvm-bus"; 9 | import { isNamedExportBindings, isNumericLiteral } from "typescript"; 10 | const ZERO_ADDRESS = Address.parse("EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c"); 11 | 12 | type UsdcTransferNextOp = OPS.REMOVE_LIQUIDITY; 13 | 14 | export class AmmLpWallet implements iTvmBusContract { 15 | private initTime: number = Date.now(); 16 | public address: Address = ZERO_ADDRESS; 17 | initMessageResultRaw?: ExecutionResult; 18 | 19 | private constructor(public readonly contract: SmartContract, myAddress: Address, balance = toNano(0.5)) { 20 | this.contract.setC7Config({ 21 | myself: myAddress, 22 | balance, 23 | }); 24 | this.address = myAddress; 25 | } 26 | 27 | async getData() { 28 | let res = await this.contract.invokeGetMethod("get_wallet_data", []); 29 | const balance = res.result[0] as BN; 30 | const owner = res.result[1] as Slice; 31 | const jettonMaster = res.result[2] as Slice; 32 | const code = res.result[3] as Cell; 33 | const stakeStart = res.result[4] as BN; 34 | 35 | return { 36 | balance, 37 | owner, 38 | jettonMaster, 39 | code, 40 | stakeStart, 41 | }; 42 | } 43 | 44 | sendInternalMessage(message: InternalMessage) { 45 | return this.contract.sendInternalMessage(message); 46 | } 47 | 48 | async sendInternalMessage2(message: InternalMessage) { 49 | const res = await this.contract.sendInternalMessage(message); 50 | return parseInternalMessageResponse(res); 51 | } 52 | 53 | async init(fakeAddress: Address) { 54 | let messageBody = new Cell(); 55 | messageBody.bits.writeUint(1, 1); 56 | let msg = new CommonMessageInfo({ body: new CellMessage(messageBody) }); 57 | 58 | let res = await this.contract.sendExternalMessage( 59 | new ExternalMessage({ 60 | to: fakeAddress, 61 | body: msg, 62 | }) 63 | ); 64 | return res; 65 | } 66 | 67 | async removeLiquidity(amount: BN, responseAddress: Address, from: Address, to: Address, bounce = true, value = new BN(100000000)) { 68 | let messageBody = AmmLpWallet.RemoveLiquidityMessage(amount, responseAddress); 69 | let res = await this.contract.sendInternalMessage( 70 | new InternalMessage({ 71 | from: from, 72 | to: to, 73 | value, 74 | bounce, 75 | body: new CommonMessageInfo({ body: new CellMessage(messageBody) }), 76 | }) 77 | ); 78 | 79 | return parseInternalMessageResponse(res); 80 | } 81 | static async GetWalletAddress(client: TonClient, minterAddress: Address, walletAddress: Address) { 82 | try { 83 | let cell = new Cell(); 84 | cell.bits.writeAddress(walletAddress); 85 | let b64dataBuffer = (await cell.toBoc({ idx: false })).toString("base64"); 86 | let res = await client.callGetMethod(minterAddress, "get_wallet_address", [["tvm.Slice", b64dataBuffer]]); 87 | 88 | return bytesToAddress(res.stack[0][1].bytes); 89 | } catch (e) { 90 | console.log("exception", e); 91 | } 92 | } 93 | static async GetWalletData(client: TonClient, jettonWallet: Address) { 94 | let res = await client.callGetMethod(jettonWallet, "get_wallet_data", []); 95 | 96 | const balance = BigInt(res.stack[0][1]); 97 | const owner = bytesToAddress(res.stack[1][1].bytes); 98 | const jettonMaster = bytesToAddress(res.stack[2][1].bytes); 99 | 100 | return { 101 | balance, 102 | owner, 103 | jettonMaster, 104 | }; 105 | } 106 | 107 | static RemoveLiquidityMessage(amount: BN, responseAddress: Address) { 108 | let messageBody = new Cell(); 109 | messageBody.bits.writeUint(OPS.Burn, 32); // action 110 | messageBody.bits.writeUint(1, 64); // query-id 111 | messageBody.bits.writeCoins(amount); 112 | messageBody.bits.writeAddress(responseAddress); 113 | return messageBody; 114 | } 115 | 116 | // transfer#f8a7ea5 query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress 117 | // response_destination:MsgAddress custom_payload:(Maybe ^Cell) 118 | // forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) 119 | // = InternalMsgBody; 120 | 121 | async transferOverloaded( 122 | from: Address, 123 | to: Address, 124 | amount: BN, 125 | responseDestination: Address, 126 | customPayload: Cell | undefined, 127 | forwardTonAmount: BN = new BN(0), 128 | overloadOp: UsdcTransferNextOp, 129 | overloadValue: BN 130 | ) { 131 | let messageBody = new Cell(); 132 | messageBody.bits.writeUint(OPS.Transfer, 32); // action 133 | messageBody.bits.writeUint(1, 64); // query-id 134 | messageBody.bits.writeCoins(amount); 135 | messageBody.bits.writeAddress(to); 136 | messageBody.bits.writeAddress(responseDestination); 137 | messageBody.bits.writeBit(false); // null custom_payload 138 | messageBody.bits.writeCoins(forwardTonAmount); 139 | messageBody.bits.writeBit(false); // forward_payload in this slice, not separate messageBody 140 | messageBody.bits.writeUint(new BN(overloadOp), 32); 141 | // if (overloadOp == OPS.ADD_LIQUIDITY){ 142 | // messageBody.bits.writeUint(overloadOp ,32); // slippage 143 | // } else if (overloadOp == OPS.SWAP_TOKEN) { 144 | // messageBody.bits.writeCoins(overloadValue); // min amount out 145 | // } 146 | 147 | let res = await this.contract.sendInternalMessage( 148 | new InternalMessage({ 149 | from: from, 150 | to: to, 151 | value: toNano(1), 152 | bounce: false, 153 | body: new CommonMessageInfo({ body: new CellMessage(messageBody) }), 154 | }) 155 | ); 156 | return parseInternalMessageResponse(res); 157 | } 158 | 159 | async transfer(from: Address, to: Address, amount: BN, responseDestination: Address, forwardTonAmount: BN = new BN(0)) { 160 | let messageBody = new Cell(); 161 | messageBody.bits.writeUint(OPS.Transfer, 32); // action 162 | messageBody.bits.writeUint(1, 64); // query-id 163 | messageBody.bits.writeCoins(amount); 164 | messageBody.bits.writeAddress(to); 165 | messageBody.bits.writeAddress(responseDestination); 166 | messageBody.bits.writeBit(false); // null custom_payload 167 | messageBody.bits.writeCoins(forwardTonAmount); 168 | messageBody.bits.writeBit(false); // forward_payload in this slice, not separate messageBody 169 | 170 | let res = await this.contract.sendInternalMessage( 171 | new InternalMessage({ 172 | from: from, 173 | to: to, 174 | value: toNano(1), 175 | bounce: false, 176 | body: new CommonMessageInfo({ body: new CellMessage(messageBody) }), 177 | }) 178 | ); 179 | return parseInternalMessageResponse(res); 180 | } 181 | 182 | setUnixTime(time: number) { 183 | this.initTime = time; 184 | this.contract.setUnixTime(time); 185 | } 186 | 187 | forwardTime(time: number, contractCurrentTime?: number) { 188 | let newTime = contractCurrentTime || this.initTime + time; 189 | this.contract.setUnixTime(newTime); 190 | return newTime; 191 | } 192 | 193 | static compileWallet() { 194 | const ammWalletCodeB64: string = compileFuncToB64(["contracts/amm-wallet.fc"]); 195 | return Cell.fromBoc(ammWalletCodeB64); 196 | } 197 | 198 | static getCodeCell(): Cell[] { 199 | const ammWalletCodeB64: string = compileFuncToB64(["contracts/amm-wallet.fc"]); 200 | return Cell.fromBoc(ammWalletCodeB64); 201 | } 202 | 203 | static async createFromMessage(code: Cell, data: Cell, initMessage: InternalMessage) { 204 | const ammWallet = await SmartContract.fromCell(code, data, { getMethodsMutate: true, debug: true }); 205 | const instance = new AmmLpWallet(ammWallet, initMessage.to); 206 | instance.setUnixTime(toUnixTime(Date.now())); 207 | const initMessageResponse = await ammWallet.sendInternalMessage(initMessage); 208 | instance.initMessageResultRaw = initMessageResponse; 209 | 210 | return instance; 211 | } 212 | } 213 | 214 | // async function buildDataCell(totalSupply: BN, admin: Address, content: string, tokenCode: Cell) { 215 | 216 | // // ds~load_coins(), ;; total_supply 217 | // // ds~load_msg_addr(), ;; admin_address 218 | // // ds~load_ref(), ;; content 219 | // // ds~load_ref() ;; jetton_wallet_code 220 | 221 | // const contentCell = new Cell(); 222 | // contentCell.bits.writeString(content); 223 | 224 | // let dataCell = new Cell() 225 | // dataCell.bits.writeCoins(totalSupply); 226 | // dataCell.bits.writeAddress(admin) // name 227 | // dataCell.refs.push(contentCell); 228 | // dataCell.refs.push(tokenCode); 229 | // return dataCell 230 | // } 231 | -------------------------------------------------------------------------------- /contracts/amm-wallet.fc: -------------------------------------------------------------------------------- 1 | #include "imports/stdlib.fc"; 2 | #include "imports/op-codes.fc"; 3 | #include "imports/params.fc"; 4 | #include "imports/jetton-utils.fc"; 5 | 6 | const MIN_TONS_FOR_STORAGE = 10000000; ;; 0.01 TON 7 | const GAS_CONSUMPTION = 10000000; ;; 0.01 TON 8 | 9 | {- 10 | Storage 11 | storage#_ balance:Coins owner_address:MsgAddressInt jetton_master_address:MsgAddressInt jetton_wallet_code:^Cell = Storage; 12 | -} 13 | 14 | (int, slice, slice, cell) load_data() inline { 15 | slice ds = get_data().begin_parse(); 16 | return (ds~load_coins(), ds~load_msg_addr(), ds~load_msg_addr(), ds~load_ref()); 17 | } 18 | 19 | () save_data (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) impure inline { 20 | set_data(pack_jetton_wallet_data(balance, owner_address, jetton_master_address, jetton_wallet_code)); 21 | } 22 | 23 | {- 24 | transfer query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress 25 | response_destination:MsgAddress custom_payload:(Maybe ^Cell) 26 | forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) 27 | = InternalMsgBody; 28 | internal_transfer query_id:uint64 amount:(VarUInteger 16) from:MsgAddress 29 | response_address:MsgAddress 30 | forward_ton_amount:(VarUInteger 16) 31 | forward_payload:(Either Cell ^Cell) 32 | = InternalMsgBody; 33 | -} 34 | 35 | () send_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure { 36 | int query_id = in_msg_body~load_uint(64); 37 | int jetton_amount = in_msg_body~load_coins(); 38 | slice to_owner_address = in_msg_body~load_msg_addr(); 39 | force_chain(to_owner_address); 40 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 41 | balance -= jetton_amount; 42 | 43 | throw_unless(705, equal_slices(owner_address, sender_address)); 44 | throw_unless(706, balance >= 0); 45 | 46 | cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, jetton_wallet_code); 47 | slice to_wallet_address = calculate_jetton_wallet_address(state_init); 48 | slice response_address = in_msg_body~load_msg_addr(); 49 | cell custom_payload = in_msg_body~load_dict(); 50 | int forward_ton_amount = in_msg_body~load_coins(); 51 | throw_unless(708, slice_bits(in_msg_body) >= 1); 52 | slice either_forward_payload = in_msg_body; 53 | var msg = begin_cell() 54 | .store_uint(0x18, 6) 55 | .store_slice(to_wallet_address) 56 | .store_coins(0) 57 | .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) 58 | .store_ref(state_init); 59 | var msg_body = begin_cell() 60 | .store_uint(OP_INTERNAL_TRANSFER, 32) 61 | .store_uint(query_id, 64) 62 | .store_coins(jetton_amount) 63 | .store_slice(owner_address) 64 | .store_slice(response_address) 65 | .store_coins(forward_ton_amount) 66 | .store_slice(either_forward_payload) 67 | .end_cell(); 68 | 69 | msg = msg.store_ref(msg_body); 70 | int fwd_count = forward_ton_amount ? 2 : 1; 71 | throw_unless(709, msg_value > 72 | forward_ton_amount + 73 | ;; 3 messages: wal1->wal2, wal2->owner, wal2->response 74 | ;; but last one is optional (it is ok if it fails) 75 | fwd_count * fwd_fee + 76 | (2 * GAS_CONSUMPTION + MIN_TONS_FOR_STORAGE)); 77 | ;; universal message send fee calculation may be activated here 78 | ;; by using this instead of fwd_fee 79 | ;; msg_fwd_fee(to_wallet, msg_body, state_init, 15) 80 | 81 | send_raw_message(msg.end_cell(), 64); ;; revert on errors 82 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 83 | } 84 | 85 | {- 86 | internal_transfer query_id:uint64 amount:(VarUInteger 16) from:MsgAddress 87 | response_address:MsgAddress 88 | forward_ton_amount:(VarUInteger 16) 89 | forward_payload:(Either Cell ^Cell) 90 | = InternalMsgBody; 91 | -} 92 | 93 | () receive_tokens (slice in_msg_body, slice sender_address, int my_ton_balance, int fwd_fee, int msg_value) impure { 94 | ;; NOTE we can not allow fails in action phase since in that case there will be 95 | ;; no bounce. Thus check and throw in computation phase. 96 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 97 | int query_id = in_msg_body~load_uint(64); 98 | int jetton_amount = in_msg_body~load_coins(); 99 | balance += jetton_amount; 100 | slice from_address = in_msg_body~load_msg_addr(); 101 | slice response_address = in_msg_body~load_msg_addr(); 102 | throw_unless(707, 103 | equal_slices(jetton_master_address, sender_address) 104 | | 105 | equal_slices(calculate_user_jetton_wallet_address(from_address, jetton_master_address, jetton_wallet_code), sender_address) 106 | ); 107 | int forward_ton_amount = in_msg_body~load_coins(); 108 | 109 | int ton_balance_before_msg = my_ton_balance - msg_value; 110 | int storage_fee = MIN_TONS_FOR_STORAGE - min(ton_balance_before_msg, MIN_TONS_FOR_STORAGE); 111 | msg_value -= (storage_fee + GAS_CONSUMPTION); 112 | if(forward_ton_amount) { 113 | msg_value -= (forward_ton_amount + fwd_fee); 114 | slice either_forward_payload = in_msg_body; 115 | 116 | var msg_body = begin_cell() 117 | .store_uint(OP_TRANSFER_NOTIFICATION, 32) 118 | .store_uint(query_id, 64) 119 | .store_coins(jetton_amount) 120 | .store_slice(from_address) 121 | .store_slice(either_forward_payload) 122 | .end_cell(); 123 | 124 | var msg = begin_cell() 125 | .store_uint(0x18, 6) ;; we should not bounce here cause receiver can have uninitialized contract 126 | .store_slice(owner_address) 127 | .store_coins(forward_ton_amount) 128 | .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) 129 | .store_ref(msg_body); 130 | 131 | send_raw_message(msg.end_cell(), 1); 132 | } 133 | 134 | if ((response_address.preload_uint(2) != 0) & (msg_value > 0)) { 135 | var msg = begin_cell() 136 | .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000 137 | .store_slice(response_address) 138 | .store_coins(msg_value) 139 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 140 | .store_uint(OP_EXCESSES, 32) 141 | .store_uint(query_id, 64); 142 | send_raw_message(msg.end_cell(), 2); 143 | } 144 | 145 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 146 | } 147 | 148 | () burn_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure { 149 | ;; NOTE we can not allow fails in action phase since in that case there will be 150 | ;; no bounce. Thus check and throw in computation phase. 151 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 152 | int query_id = in_msg_body~load_uint(64); 153 | int jetton_amount = in_msg_body~load_coins(); 154 | slice response_address = in_msg_body~load_msg_addr(); 155 | ;; ignore custom payload 156 | ;; slice custom_payload = in_msg_body~load_dict(); 157 | balance -= jetton_amount; 158 | throw_unless(705, equal_slices(owner_address, sender_address)); 159 | throw_unless(706, balance >= 0); 160 | throw_unless(707, msg_value > fwd_fee + 2 * GAS_CONSUMPTION); 161 | 162 | var msg_body = begin_cell() 163 | .store_uint(OP_BURN_NOTIFICAITON, 32) 164 | .store_uint(query_id, 64) 165 | .store_coins(jetton_amount) 166 | .store_slice(owner_address) 167 | .store_slice(response_address) 168 | .end_cell(); 169 | 170 | var msg = begin_cell() 171 | .store_uint(0x18, 6) 172 | .store_slice(jetton_master_address) 173 | .store_coins(0) 174 | .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) 175 | .store_ref(msg_body); 176 | 177 | send_raw_message(msg.end_cell(), 64); 178 | 179 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 180 | } 181 | 182 | () on_bounce (slice in_msg_body) impure { 183 | in_msg_body~skip_bits(32); ;; 0xFFFFFFFF 184 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 185 | int op = in_msg_body~load_uint(32); 186 | throw_unless(709, (op == OP_INTERNAL_TRANSFER) | (op == OP_BURN_NOTIFICAITON)); 187 | int query_id = in_msg_body~load_uint(64); 188 | int jetton_amount = in_msg_body~load_coins(); 189 | balance += jetton_amount; 190 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 191 | } 192 | 193 | () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { 194 | if (in_msg_body.slice_empty?()) { ;; ignore empty messages 195 | return (); 196 | } 197 | 198 | slice cs = in_msg_full.begin_parse(); 199 | int flags = cs~load_uint(4); 200 | if (flags & 1) { 201 | on_bounce(in_msg_body); 202 | return (); 203 | } 204 | slice sender_address = cs~load_msg_addr(); 205 | cs~load_msg_addr(); ;; skip dst 206 | cs~load_coins(); ;; skip value 207 | cs~skip_bits(1); ;; skip extracurrency collection 208 | cs~load_coins(); ;; skip ihr_fee 209 | int fwd_fee = cs~load_coins(); ;; we use message fwd_fee for estimation of forward_payload costs 210 | 211 | int op = in_msg_body~load_uint(32); 212 | 213 | if (op == OP_TRANSFER) { ;; outgoing transfer 214 | send_tokens(in_msg_body, sender_address, msg_value, fwd_fee); 215 | return (); 216 | } 217 | 218 | if (op == OP_INTERNAL_TRANSFER) { ;; incoming transfer 219 | receive_tokens(in_msg_body, sender_address, my_balance, fwd_fee, msg_value); 220 | return (); 221 | } 222 | 223 | if (op == OP_BURN) { ;; burn 224 | burn_tokens(in_msg_body, sender_address, msg_value, fwd_fee); 225 | return (); 226 | } 227 | 228 | throw(0xffff); 229 | } 230 | 231 | (int, slice, slice, cell) get_wallet_data() method_id { 232 | return load_data(); 233 | } -------------------------------------------------------------------------------- /test/jetton-wallet.fc: -------------------------------------------------------------------------------- 1 | #include "imports/stdlib.fc"; 2 | #include "imports/params.fc"; 3 | #include "imports/op-codes.fc"; 4 | #include "imports/jetton-utils.fc"; 5 | 6 | ;; Jetton Wallet Smart Contract 7 | 8 | {- 9 | 10 | NOTE that this tokens can be transferred within the same workchain. 11 | 12 | This is suitable for most tokens, if you need tokens transferable between workchains there are two solutions: 13 | 14 | 1) use more expensive but universal function to calculate message forward fee for arbitrary destination (see `misc/forward-fee-calc.cs`) 15 | 16 | 2) use token holder proxies in target workchain (that way even 'non-universal' token can be used from any workchain) 17 | 18 | -} 19 | 20 | int min_tons_for_storage() asm "10000000 PUSHINT"; ;; 0.01 TON 21 | int gas_consumption() asm "10000000 PUSHINT"; ;; 0.01 TON 22 | 23 | {- 24 | Storage 25 | storage#_ balance:Coins owner_address:MsgAddressInt jetton_master_address:MsgAddressInt jetton_wallet_code:^Cell = Storage; 26 | -} 27 | 28 | (int, slice, slice, cell) load_data() inline { 29 | slice ds = get_data().begin_parse(); 30 | return (ds~load_coins(), ds~load_msg_addr(), ds~load_msg_addr(), ds~load_ref()); 31 | } 32 | 33 | () save_data (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) impure inline { 34 | set_data(pack_jetton_wallet_data(balance, owner_address, jetton_master_address, jetton_wallet_code)); 35 | } 36 | 37 | {- 38 | transfer query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress 39 | response_destination:MsgAddress custom_payload:(Maybe ^Cell) 40 | forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) 41 | = InternalMsgBody; 42 | internal_transfer query_id:uint64 amount:(VarUInteger 16) from:MsgAddress 43 | response_address:MsgAddress 44 | forward_ton_amount:(VarUInteger 16) 45 | forward_payload:(Either Cell ^Cell) 46 | = InternalMsgBody; 47 | -} 48 | 49 | () send_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure { 50 | int query_id = in_msg_body~load_uint(64); 51 | int jetton_amount = in_msg_body~load_coins(); 52 | slice to_owner_address = in_msg_body~load_msg_addr(); 53 | force_chain(to_owner_address); 54 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 55 | balance -= jetton_amount; 56 | 57 | throw_unless(705, equal_slices(owner_address, sender_address)); 58 | throw_unless(706, balance >= 0); 59 | 60 | cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, jetton_wallet_code); 61 | slice to_wallet_address = calculate_jetton_wallet_address(state_init); 62 | slice response_address = in_msg_body~load_msg_addr(); 63 | cell custom_payload = in_msg_body~load_dict(); 64 | int forward_ton_amount = in_msg_body~load_coins(); 65 | throw_unless(708, slice_bits(in_msg_body) >= 1); 66 | slice either_forward_payload = in_msg_body; 67 | var msg = begin_cell() 68 | .store_uint(0x18, 6) 69 | .store_slice(to_wallet_address) 70 | .store_coins(0) 71 | .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) 72 | .store_ref(state_init); 73 | var msg_body = begin_cell() 74 | .store_uint(OP_INTERNAL_TRANSFER, 32) 75 | .store_uint(query_id, 64) 76 | .store_coins(jetton_amount) 77 | .store_slice(owner_address) 78 | .store_slice(response_address) 79 | .store_coins(forward_ton_amount) 80 | .store_slice(either_forward_payload) 81 | .end_cell(); 82 | 83 | msg = msg.store_ref(msg_body); 84 | int fwd_count = forward_ton_amount ? 2 : 1; 85 | throw_unless(709, msg_value > 86 | forward_ton_amount + 87 | ;; 3 messages: wal1->wal2, wal2->owner, wal2->response 88 | ;; but last one is optional (it is ok if it fails) 89 | fwd_count * fwd_fee + 90 | (2 * gas_consumption() + min_tons_for_storage())); 91 | ;; universal message send fee calculation may be activated here 92 | ;; by using this instead of fwd_fee 93 | ;; msg_fwd_fee(to_wallet, msg_body, state_init, 15) 94 | 95 | send_raw_message(msg.end_cell(), 64); ;; revert on errors 96 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 97 | } 98 | 99 | {- 100 | internal_transfer query_id:uint64 amount:(VarUInteger 16) from:MsgAddress 101 | response_address:MsgAddress 102 | forward_ton_amount:(VarUInteger 16) 103 | forward_payload:(Either Cell ^Cell) 104 | = InternalMsgBody; 105 | -} 106 | 107 | () receive_tokens (slice in_msg_body, slice sender_address, int my_ton_balance, int fwd_fee, int msg_value) impure { 108 | ;; NOTE we can not allow fails in action phase since in that case there will be 109 | ;; no bounce. Thus check and throw in computation phase. 110 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 111 | int query_id = in_msg_body~load_uint(64); 112 | int jetton_amount = in_msg_body~load_coins(); 113 | balance += jetton_amount; 114 | slice from_address = in_msg_body~load_msg_addr(); 115 | slice response_address = in_msg_body~load_msg_addr(); 116 | throw_unless(707, 117 | equal_slices(jetton_master_address, sender_address) 118 | | 119 | equal_slices(calculate_user_jetton_wallet_address(from_address, jetton_master_address, jetton_wallet_code), sender_address) 120 | ); 121 | int forward_ton_amount = in_msg_body~load_coins(); 122 | 123 | int ton_balance_before_msg = my_ton_balance - msg_value; 124 | int storage_fee = min_tons_for_storage() - min(ton_balance_before_msg, min_tons_for_storage()); 125 | msg_value -= (storage_fee + gas_consumption()); 126 | if(forward_ton_amount) { 127 | msg_value -= (forward_ton_amount + fwd_fee); 128 | slice either_forward_payload = in_msg_body; 129 | 130 | var msg_body = begin_cell() 131 | .store_uint(OP_TRANSFER_NOTIFICATION, 32) 132 | .store_uint(query_id, 64) 133 | .store_coins(jetton_amount) 134 | .store_slice(from_address) 135 | .store_slice(either_forward_payload) 136 | .end_cell(); 137 | 138 | var msg = begin_cell() 139 | .store_uint(0x10, 6) ;; we should not bounce here cause receiver can have uninitialized contract 140 | .store_slice(owner_address) 141 | .store_coins(forward_ton_amount) 142 | .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) 143 | .store_ref(msg_body); 144 | 145 | send_raw_message(msg.end_cell(), 1); 146 | } 147 | 148 | if ((response_address.preload_uint(2) != 0) & (msg_value > 0)) { 149 | var msg = begin_cell() 150 | .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000 151 | .store_slice(response_address) 152 | .store_coins(msg_value) 153 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 154 | .store_uint(OP_EXCESSES, 32) 155 | .store_uint(query_id, 64); 156 | send_raw_message(msg.end_cell(), 2); 157 | } 158 | 159 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 160 | } 161 | 162 | () burn_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure { 163 | ;; NOTE we can not allow fails in action phase since in that case there will be 164 | ;; no bounce. Thus check and throw in computation phase. 165 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 166 | int query_id = in_msg_body~load_uint(64); 167 | int jetton_amount = in_msg_body~load_coins(); 168 | slice response_address = in_msg_body~load_msg_addr(); 169 | ;; ignore custom payload 170 | ;; slice custom_payload = in_msg_body~load_dict(); 171 | balance -= jetton_amount; 172 | throw_unless(705, equal_slices(owner_address, sender_address)); 173 | throw_unless(706, balance >= 0); 174 | throw_unless(707, msg_value > fwd_fee + 2 * gas_consumption()); 175 | 176 | var msg_body = begin_cell() 177 | .store_uint(OP_BURN_NOTIFICAITON, 32) 178 | .store_uint(query_id, 64) 179 | .store_coins(jetton_amount) 180 | .store_slice(owner_address) 181 | .store_slice(response_address) 182 | .end_cell(); 183 | 184 | var msg = begin_cell() 185 | .store_uint(0x18, 6) 186 | .store_slice(jetton_master_address) 187 | .store_coins(0) 188 | .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) 189 | .store_ref(msg_body); 190 | 191 | send_raw_message(msg.end_cell(), 64); 192 | 193 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 194 | } 195 | 196 | () on_bounce (slice in_msg_body) impure { 197 | in_msg_body~skip_bits(32); ;; 0xFFFFFFFF 198 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 199 | int op = in_msg_body~load_uint(32); 200 | throw_unless(709, (op == OP_INTERNAL_TRANSFER) | (op == OP_BURN_NOTIFICAITON)); 201 | int query_id = in_msg_body~load_uint(64); 202 | int jetton_amount = in_msg_body~load_coins(); 203 | balance += jetton_amount; 204 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 205 | } 206 | 207 | () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { 208 | if (in_msg_body.slice_empty?()) { ;; ignore empty messages 209 | return (); 210 | } 211 | 212 | slice cs = in_msg_full.begin_parse(); 213 | int flags = cs~load_uint(4); 214 | if (flags & 1) { 215 | on_bounce(in_msg_body); 216 | return (); 217 | } 218 | slice sender_address = cs~load_msg_addr(); 219 | cs~load_msg_addr(); ;; skip dst 220 | cs~load_coins(); ;; skip value 221 | cs~skip_bits(1); ;; skip extracurrency collection 222 | cs~load_coins(); ;; skip ihr_fee 223 | int fwd_fee = cs~load_coins(); ;; we use message fwd_fee for estimation of forward_payload costs 224 | 225 | int op = in_msg_body~load_uint(32); 226 | 227 | if (op == OP_TRANSFER) { ;; outgoing transfer 228 | send_tokens(in_msg_body, sender_address, msg_value, fwd_fee); 229 | return (); 230 | } 231 | 232 | if (op == OP_INTERNAL_TRANSFER) { ;; incoming transfer 233 | receive_tokens(in_msg_body, sender_address, my_balance, fwd_fee, msg_value); 234 | return (); 235 | } 236 | 237 | if (op == OP_BURN) { ;; burn 238 | burn_tokens(in_msg_body, sender_address, msg_value, fwd_fee); 239 | return (); 240 | } 241 | 242 | throw(0xffff); 243 | } 244 | 245 | (int, slice, slice, cell) get_wallet_data() method_id { 246 | return load_data(); 247 | } -------------------------------------------------------------------------------- /src/jetton-minter.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { SmartContract, SuccessfulExecutionResult } from "ton-contract-executor"; 3 | 4 | import { 5 | Address, 6 | Cell, 7 | CellMessage, 8 | InternalMessage, 9 | Slice, 10 | CommonMessageInfo, 11 | ExternalMessage, 12 | toNano, 13 | TonClient, 14 | contractAddress, 15 | Contract, 16 | beginCell, 17 | } from "ton"; 18 | import BN from "bn.js"; 19 | import { toUnixTime, sliceToAddress, bytesToBase64, writeString, filterLogs } from "./utils"; 20 | import { compileFuncToB64 } from "../utils/funcToB64"; 21 | import { bytesToAddress } from "../utils/deploy-utils"; 22 | import { OPS } from "./ops"; 23 | import { TvmBus, iTvmBusContract, ExecutionResult } from "ton-tvm-bus"; 24 | 25 | const OFFCHAIN_CONTENT_PREFIX = 0x01; 26 | 27 | export class JettonMinter implements iTvmBusContract { 28 | private constructor(contract: SmartContract, myAddress: Address, balance: BN) { 29 | this.contract = contract; 30 | this.address = myAddress; 31 | 32 | this.contract.setC7Config({ 33 | myself: myAddress, 34 | balance: balance, 35 | }); 36 | } 37 | 38 | contract?: SmartContract; 39 | public address?: Address; 40 | 41 | async getData() { 42 | if (!this.contract) { 43 | return; 44 | } 45 | 46 | let res = await this.contract.invokeGetMethod("get_jetton_data", []); 47 | //@ts-ignore 48 | const totalSupply = res.result[0] as BN; 49 | const wc = res.result[1] as BN; 50 | const jettonMaster = res.result[2] as Slice; 51 | const content = res.result[2] as Cell; 52 | const code = res.result[3] as Cell; 53 | 54 | return { 55 | totalSupply, 56 | wc, 57 | content, 58 | jettonMaster, 59 | code, 60 | }; 61 | } 62 | 63 | async init(fakeAddress: Address) { 64 | if (!this.contract) { 65 | return; 66 | } 67 | let messageBody = new Cell(); 68 | messageBody.bits.writeUint(1, 1); 69 | let msg = new CommonMessageInfo({ body: new CellMessage(messageBody) }); 70 | 71 | let res = await this.contract.sendExternalMessage( 72 | new ExternalMessage({ 73 | to: fakeAddress, 74 | body: msg, 75 | }) 76 | ); 77 | return res; 78 | } 79 | 80 | // const body = new Cell(); 81 | // body.bits.writeUint(21, 32); // OP mint 82 | // body.bits.writeUint(params.queryId || 0, 64); // query_id 83 | // body.bits.writeAddress(params.destination); 84 | // body.bits.writeCoins(params.amount); // in Toncoins 85 | 86 | // const transferBody = new Cell(); // internal transfer 87 | // transferBody.bits.writeUint(0x178d4519, 32); // internal_transfer op 88 | // transferBody.bits.writeUint(params.queryId || 0, 64); 89 | // transferBody.bits.writeCoins(params.jettonAmount); 90 | // transferBody.bits.writeAddress(null); // from_address 91 | // transferBody.bits.writeAddress(null); // response_address 92 | // transferBody.bits.writeCoins(new BN(0)); // forward_amount 93 | // transferBody.bits.writeBit(false); // forward_payload in this slice, not separate cell 94 | 95 | static Mint(receiver: Address, jettonAmount: BN, tonAmount = toNano(0.04)) { 96 | let messageBody = new Cell(); 97 | messageBody.bits.writeUint(OPS.MINT, 32); // action; 98 | messageBody.bits.writeUint(1, 64); // query; 99 | messageBody.bits.writeAddress(receiver); 100 | messageBody.bits.writeCoins(tonAmount); 101 | 102 | const masterMessage = new Cell(); 103 | masterMessage.bits.writeUint(0x178d4519, 32); // action; 104 | masterMessage.bits.writeUint(0, 64); // query; 105 | masterMessage.bits.writeCoins(jettonAmount); 106 | masterMessage.bits.writeAddress(null); // from_address 107 | masterMessage.bits.writeAddress(null); // response_address 108 | masterMessage.bits.writeCoins(new BN(0)); // forward_amount 109 | masterMessage.bits.writeBit(false); // forward_payload in this slice, not separate cell 110 | 111 | messageBody.refs.push(masterMessage); 112 | return messageBody; 113 | } 114 | 115 | static async GetWalletAddress(client: TonClient, minterAddress: Address, walletAddress: Address) { 116 | try { 117 | let cell = new Cell(); 118 | cell.bits.writeAddress(walletAddress); 119 | 120 | // tonweb style 121 | const b64data = bytesToBase64(await cell.toBoc({ idx: false })); 122 | // nodejs buffer 123 | let b64dataBuffer = (await cell.toBoc({ idx: false })).toString("base64"); 124 | let res = await client.callGetMethod(minterAddress, "get_wallet_address", [["tvm.Slice", b64dataBuffer]]); 125 | 126 | return bytesToAddress(res.stack[0][1].bytes); 127 | } catch (e) { 128 | console.log("exception", e); 129 | } 130 | } 131 | 132 | async mint(sender: Address, receiver: Address, jettonAmount: BN) { 133 | if (!this.contract) { 134 | return; 135 | } 136 | let res = await this.contract.sendInternalMessage( 137 | new InternalMessage({ 138 | from: sender, 139 | to: this.address as Address, 140 | value: new BN(10001), 141 | bounce: false, 142 | body: new CommonMessageInfo({ 143 | body: new CellMessage(JettonMinter.Mint(receiver, jettonAmount)), 144 | }), 145 | }) 146 | ); 147 | 148 | let successResult = res as SuccessfulExecutionResult; 149 | 150 | return { 151 | ...res, 152 | returnValue: res.result[1] as BN, 153 | logs: filterLogs(res.logs), 154 | }; 155 | } 156 | 157 | async sendInternalMessage(message: InternalMessage) { 158 | if (!this.contract) { 159 | return Promise.resolve({} as ExecutionResult); 160 | } 161 | return this.contract.sendInternalMessage(message); 162 | } 163 | 164 | async mintMessage(sender: Address, receiver: Address, jettonAmount: BN) { 165 | return new InternalMessage({ 166 | from: sender, 167 | to: this.address as Address, 168 | value: toNano(0.1), 169 | bounce: false, 170 | body: new CommonMessageInfo({ 171 | body: new CellMessage(JettonMinter.Mint(receiver, jettonAmount)), 172 | }), 173 | }); 174 | } 175 | 176 | // burn#595f07bc query_id:uint64 amount:(VarUInteger 16) 177 | // response_destination:MsgAddress custom_payload:(Maybe ^Cell) 178 | // = InternalMsgBody; 179 | async receiveBurn(subWalletOwner: Address, sourceWallet: Address, amount: BN) { 180 | if (!this.contract) { 181 | return; 182 | } 183 | let messageBody = new Cell(); 184 | messageBody.bits.writeUint(OPS.Burn_notification, 32); // action 185 | messageBody.bits.writeUint(1, 64); // query-id 186 | messageBody.bits.writeCoins(amount); // jetton amount received 187 | messageBody.bits.writeAddress(sourceWallet); 188 | 189 | const removeLiquidityAmount = 300000; 190 | let customPayload = new Cell(); 191 | customPayload.bits.writeUint(2, 32); // sub op for removing liquidty 192 | customPayload.bits.writeCoins(removeLiquidityAmount); // sub op for removing liquidty 193 | 194 | messageBody.refs.push(customPayload); 195 | 196 | let res = await this.contract.sendInternalMessage( 197 | new InternalMessage({ 198 | from: subWalletOwner, 199 | to: this.address as Address, 200 | value: new BN(10000), 201 | bounce: false, 202 | body: new CommonMessageInfo({ body: new CellMessage(messageBody) }), 203 | }) 204 | ); 205 | 206 | let successResult = res as SuccessfulExecutionResult; 207 | //console.log(res); 208 | return { 209 | ...res, 210 | returnValue: res.result[1] as BN, 211 | logs: filterLogs(res.logs), 212 | }; 213 | } 214 | 215 | async getJettonData() { 216 | if (!this.contract) { 217 | return; 218 | } 219 | let data = await this.contract.invokeGetMethod("get_jetton_data", []); 220 | const rawAddress = data.result[2] as Slice; 221 | return { 222 | totalSupply: data.result[0] as BN, 223 | mintable: data.result[1] as BN, 224 | adminAddress: sliceToAddress(rawAddress, true), 225 | content: data.result[3], 226 | jettonWalletCode: data.result[4], 227 | }; 228 | } 229 | 230 | setUnixTime(time: number) { 231 | if (!this.contract) { 232 | return; 233 | } 234 | this.contract.setUnixTime(time); 235 | } 236 | 237 | static async createDeployData(totalSupply: BN, tokenAdmin: Address, content: string) { 238 | const jettonWalletCode = await serializeWalletCodeToCell(); 239 | const initDataCell = await buildStateInit(totalSupply, tokenAdmin, content, jettonWalletCode[0]); 240 | const minterSources = await serializeMinterCodeToCell(); 241 | return { 242 | codeCell: minterSources, 243 | initDataCell, 244 | }; 245 | } 246 | 247 | static async Create(totalSupply: BN, tokenAdmin: Address, content: string, tvmBus?: TvmBus, balance = toNano("1")) { 248 | const jettonWalletCode = await serializeWalletCodeToCell(); 249 | const stateInit = await buildStateInit(totalSupply, tokenAdmin, content, jettonWalletCode[0]); 250 | const cellCode = await CompileCodeToCell(); 251 | 252 | let contract = await SmartContract.fromCell(cellCode[0], stateInit, { 253 | getMethodsMutate: true, 254 | }); 255 | const myAddress = await contractAddress({ 256 | workchain: 0, 257 | initialData: stateInit, 258 | initialCode: jettonWalletCode[0], 259 | }); 260 | 261 | const instance = new JettonMinter(contract, myAddress, balance); 262 | if (tvmBus) { 263 | tvmBus.registerContract(instance); 264 | } 265 | 266 | instance.setUnixTime(toUnixTime(Date.now())); 267 | return instance; 268 | } 269 | } 270 | 271 | // custom solution, using func to compile, and fift to serialize the code into a string 272 | async function serializeWalletCodeToCell() { 273 | const jettonWalletCodeB64: string = compileFuncToB64(["test/jetton-wallet.fc"]); 274 | return Cell.fromBoc(jettonWalletCodeB64); 275 | } 276 | 277 | async function serializeMinterCodeToCell() { 278 | const jettonMinterCodeB64: string = compileFuncToB64(["test/jetton-minter.fc"]); 279 | return Cell.fromBoc(jettonMinterCodeB64); 280 | } 281 | 282 | async function CompileCodeToCell() { 283 | const ammMinterCodeB64: string = compileFuncToB64(["test/jetton-minter.fc"]); 284 | return Cell.fromBoc(ammMinterCodeB64); 285 | } 286 | 287 | async function buildStateInit(totalSupply: BN, admin: Address, contentUri: string, tokenCode: Cell) { 288 | let contentCell = beginCell().storeInt(OFFCHAIN_CONTENT_PREFIX, 8).storeBuffer(Buffer.from(contentUri, "ascii")).endCell(); 289 | 290 | let dataCell = new Cell(); 291 | dataCell.bits.writeCoins(totalSupply); 292 | dataCell.bits.writeAddress(admin); 293 | dataCell.refs.push(contentCell); 294 | dataCell.refs.push(tokenCode); 295 | return dataCell; 296 | } 297 | -------------------------------------------------------------------------------- /contracts/amm-minter.fc: -------------------------------------------------------------------------------- 1 | #include "imports/stdlib.fc"; 2 | #include "imports/op-codes.fc"; 3 | #include "imports/op-codes-amm.fc"; 4 | #include "imports/params.fc"; 5 | #include "imports/jetton-utils.fc"; ;; https://github.com/ton-blockchain/token-contract/blob/main/ft/jetton-utils.fc 6 | #include "imports/store.fc"; 7 | #include "imports/amm-minter-utils.fc"; 8 | 9 | const MIN_JETTON_STORAGE = 10000000; ;; 0.01 TON 10 | const JETTON_GAS_COMPUTATION = 10000000; ;; 0.01 TON 11 | const ADD_LIQUIDTY_GAS_CONSUMPTION = 4000000; ;; 0.04 12 | const ADD_LIQUIDTY_DUST = 100000000; ;; 0.1 13 | const zero_address = "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c"a; 14 | const MAX_SLIPAGE = 99; ;; TODO 15 | 16 | 17 | () mint_tokens(slice to_address, int query_id, int amount, int ton_leftovers) impure { 18 | 19 | ;; end wallet is expecting the amount and owner, 20 | cell mint_msg = begin_cell() 21 | .store_uint(OP_INTERNAL_TRANSFER, 32) 22 | .store_uint(query_id, 64) 23 | .store_coins(amount) 24 | .store_slice(to_address) ;; 25 | .store_slice(to_address) ;; response_destination -> the address should get the jetton leftovers via #Excesses 26 | .store_coins(0) 27 | .store_uint(0, 1) 28 | .end_cell(); 29 | 30 | cell state_init = calculate_jetton_wallet_state_init( 31 | to_address, 32 | my_address(), 33 | store::jetton_wallet_code 34 | ); 35 | 36 | slice to_wallet_address = calculate_jetton_wallet_address(state_init); 37 | var msg = begin_cell() 38 | .store_uint(0x18, 6) 39 | .store_slice(to_wallet_address) 40 | .store_coins(ton_leftovers) 41 | .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) 42 | .store_ref(state_init) 43 | .store_ref(mint_msg); 44 | 45 | send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors 46 | } 47 | 48 | 49 | () add_liquidity( 50 | slice in_msg_body, 51 | int jetton_amount, 52 | slice jetton_sender, 53 | int msg_value, 54 | slice sender, 55 | int fwd_fee, 56 | int query_id) impure { 57 | int slippage = in_msg_body~load_uint(32); 58 | int ton_liquidity = in_msg_body~load_grams(); 59 | if (0 == jetton_amount) | (msg_value == 0) | slippage > MAX_SLIPAGE { 60 | revert_add_liquidity( 61 | ton_liquidity, 62 | jetton_amount, 63 | jetton_sender, 64 | query_id, 65 | msg_value, 66 | fwd_fee 67 | ); 68 | return (); 69 | } 70 | ;; - ton_liquidity is the amount that stays in the reserves 71 | ;; - fwd_fee + gas_consumption needs to cover the gas fees in the execuation 72 | int add_liquidity_gas_cost = msg_value - (ton_liquidity + jetton_transfer_fee(fwd_fee) + ADD_LIQUIDTY_GAS_CONSUMPTION); 73 | ;; add_liquidity_gas_cost should cover the new LP or updating it ,and the rest should return 74 | ;; to jetton owenr via excess message 75 | 76 | ;; min jetton storage for the lp wallet plus 1 message for mint 1 77 | int next_message_gas_cost = (fwd_fee * 2) + JETTON_GAS_COMPUTATION + MIN_JETTON_STORAGE; 78 | ;; gas_left should cover 79 | if( add_liquidity_gas_cost < next_message_gas_cost ) 80 | | 81 | ( ton_liquidity < ADD_LIQUIDTY_DUST) { 82 | revert_add_liquidity( 83 | ton_liquidity, 84 | jetton_amount, 85 | jetton_sender, 86 | query_id, 87 | msg_value, 88 | fwd_fee 89 | ); 90 | return (); 91 | } 92 | int should_revert = 0; 93 | 94 | if store::ton_reserves > 0 { 95 | 96 | int optimal_ton = quote(jetton_amount, store::token_reserves, store::ton_reserves); 97 | int optimal_jetton = quote(ton_liquidity, store::ton_reserves, store::token_reserves); 98 | 99 | int ton_liquidity_min = muldiv(ton_liquidity, ( 100 - slippage ), 100); 100 | int jeton_liquidity_min = muldiv(jetton_amount, ( 100 - slippage ), 100); 101 | 102 | if( ( optimal_ton <= ton_liquidity_min ) | (optimal_jetton <= jeton_liquidity_min) ) { 103 | revert_add_liquidity( 104 | ton_liquidity, 105 | jetton_amount, 106 | jetton_sender, 107 | query_id, 108 | msg_value, 109 | fwd_fee 110 | ); 111 | should_revert = 1; 112 | } else { 113 | int extra_ton = ton_liquidity - optimal_ton; 114 | int extra_jeton = jetton_amount - optimal_jetton; 115 | 116 | ;; return extra's 117 | if extra_jeton > ADD_LIQUIDTY_DUST { 118 | transfer_token(query_id, jetton_sender, extra_jeton, fwd_fee * 2 ); ;; TODO send 0.1TON use fwd 119 | ton_liquidity = optimal_ton; 120 | } 121 | elseif extra_ton > ADD_LIQUIDTY_DUST { 122 | send_grams(jetton_sender ,extra_ton); 123 | ;; token_liquidity = optimal_token; 124 | } 125 | } 126 | } 127 | 128 | if should_revert == 1 { 129 | return (); 130 | } 131 | 132 | int minted_lp = calculate_new_lp(ton_liquidity, jetton_amount); 133 | if equal_slices(store::token_wallet_address, zero_address) { 134 | store::token_wallet_address = sender; 135 | } 136 | 137 | mint_tokens(jetton_sender, query_id, minted_lp, add_liquidity_gas_cost); 138 | save_data(); 139 | 140 | return (); 141 | } 142 | 143 | () remove_liquidity( 144 | int jetton_amount, 145 | slice from_address, 146 | int query_id, 147 | slice sender, 148 | int msg_value, 149 | int fwd_fee) impure inline { 150 | 151 | int ton_to_remove = muldiv(jetton_amount, store::ton_reserves, store::total_supply); 152 | int token_to_remove = muldiv(jetton_amount, store::token_reserves, store::total_supply); 153 | 154 | throw_unless(ERROR::WRONG_JETTON_SENDER_ADDRESS, 155 | equal_slices( 156 | calculate_user_jetton_wallet_address(from_address,my_address(), 157 | store::jetton_wallet_code 158 | ) 159 | , sender) 160 | ); 161 | 162 | ;; 1 fwd-fee for grams message 163 | ;; 1 token_transfer_fee 164 | msg_value -= jetton_transfer_fee(fwd_fee) + fwd_fee; 165 | 166 | 167 | transfer_token( 168 | query_id, 169 | from_address, 170 | token_to_remove, 171 | msg_value 172 | ); 173 | send_grams(from_address, ton_to_remove); 174 | 175 | store::ton_reserves -= ton_to_remove; 176 | store::token_reserves -= token_to_remove; 177 | store::total_supply -= jetton_amount; 178 | save_data(); 179 | return (); 180 | } 181 | 182 | () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { 183 | if (in_msg_body.slice_empty?()) { ;; ignore empty messages 184 | return (); 185 | } 186 | slice cs = in_msg_full.begin_parse(); 187 | int flags = cs~load_uint(4); 188 | if (flags & 1) { 189 | return (); 190 | } 191 | 192 | slice sender = cs~load_msg_addr(); 193 | cs~load_msg_addr(); ;; skip dst 194 | cs~load_coins(); ;; skip value 195 | cs~skip_bits(1); ;; skip extracurrency collection 196 | cs~load_coins(); ;; skip ihr_fee 197 | int fwd_fee = cs~load_coins(); ;; we use message fwd_fee for estimation of forward_payload costs 198 | 199 | int op = in_msg_body~load_uint(32); 200 | int query_id = in_msg_body~load_uint(64); 201 | 202 | load_data(); 203 | 204 | if (op == OP_BURN_NOTIFICAITON) { 205 | 206 | int jetton_amount = in_msg_body ~load_coins(); 207 | slice from_address = in_msg_body ~load_msg_addr(); 208 | 209 | remove_liquidity( 210 | jetton_amount, 211 | from_address, 212 | query_id, 213 | sender, 214 | msg_value, 215 | fwd_fee 216 | ); 217 | return (); 218 | } 219 | 220 | if (op == OP_SWAP_TON ) { 221 | 222 | int ton_to_swap = in_msg_body~load_coins(); 223 | int min_amount_out = in_msg_body~load_coins(); 224 | throw_if(ERROR::INSFUCIENT_FUNDS, min_amount_out > store::token_reserves); 225 | throw_if(ERROR::ZERO_RESERVES, 0 == store::token_reserves); 226 | throw_if(ERROR::NULL_JETTON_WALLET, equal_slices(store::token_wallet_address, zero_address) ); 227 | ;; insfucient ton in the message 228 | throw_if(ERROR::MSG_VALUE_INSUFFICIENT, ton_to_swap > msg_value ); 229 | 230 | swap_tokens( 231 | min_amount_out, 232 | ton_to_swap, 233 | true, 234 | sender, 235 | query_id, 236 | msg_value, 237 | fwd_fee 238 | ); 239 | save_data(); 240 | return (); 241 | } 242 | 243 | if (op == OP_TRANSFER_NOTIFICATION ) { 244 | 245 | int jetton_amount = in_msg_body~load_coins(); 246 | slice jetton_sender = in_msg_body~load_msg_addr(); 247 | int noPayload = in_msg_body~load_uint(1); 248 | if (noPayload == -1 ) { 249 | return (); 250 | } 251 | 252 | ;; accept add-liquidity and swap actions only from token_wallet_address 253 | ;; W-ADDRESS != ZERO && W-ADDRESS != SENDER => THROW , WRONG SENDER 254 | if (equal_slices(store::token_wallet_address, zero_address) == false) 255 | & 256 | (equal_slices(store::token_wallet_address, sender) == false) { 257 | 258 | throw(ERROR::WRONG_JETTON_WALLET); 259 | } 260 | 261 | int sub_op = in_msg_body~load_uint(32); 262 | 263 | if( sub_op == OP_ADD_LIQUIDITY ) { 264 | add_liquidity( 265 | in_msg_body, 266 | jetton_amount, 267 | jetton_sender, 268 | msg_value, 269 | sender, 270 | fwd_fee, 271 | query_id 272 | ); 273 | return (); 274 | } 275 | 276 | ;; swap TRC20 -> TON 277 | if (sub_op == OP_SWAP_TOKEN ) { 278 | ;; bonuce message will be handled properly by the jetton sender 279 | throw_if(ERROR::NULL_JETTON_WALLET, equal_slices(store::token_wallet_address, zero_address)); 280 | 281 | int min_amount_out = in_msg_body~load_grams(); 282 | swap_tokens( 283 | min_amount_out, 284 | jetton_amount, 285 | false, 286 | jetton_sender, 287 | query_id, 288 | msg_value, 289 | fwd_fee 290 | ); 291 | 292 | save_data(); 293 | return (); 294 | } 295 | return (); 296 | } 297 | 298 | if (op == OP_CODE_UPGRADE & equal_slices(sender, store::admin) ) { 299 | cell code = in_msg_body~load_ref(); 300 | set_code(code); 301 | return (); 302 | } 303 | 304 | 305 | throw(0xffff); 306 | } 307 | 308 | 309 | (int, int, slice, int, int, slice, cell, cell) get_jetton_data() method_id { 310 | load_data(); 311 | 312 | return ( 313 | store::total_supply, 314 | -1, 315 | store::token_wallet_address, 316 | store::ton_reserves, 317 | store::token_reserves, 318 | store::admin, 319 | store::content, 320 | store::jetton_wallet_code 321 | ); 322 | } 323 | 324 | slice get_wallet_address(slice owner_address) method_id { 325 | load_data(); 326 | 327 | return calculate_user_jetton_wallet_address( 328 | owner_address, 329 | my_address(), 330 | store::jetton_wallet_code 331 | ); 332 | } 333 | 334 | 335 | int version() method_id { 336 | return 101; 337 | } -------------------------------------------------------------------------------- /src/amm-minter.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore 2 | import { SmartContract } from "ton-contract-executor"; 3 | import BN from "bn.js"; 4 | import { filterLogs, parseInternalMessageResponse } from "./utils"; 5 | import { AmmLpWallet } from "./amm-wallet"; 6 | import { Address, Cell, CellMessage, InternalMessage, Slice, CommonMessageInfo, TonClient, contractAddress, toNano, fromNano } from "ton"; 7 | import { sliceToAddress267, toUnixTime } from "./utils"; 8 | import { OPS } from "./ops"; 9 | import { compileFuncToB64 } from "../utils/funcToB64"; 10 | import { bytesToAddress } from "../utils/deploy-utils"; 11 | import { writeString } from "./utils"; 12 | import { OutAction } from "ton-contract-executor"; 13 | import { printChain, TvmBus, iTvmBusContract } from "ton-tvm-bus"; 14 | 15 | const myContractAddress = Address.parse("EQBZjEzfbSslRltL4z7ncz_tb-t1z5R98zAKUuWLO1QTINTA"); 16 | 17 | export class AmmMinterMessages { 18 | static swapTon(tonToSwap: BN, minAmountOut: BN): Cell { 19 | let cell = new Cell(); 20 | cell.bits.writeUint(OPS.SWAP_TON, 32); // action 21 | cell.bits.writeUint(1, 64); // query-id 22 | cell.bits.writeCoins(tonToSwap); // swapping amount of tons 23 | cell.bits.writeCoins(minAmountOut); // minimum received 24 | return cell; 25 | } 26 | static compileCodeToCell() { 27 | const ammMinterCodeB64: string = compileFuncToB64(["contracts/amm-minter.fc"]); 28 | return Cell.fromBoc(ammMinterCodeB64); 29 | } 30 | 31 | static buildDataCell(content: string, admin: Address) { 32 | const contentCell = new Cell(); 33 | writeString(contentCell, content); 34 | 35 | const dataCell = new Cell(); 36 | dataCell.bits.writeCoins(0); // total-supply 37 | dataCell.bits.writeAddress(zeroAddress()); // token_wallet_address starts as null 38 | dataCell.bits.writeCoins(0); // ton-reserves 39 | dataCell.bits.writeCoins(0); // token-reserves 40 | dataCell.bits.writeAddress(admin); 41 | dataCell.refs.push(contentCell); 42 | dataCell.refs.push(AmmLpWallet.compileWallet()[0]); 43 | return { 44 | initDataCell: dataCell, 45 | codeCell: this.compileCodeToCell(), 46 | }; 47 | } 48 | 49 | static getCodeUpgrade() { 50 | const ammMinterCodeB64: string = compileFuncToB64(["contracts/amm-minter-upgrade.fc"]); 51 | return Cell.fromBoc(ammMinterCodeB64); 52 | } 53 | } 54 | 55 | export class AmmMinterRPC { 56 | address = zeroAddress(); 57 | client: TonClient; 58 | resolveReady = () => { }; 59 | ready = new Promise(this.resolveReady); 60 | 61 | constructor(opts: { address?: Address; rpcClient: TonClient }) { 62 | this.client = opts.rpcClient; 63 | if (opts.address) { 64 | this.address = opts.address; 65 | this.resolveReady(); 66 | } 67 | } 68 | 69 | setAddress(address: Address) { 70 | this.address = address; 71 | this.resolveReady(); 72 | } 73 | 74 | async getWalletAddress(walletAddress: Address) { 75 | try { 76 | let cell = new Cell(); 77 | cell.bits.writeAddress(walletAddress); 78 | let b64dataBuffer = (await cell.toBoc({ idx: false })).toString("base64"); 79 | let res = await this.client.callGetMethod(this.address, "get_wallet_address", [["tvm.Slice", b64dataBuffer]]); 80 | return bytesToAddress(res.stack[0][1].bytes); 81 | } catch (e) { 82 | console.log("exception", e); 83 | } 84 | } 85 | 86 | async getAmountOut(amountIn: BN, reserveIn: BN, reserveOut: BN) { 87 | let res = await this.client.callGetMethod(this.address, "get_amount_out", [ 88 | ["num", amountIn.toString()], 89 | ["num", reserveIn.toString()], 90 | ["num", reserveOut.toString()], 91 | ]); 92 | return { 93 | minAmountOut: BigInt(res.stack[0][1]), 94 | }; 95 | } 96 | //int get_amount_in(int amountOut, int reserveIn, int reserveOut) method_id { 97 | async getAmountIn(amountOut: BN, reserveIn: BN, reserveOut: BN) { 98 | let res = await this.client.callGetMethod(this.address, "get_amount_in", [ 99 | ["num", amountOut.toString()], 100 | ["num", reserveIn.toString()], 101 | ["num", reserveOut.toString()], 102 | ]); 103 | return { 104 | amountIn: BigInt(res.stack[0][1]), 105 | }; 106 | } 107 | 108 | async getJettonData() { 109 | let res = await this.client.callGetMethod(this.address, "get_jetton_data", []); 110 | 111 | const totalSupply = res.stack[0][1] as string; 112 | const mintable = res.stack[1][1] as string; 113 | const jettonWalletAddressBytes = res.stack[2][1].bytes as string; 114 | const tonReserves = res.stack[3][1] as string; 115 | const tokenReserves = res.stack[4][1] as string; 116 | return { 117 | totalSupply, 118 | jettonWalletAddress: bytesToAddress(jettonWalletAddressBytes), 119 | mintable, 120 | tonReserves, 121 | tokenReserves, 122 | }; 123 | } 124 | } 125 | 126 | export class AmmMinterTVM implements iTvmBusContract { 127 | contract?: SmartContract; 128 | ready?: Promise; 129 | balance?: BN; 130 | address?: Address; 131 | 132 | constructor(contentUri: string, admin: Address, tvmBus?: TvmBus, balance = toNano(1)) { 133 | this.init(contentUri, admin, balance, tvmBus); 134 | } 135 | 136 | async init(contentUri: string, admin: Address, balance: BN, tvmBus?: TvmBus) { 137 | const data = AmmMinterMessages.buildDataCell(contentUri, admin); 138 | const code = AmmMinterMessages.compileCodeToCell(); 139 | const address = contractAddress({ 140 | workchain: 0, 141 | initialCode: code[0], 142 | initialData: data.initDataCell, 143 | }); 144 | this.address = address; 145 | 146 | this.ready = SmartContract.fromCell(code[0], data.initDataCell, { 147 | getMethodsMutate: true, 148 | debug: true, 149 | }); 150 | const contract = await this.ready; 151 | this.contract = contract; 152 | this.balance = balance; 153 | 154 | contract.setUnixTime(toUnixTime(Date.now())); 155 | contract.setC7Config({ 156 | myself: address, 157 | balance: balance, 158 | }); 159 | if (tvmBus) { 160 | tvmBus.registerContract(this); 161 | } 162 | } 163 | 164 | async getData() { 165 | if (!this.contract) { 166 | throw "contract is not initialized"; 167 | } 168 | let res = await this.contract.invokeGetMethod("get_jetton_data", []); 169 | const totalSupply = res.result[0] as BN; 170 | const mintable = res.result[1] as BN; 171 | //@ts-ignore 172 | const tokenWalletAddress = sliceToAddress267(res.result[2] as BN).toFriendly(); 173 | const tonReserves = res.result[3] as BN; 174 | const tokenReserves = res.result[4] as BN; 175 | //@ts-ignore 176 | // const admin = sliceToAddress267(res.result[5] as BN).toFriendly(); 177 | const content = res.result[2] as Cell; 178 | 179 | return { 180 | totalSupply, 181 | mintable, 182 | tokenWalletAddress, 183 | tonReserves, 184 | tokenReserves, 185 | content, 186 | }; 187 | } 188 | 189 | async sendInternalMessage(message: InternalMessage) { 190 | if (!this.contract) { 191 | throw "contract is not initialized"; 192 | } 193 | 194 | let nMessage = new InternalMessage({ 195 | to: message.to, 196 | from: message.from, 197 | value: message.value, //ignore this.balance for now 198 | bounce: message.bounce, 199 | body: message.body, 200 | }); 201 | 202 | let res = await this.contract.sendInternalMessage(nMessage); 203 | let parsedResponse = parseInternalMessageResponse(res); 204 | if (res.exit_code == 0) { 205 | // this.calculateBalance(message.value, parsedResponse.actions); 206 | } 207 | return parsedResponse; 208 | } 209 | 210 | async swapTonTVM(from: Address, tonToSwap: BN, minAmountOut: BN, valueOverride?: BN) { 211 | const gasFee = new BN(200000000); 212 | let messageBody = AmmMinterMessages.swapTon(tonToSwap, minAmountOut); 213 | 214 | if (valueOverride) { 215 | tonToSwap = valueOverride.add(gasFee); 216 | } 217 | return this.sendTvmMessage({ messageBody, from, value: tonToSwap.add(gasFee), to: myContractAddress, bounce: true }); 218 | } 219 | 220 | // burn#595f07bc query_id:uint64 amount:(VarUInteger 16) 221 | // response_destination:MsgAddress custom_payload:(Maybe ^Cell) 222 | // = InternalMsgBody; 223 | async receiveBurn(subWalletOwner: Address, sourceWallet: Address, amount: BN) { 224 | let messageBody = new Cell(); 225 | messageBody.bits.writeUint(OPS.Burn_notification, 32); // action 226 | messageBody.bits.writeUint(1, 64); // query-id 227 | messageBody.bits.writeCoins(amount); // jetton amount received 228 | messageBody.bits.writeAddress(sourceWallet); 229 | 230 | const removeLiquidityAmount = 300000; 231 | let customPayload = new Cell(); 232 | customPayload.bits.writeUint(2, 32); // sub op for removing liquidty 233 | customPayload.bits.writeCoins(removeLiquidityAmount); // sub op for removing liquidty 234 | 235 | messageBody.refs.push(customPayload); 236 | 237 | return this.sendTvmMessage({ messageBody, from: subWalletOwner, value: new BN(10000), to: myContractAddress, bounce: true }); 238 | } 239 | 240 | async getAmountOut(amountIn: BN, reserveIn: BN, reserveOut: BN) { 241 | if (!this.contract) { 242 | throw "contract is not initialized"; 243 | } 244 | let res = await this.contract.invokeGetMethod("get_amount_out", [ 245 | { type: "int", value: amountIn.toString() }, 246 | { type: "int", value: reserveIn.toString() }, 247 | { type: "int", value: reserveOut.toString() }, 248 | ]); 249 | 250 | const minAmountOut = res.result[0] as BN; 251 | return { 252 | minAmountOut, 253 | }; 254 | } 255 | 256 | async sendTvmMessage(opts: { messageBody: Cell; from: Address; to: Address; value: BN; bounce: boolean }) { 257 | if (!this.contract) { 258 | throw "contract is not initialized"; 259 | } 260 | 261 | let res = await this.contract.sendInternalMessage( 262 | new InternalMessage({ 263 | from: opts.from, 264 | to: myContractAddress, 265 | value: opts.value, 266 | bounce: false, 267 | body: new CommonMessageInfo({ body: new CellMessage(opts.messageBody) }), 268 | }) 269 | ); 270 | 271 | // let successResult = res as SuccessfulExecutionResult; 272 | // const actions = parseActionsList(successResult.action_list_cell); 273 | // if (res.exit_code == 0) { 274 | // this.calculateBalance(opts.value, actions); 275 | // } 276 | 277 | return { 278 | ...res, 279 | logs: filterLogs(res.logs), 280 | }; 281 | } 282 | 283 | // calculateBalance(inValue: BN, actions: OutAction[]) { 284 | // this.balance = this.balance?.add(inValue); 285 | // let outGoingTon = actions.map((action: OutAction) => { 286 | // // @ts-ignore 287 | // const sendMessageAction = action as SendMsgOutAction; 288 | // return sendMessageAction.value?.coins; 289 | // }); 290 | // let outGoingTonSum = new BN(0); 291 | // outGoingTon.forEach((it: BN) => { 292 | // if (it) { 293 | // outGoingTonSum.add(it); 294 | // } 295 | // }); 296 | 297 | // //console.log({ outGoingTonSum: outGoingTonSum.toString(), balance: this.balance.toString() }); 298 | // this.balance = this.balance?.sub(outGoingTonSum); 299 | // } 300 | 301 | async getHowOld() { 302 | if (!this.contract) { 303 | throw "contract is not initialized"; 304 | } 305 | let res = await this.contract.invokeGetMethod("how_old", []); 306 | return res; 307 | } 308 | } 309 | 310 | // Null Address EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c 311 | function zeroAddress() { 312 | let cell = new Cell(); 313 | cell.bits.writeUint(2, 2); 314 | cell.bits.writeUint(0, 1); 315 | cell.bits.writeUint(0, 8); 316 | cell.bits.writeUint(0x0000000000000000, 256); 317 | 318 | return cell.beginParse().readAddress() as Address; 319 | } 320 | -------------------------------------------------------------------------------- /deploy/e2e.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | TonClient, 4 | Cell, 5 | WalletContract, 6 | InternalMessage, 7 | CommonMessageInfo, 8 | StateInit, 9 | contractAddress, 10 | toNano, 11 | fromNano, 12 | beginCell, 13 | } from "ton"; 14 | import { JettonMinter } from "../src/jetton-minter"; 15 | import BN from "bn.js"; 16 | import fs from "fs"; 17 | 18 | import { 19 | hexToBn, 20 | initDeployKey, 21 | initWallet, 22 | printAddresses, 23 | printBalances, 24 | printDeployerBalances, 25 | sendTransaction, 26 | sleep, 27 | waitForContractToBeDeployed, 28 | } from "../utils/deploy-utils"; 29 | import { JettonWallet } from "../src/jetton-wallet"; 30 | import { AmmMinterRPC } from "../src/amm-minter"; 31 | import { OPS } from "../src/ops"; 32 | import { AmmLpWallet } from "../src/amm-wallet"; 33 | import { compileFuncToB64 } from "../utils/funcToB64"; 34 | 35 | //axiosThrottle.use(axios, { requestsPerSecond: 0.5 }); // required since toncenter jsonRPC limits to 1 req/sec without API key 36 | const client = new TonClient({ 37 | endpoint: "https://sandbox.tonhubapi.com/jsonRPC", 38 | // endpoint: "https://scalable-api.tonwhales.com/jsonRPC", 39 | // endpoint: "https://toncenter.com/api/v2/jsonRPC", 40 | }); 41 | 42 | enum GAS_FEES { 43 | ADD_LIQUIDITY = 0.2, 44 | REMOVE_LIQUIDITY = 0.25, 45 | SWAP_FEE = 0.04, 46 | SWAP_TON_FEE = 0.08, 47 | SWAP_FORWARD_TON = 0.04, 48 | MINT = 0.2, 49 | } 50 | 51 | const metadataURI = "https://api.jsonbin.io/b/eloelo1"; // Salt for ammMinter contract 52 | 53 | const TON_LIQUIDITY = 4; 54 | const TOKEN_LIQUIDITY = toNano(100); 55 | const TOKEN_TO_SWAP = toNano(25); 56 | const TON_TO_SWAP = toNano(1); 57 | const MINT_SIZE = toNano(100); 58 | const JETTON_URI = "http://tonswap.co/token/usdc31.json"; // Salt for USDC contract 59 | 60 | const BLOCK_TIME = 10000; 61 | 62 | async function deployUSDCMinter( 63 | client: TonClient, 64 | walletContract: WalletContract, 65 | owner: Address, 66 | privateKey: Buffer, 67 | jettonUri: string, 68 | workchain = 0 69 | ) { 70 | const { codeCell, initDataCell } = await JettonMinter.createDeployData(new BN(20000), owner, jettonUri); 71 | 72 | const newContractAddress = await contractAddress({ 73 | workchain, 74 | initialData: initDataCell, 75 | initialCode: codeCell[0], 76 | }); 77 | 78 | if (await client.isContractDeployed(newContractAddress)) { 79 | console.log(`contract: ${newContractAddress.toFriendly()} already Deployed`); 80 | return { 81 | address: newContractAddress, 82 | }; 83 | } 84 | 85 | const seqno = await walletContract.getSeqNo(); 86 | const transfer = await walletContract.createTransfer({ 87 | secretKey: privateKey, 88 | seqno: seqno, 89 | sendMode: 1 + 2, 90 | order: new InternalMessage({ 91 | to: newContractAddress, 92 | value: toNano(0.25), 93 | bounce: false, 94 | body: new CommonMessageInfo({ 95 | stateInit: new StateInit({ data: initDataCell, code: codeCell[0] }), 96 | body: null, 97 | }), 98 | }), 99 | }); 100 | 101 | await client.sendExternalMessage(walletContract, transfer); 102 | await waitForContractToBeDeployed(client, newContractAddress); 103 | console.log(`- Deploy transaction sent successfully to -> ${newContractAddress.toFriendly()} [seqno:${seqno}]`); 104 | await sleep(BLOCK_TIME); 105 | return { 106 | address: newContractAddress, 107 | }; 108 | } 109 | 110 | async function deployMinter(client: TonClient, deployWallet: WalletContract, privateKey: Buffer) { 111 | const usdcMinter = await deployUSDCMinter(client, deployWallet, deployWallet.address, privateKey, JETTON_URI); 112 | const data = await client.callGetMethod(usdcMinter.address, "get_jetton_data", []); 113 | console.log(`usdcMinter totalSupply: ${fromNano(data.stack[0][1]).toString()}`); 114 | saveAddress("USDC-Minter", usdcMinter.address); 115 | return usdcMinter; 116 | } 117 | 118 | async function deployAmmMinter(client: TonClient, walletContract: WalletContract, privateKey: Buffer, workchain = 0) { 119 | const ammMinterRPC = new AmmMinterRPC({ rpcClient: client }); 120 | const { codeCell, initDataCell } = await ammMinterRPC.buildDataCell(metadataURI, walletContract.address); 121 | 122 | const newContractAddress = await contractAddress({ 123 | workchain, 124 | initialData: initDataCell, 125 | initialCode: codeCell[0], 126 | }); 127 | 128 | saveAddress("AMM-Minter", newContractAddress); 129 | console.log(` - Deploy AmmMinter the contract on-chain.. [${newContractAddress.toFriendly()}]`); 130 | 131 | if (await client.isContractDeployed(newContractAddress)) { 132 | console.log(`${newContractAddress.toFriendly()} already Deployed`); 133 | return new AmmMinterRPC({ 134 | address: newContractAddress, 135 | rpcClient: client, 136 | }); 137 | } 138 | 139 | const seqno = await walletContract.getSeqNo(); 140 | 141 | const transfer = await walletContract.createTransfer({ 142 | secretKey: privateKey, 143 | seqno: seqno, 144 | sendMode: 1 + 2, 145 | order: new InternalMessage({ 146 | to: newContractAddress, 147 | value: toNano(0.15), 148 | bounce: false, 149 | body: new CommonMessageInfo({ 150 | stateInit: new StateInit({ data: initDataCell, code: codeCell[0] }), 151 | body: null, 152 | }), 153 | }), 154 | }); 155 | 156 | await client.sendExternalMessage(walletContract, transfer); 157 | await waitForContractToBeDeployed(client, newContractAddress); 158 | 159 | console.log(`- Deploy transaction sent successfully to -> ${newContractAddress.toFriendly()}`); 160 | 161 | // new contracts takes time 162 | await sleep(BLOCK_TIME); 163 | 164 | ammMinterRPC.setAddress(newContractAddress); 165 | return ammMinterRPC; 166 | } 167 | 168 | export const addressToName: { [key: string]: string } = {}; 169 | 170 | function saveAddress(name: string, addr: Address) { 171 | addressToName[addr.toFriendly()] = name; 172 | } 173 | 174 | async function mintUSDC(usdcMinter: Address, deployWallet: WalletContract, privateKey: Buffer) { 175 | console.log(` 176 | 🎬 minting deployer some usdc's , 100$ 177 | `); 178 | await sendTransaction( 179 | client, 180 | deployWallet, 181 | usdcMinter, 182 | toNano(GAS_FEES.MINT), 183 | privateKey, 184 | JettonMinter.Mint(deployWallet.address, toNano(MINT_SIZE).add(toNano(TOKEN_TO_SWAP))) 185 | ); 186 | } 187 | 188 | async function addLiquidity(ammMinter: AmmMinterRPC, deployWallet: WalletContract, deployerUSDCAddress: Address, privateKey: Buffer) { 189 | console.log(` 190 | 🎬 sending Add Liquidity message | ${TON_LIQUIDITY} 💎 : ${fromNano(TOKEN_LIQUIDITY)}💲 191 | `); 192 | const addLiquidityMessage = JettonWallet.TransferOverloaded( 193 | ammMinter.address, 194 | TOKEN_LIQUIDITY, // jetton-amount 195 | ammMinter.address, 196 | toNano(TON_LIQUIDITY + GAS_FEES.ADD_LIQUIDITY / 2), 197 | OPS.ADD_LIQUIDITY, 198 | new BN(5), // Slippage 199 | toNano(TON_LIQUIDITY) 200 | ); 201 | 202 | await sendTransaction( 203 | client, 204 | deployWallet, 205 | deployerUSDCAddress as Address, 206 | toNano(TON_LIQUIDITY + GAS_FEES.ADD_LIQUIDITY), 207 | privateKey, 208 | addLiquidityMessage 209 | ); 210 | 211 | printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 212 | } 213 | 214 | async function swapUsdcToTon( 215 | ammMinter: AmmMinterRPC, 216 | deployWallet: WalletContract, 217 | deployerUSDCAddress: Address, 218 | privateKey: Buffer, 219 | tokenToSell: BN 220 | ) { 221 | const ammData2 = await ammMinter.getJettonData(); 222 | 223 | const minTonToReceive = await ( 224 | await ammMinter.getAmountOut(tokenToSell, hexToBn(ammData2.tokenReserves), hexToBn(ammData2.tonReserves)) 225 | ).minAmountOut; 226 | 227 | console.log( 228 | `\n🎬 Swap ${fromNano(new BN(tokenToSell.toString())).toString()}$ USDC to 💎Ton (expecting for ${fromNano( 229 | minTonToReceive.toString() 230 | )} 💎Ton)` 231 | ); 232 | const swapTokenMessage = JettonWallet.TransferOverloaded( 233 | ammMinter.address, 234 | new BN(tokenToSell.toString()), 235 | deployWallet.address, // i should get the change 236 | toNano(GAS_FEES.SWAP_FORWARD_TON), 237 | OPS.SWAP_TOKEN, 238 | new BN(minTonToReceive.toString()) // Min Amount out (TON) 239 | ); 240 | 241 | await sendTransaction( 242 | client, 243 | deployWallet, 244 | deployerUSDCAddress as Address, 245 | toNano(GAS_FEES.SWAP_FORWARD_TON + GAS_FEES.SWAP_FEE), 246 | privateKey, 247 | swapTokenMessage 248 | ); 249 | 250 | //await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 251 | } 252 | 253 | async function swapTonToUsdc( 254 | ammMinter: AmmMinterRPC, 255 | deployWallet: WalletContract, 256 | deployerUSDCAddress: Address, 257 | privateKey: Buffer, 258 | tokenToReceive: BN 259 | ) { 260 | const ammData = await ammMinter.getJettonData(); 261 | 262 | const tonToSwap = await ammMinter.getAmountIn(tokenToReceive, hexToBn(ammData.tonReserves), hexToBn(ammData.tokenReserves)); 263 | 264 | console.log(` 265 | 🎬 Swap ${fromNano(tonToSwap.amountIn.toString())}💎 to $ (expecting for ${fromNano(tokenToReceive.toString())} $ ) 266 | `); 267 | const tonToSend = new BN(tonToSwap.amountIn.toString()); 268 | const swapTonMessage = ammMinter.swapTon(tonToSend, tokenToReceive); 269 | 270 | await sendTransaction( 271 | client, 272 | deployWallet, 273 | ammMinter.address, 274 | tonToSend.add(toNano(GAS_FEES.SWAP_TON_FEE)), 275 | privateKey, 276 | swapTonMessage 277 | ); 278 | 279 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 280 | } 281 | 282 | async function removeLiquidity( 283 | ammMinter: AmmMinterRPC, 284 | deployWallet: WalletContract, 285 | deployerUSDCAddress: Address, 286 | privateKey: Buffer, 287 | sendMode = 3 288 | ) { 289 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 290 | 291 | const lpAddress = (await ammMinter.getWalletAddress(deployWallet.address)) as Address; 292 | const lpData = await AmmLpWallet.GetWalletData(client, lpAddress); 293 | saveAddress("LP-Wallet", lpAddress); 294 | 295 | console.log(` 296 | 🎬 Remove Liquidity of ${fromNano(lpData.balance.toString())} LP's 297 | `); 298 | 299 | const removeLiqMessage = AmmLpWallet.RemoveLiquidityMessage(new BN(lpData.balance.toString()), deployWallet.address); 300 | 301 | let messageIsBounceable = true; 302 | 303 | await sendTransaction( 304 | client, 305 | deployWallet, 306 | lpAddress, 307 | toNano(GAS_FEES.REMOVE_LIQUIDITY), 308 | privateKey, 309 | removeLiqMessage, 310 | messageIsBounceable, 311 | sendMode 312 | ); 313 | 314 | sleep(BLOCK_TIME); 315 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 316 | } 317 | 318 | // async function codeUpgrade(ammMinter: AmmMinterRPC, deployWallet: WalletContract, deployerUSDCAddress: Address, privateKey: Buffer) { 319 | // const ammMinterCodeB64: string = compileFuncToB64(["contracts/amm-minter-upgrade.fc"]); 320 | // let codeCell = Cell.fromBoc(ammMinterCodeB64); 321 | // const upgradeMessage = beginCell().storeUint(26, 32).storeUint(1, 64).storeRef(codeCell[0]).endCell(); 322 | // await sendTransaction(client, deployWallet, ammMinter.address, toNano(GAS_FEES.SWAP_FEE), privateKey, upgradeMessage); 323 | // let age = await client.callGetMethod(ammMinter.address, "how_old", []); 324 | // console.log(age); 325 | // } 326 | 327 | async function main() { 328 | if (fs.existsSync("./build/tmp.fif")) { 329 | fs.rmSync("./build/tmp.fif"); 330 | } 331 | 332 | const walletKey = await initDeployKey(); 333 | let { wallet: deployWallet, walletBalance } = await initWallet(client, walletKey.publicKey); 334 | saveAddress("Deployer", deployWallet.address); 335 | 336 | const usdcMinter = await deployMinter(client, deployWallet, walletKey.secretKey); 337 | // await sleep(BLOCK_TIME); 338 | // await mintUSDC(usdcMinter.address, deployWallet, walletKey.secretKey); 339 | 340 | const deployerUSDCAddress = (await JettonMinter.GetWalletAddress(client, usdcMinter.address, deployWallet.address)) as Address; 341 | saveAddress("DeployerUSDC", deployerUSDCAddress); 342 | printDeployerBalances(client, deployWallet.address, deployerUSDCAddress); 343 | 344 | const ammMinter = await deployAmmMinter(client, deployWallet, walletKey.secretKey); 345 | 346 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 347 | 348 | // // ======== add-liquidity 349 | await addLiquidity(ammMinter, deployWallet, deployerUSDCAddress as Address, walletKey.secretKey); 350 | await sleep(BLOCK_TIME * 2); 351 | 352 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 353 | 354 | const swapCount = 8; 355 | console.log(`Swapping ${swapCount} times both ways`); 356 | 357 | for (let i = 0; i < swapCount; i++) { 358 | // // ======== Swap Ton -> USDC 359 | await swapTonToUsdc(ammMinter, deployWallet, deployerUSDCAddress as Address, walletKey.secretKey, TOKEN_TO_SWAP); 360 | await sleep(BLOCK_TIME * 2); 361 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 362 | 363 | // ======== Swap Usdc -> TON 364 | 365 | await swapUsdcToTon(ammMinter, deployWallet, deployerUSDCAddress as Address, walletKey.secretKey, TOKEN_TO_SWAP); 366 | await sleep(BLOCK_TIME * 2); 367 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 368 | } 369 | 370 | await sleep(BLOCK_TIME); 371 | 372 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 373 | 374 | // // ======= Remove Liquidity 375 | 376 | await removeLiquidity(ammMinter, deployWallet, deployerUSDCAddress as Address, walletKey.secretKey, 64); 377 | await sleep(BLOCK_TIME); 378 | 379 | // await codeUpgrade(ammMinter, deployWallet, deployerUSDCAddress as Address, walletKey.secretKey); 380 | 381 | const currentBalance = await client.getBalance(deployWallet.address); 382 | console.log(`Deployer spent about ${fromNano(currentBalance.sub(walletBalance))} 💎`); 383 | 384 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 385 | printAddresses(addressToName); 386 | } 387 | 388 | (async () => { 389 | await main(); 390 | })(); 391 | -------------------------------------------------------------------------------- /test/imports/stdlib.fc: -------------------------------------------------------------------------------- 1 | ;; Standard library for funC 2 | ;; 3 | 4 | forall X -> tuple cons(X head, tuple tail) asm "CONS"; 5 | forall X -> (X, tuple) uncons(tuple list) asm "UNCONS"; 6 | forall X -> (tuple, X) list_next(tuple list) asm( -> 1 0) "UNCONS"; 7 | forall X -> X car(tuple list) asm "CAR"; 8 | tuple cdr(tuple list) asm "CDR"; 9 | tuple empty_tuple() asm "NIL"; 10 | forall X -> tuple tpush(tuple t, X value) asm "TPUSH"; 11 | forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH"; 12 | forall X -> [X] single(X x) asm "SINGLE"; 13 | forall X -> X unsingle([X] t) asm "UNSINGLE"; 14 | forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR"; 15 | forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR"; 16 | forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE"; 17 | forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE"; 18 | forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE"; 19 | forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE"; 20 | forall X -> X first(tuple t) asm "FIRST"; 21 | forall X -> X second(tuple t) asm "SECOND"; 22 | forall X -> X third(tuple t) asm "THIRD"; 23 | forall X -> X fourth(tuple t) asm "3 INDEX"; 24 | forall X, Y -> X pair_first([X, Y] p) asm "FIRST"; 25 | forall X, Y -> Y pair_second([X, Y] p) asm "SECOND"; 26 | forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST"; 27 | forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND"; 28 | forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD"; 29 | forall X -> X null() asm "PUSHNULL"; 30 | forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP"; 31 | 32 | int now() asm "NOW"; 33 | slice my_address() asm "MYADDR"; 34 | [int, cell] get_balance() asm "BALANCE"; 35 | int cur_lt() asm "LTIME"; 36 | int block_lt() asm "BLOCKLT"; 37 | 38 | int cell_hash(cell c) asm "HASHCU"; 39 | int slice_hash(slice s) asm "HASHSU"; 40 | int string_hash(slice s) asm "SHA256U"; 41 | 42 | int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU"; 43 | int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS"; 44 | 45 | (int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE"; 46 | (int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE"; 47 | (int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; 48 | (int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; 49 | 50 | ;; () throw_if(int excno, int cond) impure asm "THROWARGIF"; 51 | 52 | () dump_stack() impure asm "DUMPSTK"; 53 | 54 | cell get_data() asm "c4 PUSH"; 55 | () set_data(cell c) impure asm "c4 POP"; 56 | cont get_c3() impure asm "c3 PUSH"; 57 | () set_c3(cont c) impure asm "c3 POP"; 58 | cont bless(slice s) impure asm "BLESS"; 59 | 60 | () accept_message() impure asm "ACCEPT"; 61 | () set_gas_limit(int limit) impure asm "SETGASLIMIT"; 62 | () commit() impure asm "COMMIT"; 63 | () buy_gas(int gram) impure asm "BUYGAS"; 64 | 65 | int min(int x, int y) asm "MIN"; 66 | int max(int x, int y) asm "MAX"; 67 | (int, int) minmax(int x, int y) asm "MINMAX"; 68 | int abs(int x) asm "ABS"; 69 | 70 | slice begin_parse(cell c) asm "CTOS"; 71 | () end_parse(slice s) impure asm "ENDS"; 72 | (slice, cell) load_ref(slice s) asm( -> 1 0) "LDREF"; 73 | cell preload_ref(slice s) asm "PLDREF"; 74 | ;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX"; 75 | ;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX"; 76 | ;; int preload_int(slice s, int len) asm "PLDIX"; 77 | ;; int preload_uint(slice s, int len) asm "PLDUX"; 78 | ;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX"; 79 | ;; slice preload_bits(slice s, int len) asm "PLDSLICEX"; 80 | (slice, int) load_grams(slice s) asm( -> 1 0) "LDGRAMS"; 81 | slice skip_bits(slice s, int len) asm "SDSKIPFIRST"; 82 | (slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST"; 83 | slice first_bits(slice s, int len) asm "SDCUTFIRST"; 84 | slice skip_last_bits(slice s, int len) asm "SDSKIPLAST"; 85 | (slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST"; 86 | slice slice_last(slice s, int len) asm "SDCUTLAST"; 87 | (slice, cell) load_dict(slice s) asm( -> 1 0) "LDDICT"; 88 | cell preload_dict(slice s) asm "PLDDICT"; 89 | slice skip_dict(slice s) asm "SKIPDICT"; 90 | 91 | (slice, cell) load_maybe_ref(slice s) asm( -> 1 0) "LDOPTREF"; 92 | cell preload_maybe_ref(slice s) asm "PLDOPTREF"; 93 | builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF"; 94 | 95 | int cell_depth(cell c) asm "CDEPTH"; 96 | 97 | int slice_refs(slice s) asm "SREFS"; 98 | int slice_bits(slice s) asm "SBITS"; 99 | (int, int) slice_bits_refs(slice s) asm "SBITREFS"; 100 | int slice_empty?(slice s) asm "SEMPTY"; 101 | int slice_data_empty?(slice s) asm "SDEMPTY"; 102 | int slice_refs_empty?(slice s) asm "SREMPTY"; 103 | int slice_depth(slice s) asm "SDEPTH"; 104 | 105 | int builder_refs(builder b) asm "BREFS"; 106 | int builder_bits(builder b) asm "BBITS"; 107 | int builder_depth(builder b) asm "BDEPTH"; 108 | 109 | builder begin_cell() asm "NEWC"; 110 | cell end_cell(builder b) asm "ENDC"; 111 | builder store_ref(builder b, cell c) asm(c b) "STREF"; 112 | ;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX"; 113 | ;; builder store_int(builder b, int x, int len) asm(x b len) "STIX"; 114 | builder store_slice(builder b, slice s) asm "STSLICER"; 115 | builder store_grams(builder b, int x) asm "STGRAMS"; 116 | builder store_dict(builder b, cell c) asm(c b) "STDICT"; 117 | 118 | (slice, slice) load_msg_addr(slice s) asm( -> 1 0) "LDMSGADDR"; 119 | tuple parse_addr(slice s) asm "PARSEMSGADDR"; 120 | (int, int) parse_std_addr(slice s) asm "REWRITESTDADDR"; 121 | (int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR"; 122 | 123 | cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; 124 | (cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; 125 | cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; 126 | (cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; 127 | cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF"; 128 | (cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF"; 129 | (cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF"; 130 | (cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF"; 131 | (cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF"; 132 | (cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL"; 133 | (cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL"; 134 | (slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT"; 135 | (slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT"; 136 | (cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; 137 | (cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; 138 | (cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; 139 | (cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; 140 | cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; 141 | (cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; 142 | cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; 143 | (cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; 144 | cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; 145 | (cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; 146 | (cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD"; 147 | (cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE"; 148 | (cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD"; 149 | (cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE"; 150 | cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; 151 | (cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; 152 | cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; 153 | (cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; 154 | cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; 155 | (cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; 156 | (cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB"; 157 | (cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB"; 158 | (cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB"; 159 | (cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB"; 160 | (cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; 161 | (cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; 162 | (cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; 163 | (cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; 164 | (cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; 165 | (cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; 166 | (cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; 167 | (cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; 168 | (cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; 169 | (cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; 170 | (cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; 171 | (cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; 172 | (int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2"; 173 | (int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2"; 174 | (int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2"; 175 | (int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2"; 176 | (int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2"; 177 | (int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2"; 178 | (int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2"; 179 | (int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2"; 180 | (int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2"; 181 | (int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2"; 182 | (int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2"; 183 | (int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2"; 184 | (int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2"; 185 | (int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2"; 186 | (int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2"; 187 | (int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2"; 188 | cell new_dict() asm "NEWDICT"; 189 | int dict_empty?(cell c) asm "DICTEMPTY"; 190 | 191 | (slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2"; 192 | (cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET"; 193 | (cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL"; 194 | 195 | cell config_param(int x) asm "CONFIGOPTPARAM"; 196 | int cell_null?(cell c) asm "ISNULL"; 197 | 198 | () raw_reserve(int amount, int mode) impure asm "RAWRESERVE"; 199 | () raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX"; 200 | () send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG"; 201 | () set_code(cell new_code) impure asm "SETCODE"; 202 | 203 | int random() impure asm "RANDU256"; 204 | int rand(int range) impure asm "RAND"; 205 | int get_seed() impure asm "RANDSEED"; 206 | int set_seed() impure asm "SETRAND"; 207 | () randomize(int x) impure asm "ADDRAND"; 208 | () randomize_lt() impure asm "LTIME" "ADDRAND"; 209 | 210 | builder store_coins(builder b, int x) asm "STVARUINT16"; 211 | (slice, int) load_coins(slice s) asm( -> 1 0) "LDVARUINT16"; 212 | 213 | int equal_slices (slice a, slice b) asm "SDEQ"; 214 | int builder_null?(builder b) asm "ISNULL"; 215 | builder store_builder(builder to, builder from) asm "STBR"; 216 | -------------------------------------------------------------------------------- /contracts/imports/stdlib.fc: -------------------------------------------------------------------------------- 1 | ;; Standard library for funC 2 | ;; 3 | 4 | forall X -> tuple cons(X head, tuple tail) asm "CONS"; 5 | forall X -> (X, tuple) uncons(tuple list) asm "UNCONS"; 6 | forall X -> (tuple, X) list_next(tuple list) asm( -> 1 0) "UNCONS"; 7 | forall X -> X car(tuple list) asm "CAR"; 8 | tuple cdr(tuple list) asm "CDR"; 9 | tuple empty_tuple() asm "NIL"; 10 | forall X -> tuple tpush(tuple t, X value) asm "TPUSH"; 11 | forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH"; 12 | forall X -> [X] single(X x) asm "SINGLE"; 13 | forall X -> X unsingle([X] t) asm "UNSINGLE"; 14 | forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR"; 15 | forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR"; 16 | forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE"; 17 | forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE"; 18 | forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE"; 19 | forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE"; 20 | forall X -> X first(tuple t) asm "FIRST"; 21 | forall X -> X second(tuple t) asm "SECOND"; 22 | forall X -> X third(tuple t) asm "THIRD"; 23 | forall X -> X fourth(tuple t) asm "3 INDEX"; 24 | forall X, Y -> X pair_first([X, Y] p) asm "FIRST"; 25 | forall X, Y -> Y pair_second([X, Y] p) asm "SECOND"; 26 | forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST"; 27 | forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND"; 28 | forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD"; 29 | forall X -> X null() asm "PUSHNULL"; 30 | forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP"; 31 | 32 | int now() asm "NOW"; 33 | slice my_address() asm "MYADDR"; 34 | [int, cell] get_balance() asm "BALANCE"; 35 | int cur_lt() asm "LTIME"; 36 | int block_lt() asm "BLOCKLT"; 37 | 38 | int cell_hash(cell c) asm "HASHCU"; 39 | int slice_hash(slice s) asm "HASHSU"; 40 | int string_hash(slice s) asm "SHA256U"; 41 | 42 | int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU"; 43 | int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS"; 44 | 45 | (int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE"; 46 | (int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE"; 47 | (int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; 48 | (int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; 49 | 50 | ;; () throw_if(int excno, int cond) impure asm "THROWARGIF"; 51 | 52 | () dump_stack() impure asm "DUMPSTK"; 53 | 54 | cell get_data() asm "c4 PUSH"; 55 | () set_data(cell c) impure asm "c4 POP"; 56 | cont get_c3() impure asm "c3 PUSH"; 57 | () set_c3(cont c) impure asm "c3 POP"; 58 | cont bless(slice s) impure asm "BLESS"; 59 | 60 | () accept_message() impure asm "ACCEPT"; 61 | () set_gas_limit(int limit) impure asm "SETGASLIMIT"; 62 | () commit() impure asm "COMMIT"; 63 | () buy_gas(int gram) impure asm "BUYGAS"; 64 | 65 | int min(int x, int y) asm "MIN"; 66 | int max(int x, int y) asm "MAX"; 67 | (int, int) minmax(int x, int y) asm "MINMAX"; 68 | int abs(int x) asm "ABS"; 69 | 70 | slice begin_parse(cell c) asm "CTOS"; 71 | () end_parse(slice s) impure asm "ENDS"; 72 | (slice, cell) load_ref(slice s) asm( -> 1 0) "LDREF"; 73 | cell preload_ref(slice s) asm "PLDREF"; 74 | ;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX"; 75 | ;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX"; 76 | ;; int preload_int(slice s, int len) asm "PLDIX"; 77 | ;; int preload_uint(slice s, int len) asm "PLDUX"; 78 | ;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX"; 79 | ;; slice preload_bits(slice s, int len) asm "PLDSLICEX"; 80 | (slice, int) load_grams(slice s) asm( -> 1 0) "LDGRAMS"; 81 | slice skip_bits(slice s, int len) asm "SDSKIPFIRST"; 82 | (slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST"; 83 | slice first_bits(slice s, int len) asm "SDCUTFIRST"; 84 | slice skip_last_bits(slice s, int len) asm "SDSKIPLAST"; 85 | (slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST"; 86 | slice slice_last(slice s, int len) asm "SDCUTLAST"; 87 | (slice, cell) load_dict(slice s) asm( -> 1 0) "LDDICT"; 88 | cell preload_dict(slice s) asm "PLDDICT"; 89 | slice skip_dict(slice s) asm "SKIPDICT"; 90 | 91 | (slice, cell) load_maybe_ref(slice s) asm( -> 1 0) "LDOPTREF"; 92 | cell preload_maybe_ref(slice s) asm "PLDOPTREF"; 93 | builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF"; 94 | 95 | int cell_depth(cell c) asm "CDEPTH"; 96 | 97 | int slice_refs(slice s) asm "SREFS"; 98 | int slice_bits(slice s) asm "SBITS"; 99 | (int, int) slice_bits_refs(slice s) asm "SBITREFS"; 100 | int slice_empty?(slice s) asm "SEMPTY"; 101 | int slice_data_empty?(slice s) asm "SDEMPTY"; 102 | int slice_refs_empty?(slice s) asm "SREMPTY"; 103 | int slice_depth(slice s) asm "SDEPTH"; 104 | 105 | int builder_refs(builder b) asm "BREFS"; 106 | int builder_bits(builder b) asm "BBITS"; 107 | int builder_depth(builder b) asm "BDEPTH"; 108 | 109 | builder begin_cell() asm "NEWC"; 110 | cell end_cell(builder b) asm "ENDC"; 111 | builder store_ref(builder b, cell c) asm(c b) "STREF"; 112 | ;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX"; 113 | ;; builder store_int(builder b, int x, int len) asm(x b len) "STIX"; 114 | builder store_slice(builder b, slice s) asm "STSLICER"; 115 | builder store_grams(builder b, int x) asm "STGRAMS"; 116 | builder store_dict(builder b, cell c) asm(c b) "STDICT"; 117 | 118 | (slice, slice) load_msg_addr(slice s) asm( -> 1 0) "LDMSGADDR"; 119 | tuple parse_addr(slice s) asm "PARSEMSGADDR"; 120 | (int, int) parse_std_addr(slice s) asm "REWRITESTDADDR"; 121 | (int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR"; 122 | 123 | cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; 124 | (cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; 125 | cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; 126 | (cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; 127 | cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF"; 128 | (cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF"; 129 | (cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF"; 130 | (cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF"; 131 | (cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF"; 132 | (cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL"; 133 | (cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL"; 134 | (slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT"; 135 | (slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT"; 136 | (cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; 137 | (cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; 138 | (cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; 139 | (cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; 140 | cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; 141 | (cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; 142 | cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; 143 | (cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; 144 | cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; 145 | (cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; 146 | (cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD"; 147 | (cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE"; 148 | (cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD"; 149 | (cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE"; 150 | cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; 151 | (cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; 152 | cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; 153 | (cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; 154 | cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; 155 | (cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; 156 | (cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB"; 157 | (cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB"; 158 | (cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB"; 159 | (cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB"; 160 | (cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; 161 | (cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; 162 | (cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; 163 | (cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; 164 | (cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; 165 | (cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; 166 | (cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; 167 | (cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; 168 | (cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; 169 | (cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; 170 | (cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; 171 | (cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; 172 | (int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2"; 173 | (int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2"; 174 | (int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2"; 175 | (int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2"; 176 | (int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2"; 177 | (int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2"; 178 | (int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2"; 179 | (int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2"; 180 | (int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2"; 181 | (int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2"; 182 | (int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2"; 183 | (int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2"; 184 | (int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2"; 185 | (int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2"; 186 | (int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2"; 187 | (int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2"; 188 | cell new_dict() asm "NEWDICT"; 189 | int dict_empty?(cell c) asm "DICTEMPTY"; 190 | 191 | (slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2"; 192 | (cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET"; 193 | (cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL"; 194 | 195 | cell config_param(int x) asm "CONFIGOPTPARAM"; 196 | int cell_null?(cell c) asm "ISNULL"; 197 | 198 | () raw_reserve(int amount, int mode) impure asm "RAWRESERVE"; 199 | () raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX"; 200 | () send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG"; 201 | () set_code(cell new_code) impure asm "SETCODE"; 202 | 203 | int random() impure asm "RANDU256"; 204 | int rand(int range) impure asm "RAND"; 205 | int get_seed() impure asm "RANDSEED"; 206 | int set_seed() impure asm "SETRAND"; 207 | () randomize(int x) impure asm "ADDRAND"; 208 | () randomize_lt() impure asm "LTIME" "ADDRAND"; 209 | 210 | builder store_coins(builder b, int x) asm "STVARUINT16"; 211 | (slice, int) load_coins(slice s) asm( -> 1 0) "LDVARUINT16"; 212 | 213 | int equal_slices (slice a, slice b) asm "SDEQ"; 214 | int builder_null?(builder b) asm "ISNULL"; 215 | builder store_builder(builder to, builder from) asm "STBR"; 216 | -------------------------------------------------------------------------------- /deploy/deploy-utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { 3 | Address, 4 | TonClient, 5 | Cell, 6 | WalletContract, 7 | InternalMessage, 8 | CommonMessageInfo, 9 | StateInit, 10 | contractAddress, 11 | toNano, 12 | CellMessage, 13 | fromNano, 14 | WalletV3R2Source, 15 | } from "ton"; 16 | import { mnemonicNew, mnemonicToWalletKey } from "ton-crypto"; 17 | import { JettonMinter } from "../src/jetton-minter"; 18 | import BN from "bn.js"; 19 | import axios from "axios"; 20 | import axiosThrottle from "axios-request-throttle"; 21 | import { AmmMinterRPC } from "../src/amm-minter"; 22 | import { OPS } from "../src/ops"; 23 | import { JettonWallet } from "../src/jetton-wallet"; 24 | import { AmmLpWallet } from "../src/amm-wallet"; 25 | 26 | enum GAS_FEES { 27 | ADD_LIQUIDITY = 0.2, 28 | REMOVE_LIQUIDITY = 0.25, 29 | SWAP_FEE = 0.2, 30 | SWAP_FORWARD_TON = 0.2, 31 | MINT = 0.2, 32 | } 33 | 34 | const TON_LIQUIDITY = 1; 35 | const TOKEN_TO_SWAP = 25; 36 | const TOKEN_LIQUIDITY = toNano(100); 37 | const TON_TO_SWAP = 2; 38 | const MINT_SIZE = 100; 39 | //const JETTON_URI = "https://api.jsonbin.io/b/629ffffffff"; 40 | 41 | const zeroAddress = Address.parse("EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c"); 42 | 43 | export const BLOCK_TIME = 30000; 44 | 45 | async function _deployJettonMinter( 46 | client: TonClient, 47 | walletContract: WalletContract, 48 | owner: Address, 49 | privateKey: Buffer, 50 | jettonUri: string, 51 | workchain = 0 52 | ) { 53 | const { codeCell, initDataCell } = await JettonMinter.createDeployData(new BN(10000), owner, jettonUri); 54 | 55 | const newContractAddress = await contractAddress({ 56 | workchain, 57 | initialData: initDataCell, 58 | initialCode: codeCell[0], 59 | }); 60 | 61 | if (await client.isContractDeployed(newContractAddress)) { 62 | console.log(`contract: ${newContractAddress.toFriendly()} already Deployed`); 63 | 64 | return { 65 | address: newContractAddress, 66 | }; 67 | } 68 | const seqno = await walletContract.getSeqNo(); 69 | 70 | const transfer = await walletContract.createTransfer({ 71 | secretKey: privateKey, 72 | seqno: seqno, 73 | sendMode: 1 + 2, 74 | order: new InternalMessage({ 75 | to: newContractAddress, 76 | value: toNano(0.25), 77 | bounce: false, 78 | body: new CommonMessageInfo({ 79 | stateInit: new StateInit({ data: initDataCell, code: codeCell[0] }), 80 | body: null, 81 | }), 82 | }), 83 | }); 84 | await client.sendExternalMessage(walletContract, transfer); 85 | waitForSeqno(walletContract, seqno); 86 | console.log(`- Deploy transaction sent successfully to -> ${newContractAddress.toFriendly()} [seqno:${seqno}]`); 87 | await sleep(BLOCK_TIME); 88 | return { 89 | address: newContractAddress, 90 | }; 91 | } 92 | export async function deployJettonMinter(client: TonClient, deployWallet: WalletContract, privateKey: Buffer, jettonUri: string) { 93 | const usdcMinter = await _deployJettonMinter(client, deployWallet, deployWallet.address, privateKey, jettonUri); 94 | let data = await client.callGetMethod(usdcMinter.address, "get_jetton_data", []); 95 | console.log(`usdcMinter totalSupply: ${fromNano(data.stack[0][1]).toString()}`); 96 | saveAddress("USDC-Minter", usdcMinter.address); 97 | return usdcMinter; 98 | } 99 | 100 | export async function deployAmmMinter( 101 | client: TonClient, 102 | walletContract: WalletContract, 103 | privateKey: Buffer, 104 | ammDataUri = "https://api.jsonbin.io/b/7628f1df905f31f77b3a7c5d0", 105 | workchain = 0 106 | ) { 107 | const ammMinterRPC = new AmmMinterRPC({ rpcClient: client }); 108 | const { codeCell, initDataCell } = await ammMinterRPC.buildDataCell(ammDataUri); 109 | 110 | const newContractAddress = await contractAddress({ 111 | workchain, 112 | initialData: initDataCell, 113 | initialCode: codeCell[0], 114 | }); 115 | 116 | saveAddress("AMM-Minter", newContractAddress); 117 | console.log(` - Deploy AmmMinter the contract on-chain.. [${newContractAddress.toFriendly()}]`); 118 | 119 | if (await client.isContractDeployed(newContractAddress)) { 120 | console.log(`${newContractAddress.toFriendly()} already Deployed`); 121 | return new AmmMinterRPC({ 122 | address: newContractAddress, 123 | rpcClient: client, 124 | }); 125 | } 126 | const seqno = await walletContract.getSeqNo(); 127 | 128 | const transfer = await walletContract.createTransfer({ 129 | secretKey: privateKey, 130 | seqno: seqno, 131 | sendMode: 1 + 2, 132 | order: new InternalMessage({ 133 | to: newContractAddress, 134 | value: toNano(0.15), 135 | bounce: false, 136 | body: new CommonMessageInfo({ 137 | stateInit: new StateInit({ data: initDataCell, code: codeCell[0] }), 138 | body: null, 139 | }), 140 | }), 141 | }); 142 | 143 | await client.sendExternalMessage(walletContract, transfer); 144 | console.log(`- Deploy transaction sent successfully to -> ${newContractAddress.toFriendly()}`); 145 | waitForSeqno(walletContract, seqno); 146 | // new contracts takes time 147 | await sleep(BLOCK_TIME); 148 | 149 | ammMinterRPC.setAddress(newContractAddress); 150 | 151 | return ammMinterRPC; 152 | } 153 | 154 | const BOC_MAX_LENGTH = 48; 155 | 156 | export async function sendTransaction( 157 | client: TonClient, 158 | walletContract: WalletContract, 159 | receivingContract: Address, 160 | value: BN, 161 | privateKey: Buffer, 162 | messageBody: Cell, 163 | bounce = false, 164 | sendMode = 3 165 | ) { 166 | const seqno = await walletContract.getSeqNo(); 167 | const bocStr = await messageBody.toString(); 168 | const boc = bocStr.substring(0, bocStr.length < BOC_MAX_LENGTH ? bocStr.length : BOC_MAX_LENGTH).toString(); 169 | console.log(`🧱 send Transaction to ${receivingContract.toFriendly()} value:${fromNano(value)}💎 [boc:${boc}]`); 170 | 171 | const transfer = await walletContract.createTransfer({ 172 | secretKey: privateKey, 173 | seqno: seqno, 174 | sendMode: sendMode, 175 | order: new InternalMessage({ 176 | to: receivingContract, 177 | value: value, 178 | bounce, 179 | body: new CommonMessageInfo({ 180 | body: new CellMessage(messageBody), 181 | }), 182 | }), 183 | }); 184 | console.log(`🚀 sending transaction to ${addressToName[receivingContract.toFriendly()]} contract [seqno:${seqno}]`); 185 | 186 | client.sendExternalMessage(walletContract, transfer); 187 | 188 | await waitForSeqno(walletContract, seqno); 189 | } 190 | 191 | var addressToName: { [key: string]: string } = {}; 192 | 193 | export function saveAddress(name: string, addr: Address) { 194 | addressToName[addr.toFriendly()] = name; 195 | } 196 | 197 | export async function mintJetton( 198 | client: TonClient, 199 | usdcMinter: Address, 200 | deployWallet: WalletContract, 201 | privateKey: Buffer, 202 | mintSize = MINT_SIZE, 203 | recipient = deployWallet.address 204 | ) { 205 | console.log(` 206 | 🎬 minting deployer some usdc's , 100$ (recipient=${recipient.toFriendly()}) 207 | `); 208 | await sendTransaction( 209 | client, 210 | deployWallet, 211 | usdcMinter, 212 | toNano(GAS_FEES.MINT), 213 | privateKey, 214 | JettonMinter.Mint(recipient, toNano(mintSize).add(toNano(TOKEN_TO_SWAP))) 215 | ); 216 | } 217 | 218 | export async function addLiquidity( 219 | client: TonClient, 220 | ammMinter: AmmMinterRPC, 221 | deployWallet: WalletContract, 222 | deployerUSDCAddress: Address, 223 | privateKey: Buffer, 224 | tonLiquidity = TON_LIQUIDITY, 225 | tokenLiquidity = TOKEN_LIQUIDITY 226 | ) { 227 | console.log(` 228 | 🎬 sending Add Liquidity message | ${tonLiquidity} 💎 : ${fromNano(tokenLiquidity)}💲 229 | `); 230 | const addLiquidityMessage = JettonWallet.TransferOverloaded( 231 | ammMinter.address, 232 | tokenLiquidity, // jetton-amount 233 | ammMinter.address, 234 | toNano(tonLiquidity), 235 | OPS.ADD_LIQUIDITY, 236 | new BN(5), // Slippage 237 | toNano(tonLiquidity) 238 | ); 239 | 240 | await sendTransaction( 241 | client, 242 | deployWallet, 243 | deployerUSDCAddress as Address, 244 | toNano(tonLiquidity + GAS_FEES.ADD_LIQUIDITY), 245 | privateKey, 246 | addLiquidityMessage 247 | ); 248 | 249 | printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 250 | } 251 | 252 | export async function removeLiquidity( 253 | client: TonClient, 254 | ammMinter: AmmMinterRPC, 255 | deployWallet: WalletContract, 256 | privateKey: Buffer, 257 | sendMode = 3 258 | ) { 259 | // await printBalances(client, ammMinter, deployWallet.address); 260 | 261 | const lpAddress = (await ammMinter.getWalletAddress(deployWallet.address)) as Address; 262 | const lpData = await AmmLpWallet.GetWalletData(client, lpAddress); 263 | saveAddress("LP-Wallet", lpAddress); 264 | 265 | console.log(` 266 | 🎬 Remove Liquidity of ${fromNano(lpData.balance.toString())} LP's 267 | `); 268 | 269 | const removeLiqMessage = AmmLpWallet.RemoveLiquidityMessage(new BN(lpData.balance.toString()), deployWallet.address); 270 | 271 | await sendTransaction( 272 | client, 273 | deployWallet, 274 | lpAddress, 275 | toNano(GAS_FEES.REMOVE_LIQUIDITY), 276 | privateKey, 277 | removeLiqMessage, 278 | false, 279 | sendMode 280 | ); 281 | 282 | sleep(BLOCK_TIME); 283 | // await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 284 | } 285 | 286 | export async function initDeployKey() { 287 | const deployConfigJson = `./build/deploy.config.json`; 288 | const deployerWalletType = "org.ton.wallets.v3.r2"; 289 | let deployerMnemonic; 290 | if (!fs.existsSync(deployConfigJson)) { 291 | console.log(`\n* Config file '${deployConfigJson}' not found, creating a new wallet for deploy..`); 292 | deployerMnemonic = (await mnemonicNew(24)).join(" "); 293 | const deployWalletJsonContent = { 294 | created: new Date().toISOString(), 295 | deployerWalletType, 296 | deployerMnemonic, 297 | }; 298 | fs.writeFileSync(deployConfigJson, JSON.stringify(deployWalletJsonContent, null, 2)); 299 | console.log(` - Created new wallet in '${deployConfigJson}' - keep this file secret!`); 300 | } else { 301 | console.log(`\n* Config file '${deployConfigJson}' found and will be used for deployment!`); 302 | const deployConfigJsonContentRaw = fs.readFileSync(deployConfigJson, "utf-8"); 303 | const deployConfigJsonContent = JSON.parse(deployConfigJsonContentRaw); 304 | if (!deployConfigJsonContent.deployerMnemonic) { 305 | console.log(` - ERROR: '${deployConfigJson}' does not have the key 'deployerMnemonic'`); 306 | process.exit(1); 307 | } 308 | deployerMnemonic = deployConfigJsonContent.deployerMnemonic; 309 | } 310 | return mnemonicToWalletKey(deployerMnemonic.split(" ")); 311 | } 312 | 313 | export function bytesToAddress(bufferB64: string) { 314 | const buff = Buffer.from(bufferB64, "base64"); 315 | let c2 = Cell.fromBoc(buff); 316 | return c2[0].beginParse().readAddress() as Address; 317 | } 318 | 319 | export function sleep(time: number) { 320 | return new Promise((resolve) => { 321 | console.log(`💤 ${time / 1000}s ...`); 322 | 323 | setTimeout(resolve, time); 324 | }); 325 | } 326 | export async function printDeployerBalances(client: TonClient, deployer: Address, deployerUSDCAddress: Address) { 327 | const usdcData = await JettonWallet.GetData(client, deployerUSDCAddress); 328 | const ton = await client.getBalance(deployer); 329 | console.log(``); 330 | console.log(`⛏ Deployer Balance: ${fromNano(ton)}💎 | ${fromNano(usdcData.balance.toString())}$ USDC `); 331 | console.log(``); 332 | } 333 | 334 | export async function printBalances(client: TonClient, ammMinter: AmmMinterRPC, deployer: Address, deployerUSDCAddress: Address) { 335 | const data = await ammMinter.getJettonData(); 336 | const balance = await client.getBalance(ammMinter.address); 337 | console.log(`-----==== AmmMinter ====----- `); 338 | console.log(`[${ammMinter.address.toFriendly()}] 339 | 💎 balance : ${fromNano(balance)} | ${balance.sub(hexToBn(data.tonReserves)).toString()} 340 | 💰 totalSupply : ${hexToBn(data.totalSupply)} (${bnFmt(hexToBn(data.totalSupply))}) 341 | 💰 tonReserves : ${hexToBn(data.tonReserves)} (${bnFmt(hexToBn(data.tonReserves))}) 342 | 💰 tokenReserves: ${hexToBn(data.tokenReserves)} (${bnFmt(hexToBn(data.tokenReserves))}) 343 | 📪 JettonWallet : ${data.jettonWalletAddress.toFriendly()} 344 | `); 345 | await printDeployerBalances(client, deployer, deployerUSDCAddress); 346 | console.log(`-----==== ***** ====-----\n`); 347 | } 348 | 349 | export function hexToBn(num: string) { 350 | return new BN(BigInt(num).toString()); 351 | } 352 | 353 | export function bnFmt(num: BN | BigInt) { 354 | let str = num.toString(); 355 | return `${BigInt(str) / BigInt(1e9)}.${BigInt(str) % BigInt(1e9)} `; 356 | } 357 | 358 | export function hexFromNano(num: string) { 359 | const res = BigInt(num) / BigInt(100000000); 360 | return res.toString(); 361 | } 362 | 363 | export function printAddresses(addressBook: { [key: string]: string }, network: "sandbox." | "test." | "" = "") { 364 | console.log(``); //br 365 | let lsSnippet = ``; 366 | for (var key in addressBook) { 367 | const address = key; 368 | console.log(`${addressBook[key]} : https://${network}tonwhales.com/explorer/address/${key}`); 369 | const ellipsisAddress = `${address.substring(0, 6)}...${address.substring(address.length - 7, address.length - 1)}`; 370 | lsSnippet += `localStorage["${key}"]="${addressBook[key]}";`; 371 | lsSnippet += `localStorage["${ellipsisAddress}"]="${addressBook[key]}";`; 372 | } 373 | console.log(``); 374 | console.log(lsSnippet); 375 | console.log(``); 376 | } 377 | 378 | export async function initWallet(client: TonClient, publicKey: Buffer, workchain = 0) { 379 | const wallet = await WalletContract.create(client, WalletV3R2Source.create({ publicKey: publicKey, workchain })); 380 | const walletBalance = await client.getBalance(wallet.address); 381 | if (parseFloat(fromNano(walletBalance)) < 1) { 382 | throw `Insufficient Deployer [${wallet.address.toFriendly()}] funds ${fromNano(walletBalance)}`; 383 | } 384 | console.log( 385 | `Init wallet ${wallet.address.toFriendly()} | 386 | balance: ${fromNano(await client.getBalance(wallet.address))} | seqno: ${await wallet.getSeqNo()} 387 | ` 388 | ); 389 | 390 | return { wallet, walletBalance }; 391 | } 392 | 393 | const seqnoStepInterval = 3000; 394 | 395 | export async function waitForSeqno(walletContract: WalletContract, seqno: number) { 396 | console.log(`⏳ waiting for seqno to update (${seqno})`); 397 | for (var attempt = 0; attempt < 10; attempt++) { 398 | await sleep(seqnoStepInterval); 399 | const seqnoAfter = await walletContract.getSeqNo(); 400 | if (seqnoAfter > seqno) break; 401 | } 402 | console.log(`⌛️ seqno update after ${((attempt + 1) * seqnoStepInterval) / 1000}s`); 403 | } 404 | -------------------------------------------------------------------------------- /deploy/collect.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | TonClient, 4 | Cell, 5 | WalletContract, 6 | InternalMessage, 7 | CommonMessageInfo, 8 | StateInit, 9 | contractAddress, 10 | toNano, 11 | CellMessage, 12 | fromNano, 13 | beginCell, 14 | } from "ton"; 15 | import { JettonMinter } from "../src/jetton-minter"; 16 | import BN from "bn.js"; 17 | import fs from "fs"; 18 | 19 | import axios from "axios"; 20 | import axiosThrottle from "axios-request-throttle"; 21 | 22 | import { 23 | hexToBn, 24 | initDeployKey, 25 | initWallet, 26 | printAddresses, 27 | printBalances, 28 | printDeployerBalances, 29 | sleep, 30 | waitForSeqno, 31 | } from "../utils/deploy-utils"; 32 | import { JettonWallet } from "../src/jetton-wallet"; 33 | import { AmmMinterRPC } from "../src/amm-minter"; 34 | import { OPS } from "../src/ops"; 35 | import { AmmLpWallet } from "../src/amm-wallet"; 36 | import { compileFuncToB64 } from "../utils/funcToB64"; 37 | 38 | axiosThrottle.use(axios, { requestsPerSecond: 0.5 }); // required since toncenter jsonRPC limits to 1 req/sec without API key 39 | const client = new TonClient({ 40 | endpoint: "https://sandbox.tonhubapi.com/jsonRPC", 41 | // endpoint: "https://scalable-api.tonwhales.com/jsonRPC", 42 | // endpoint: "https://testnet.toncenter.com/api/v2/jsonRPC", 43 | }); 44 | 45 | enum GAS_FEES { 46 | ADD_LIQUIDITY = 0.2, 47 | REMOVE_LIQUIDITY = 0.25, 48 | SWAP_FEE = 0.04, 49 | SWAP_TON_FEE = 0.08, 50 | SWAP_FORWARD_TON = 0.04, 51 | MINT = 0.2, 52 | } 53 | 54 | enum NETWORK { 55 | SANDBOX = "sandbox.", 56 | TESTNET = "test.", 57 | MAINNET = "", 58 | } 59 | 60 | const metadataURI = "https://api.jsonbin.io/b/abcd"; 61 | 62 | const TON_LIQUIDITY = 4; 63 | const TOKEN_LIQUIDITY = toNano(100); 64 | const TOKEN_TO_SWAP = 25; 65 | const TON_TO_SWAP = 1; 66 | const MINT_SIZE = toNano(100); 67 | const JETTON_URI = "http://tonswap.co/token/usdc2xxxx.json"; 68 | 69 | const BLOCK_TIME = 10000; 70 | 71 | async function deployUSDCMinter( 72 | client: TonClient, 73 | walletContract: WalletContract, 74 | owner: Address, 75 | privateKey: Buffer, 76 | jettonUri: string, 77 | workchain = 0 78 | ) { 79 | const { codeCell, initDataCell } = await JettonMinter.createDeployData(new BN(20000), owner, jettonUri); 80 | 81 | const newContractAddress = await contractAddress({ 82 | workchain, 83 | initialData: initDataCell, 84 | initialCode: codeCell[0], 85 | }); 86 | 87 | if (await client.isContractDeployed(newContractAddress)) { 88 | console.log(`contract: ${newContractAddress.toFriendly()} already Deployed`); 89 | 90 | return { 91 | address: newContractAddress, 92 | }; 93 | } 94 | 95 | 96 | const seqno = await walletContract.getSeqNo(); 97 | 98 | const transfer = await walletContract.createTransfer({ 99 | secretKey: privateKey, 100 | seqno: seqno, 101 | sendMode: 1 + 2, 102 | order: new InternalMessage({ 103 | to: newContractAddress, 104 | value: toNano(0.25), 105 | bounce: false, 106 | body: new CommonMessageInfo({ 107 | stateInit: new StateInit({ data: initDataCell, code: codeCell[0] }), 108 | body: null, 109 | }), 110 | }), 111 | }); 112 | await client.sendExternalMessage(walletContract, transfer); 113 | waitForSeqno(walletContract, seqno); 114 | console.log(`- Deploy transaction sent successfully to -> ${newContractAddress.toFriendly()} [seqno:${seqno}]`); 115 | await sleep(BLOCK_TIME); 116 | return { 117 | address: newContractAddress, 118 | }; 119 | } 120 | async function deployMinter(client: TonClient, deployWallet: WalletContract, privateKey: Buffer) { 121 | const usdcMinter = await deployUSDCMinter(client, deployWallet, deployWallet.address, privateKey, JETTON_URI); 122 | let data = await client.callGetMethod(usdcMinter.address, "get_jetton_data", []); 123 | console.log(`usdcMinter totalSupply: ${fromNano(data.stack[0][1]).toString()}`); 124 | saveAddress("USDC-Minter", usdcMinter.address); 125 | return usdcMinter; 126 | } 127 | 128 | async function deployAmmMinter(client: TonClient, walletContract: WalletContract, privateKey: Buffer, workchain = 0) { 129 | const ammMinterRPC = new AmmMinterRPC({ rpcClient: client }); 130 | const { codeCell, initDataCell } = await ammMinterRPC.buildDataCell(metadataURI, walletContract.address); 131 | 132 | const newContractAddress = await contractAddress({ 133 | workchain, 134 | initialData: initDataCell, 135 | initialCode: codeCell[0], 136 | }); 137 | 138 | saveAddress("AMM-Minter", newContractAddress); 139 | console.log(` - Deploy AmmMinter the contract on-chain.. [${newContractAddress.toFriendly()}]`); 140 | 141 | if (await client.isContractDeployed(newContractAddress)) { 142 | console.log(`${newContractAddress.toFriendly()} already Deployed`); 143 | return new AmmMinterRPC({ 144 | address: newContractAddress, 145 | rpcClient: client, 146 | }); 147 | } 148 | const seqno = await walletContract.getSeqNo(); 149 | 150 | const transfer = await walletContract.createTransfer({ 151 | secretKey: privateKey, 152 | seqno: seqno, 153 | sendMode: 1 + 2, 154 | order: new InternalMessage({ 155 | to: newContractAddress, 156 | value: toNano(0.15), 157 | bounce: false, 158 | body: new CommonMessageInfo({ 159 | stateInit: new StateInit({ data: initDataCell, code: codeCell[0] }), 160 | body: null, 161 | }), 162 | }), 163 | }); 164 | 165 | await client.sendExternalMessage(walletContract, transfer); 166 | console.log(`- Deploy transaction sent successfully to -> ${newContractAddress.toFriendly()}`); 167 | waitForSeqno(walletContract, seqno); 168 | // new contracts takes time 169 | await sleep(BLOCK_TIME); 170 | 171 | ammMinterRPC.setAddress(newContractAddress); 172 | 173 | return ammMinterRPC; 174 | } 175 | 176 | const BOC_MAX_LENGTH = 48; 177 | 178 | async function sendTransaction( 179 | client: TonClient, 180 | walletContract: WalletContract, 181 | receivingContract: Address, 182 | value: BN, 183 | privateKey: Buffer, 184 | messageBody: Cell, 185 | bounce = false, 186 | sendMode = 3 187 | ) { 188 | const seqno = await walletContract.getSeqNo(); 189 | const bocStr = await messageBody.toString(); 190 | const boc = bocStr.substring(0, bocStr.length < BOC_MAX_LENGTH ? bocStr.length : BOC_MAX_LENGTH).toString(); 191 | console.log(`🧱 send Transaction to ${receivingContract.toFriendly()} value:${fromNano(value)}💎 [boc:${boc}]`); 192 | 193 | const transfer = await walletContract.createTransfer({ 194 | secretKey: privateKey, 195 | seqno: seqno, 196 | sendMode: sendMode, 197 | order: new InternalMessage({ 198 | to: receivingContract, 199 | value: value, 200 | bounce, 201 | body: new CommonMessageInfo({ 202 | body: new CellMessage(messageBody), 203 | }), 204 | }), 205 | }); 206 | console.log(`🚀 sending transaction to ${addressToName[receivingContract.toFriendly()]} contract [seqno:${seqno}]`); 207 | 208 | client.sendExternalMessage(walletContract, transfer); 209 | 210 | await waitForSeqno(walletContract, seqno); 211 | } 212 | 213 | var addressToName: { [key: string]: string } = {}; 214 | 215 | function saveAddress(name: string, addr: Address) { 216 | addressToName[addr.toFriendly()] = name; 217 | } 218 | 219 | async function mintUSDC(usdcMinter: Address, deployWallet: WalletContract, privateKey: Buffer) { 220 | console.log(` 221 | 🎬 minting deployer some usdc's , 100$ 222 | `); 223 | await sendTransaction( 224 | client, 225 | deployWallet, 226 | usdcMinter, 227 | toNano(GAS_FEES.MINT), 228 | privateKey, 229 | JettonMinter.Mint(deployWallet.address, toNano(MINT_SIZE).add(toNano(TOKEN_TO_SWAP))) 230 | ); 231 | } 232 | 233 | async function addLiquidity(ammMinter: AmmMinterRPC, deployWallet: WalletContract, deployerUSDCAddress: Address, privateKey: Buffer) { 234 | console.log(` 235 | 🎬 sending Add Liquidity message | ${TON_LIQUIDITY} 💎 : ${fromNano(TOKEN_LIQUIDITY)}💲 236 | `); 237 | const addLiquidityMessage = JettonWallet.TransferOverloaded( 238 | ammMinter.address, 239 | TOKEN_LIQUIDITY, // jetton-amount 240 | ammMinter.address, 241 | toNano(TON_LIQUIDITY + GAS_FEES.ADD_LIQUIDITY / 2), 242 | OPS.ADD_LIQUIDITY, 243 | new BN(5), // Slippage 244 | toNano(TON_LIQUIDITY) 245 | ); 246 | 247 | await sendTransaction( 248 | client, 249 | deployWallet, 250 | deployerUSDCAddress as Address, 251 | toNano(TON_LIQUIDITY + GAS_FEES.ADD_LIQUIDITY), 252 | privateKey, 253 | addLiquidityMessage 254 | ); 255 | 256 | printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 257 | } 258 | 259 | async function swapUsdcToTon( 260 | ammMinter: AmmMinterRPC, 261 | deployWallet: WalletContract, 262 | deployerUSDCAddress: Address, 263 | privateKey: Buffer, 264 | portion = new BN("4") 265 | ) { 266 | const ammData2 = await ammMinter.getJettonData(); 267 | //printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 268 | // portion is used to take all available liquidity by factor 269 | const tokenSwapAmount = TOKEN_LIQUIDITY.div(portion); 270 | const amountOut = await ammMinter.getAmountOut(tokenSwapAmount, hexToBn(ammData2.tokenReserves), hexToBn(ammData2.tonReserves)); 271 | 272 | console.log(` 273 | 🎬 Swap ${fromNano(tokenSwapAmount).toString()}$ USDC to 💎Ton (expecting for ${fromNano(amountOut.minAmountOut.toString())} 💎Ton) 274 | `); 275 | const swapTokenMessage = JettonWallet.TransferOverloaded( 276 | ammMinter.address, 277 | tokenSwapAmount, 278 | deployWallet.address, // i should get the change 279 | toNano(GAS_FEES.SWAP_FORWARD_TON), 280 | OPS.SWAP_TOKEN, 281 | new BN(amountOut.minAmountOut.toString()) // Min Amount out (TON) 282 | ); 283 | await sendTransaction( 284 | client, 285 | deployWallet, 286 | deployerUSDCAddress as Address, 287 | toNano(GAS_FEES.SWAP_FORWARD_TON + GAS_FEES.SWAP_FEE), 288 | privateKey, 289 | swapTokenMessage 290 | ); 291 | 292 | //await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 293 | } 294 | 295 | async function swapTonToUsdc(ammMinter: AmmMinterRPC, deployWallet: WalletContract, deployerUSDCAddress: Address, privateKey: Buffer) { 296 | const ammData3 = await ammMinter.getJettonData(); 297 | const tonSwapAmount = toNano(TON_TO_SWAP); 298 | 299 | const tonAmountOut = await ammMinter.getAmountOut(tonSwapAmount, hexToBn(ammData3.tonReserves), hexToBn(ammData3.tokenReserves)); 300 | 301 | console.log(` 302 | 🎬 Swap ${fromNano(tonSwapAmount).toString()}💎 to $ (expecting for ${fromNano(tonAmountOut.minAmountOut.toString())} $ ) 303 | `); 304 | const swapTonMessage = ammMinter.swapTon(tonSwapAmount, new BN(tonAmountOut.minAmountOut.toString())); 305 | await sendTransaction( 306 | client, 307 | deployWallet, 308 | ammMinter.address, 309 | tonSwapAmount.add(toNano(GAS_FEES.SWAP_TON_FEE)), 310 | privateKey, 311 | swapTonMessage 312 | ); 313 | 314 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 315 | } 316 | 317 | async function removeLiquidity( 318 | ammMinter: AmmMinterRPC, 319 | deployWallet: WalletContract, 320 | deployerUSDCAddress: Address, 321 | privateKey: Buffer, 322 | sendMode = 3 323 | ) { 324 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 325 | 326 | const lpAddress = (await ammMinter.getWalletAddress(deployWallet.address)) as Address; 327 | const lpData = await AmmLpWallet.GetWalletData(client, lpAddress); 328 | saveAddress("LP-Wallet", lpAddress); 329 | 330 | console.log(` 331 | 🎬 Remove Liquidity of ${fromNano(lpData.balance.toString())} LP's 332 | `); 333 | 334 | const removeLiqMessage = AmmLpWallet.RemoveLiquidityMessage(new BN(lpData.balance.toString()), deployWallet.address); 335 | 336 | let messageIsBounceable = true; 337 | await sendTransaction( 338 | client, 339 | deployWallet, 340 | lpAddress, 341 | toNano(GAS_FEES.REMOVE_LIQUIDITY), 342 | privateKey, 343 | removeLiqMessage, 344 | messageIsBounceable, 345 | sendMode 346 | ); 347 | 348 | sleep(BLOCK_TIME); 349 | await printBalances(client, ammMinter, deployWallet.address, deployerUSDCAddress); 350 | } 351 | 352 | async function codeUpgrade(ammMinter: AmmMinterRPC, deployWallet: WalletContract, privateKey: Buffer) { 353 | const ammMinterCodeB64: string = compileFuncToB64(["contracts/amm-minter-w.fc"]); 354 | let codeCell = Cell.fromBoc(ammMinterCodeB64); 355 | const upgradeMessage = beginCell().storeUint(26, 32).storeUint(1, 64).storeRef(codeCell[0]).endCell(); 356 | await sendTransaction(client, deployWallet, ammMinter.address, toNano(GAS_FEES.SWAP_FEE), privateKey, upgradeMessage); 357 | } 358 | 359 | async function collectFunds(ammMinter: AmmMinterRPC, deployWallet: WalletContract, privateKey: Buffer) { 360 | const upgradeMessage = beginCell().storeUint(77, 32).storeUint(1, 64).endCell(); 361 | await sendTransaction(client, deployWallet, ammMinter.address, toNano(GAS_FEES.SWAP_FEE), privateKey, upgradeMessage); 362 | } 363 | 364 | 365 | async function main() { 366 | 367 | if (fs.existsSync("./build/tmp.fif")) { 368 | fs.rmSync("./build/tmp.fif"); 369 | } 370 | 371 | const walletKey = await initDeployKey(); 372 | let { wallet: deployWallet, walletBalance } = await initWallet(client, walletKey.publicKey); 373 | 374 | 375 | let address = Address.parse("EQAFyxgRs1ixeCJoo3zG2Sf1J8OCL8sXPkW1zHDrgYT4a5zX") 376 | 377 | const ammMinterRPC = new AmmMinterRPC({ rpcClient: client, address }); 378 | await codeUpgrade(ammMinterRPC, deployWallet, walletKey.secretKey); 379 | 380 | const currentBalance = await client.getBalance(deployWallet.address); 381 | console.log(`Deployer spent about ${fromNano(currentBalance.sub(walletBalance))} 💎`); 382 | 383 | await printBalances(client, ammMinterRPC, deployWallet.address); 384 | printAddresses(addressToName); 385 | } 386 | 387 | (async () => { 388 | await main(); 389 | // await printBalances(client, Address.parse("EQCAbOdZOdk0C2GlwSwLLKmYe2NTXJCxcWndi1rYpMhs41rO")); 390 | 391 | // await printDeployer( 392 | // client, 393 | // Address.parse("EQBdPuDE6-9QE6c7dZZWbfhsE2jS--EfcwfEvGaWjKeW8vfO"), 394 | // Address.parse("kQD05JqOhN8IY1FU_RspKhx4o9jn5aLlqJouYMZgpIi6Zu9h") 395 | // ); 396 | //await testJettonAddressCalc(); 397 | })(); 398 | 399 | // async function testJettonAddressCalc() { 400 | // const ammUSDCWallet = (await JettonMinter.GetWalletAddress( 401 | // client, 402 | // Address.parse("kQDF1uSarM0trnmYTFh5tW1ud7yHXUBiG7VCjtS2rIiU2hSW"), //amm-minter 403 | // Address.parse("kQBdPuDE6-9QE6c7dZZWbfhsE2jS--EfcwfEvGaWjKeW8kxE") //Deployer-X 404 | // )) as Address; 405 | 406 | // console.log(`ammUSDCWallet https://test.tonwhales.com/explorer/address/${ammUSDCWallet.toFriendly()}`); 407 | // } 408 | 409 | // // needs work (address are messed up) 410 | // const aliceJettonUSDCData = await JettonWallet.GetData( 411 | // client, 412 | // Address.parse("EQBR0NIocQxhxo8f8Nbf2XZBnCiDROPLbafhWSVr1Sk1QEEQ") 413 | // ); 414 | // console.log(`aliceJettonUSDCData`, aliceJettonUSDCData); 415 | 416 | // deploy Amm Minter 417 | 418 | // const transfer = await deployWallet.createTransfer({ 419 | // secretKey: walletKey.secretKey, 420 | // seqno: await deployWallet.getSeqNo(), 421 | // sendMode: 1 + 2, 422 | // order: new InternalMessage({ 423 | // to: Address.parse("EQBod5J-GXAAgXI7OxoOtZRZhrYM9ll7MSpknZ1rPn-LosCz"), 424 | // value: toNano(20), 425 | // bounce: false, 426 | // body: new CommonMessageInfo(), 427 | // }), 428 | // }); 429 | // await client.sendExternalMessage(deployWallet, transfer); 430 | -------------------------------------------------------------------------------- /test/bus.spec.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js"; 2 | import { Address, Cell, CellMessage, CommonMessageInfo, InternalMessage, toNano } from "ton"; 3 | import { SendMsgAction } from "ton-contract-executor"; 4 | import { AmmMinterMessages, AmmMinterTVM } from "../src/amm-minter"; 5 | // import { actionToMessage } from "../src/amm-utils"; 6 | import { AmmLpWallet } from "../src/amm-wallet"; 7 | import { JettonMinter } from "../src/jetton-minter"; 8 | import { JettonWallet } from "../src/jetton-wallet"; 9 | import { Wallet } from "../src/wallet"; 10 | import { OPS } from "../src/ops"; 11 | 12 | import { printChain, TvmBus } from "ton-tvm-bus"; 13 | 14 | const JETTON_LIQUIDITY = toNano(1000); 15 | const TON_LIQUIDITY = toNano(500); 16 | const LP_DEFAULT_AMOUNT = 707106781; 17 | const INITIAL_JETTON_MINT = toNano(2050); 18 | 19 | const GAS_FEES = { 20 | ADD_LIQUIDITY: 0.2, 21 | SWAP_FEE: 0.09, 22 | ADD_LIQUIDITY_FORWARD_TON:0.09, 23 | REMOVE_LIQUIDITY: 0.1 24 | }; 25 | 26 | const alice = Address.parse("EQCLjyIQ9bF5t9h3oczEX3hPVK4tpW2Dqby0eHOH1y5_Nvb7"); 27 | 28 | describe("Ton Swap Bus Test Suite", () => { 29 | it("mint USDC", async () => { 30 | const tvmBus = new TvmBus(); 31 | const { usdcMinter, jettonWallet } = await createBaseContracts(tvmBus); 32 | const data = await usdcMinter.getData(); 33 | expect((await jettonWallet.getData()).balance.toString()).toBe(data?.totalSupply.toString()); 34 | }); 35 | 36 | it("mint USDC twice", async () => { 37 | const tvmBus = new TvmBus(); 38 | const { usdcMinter, jettonWallet, deployWallet } = await createBaseContracts(tvmBus); 39 | 40 | let mintMessage = await usdcMinter.mintMessage(deployWallet.address, deployWallet.address, toNano(7505)); 41 | let res = await tvmBus.broadcast(mintMessage); 42 | 43 | const data = await usdcMinter.getData(); 44 | 45 | expect((await jettonWallet.getData()).balance.toString()).toBe(data?.totalSupply.toString()); 46 | }); 47 | 48 | it("add liquidity", async () => { 49 | // const { usdcMinter, jettonWallet } = await createBaseContracts(tvmBus); 50 | await initAMM({}); 51 | }); 52 | 53 | it("add liquidity twice and fail the second time, send back funds to sender", async () => { 54 | const { usdcMinter, tvmBus, deployWallet, ammMinter, deployerJetton } = await initAMM({}); 55 | 56 | let mintAmount = INITIAL_JETTON_MINT; 57 | let mintMessage = await usdcMinter.mintMessage(deployWallet.address, deployWallet.address, mintAmount); 58 | let result = await tvmBus.broadcast(mintMessage); 59 | 60 | const tonValue = TON_LIQUIDITY.sub(toNano(100)); 61 | const messageBody = JettonWallet.TransferOverloaded( 62 | ammMinter.address as Address, 63 | JETTON_LIQUIDITY, 64 | deployWallet.address as Address, 65 | tonValue.add(toNano(GAS_FEES.ADD_LIQUIDITY_FORWARD_TON)), 66 | OPS.ADD_LIQUIDITY, 67 | new BN(5), 68 | tonValue 69 | ); 70 | const addLiquidityMessage = messageGenerator({ 71 | from: deployWallet.address, 72 | to: deployerJetton.address, 73 | value: tonValue.add(toNano(GAS_FEES.ADD_LIQUIDITY)), 74 | body: messageBody, 75 | }); 76 | const messagesLog = await tvmBus.broadcast(addLiquidityMessage); 77 | //console.log(messagesLog); 78 | 79 | 80 | printChain(messagesLog, "add liquidity twice and fail the second time, send back funds to sender"); 81 | 82 | expect(messagesLog.length).toBe(8); 83 | }); 84 | 85 | it("remove liquidity", async () => { 86 | const { deployerLpWallet, tvmBus, deployWallet } = await initAMM({}); 87 | 88 | const lpData = await deployerLpWallet.getData(); 89 | 90 | let messageBody = AmmLpWallet.RemoveLiquidityMessage(lpData.balance, deployWallet.address); 91 | const message = messageGenerator({ 92 | from: deployWallet.address, 93 | to: deployerLpWallet.address as Address, 94 | value: toNano(GAS_FEES.REMOVE_LIQUIDITY), 95 | body: messageBody, 96 | }); 97 | 98 | let messagesLog = await tvmBus.broadcast(message); 99 | console.log(messagesLog); 100 | 101 | let deployerLpWallet2 = messagesLog[0].contractImpl as AmmLpWallet; 102 | 103 | let lpWallet = messagesLog[0].contractImpl as AmmLpWallet; 104 | let ammMinter = messagesLog[1].contractImpl as AmmMinterTVM; 105 | let wallet = messagesLog[0].contractImpl as Wallet; 106 | let ammJetton = messagesLog[3].contractImpl as JettonWallet; 107 | let deployerJetton = messagesLog[4].contractImpl as JettonWallet; 108 | 109 | // deployer lp = 0 110 | const deployerLpWalletData = await deployerLpWallet2.getData(); 111 | expect(deployerLpWalletData.balance.toString()).toBe("0"); 112 | 113 | // reserves should be 0 after remove liquidity 114 | const ammMinterData = await ammMinter.getData(); 115 | expect(ammMinterData.tonReserves.toString()).toBe("0"); 116 | expect(ammMinterData.tokenReserves.toString()).toBe("0"); 117 | 118 | // amm's jetton wallet balance should be 0 119 | const ammJettonData = await ammJetton.getData(); 120 | expect(ammJettonData.balance.toString()).toBe("0"); 121 | 122 | const deployerJettonData = await deployerJetton.getData(); 123 | expect(deployerJettonData.balance.toString()).toBe(INITIAL_JETTON_MINT.toString()); 124 | 125 | printChain(messagesLog, "remove liquidity"); 126 | }); 127 | 128 | it("swap ton to token", async () => { 129 | let { ammMinter, tvmBus, deployWallet } = await initAMM({}); 130 | const tonSide = toNano(1); 131 | const preSwapData = await ammMinter.getData(); 132 | 133 | const { tonReserves, tokenReserves } = await ammMinter.getData(); 134 | const { minAmountOut } = await ammMinter.getAmountOut(tonSide, tonReserves, tokenReserves); 135 | const swapTonMessage = AmmMinterMessages.swapTon(toNano(1), minAmountOut); 136 | 137 | const message = messageGenerator({ 138 | from: deployWallet.address, 139 | to: ammMinter.address as Address, 140 | value: tonSide.add(toNano(GAS_FEES.SWAP_FEE)), 141 | body: swapTonMessage, 142 | }); 143 | let messagesLog = await tvmBus.broadcast(message); 144 | 145 | ammMinter = messagesLog[0].contractImpl as AmmMinterTVM; 146 | const ammMinterData = await ammMinter.getData(); 147 | expect(ammMinterData.tonReserves.toString()).toBe(preSwapData.tonReserves.add(tonSide).toString()); 148 | expect(ammMinterData.tokenReserves.toString()).toBe(preSwapData.tokenReserves.sub(minAmountOut).toString()); 149 | printChain(messagesLog, "swap ton to token"); 150 | }); 151 | 152 | it("swap ton to token and revert", async () => { 153 | let { ammMinter, tvmBus, deployWallet } = await initAMM({}); 154 | const tonSide = toNano(1); 155 | const initialPoolData = await ammMinter.getData(); 156 | 157 | const { tonReserves, tokenReserves } = await ammMinter.getData(); 158 | const { minAmountOut } = await ammMinter.getAmountOut(tonSide, tonReserves, tokenReserves); 159 | const swapTonMessage = AmmMinterMessages.swapTon(toNano(1), minAmountOut.mul(new BN(2))); // using min amount is X2 should fail the transaction 160 | 161 | const message = messageGenerator({ 162 | from: deployWallet.address, 163 | to: ammMinter.address as Address, 164 | value: tonSide.add(toNano("0.2")), 165 | body: swapTonMessage, 166 | }); 167 | let messagesLog = await tvmBus.broadcast(message); 168 | 169 | ammMinter = messagesLog[0].contractImpl as AmmMinterTVM; 170 | const ammMinterData = await ammMinter.getData(); 171 | expect(ammMinterData.tonReserves.toString()).toBe(initialPoolData.tonReserves.toString()); 172 | expect(ammMinterData.tokenReserves.toString()).toBe(initialPoolData.tokenReserves.toString()); 173 | printChain(messagesLog, "swap ton to token and revert"); 174 | }); 175 | 176 | it("swap token to ton", async () => { 177 | let { ammMinter, deployerJetton, tvmBus, deployWallet } = await initAMM({}); 178 | const { tonReserves, tokenReserves } = await ammMinter.getData(); 179 | const jettonSide = toNano(10); 180 | const preSwapData = await ammMinter.getData(); 181 | 182 | const { minAmountOut } = await ammMinter.getAmountOut(jettonSide, tokenReserves, tonReserves); 183 | 184 | const tonLiquidity = toNano(0.2); 185 | const forwardTon = tonLiquidity.add(toNano(GAS_FEES.ADD_LIQUIDITY)); 186 | const swapTokenMessage = JettonWallet.TransferOverloaded( 187 | ammMinter.address as Address, 188 | jettonSide, 189 | deployWallet.address, 190 | forwardTon, 191 | OPS.SWAP_TOKEN, 192 | minAmountOut, 193 | tonLiquidity 194 | ); 195 | 196 | const message = messageGenerator({ 197 | from: deployWallet.address, 198 | to: deployerJetton.address, 199 | value: forwardTon.add(toNano(GAS_FEES.SWAP_FEE)), 200 | body: swapTokenMessage, 201 | }); 202 | let messagesLog = await tvmBus.broadcast(message); 203 | 204 | ammMinter = messagesLog[3].contractImpl as AmmMinterTVM; 205 | const ammMinterData = await ammMinter.getData(); 206 | expect(ammMinterData.tokenReserves.toString()).toBe(preSwapData.tokenReserves.add(jettonSide).toString()); 207 | expect(ammMinterData.tonReserves.toString()).toBe(preSwapData.tonReserves.sub(minAmountOut).toString()); 208 | 209 | printChain(messagesLog, "swap ton to token and revert"); 210 | }); 211 | 212 | it("swap token to TON and revert", async () => { 213 | let { ammMinter, deployerJetton, tvmBus, deployWallet } = await initAMM({}); 214 | 215 | const { tonReserves, tokenReserves } = await ammMinter.getData(); 216 | const jettonSide = toNano(10); 217 | const preSwapData = await ammMinter.getData(); 218 | const initialJettonBalance = await (await deployerJetton.getData()).balance; 219 | 220 | const tonLiquidity = toNano(0.1); 221 | const forwardTon = tonLiquidity.add(toNano(GAS_FEES.ADD_LIQUIDITY)); 222 | 223 | const { minAmountOut } = await ammMinter.getAmountOut(jettonSide, tokenReserves, tonReserves); 224 | const swapTokenMessage = JettonWallet.TransferOverloaded( 225 | ammMinter.address as Address, 226 | jettonSide, 227 | ammMinter.address as Address, 228 | forwardTon, 229 | OPS.SWAP_TOKEN, 230 | minAmountOut.mul(new BN(2)) // this should fail the swap 231 | ); 232 | 233 | const message = messageGenerator({ 234 | from: deployWallet.address, 235 | to: deployerJetton.address, 236 | value: forwardTon.add(toNano(GAS_FEES.SWAP_FEE)), 237 | body: swapTokenMessage, 238 | }); 239 | let messagesLog = await tvmBus.broadcast(message); 240 | 241 | expect(messagesLog.length).toBe(7); 242 | 243 | const deployerWallet = messagesLog[5].contractImpl as JettonWallet; 244 | 245 | expect(deployerWallet).toBe(deployerJetton); 246 | const currentBalance = (await deployerWallet.getData()).balance; 247 | expect(currentBalance.toString()).toBe(initialJettonBalance.toString()); 248 | 249 | const ammMinterData = await ammMinter.getData(); 250 | expect(ammMinterData.tokenReserves.toString()).toBe(preSwapData.tokenReserves.toString()); 251 | expect(ammMinterData.tonReserves.toString()).toBe(preSwapData.tonReserves.toString()); 252 | 253 | printChain(messagesLog, "swap ton to token and revert"); 254 | }); 255 | }); 256 | 257 | async function initAMM({ jettonLiquidity = JETTON_LIQUIDITY, tonLiquidity = TON_LIQUIDITY, addLiquiditySlippage = new BN(5) }) { 258 | const tvmBus = new TvmBus(); 259 | tvmBus.registerCode(AmmLpWallet); 260 | 261 | const { jettonWallet, ammMinter, deployWallet, usdcMinter } = await createBaseContracts(tvmBus); 262 | 263 | const forwardTon = tonLiquidity.add(toNano(GAS_FEES.ADD_LIQUIDITY)); 264 | // amm minter spec 265 | const messageBody = JettonWallet.TransferOverloaded( 266 | ammMinter.address as Address, 267 | jettonLiquidity, 268 | deployWallet.address, // receive excess back 269 | forwardTon, 270 | OPS.ADD_LIQUIDITY, 271 | addLiquiditySlippage, 272 | tonLiquidity 273 | ); 274 | 275 | const addLiquidityMessage = messageGenerator({ 276 | from: deployWallet.address, 277 | to: jettonWallet.address, 278 | value: tonLiquidity.add(toNano("0.3")), 279 | body: messageBody, 280 | }); 281 | 282 | let messagesLog = await tvmBus.broadcast(addLiquidityMessage); 283 | // console.log(messagesLog); 284 | 285 | const deployerJettonData = await (messagesLog[0].contractImpl as JettonWallet).getData(); 286 | expect(deployerJettonData.balance.toString()).toBe(INITIAL_JETTON_MINT.sub(jettonLiquidity).toString()); 287 | 288 | const ammJettonData = await (messagesLog[1].contractImpl as JettonWallet).getData(); 289 | expect(ammJettonData.balance.toString()).toBe(jettonLiquidity.toString()); 290 | 291 | const ammMinterData = await (messagesLog[3].contractImpl as AmmMinterTVM).getData(); 292 | expect(ammMinterData.tonReserves.toString()).toBe(tonLiquidity.toString()); 293 | 294 | const deployerLpWalletData = await (messagesLog[4].contractImpl as AmmLpWallet).getData(); 295 | 296 | expect(deployerLpWalletData.balance.toString()).toBe(LP_DEFAULT_AMOUNT.toString()); 297 | 298 | return { 299 | tvmBus, 300 | deployWallet, 301 | usdcMinter, 302 | deployerJetton: messagesLog[0].contractImpl as JettonWallet, 303 | ammJetton: messagesLog[1].contractImpl as JettonWallet, 304 | ammMinter: messagesLog[3].contractImpl as AmmMinterTVM, 305 | deployerLpWallet: messagesLog[4].contractImpl as AmmLpWallet, 306 | }; 307 | } 308 | 309 | function messageGenerator(opts: { to: Address; from: Address; body: Cell; value: BN; bounce?: boolean }) { 310 | return new InternalMessage({ 311 | from: opts.from, 312 | to: opts.to, 313 | value: opts.value, 314 | bounce: opts.bounce || false, 315 | body: new CommonMessageInfo({ 316 | body: new CellMessage(opts.body), 317 | }), 318 | }); 319 | } 320 | 321 | async function createBaseContracts(tvmBus: TvmBus) { 322 | const deployWallet = await Wallet.Create(tvmBus, toNano(10), new BN(101), 0); // address EQCG-Qj2cpnPsGR-nkRokEgHdzblUlug1MH2twgpRJf5DUOI 323 | const deployerAddress = deployWallet.address; 324 | 325 | const usdcMinter = await JettonMinter.Create(new BN(0), deployerAddress, "https://ipfs.io/ipfs/dasadas", tvmBus, toNano("0.2")); 326 | const data = await usdcMinter.getData(); 327 | expect(data?.totalSupply.toString()).toBe("0"); 328 | tvmBus.registerCode(JettonWallet); 329 | 330 | let mintAmount = INITIAL_JETTON_MINT; 331 | let mintMessage = await usdcMinter.mintMessage(deployerAddress, deployerAddress, mintAmount); 332 | 333 | let messageList = await tvmBus.broadcast(mintMessage); 334 | 335 | const data2 = await usdcMinter.getData(); 336 | expect(data2?.totalSupply.toString()).toBe(mintAmount.toString()); 337 | 338 | const jettonWallet = messageList[1].contractImpl as JettonWallet; 339 | 340 | expect((await jettonWallet.getData()).balance.toString()).toBe(mintAmount.toString()); 341 | 342 | const ammMinter = new AmmMinterTVM("https://ipfs.io/ipfs/dasadas", alice, tvmBus, toNano(0.2)); 343 | 344 | return { 345 | usdcMinter, 346 | jettonWallet, 347 | ammMinter, 348 | deployWallet, 349 | }; 350 | } 351 | --------------------------------------------------------------------------------