├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── audits └── 202403TON_Foundation_Stablecoin_Contracts_Report_+_Fix_Review.pdf ├── build ├── JettonMinter.compiled.json ├── JettonWallet.compiled.json ├── Librarian.compiled.json └── print-hex.fif ├── compile.sh ├── contracts ├── gas.fc ├── helpers │ └── librarian.func ├── jetton-minter.fc ├── jetton-utils.fc ├── jetton-wallet.fc ├── jetton.tlb ├── op-codes.fc ├── stdlib.fc └── workchain.fc ├── gasUtils.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── sandbox_tests ├── JettonWallet.spec.ts ├── StateInit.spec.ts └── utils.ts ├── scripts ├── JettonMinterChecker.ts ├── JettonWalletChecker.ts ├── changeAdmin.ts ├── changeMetadataUrl.ts ├── claimAdmin.ts ├── deployJettonMinter.ts ├── deployLibrary.ts ├── forceBurn.ts ├── forceTransfer.ts ├── mint.ts ├── setStatus.ts ├── showJettonMinterInfo.ts ├── showJettonWalletInfo.ts ├── topUp.ts └── units.ts ├── tsconfig.json └── wrappers ├── JettonConstants.ts ├── JettonMinter.compile.ts ├── JettonMinter.ts ├── JettonWallet.compile.ts ├── JettonWallet.ts ├── Librarian.compile.ts ├── Librarian.ts └── ui-utils.ts /.env.example: -------------------------------------------------------------------------------- 1 | WALLET_MNEMONIC= 2 | WALLET_VERSION=v3r2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | node_modules 4 | .env* 5 | !.env.example 6 | .wget-hsts 7 | bin 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 TON Core 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jetton with Governance 2 | 3 | Jetton-with-governance FunC smart contracts. 4 | 5 | # Targets and goals 6 | 7 | This project was created to allow users to exchange and buy assets in the TON DeFi ecosystem for a jetton (token or currency) that is not subject to volatile fluctuations. To meet regulatory requirements, the issuer of the tokens must have additional control over the tokens. 8 | 9 | Thus this jetton represents a [standard TON jetton smart contracts](https://github.com/ton-blockchain/token-contract/tree/369ae089255edbd807eb499792a0a838c2e1b272/ft) with additional functionality: 10 | 11 | - Admin of jetton can make transfers from user's jetton wallet. 12 | 13 | - Admin of jetton can burn user's jettons. 14 | 15 | - Admin of jetton can lock/unlock user's jetton wallet (`set_status`). Admin can make transfer and burn even if wallet locked. 16 | 17 | - Admin of jetton can change jetton-minter code and it's full data. 18 | 19 | __⚠️ It is critically important for issuer to carefully manage the admin's account private key to avoid any potential risks of being hacked. It is highly recommend to use multi-signature wallet as admin account with private keys stored on different air-gapped hosts / hardware wallets.__ 20 | 21 | __⚠️ The contract does not check the code and data on `upgrade` message, so it is possible to brick the contract if you send invalid data or code. Therefore you should always check the upgrade in the testnet.__ 22 | 23 | # Local Development 24 | 25 | ## Install Dependencies 26 | 27 | `npm install` 28 | 29 | ## Compile Contracts 30 | 31 | `npm run build` 32 | 33 | ## Run Tests 34 | 35 | `npm run test` 36 | 37 | ### Deploy or run another script 38 | 39 | `npx blueprint run` or `yarn blueprint run` 40 | 41 | use Toncenter API: 42 | 43 | `npx blueprint run --custom https://testnet.toncenter.com/api/v2/ --custom-version v2 --custom-type testnet --custom-key ` 44 | 45 | API_KEY can be obtained on https://toncenter.com or https://testnet.toncenter.com 46 | 47 | ## Notes 48 | 49 | - The jetton-wallet contract does not include functionality that allows the owner to withdraw Toncoin funds from jetton-wallet Toncoin balance. 50 | 51 | - The contract prices gas based on the *current* blockchain configuration. 52 | It is worth keeping in mind the situation when the configuration has changed at the moment when the message goes from one jetton-wallet to another. 53 | Reducing fees in a blockchain configuration does not require additional actions. 54 | However, increasing fees in a blockchain configuration requires preliminary preparation - e.g. wallets and services must start sending Toncoins for gas in advance based on future parameters. 55 | 56 | - If you set the status of Jetton Wallet to prohibit receiving jettons - there is no guarantee that when you send jettons to such a jetton-wallet, jettons will bounce back and be credited to the sender. In case of gas shortage they can be lost. 57 | Toncoin for gas and forward will also not be returned to the sender but will remain on the sender’s jetton-wallet. 58 | 59 | # Security 60 | 61 | The stablecoin contract has been created by TON Core team and audited by security companies: 62 | 63 | - Trail of Bits: [Audit Report](https://github.com/ton-blockchain/stablecoin-contract/blob/main/audits/202403TON_Foundation_Stablecoin_Contracts_Report_+_Fix_Review.pdf) 64 | 65 | Feel free to review these reports for a detailed understanding of the contract's security measures. -------------------------------------------------------------------------------- /audits/202403TON_Foundation_Stablecoin_Contracts_Report_+_Fix_Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ton-blockchain/stablecoin-contract/56fd5b983f18288d42d65ab9c937f3637e27fa0d/audits/202403TON_Foundation_Stablecoin_Contracts_Report_+_Fix_Review.pdf -------------------------------------------------------------------------------- /build/JettonMinter.compiled.json: -------------------------------------------------------------------------------- 1 | {"hex":"b5ee9c72410218010005bb000114ff00f4a413f4bcf2c80b0102016207020201200603020271050400cfaf16f6a2687d007d207d206a6a68bf99e836c1783872ebdb514d9c97c283b7f0ae5179029e2b6119c39462719e4f46ed8f7413e62c780a417877407e978f01a40711411b1acb773a96bdd93fa83bb5ca8435013c8c4b3ac91f4589b4780a38646583fa0064a180400085adbcf6a2687d007d207d206a6a688a2f827c1400b82a3002098a81e46581ac7d0100e78b00e78b6490e4658089fa00097a00658064fc80383a6465816503e5ffe4e8400025bd9adf6a2687d007d207d206a6a6888122f8240202cb0908001da23864658380e78b64814183fa0bc002f3d0cb434c0c05c6c238ecc200835c874c7c0608405e351466ea44c38601035c87e800c3b51343e803e903e90353534541168504d3214017e809400f3c58073c5b333327b55383e903e900c7e800c7d007e800c7e80004c5c3e0e80b4c7c04074cfc044bb51343e803e903e9035353449a084190adf41eeb8c089a150a03fa82107bdd97deba8ee7363805fa00fa40f82854120a70546004131503c8cb0358fa0201cf1601cf16c921c8cb0113f40012f400cb00c9f9007074c8cb02ca07cbffc9d05008c705f2e04a12a14414506603c85005fa025003cf1601cf16ccccc9ed54fa40d120d70b01c000b3915be30de02682102c76b973bae302352514120b04f882106501f354ba8e223134365145c705f2e04902fa40d1103402c85005fa025003cf1601cf16ccccc9ed54e0258210fb88e119ba8e2132343603d15131c705f2e0498b025512c85005fa025003cf1601cf16ccccc9ed54e034248210235caf52bae30237238210cb862902bae302365b2082102508d66abae3026c310f0e0d0c00188210d372158cbadc840ff2f0001e3002c705f2e049d4d4d101ed54fb040044335142c705f2e049c85003cf16c9134440c85005fa025003cf1601cf16ccccc9ed5402ec3031325033c705f2e049fa40fa00d4d120d0d31f01018040d7212182100f8a7ea5ba8e4d36208210595f07bcba8e2c3004fa0031fa4031f401d120f839206e943081169fde718102f270f8380170f836a0811a7770f836a0bcf2b08e138210eed236d3ba9504d30331d19434f2c048e2e2e30d500370111000c082103b9aca0070fb02f828450470546004131503c8cb0358fa0201cf1601cf16c921c8cb0113f40012f400cb00c920f9007074c8cb02ca07cbffc9d0c8801801cb0501cf1658fa02029858775003cb6bcccc9730017158cb6acce2c98011fb0000ce31fa0031fa4031fa4031f401fa0020d70b009ad74bc00101c001b0f2b19130e25442162191729171e2f839206e938124279120e2216e94318128739101e25023a813a0738103a370f83ca00270f83612a00170f836a07381040982100966018070f837a0bcf2b001fc145f04323401fa40d2000101d195c821cf16c9916de2c8801001cb055004cf1670fa027001cb6a8210d173540001cb1f500401cb3f23fa4430c0008e35f828440470546004131503c8cb0358fa0201cf1601cf16c921c8cb0113f40012f400cb00c9f9007074c8cb02ca07cbffc9d012cf1697316c127001cb01e2f400c91300088050fb000044c8801001cb0501cf1670fa027001cb6a8210d53276db01cb1f0101cb3fc98042fb00019635355161c705f2e04904fa4021fa4430c000f2e14dfa00d4d120d0d31f018210178d4519baf2e0488040d721fa00fa4031fa4031fa0020d70b009ad74bc00101c001b0f2b19130e254431b16018e2191729171e2f839206e938124279120e2216e94318128739101e25023a813a0738103a370f83ca00270f83612a00170f836a07381040982100966018070f837a0bcf2b025597f1700ec82103b9aca0070fb02f828450470546004131503c8cb0358fa0201cf1601cf16c921c8cb0113f40012f400cb00c920f9007074c8cb02ca07cbffc9d0c8801801cb0501cf1658fa02029858775003cb6bcccc9730017158cb6acce2c98011fb005005a04314c85005fa025003cf1601cf16ccccc9ed546f6e5bfb"} -------------------------------------------------------------------------------- /build/JettonWallet.compiled.json: -------------------------------------------------------------------------------- 1 | {"hex":"b5ee9c7241020f010003d1000114ff00f4a413f4bcf2c80b01020162050202012004030021bc508f6a2686981fd007d207d2068af81c0027bfd8176a2686981fd007d207d206899fc152098402f8d001d0d3030171b08e48135f038020d721ed44d0d303fa00fa40fa40d104d31f01840f218210178d4519ba0282107bdd97deba12b1f2f48040d721fa003012a0401303c8cb0358fa0201cf1601cf16c9ed54e0fa40fa4031fa0031f401fa0031fa00013170f83a02d31f012082100f8a7ea5ba8e85303459db3ce0330c0602d0228210178d4519ba8e84325adb3ce034218210595f07bcba8e843101db3ce032208210eed236d3ba8e2f30018040d721d303d1ed44d0d303fa00fa40fa40d1335142c705f2e04a403303c8cb0358fa0201cf1601cf16c9ed54e06c218210d372158cbadc840ff2f0080701f2ed44d0d303fa00fa40fa40d106d33f0101fa00fa40f401d15141a15288c705f2e04926c2fff2afc882107bdd97de01cb1f5801cb3f01fa0221cf1658cf16c9c8801801cb0526cf1670fa02017158cb6accc903f839206e943081169fde718102f270f8380170f836a0811a7770f836a0bcf2b0028050fb00030903f4ed44d0d303fa00fa40fa40d12372b0c002f26d07d33f0101fa005141a004fa40fa4053bac705f82a5464e070546004131503c8cb0358fa0201cf1601cf16c921c8cb0113f40012f400cb00c9f9007074c8cb02ca07cbffc9d0500cc7051bb1f2e04a09fa0021925f04e30d26d70b01c000b393306c33e30d55020b0a09002003c8cb0358fa0201cf1601cf16c9ed54007a5054a1f82fa07381040982100966018070f837b60972fb02c8801001cb055005cf1670fa027001cb6a8210d53276db01cb1f5801cb3fc9810082fb00590060c882107362d09c01cb1f2501cb3f5004fa0258cf1658cf16c9c8801001cb0524cf1658fa02017158cb6accc98011fb0001f203d33f0101fa00fa4021fa4430c000f2e14ded44d0d303fa00fa40fa40d15309c7052471b0c00021b1f2ad522bc705500ab1f2e0495115a120c2fff2aff82a54259070546004131503c8cb0358fa0201cf1601cf16c921c8cb0113f40012f400cb00c920f9007074c8cb02ca07cbffc9d004fa40f401fa00200d019820d70b009ad74bc00101c001b0f2b19130e2c88210178d451901cb1f500a01cb3f5008fa0223cf1601cf1626fa025007cf16c9c8801801cb055004cf1670fa024063775003cb6bccccc945370e00b42191729171e2f839206e938124279120e2216e94318128739101e25023a813a0738103a370f83ca00270f83612a00170f836a07381040982100966018070f837a0bcf2b0048050fb005803c8cb0358fa0201cf1601cf16c9ed5401f9319e"} -------------------------------------------------------------------------------- /build/Librarian.compiled.json: -------------------------------------------------------------------------------- 1 | {"hex":"b5ee9c7241010301005c000114ff00f4a413f4bcf2c80b010292d33031d0d30331fa4030ed44f80721830af94130f8075003a17ff83b028210bbf81e007ff837a08010fb02c8801001cb0558cf1670fa027001cb6ac98306fb0072fb0688fb0488ed5402020000dc6a1953"} -------------------------------------------------------------------------------- /build/print-hex.fif: -------------------------------------------------------------------------------- 1 | #!/usr/bin/fift -s 2 | "TonUtil.fif" include 3 | "Asm.fif" include 4 | 5 | 6 | "jetton-minter.fif" include 7 | ."jetton-minter hash:" cr 8 | dup hashB dup Bx. cr drop 9 | ."jetton-minter code:" cr 10 | boc>B dup Bx. cr 11 | 12 | 13 | "jetton-wallet.fif" include 14 | ."jetton-wallet hash:" cr 15 | dup hashB dup Bx. cr drop 16 | ."jetton-wallet code:" cr 17 | boc>B dup Bx. cr 18 | 19 | -------------------------------------------------------------------------------- /compile.sh: -------------------------------------------------------------------------------- 1 | func -PA -o ./build/jetton-wallet.fif contracts/jetton-wallet.fc 2 | func -PA -o ./build/jetton-minter.fif contracts/jetton-minter.fc 3 | fift -s build/print-hex.fif -------------------------------------------------------------------------------- /contracts/gas.fc: -------------------------------------------------------------------------------- 1 | #include "workchain.fc"; 2 | 3 | const ONE_TON = 1000000000; 4 | 5 | const MIN_STORAGE_DURATION = 5 * 365 * 24 * 3600; ;; 5 years 6 | 7 | ;;# Precompiled constants 8 | ;; 9 | ;;All of the contents are result of contract emulation tests 10 | ;; 11 | 12 | ;;## Minimal fees 13 | ;; 14 | ;;- Transfer [/sandbox_tests/JettonWallet.spec.ts#L935](L935) `0.028627415` TON 15 | ;;- Burn [/sandbox_tests/JettonWallet.spec.ts#L1185](L1185) `0.016492002` TON 16 | 17 | 18 | ;;## Storage 19 | ;; 20 | ;;Get calculated in a separate test file [/sandbox_tests/StateInit.spec.ts](StateInit.spec.ts) 21 | 22 | ;;- `JETTON_WALLET_BITS` [/sandbox_tests/StateInit.spec.ts#L92](L92) 23 | const JETTON_WALLET_BITS = 1033; 24 | 25 | ;;- `JETTON_WALLET_CELLS`: [/sandbox_tests/StateInit.spec.ts#L92](L92) 26 | const JETTON_WALLET_CELLS = 3; 27 | 28 | ;; difference in JETTON_WALLET_BITS/JETTON_WALLET_INITSTATE_BITS is difference in 29 | ;; StateInit and AccountStorage (https://github.com/ton-blockchain/ton/blob/master/crypto/block/block.tlb) 30 | ;; we count bits as if balances are max possible 31 | ;;- `JETTON_WALLET_INITSTATE_BITS` [/sandbox_tests/StateInit.spec.ts#L95](L95) 32 | const JETTON_WALLET_INITSTATE_BITS = 931; 33 | ;;- `JETTON_WALLET_INITSTATE_CELLS` [/sandbox_tests/StateInit.spec.ts#L95](L95) 34 | const JETTON_WALLET_INITSTATE_CELLS = 3; 35 | 36 | ;; jetton-wallet.fc#L163 - maunal bits counting 37 | const BURN_NOTIFICATION_BITS = 754; ;; body = 32+64+124+(3+8+256)+(3+8+256) 38 | const BURN_NOTIFICATION_CELLS = 1; ;; body always in ref 39 | 40 | ;;## Gas 41 | ;; 42 | ;;Gas constants are calculated in the main test suite. 43 | ;;First the related transaction is found, and then it's 44 | ;;resulting gas consumption is printed to the console. 45 | 46 | ;;- `SEND_TRANSFER_GAS_CONSUMPTION` [/sandbox_tests/JettonWallet.spec.ts#L853](L853) 47 | const SEND_TRANSFER_GAS_CONSUMPTION = 9255; 48 | 49 | ;;- `RECEIVE_TRANSFER_GAS_CONSUMPTION` [/sandbox_tests/JettonWallet.spec.ts#L862](L862) 50 | const RECEIVE_TRANSFER_GAS_CONSUMPTION = 10355; 51 | 52 | ;;- `SEND_BURN_GAS_CONSUMPTION` [/sandbox_tests/JettonWallet.spec.ts#L1154](L1154) 53 | const SEND_BURN_GAS_CONSUMPTION = 5791; 54 | 55 | ;;- `RECEIVE_BURN_GAS_CONSUMPTION` [/sandbox_tests/JettonWallet.spec.ts#L1155](L1155) 56 | const RECEIVE_BURN_GAS_CONSUMPTION = 6775; 57 | 58 | 59 | int calculate_jetton_wallet_min_storage_fee() inline { 60 | return get_storage_fee(MY_WORKCHAIN, MIN_STORAGE_DURATION, JETTON_WALLET_BITS, JETTON_WALLET_CELLS); 61 | } 62 | 63 | int forward_init_state_overhead() inline { 64 | return get_simple_forward_fee(MY_WORKCHAIN, JETTON_WALLET_INITSTATE_BITS, JETTON_WALLET_INITSTATE_CELLS); 65 | } 66 | 67 | () check_amount_is_enough_to_transfer(int msg_value, int forward_ton_amount, int fwd_fee) impure inline { 68 | int fwd_count = forward_ton_amount ? 2 : 1; ;; second sending (forward) will be cheaper that first 69 | 70 | int jetton_wallet_gas_consumption = get_precompiled_gas_consumption(); 71 | int send_transfer_gas_consumption = null?(jetton_wallet_gas_consumption) ? SEND_TRANSFER_GAS_CONSUMPTION : jetton_wallet_gas_consumption; 72 | int receive_transfer_gas_consumption = null?(jetton_wallet_gas_consumption) ? RECEIVE_TRANSFER_GAS_CONSUMPTION : jetton_wallet_gas_consumption; 73 | 74 | throw_unless(error::not_enough_gas, msg_value > 75 | forward_ton_amount + 76 | ;; 3 messages: wal1->wal2, wal2->owner, wal2->response 77 | ;; but last one is optional (it is ok if it fails) 78 | fwd_count * fwd_fee + 79 | forward_init_state_overhead() + ;; additional fwd fees related to initstate in iternal_transfer 80 | get_compute_fee(MY_WORKCHAIN, send_transfer_gas_consumption) + 81 | get_compute_fee(MY_WORKCHAIN, receive_transfer_gas_consumption) + 82 | calculate_jetton_wallet_min_storage_fee() ); 83 | } 84 | 85 | 86 | 87 | () check_amount_is_enough_to_burn(int msg_value) impure inline { 88 | int jetton_wallet_gas_consumption = get_precompiled_gas_consumption(); 89 | int send_burn_gas_consumption = null?(jetton_wallet_gas_consumption) ? SEND_BURN_GAS_CONSUMPTION : jetton_wallet_gas_consumption; 90 | 91 | throw_unless(error::not_enough_gas, msg_value > get_forward_fee(MY_WORKCHAIN, BURN_NOTIFICATION_BITS, BURN_NOTIFICATION_CELLS) + get_compute_fee(MY_WORKCHAIN, send_burn_gas_consumption) + get_compute_fee(MY_WORKCHAIN, RECEIVE_BURN_GAS_CONSUMPTION)); 92 | } 93 | -------------------------------------------------------------------------------- /contracts/helpers/librarian.func: -------------------------------------------------------------------------------- 1 | ;; Simple library keeper 2 | 3 | #include "../stdlib.fc"; 4 | 5 | const int DEFAULT_DURATION = 3600 * 24 * 365 * 100; ;; 100 years, can top-up in any time 6 | const int ONE_TON = 1000000000; 7 | 8 | cell empty() asm " PUSHREF"; 9 | 10 | ;; https://docs.ton.org/tvm.pdf, page 138, SETLIBCODE 11 | () set_lib_code(cell code, int mode) impure asm "SETLIBCODE"; 12 | 13 | () recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure { 14 | slice cs = in_msg_full.begin_parse(); 15 | int flags = cs~load_uint(4); 16 | slice sender = cs~load_msg_addr(); 17 | 18 | 19 | cell lib_to_publish = get_data(); 20 | 21 | int initial_gas = gas_consumed(); 22 | (int order_cells, int order_bits, _) = compute_data_size(lib_to_publish, 2048); 23 | int size_counting_gas = gas_consumed() - initial_gas; 24 | 25 | int to_reserve = get_simple_compute_fee(MASTERCHAIN, size_counting_gas) + 26 | get_storage_fee(MASTERCHAIN, DEFAULT_DURATION, order_bits, order_cells); 27 | raw_reserve(to_reserve, RESERVE_BOUNCE_ON_ACTION_FAIL); 28 | cell msg = begin_cell() 29 | .store_msg_flags_and_address_none(NON_BOUNCEABLE) 30 | .store_slice(sender) 31 | .store_coins(0) 32 | .store_prefix_only_body() 33 | .end_cell(); 34 | send_raw_message(msg, SEND_MODE_CARRY_ALL_BALANCE); 35 | ;; https://docs.ton.org/tvm.pdf, page 138, SETLIBCODE 36 | set_lib_code(lib_to_publish, 2); ;; if x = 2, the library is added as a public library (and becomes available to all smart contracts if the current smart contract resides in the masterchain); 37 | ;; brick contract 38 | set_code(empty()); 39 | set_data(empty()); 40 | } 41 | -------------------------------------------------------------------------------- /contracts/jetton-minter.fc: -------------------------------------------------------------------------------- 1 | ;; Jetton minter smart contract 2 | 3 | #pragma version >=0.4.3; 4 | 5 | #include "stdlib.fc"; 6 | #include "op-codes.fc"; 7 | #include "workchain.fc"; 8 | #include "jetton-utils.fc"; 9 | #include "gas.fc"; 10 | 11 | ;; storage#_ total_supply:Coins admin_address:MsgAddress next_admin_address:MsgAddress jetton_wallet_code:^Cell metadata_uri:^Cell = Storage; 12 | (int, slice, slice, cell, cell) load_data() inline { 13 | slice ds = get_data().begin_parse(); 14 | var data = ( 15 | ds~load_coins(), ;; total_supply 16 | ds~load_msg_addr(), ;; admin_address 17 | ds~load_msg_addr(), ;; next_admin_address 18 | ds~load_ref(), ;; jetton_wallet_code 19 | ds~load_ref() ;; metadata url (contains snake slice without 0x0 prefix) 20 | ); 21 | ds.end_parse(); 22 | return data; 23 | } 24 | 25 | () save_data(int total_supply, slice admin_address, slice next_admin_address, cell jetton_wallet_code, cell metadata_uri) impure inline { 26 | set_data( 27 | begin_cell() 28 | .store_coins(total_supply) 29 | .store_slice(admin_address) 30 | .store_slice(next_admin_address) 31 | .store_ref(jetton_wallet_code) 32 | .store_ref(metadata_uri) 33 | .end_cell() 34 | ); 35 | } 36 | 37 | () send_to_jetton_wallet(slice to_address, cell jetton_wallet_code, int ton_amount, cell master_msg, int need_state_init) impure inline { 38 | raw_reserve(ONE_TON, RESERVE_REGULAR); ;; reserve for storage fees 39 | 40 | cell state_init = calculate_jetton_wallet_state_init(to_address, my_address(), jetton_wallet_code); 41 | slice to_wallet_address = calculate_jetton_wallet_address(state_init); 42 | 43 | ;; build MessageRelaxed, see TL-B layout in stdlib.fc#L733 44 | var msg = begin_cell() 45 | .store_msg_flags_and_address_none(BOUNCEABLE) 46 | .store_slice(to_wallet_address) ;; dest 47 | .store_coins(ton_amount); 48 | 49 | if (need_state_init) { 50 | msg = msg.store_statinit_ref_and_body_ref(state_init, master_msg); 51 | } else { 52 | msg = msg.store_only_body_ref(master_msg); 53 | } 54 | 55 | send_raw_message(msg.end_cell(), SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_BOUNCE_ON_ACTION_FAIL); 56 | } 57 | 58 | () recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure { 59 | slice in_msg_full_slice = in_msg_full.begin_parse(); 60 | int msg_flags = in_msg_full_slice~load_msg_flags(); 61 | 62 | if (msg_flags & 1) { ;; is bounced 63 | in_msg_body~skip_bounced_prefix(); 64 | ;; process only mint bounces 65 | ifnot (in_msg_body~load_op() == op::internal_transfer) { 66 | return (); 67 | } 68 | in_msg_body~skip_query_id(); 69 | int jetton_amount = in_msg_body~load_coins(); 70 | (int total_supply, slice admin_address, slice next_admin_address, cell jetton_wallet_code, cell metadata_uri) = load_data(); 71 | save_data(total_supply - jetton_amount, admin_address, next_admin_address, jetton_wallet_code, metadata_uri); 72 | return (); 73 | } 74 | slice sender_address = in_msg_full_slice~load_msg_addr(); 75 | int fwd_fee_from_in_msg = in_msg_full_slice~retrieve_fwd_fee(); 76 | int fwd_fee = get_original_fwd_fee(MY_WORKCHAIN, fwd_fee_from_in_msg); ;; we use message fwd_fee for estimation of forward_payload costs 77 | 78 | (int op, int query_id) = in_msg_body~load_op_and_query_id(); 79 | 80 | (int total_supply, slice admin_address, slice next_admin_address, cell jetton_wallet_code, cell metadata_uri) = load_data(); 81 | 82 | if (op == op::mint) { 83 | throw_unless(error::not_owner, equal_slices_bits(sender_address, admin_address)); 84 | slice to_address = in_msg_body~load_msg_addr(); 85 | check_same_workchain(to_address); 86 | int ton_amount = in_msg_body~load_coins(); 87 | cell master_msg = in_msg_body~load_ref(); 88 | in_msg_body.end_parse(); 89 | 90 | ;; see internal_transfer TL-B layout in jetton.tlb 91 | slice master_msg_slice = master_msg.begin_parse(); 92 | throw_unless(error::invalid_op, master_msg_slice~load_op() == op::internal_transfer); 93 | master_msg_slice~skip_query_id(); 94 | int jetton_amount = master_msg_slice~load_coins(); 95 | master_msg_slice~load_msg_addr(); ;; from_address 96 | master_msg_slice~load_msg_addr(); ;; response_address 97 | int forward_ton_amount = master_msg_slice~load_coins(); ;; forward_ton_amount 98 | check_either_forward_payload(master_msg_slice); ;; either_forward_payload 99 | 100 | ;; a little more than needed, it’s ok since it’s sent by the admin and excesses will return back 101 | check_amount_is_enough_to_transfer(ton_amount, forward_ton_amount, fwd_fee); 102 | 103 | send_to_jetton_wallet(to_address, jetton_wallet_code, ton_amount, master_msg, TRUE); 104 | save_data(total_supply + jetton_amount, admin_address, next_admin_address, jetton_wallet_code, metadata_uri); 105 | return (); 106 | } 107 | 108 | if (op == op::burn_notification) { 109 | ;; see burn_notification TL-B layout in jetton.tlb 110 | int jetton_amount = in_msg_body~load_coins(); 111 | slice from_address = in_msg_body~load_msg_addr(); 112 | throw_unless(error::not_valid_wallet, 113 | equal_slices_bits(calculate_user_jetton_wallet_address(from_address, my_address(), jetton_wallet_code), sender_address) 114 | ); 115 | save_data(total_supply - jetton_amount, admin_address, next_admin_address, jetton_wallet_code, metadata_uri); 116 | slice response_address = in_msg_body~load_msg_addr(); 117 | in_msg_body.end_parse(); 118 | 119 | if (~ is_address_none(response_address)) { 120 | ;; build MessageRelaxed, see TL-B layout in stdlib.fc#L733 121 | var msg = begin_cell() 122 | .store_msg_flags_and_address_none(NON_BOUNCEABLE) 123 | .store_slice(response_address) ;; dest 124 | .store_coins(0) 125 | .store_prefix_only_body() 126 | .store_op(op::excesses) 127 | .store_query_id(query_id); 128 | send_raw_message(msg.end_cell(), SEND_MODE_IGNORE_ERRORS | SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE); 129 | } 130 | return (); 131 | } 132 | 133 | if (op == op::provide_wallet_address) { 134 | ;; see provide_wallet_address TL-B layout in jetton.tlb 135 | slice owner_address = in_msg_body~load_msg_addr(); 136 | int include_address? = in_msg_body~load_bool(); 137 | in_msg_body.end_parse(); 138 | 139 | cell included_address = include_address? 140 | ? begin_cell().store_slice(owner_address).end_cell() 141 | : null(); 142 | 143 | ;; build MessageRelaxed, see TL-B layout in stdlib.fc#L733 144 | var msg = begin_cell() 145 | .store_msg_flags_and_address_none(NON_BOUNCEABLE) 146 | .store_slice(sender_address) 147 | .store_coins(0) 148 | .store_prefix_only_body() 149 | .store_op(op::take_wallet_address) 150 | .store_query_id(query_id); 151 | 152 | if (is_same_workchain(owner_address)) { 153 | msg = msg.store_slice(calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code)); 154 | } else { 155 | msg = msg.store_address_none(); 156 | } 157 | 158 | cell msg_cell = msg.store_maybe_ref(included_address).end_cell(); 159 | 160 | send_raw_message(msg_cell, SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL); 161 | return (); 162 | } 163 | 164 | if (op == op::change_admin) { 165 | throw_unless(error::not_owner, equal_slices_bits(sender_address, admin_address)); 166 | next_admin_address = in_msg_body~load_msg_addr(); 167 | in_msg_body.end_parse(); 168 | save_data(total_supply, admin_address, next_admin_address, jetton_wallet_code, metadata_uri); 169 | return (); 170 | } 171 | 172 | if (op == op::claim_admin) { 173 | in_msg_body.end_parse(); 174 | throw_unless(error::not_owner, equal_slices_bits(sender_address, next_admin_address)); 175 | save_data(total_supply, next_admin_address, address_none(), jetton_wallet_code, metadata_uri); 176 | return (); 177 | } 178 | 179 | ;; can be used to lock, unlock or reedem funds 180 | if (op == op::call_to) { 181 | throw_unless(error::not_owner, equal_slices_bits(sender_address, admin_address)); 182 | slice to_address = in_msg_body~load_msg_addr(); 183 | int ton_amount = in_msg_body~load_coins(); 184 | cell master_msg = in_msg_body~load_ref(); 185 | in_msg_body.end_parse(); 186 | 187 | slice master_msg_slice = master_msg.begin_parse(); 188 | int master_op = master_msg_slice~load_op(); 189 | master_msg_slice~skip_query_id(); 190 | ;; parse-validate messages 191 | if (master_op == op::transfer) { 192 | ;; see transfer TL-B layout in jetton.tlb 193 | master_msg_slice~load_coins(); ;; jetton_amount 194 | master_msg_slice~load_msg_addr(); ;; to_owner_address 195 | master_msg_slice~load_msg_addr(); ;; response_address 196 | master_msg_slice~skip_maybe_ref(); ;; custom_payload 197 | int forward_ton_amount = master_msg_slice~load_coins(); ;; forward_ton_amount 198 | check_either_forward_payload(master_msg_slice); ;; either_forward_payload 199 | 200 | check_amount_is_enough_to_transfer(ton_amount, forward_ton_amount, fwd_fee); 201 | 202 | } elseif (master_op == op::burn) { 203 | ;; see burn TL-B layout in jetton.tlb 204 | master_msg_slice~load_coins(); ;; jetton_amount 205 | master_msg_slice~load_msg_addr(); ;; response_address 206 | master_msg_slice~skip_maybe_ref(); ;; custom_payload 207 | master_msg_slice.end_parse(); 208 | 209 | check_amount_is_enough_to_burn(ton_amount); 210 | 211 | } elseif (master_op == op::set_status) { 212 | master_msg_slice~load_uint(STATUS_SIZE); ;; status 213 | master_msg_slice.end_parse(); 214 | } else { 215 | throw(error::invalid_op); 216 | } 217 | send_to_jetton_wallet(to_address, jetton_wallet_code, ton_amount, master_msg, FALSE); 218 | return (); 219 | } 220 | 221 | if (op == op::change_metadata_uri) { 222 | throw_unless(error::not_owner, equal_slices_bits(sender_address, admin_address)); 223 | save_data(total_supply, admin_address, next_admin_address, jetton_wallet_code, begin_cell().store_slice(in_msg_body).end_cell()); 224 | return (); 225 | } 226 | 227 | if (op == op::upgrade) { 228 | throw_unless(error::not_owner, equal_slices_bits(sender_address, admin_address)); 229 | (cell new_data, cell new_code) = (in_msg_body~load_ref(), in_msg_body~load_ref()); 230 | in_msg_body.end_parse(); 231 | set_data(new_data); 232 | set_code(new_code); 233 | return (); 234 | } 235 | 236 | if (op == op::top_up) { 237 | return (); ;; just accept tons 238 | } 239 | 240 | throw(error::wrong_op); 241 | } 242 | 243 | cell build_content_cell(slice metadata_uri) inline { 244 | cell content_dict = new_dict(); 245 | content_dict~set_token_snake_metadata_entry("uri"H, metadata_uri); 246 | content_dict~set_token_snake_metadata_entry("decimals"H, "6"); 247 | return create_token_onchain_metadata(content_dict); 248 | } 249 | 250 | (int, int, slice, cell, cell) get_jetton_data() method_id { 251 | (int total_supply, slice admin_address, slice next_admin_address, cell jetton_wallet_code, cell metadata_uri) = load_data(); 252 | return (total_supply, TRUE, admin_address, build_content_cell(metadata_uri.begin_parse()), jetton_wallet_code); 253 | } 254 | 255 | slice get_wallet_address(slice owner_address) method_id { 256 | (int total_supply, slice admin_address, slice next_admin_address, cell jetton_wallet_code, cell metadata_uri) = load_data(); 257 | return calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code); 258 | } 259 | 260 | slice get_next_admin_address() method_id { 261 | (int total_supply, slice admin_address, slice next_admin_address, cell jetton_wallet_code, cell metadata_uri) = load_data(); 262 | return next_admin_address; 263 | } 264 | -------------------------------------------------------------------------------- /contracts/jetton-utils.fc: -------------------------------------------------------------------------------- 1 | #include "workchain.fc"; 2 | 3 | const int STATUS_SIZE = 4; 4 | 5 | cell pack_jetton_wallet_data(int status, int balance, slice owner_address, slice jetton_master_address) inline { 6 | return begin_cell() 7 | .store_uint(status, STATUS_SIZE) 8 | .store_coins(balance) 9 | .store_slice(owner_address) 10 | .store_slice(jetton_master_address) 11 | .end_cell(); 12 | } 13 | 14 | cell calculate_jetton_wallet_state_init(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { 15 | {- 16 | https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L144 17 | _ split_depth:(Maybe (## 5)) special:(Maybe TickTock) 18 | code:(Maybe ^Cell) data:(Maybe ^Cell) 19 | library:(Maybe ^Cell) = StateInit; 20 | -} 21 | return begin_cell() 22 | .store_uint(0, 2) ;; 0b00 - No split_depth; No special 23 | .store_maybe_ref(jetton_wallet_code) 24 | .store_maybe_ref( 25 | pack_jetton_wallet_data( 26 | 0, ;; status 27 | 0, ;; balance 28 | owner_address, 29 | jetton_master_address) 30 | ) 31 | .store_uint(0, 1) ;; Empty libraries 32 | .end_cell(); 33 | } 34 | 35 | slice calculate_jetton_wallet_address(cell state_init) inline { 36 | {- 37 | https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L105 38 | addr_std$10 anycast:(Maybe Anycast) workchain_id:int8 address:bits256 = MsgAddressInt; 39 | -} 40 | return begin_cell() 41 | .store_uint(4, 3) ;; 0b100 = addr_std$10 tag; No anycast 42 | .store_int(MY_WORKCHAIN, 8) 43 | .store_uint(cell_hash(state_init), 256) 44 | .end_cell() 45 | .begin_parse(); 46 | } 47 | 48 | slice calculate_user_jetton_wallet_address(slice owner_address, slice jetton_master_address, cell jetton_wallet_code) inline { 49 | return calculate_jetton_wallet_address(calculate_jetton_wallet_state_init(owner_address, jetton_master_address, jetton_wallet_code)); 50 | } 51 | 52 | () check_either_forward_payload(slice s) impure inline { 53 | if (s.preload_uint(1)) { 54 | ;; forward_payload in ref 55 | (int remain_bits, int remain_refs) = slice_bits_refs(s); 56 | throw_unless(error::invalid_message, (remain_refs == 1) & (remain_bits == 1)); ;; we check that there is no excess in the slice 57 | } 58 | ;; else forward_payload in slice - arbitrary bits and refs 59 | } -------------------------------------------------------------------------------- /contracts/jetton-wallet.fc: -------------------------------------------------------------------------------- 1 | ;; Jetton Wallet Smart Contract 2 | 3 | #pragma version >=0.4.3; 4 | 5 | #include "stdlib.fc"; 6 | #include "op-codes.fc"; 7 | #include "workchain.fc"; 8 | #include "jetton-utils.fc"; 9 | #include "gas.fc"; 10 | 11 | {- 12 | Storage 13 | 14 | Note, status==0 means unlocked - user can freely transfer and recieve jettons (only admin can burn). 15 | (status & 1) bit means user can not send jettons 16 | (status & 2) bit means user can not receive jettons. 17 | Master (minter) smart-contract able to make outgoing actions (transfer, burn jettons) with any status. 18 | 19 | storage#_ status:uint4 20 | balance:Coins owner_address:MsgAddressInt 21 | jetton_master_address:MsgAddressInt = Storage; 22 | -} 23 | 24 | (int, int, slice, slice) load_data() inline { 25 | slice ds = get_data().begin_parse(); 26 | var data = ( 27 | ds~load_uint(STATUS_SIZE), ;; status 28 | ds~load_coins(), ;; balance 29 | ds~load_msg_addr(), ;; owner_address 30 | ds~load_msg_addr() ;; jetton_master_address 31 | ); 32 | ds.end_parse(); 33 | return data; 34 | } 35 | 36 | () save_data(int status, int balance, slice owner_address, slice jetton_master_address) impure inline { 37 | set_data(pack_jetton_wallet_data(status, balance, owner_address, jetton_master_address)); 38 | } 39 | 40 | () send_jettons(slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure inline_ref { 41 | ;; see transfer TL-B layout in jetton.tlb 42 | int query_id = in_msg_body~load_query_id(); 43 | int jetton_amount = in_msg_body~load_coins(); 44 | slice to_owner_address = in_msg_body~load_msg_addr(); 45 | check_same_workchain(to_owner_address); 46 | (int status, int balance, slice owner_address, slice jetton_master_address) = load_data(); 47 | int is_from_master = equal_slices_bits(jetton_master_address, sender_address); 48 | int outgoing_transfers_unlocked = ((status & 1) == 0); 49 | throw_unless(error::contract_locked, outgoing_transfers_unlocked | is_from_master); 50 | throw_unless(error::not_owner, equal_slices_bits(owner_address, sender_address) | is_from_master); 51 | 52 | balance -= jetton_amount; 53 | throw_unless(error::balance_error, balance >= 0); 54 | 55 | cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, my_code()); 56 | slice to_wallet_address = calculate_jetton_wallet_address(state_init); 57 | slice response_address = in_msg_body~load_msg_addr(); 58 | in_msg_body~skip_maybe_ref(); ;; custom_payload 59 | int forward_ton_amount = in_msg_body~load_coins(); 60 | check_either_forward_payload(in_msg_body); 61 | slice either_forward_payload = in_msg_body; 62 | 63 | ;; see internal TL-B layout in jetton.tlb 64 | cell msg_body = begin_cell() 65 | .store_op(op::internal_transfer) 66 | .store_query_id(query_id) 67 | .store_coins(jetton_amount) 68 | .store_slice(owner_address) 69 | .store_slice(response_address) 70 | .store_coins(forward_ton_amount) 71 | .store_slice(either_forward_payload) 72 | .end_cell(); 73 | 74 | ;; build MessageRelaxed, see TL-B layout in stdlib.fc#L733 75 | cell msg = begin_cell() 76 | .store_msg_flags_and_address_none(BOUNCEABLE) 77 | .store_slice(to_wallet_address) 78 | .store_coins(0) 79 | .store_statinit_ref_and_body_ref(state_init, msg_body) 80 | .end_cell(); 81 | 82 | check_amount_is_enough_to_transfer(msg_value, forward_ton_amount, fwd_fee); 83 | 84 | send_raw_message(msg, SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL); 85 | 86 | save_data(status, balance, owner_address, jetton_master_address); 87 | } 88 | 89 | () receive_jettons(slice in_msg_body, slice sender_address, int my_ton_balance, int msg_value) impure inline_ref { 90 | (int status, int balance, slice owner_address, slice jetton_master_address) = load_data(); 91 | int incoming_transfers_locked = ((status & 2) == 2); 92 | throw_if(error::contract_locked, incoming_transfers_locked); 93 | ;; see internal TL-B layout in jetton.tlb 94 | int query_id = in_msg_body~load_query_id(); 95 | int jetton_amount = in_msg_body~load_coins(); 96 | balance += jetton_amount; 97 | slice from_address = in_msg_body~load_msg_addr(); 98 | slice response_address = in_msg_body~load_msg_addr(); 99 | throw_unless(error::not_valid_wallet, 100 | equal_slices_bits(jetton_master_address, sender_address) 101 | | 102 | equal_slices_bits(calculate_user_jetton_wallet_address(from_address, jetton_master_address, my_code()), sender_address) 103 | ); 104 | int forward_ton_amount = in_msg_body~load_coins(); 105 | 106 | if (forward_ton_amount) { 107 | slice either_forward_payload = in_msg_body; 108 | 109 | ;; see transfer_notification TL-B layout in jetton.tlb 110 | cell msg_body = begin_cell() 111 | .store_op(op::transfer_notification) 112 | .store_query_id(query_id) 113 | .store_coins(jetton_amount) 114 | .store_slice(from_address) 115 | .store_slice(either_forward_payload) 116 | .end_cell(); 117 | 118 | ;; build MessageRelaxed, see TL-B layout in stdlib.fc#L733 119 | cell msg = begin_cell() 120 | .store_msg_flags_and_address_none(NON_BOUNCEABLE) 121 | .store_slice(owner_address) 122 | .store_coins(forward_ton_amount) 123 | .store_only_body_ref(msg_body) 124 | .end_cell(); 125 | 126 | send_raw_message(msg, SEND_MODE_PAY_FEES_SEPARATELY | SEND_MODE_BOUNCE_ON_ACTION_FAIL); 127 | } 128 | 129 | if (~ is_address_none(response_address)) { 130 | int to_leave_on_balance = my_ton_balance - msg_value + my_storage_due(); 131 | raw_reserve(max(to_leave_on_balance, calculate_jetton_wallet_min_storage_fee()), RESERVE_AT_MOST); 132 | 133 | ;; build MessageRelaxed, see TL-B layout in stdlib.fc#L733 134 | cell msg = begin_cell() 135 | .store_msg_flags_and_address_none(NON_BOUNCEABLE) 136 | .store_slice(response_address) 137 | .store_coins(0) 138 | .store_prefix_only_body() 139 | .store_op(op::excesses) 140 | .store_query_id(query_id) 141 | .end_cell(); 142 | send_raw_message(msg, SEND_MODE_CARRY_ALL_BALANCE | SEND_MODE_IGNORE_ERRORS); 143 | } 144 | 145 | save_data(status, balance, owner_address, jetton_master_address); 146 | } 147 | 148 | () burn_jettons(slice in_msg_body, slice sender_address, int msg_value) impure inline_ref { 149 | (int status, int balance, slice owner_address, slice jetton_master_address) = load_data(); 150 | int query_id = in_msg_body~load_query_id(); 151 | int jetton_amount = in_msg_body~load_coins(); 152 | slice response_address = in_msg_body~load_msg_addr(); 153 | in_msg_body~skip_maybe_ref(); ;; custom_payload 154 | in_msg_body.end_parse(); 155 | 156 | balance -= jetton_amount; 157 | int is_from_master = equal_slices_bits(jetton_master_address, sender_address); 158 | throw_unless(error::not_owner, is_from_master); 159 | throw_unless(error::balance_error, balance >= 0); 160 | 161 | ;; see burn_notification TL-B layout in jetton.tlb 162 | cell msg_body = begin_cell() 163 | .store_op(op::burn_notification) 164 | .store_query_id(query_id) 165 | .store_coins(jetton_amount) 166 | .store_slice(owner_address) 167 | .store_slice(response_address) 168 | .end_cell(); 169 | 170 | ;; build MessageRelaxed, see TL-B layout in stdlib.fc#L733 171 | cell msg = begin_cell() 172 | .store_msg_flags_and_address_none(BOUNCEABLE) 173 | .store_slice(jetton_master_address) 174 | .store_coins(0) 175 | .store_only_body_ref(msg_body) 176 | .end_cell(); 177 | 178 | check_amount_is_enough_to_burn(msg_value); 179 | 180 | send_raw_message(msg, SEND_MODE_CARRY_ALL_REMAINING_MESSAGE_VALUE | SEND_MODE_BOUNCE_ON_ACTION_FAIL); 181 | 182 | save_data(status, balance, owner_address, jetton_master_address); 183 | } 184 | 185 | () on_bounce(slice in_msg_body) impure inline { 186 | in_msg_body~skip_bounced_prefix(); 187 | (int status, int balance, slice owner_address, slice jetton_master_address) = load_data(); 188 | int op = in_msg_body~load_op(); 189 | throw_unless(error::wrong_op, (op == op::internal_transfer) | (op == op::burn_notification)); 190 | in_msg_body~skip_query_id(); 191 | int jetton_amount = in_msg_body~load_coins(); 192 | save_data(status, balance + jetton_amount, owner_address, jetton_master_address); 193 | } 194 | 195 | () recv_internal(int my_ton_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { 196 | slice in_msg_full_slice = in_msg_full.begin_parse(); 197 | int msg_flags = in_msg_full_slice~load_msg_flags(); 198 | if (msg_flags & 1) { ;; is bounced 199 | on_bounce(in_msg_body); 200 | return (); 201 | } 202 | slice sender_address = in_msg_full_slice~load_msg_addr(); 203 | int fwd_fee_from_in_msg = in_msg_full_slice~retrieve_fwd_fee(); 204 | int fwd_fee = get_original_fwd_fee(MY_WORKCHAIN, fwd_fee_from_in_msg); ;; we use message fwd_fee for estimation of forward_payload costs 205 | 206 | int op = in_msg_body~load_op(); 207 | 208 | ;; outgoing transfer 209 | if (op == op::transfer) { 210 | send_jettons(in_msg_body, sender_address, msg_value, fwd_fee); 211 | return (); 212 | } 213 | 214 | ;; incoming transfer 215 | if (op == op::internal_transfer) { 216 | receive_jettons(in_msg_body, sender_address, my_ton_balance, msg_value); 217 | return (); 218 | } 219 | 220 | ;; burn 221 | if (op == op::burn) { 222 | burn_jettons(in_msg_body, sender_address, msg_value); 223 | return (); 224 | } 225 | 226 | if (op == op::set_status) { 227 | in_msg_body~skip_query_id(); 228 | int new_status = in_msg_body~load_uint(STATUS_SIZE); 229 | in_msg_body.end_parse(); 230 | (_, int balance, slice owner_address, slice jetton_master_address) = load_data(); 231 | throw_unless(error::not_valid_wallet, equal_slices_bits(sender_address, jetton_master_address)); 232 | save_data(new_status, balance, owner_address, jetton_master_address); 233 | return (); 234 | } 235 | 236 | if (op == op::top_up) { 237 | return (); ;; just accept tons 238 | } 239 | 240 | throw(error::wrong_op); 241 | } 242 | 243 | (int, slice, slice, cell) get_wallet_data() method_id { 244 | (_, int balance, slice owner_address, slice jetton_master_address) = load_data(); 245 | return (balance, owner_address, jetton_master_address, my_code()); 246 | } 247 | 248 | int get_status() method_id { 249 | (int status, _, _, _) = load_data(); 250 | return status; 251 | } 252 | -------------------------------------------------------------------------------- /contracts/jetton.tlb: -------------------------------------------------------------------------------- 1 | //======================================================================= 2 | // BASIC 3 | // https://github.com/ton-blockchain/ton/blob/master/crypto/block/block.tlb 4 | 5 | bool_false$0 = Bool; 6 | bool_true$1 = Bool; 7 | 8 | nothing$0 {X:Type} = Maybe X; 9 | just$1 {X:Type} value:X = Maybe X; 10 | 11 | 12 | left$0 {X:Type} {Y:Type} value:X = Either X Y; 13 | right$1 {X:Type} {Y:Type} value:Y = Either X Y; 14 | 15 | addr_none$00 = MsgAddressExt; 16 | addr_extern$01 len:(## 9) external_address:(bits len) 17 | = MsgAddressExt; 18 | anycast_info$_ depth:(#<= 30) { depth >= 1 } 19 | rewrite_pfx:(bits depth) = Anycast; 20 | addr_std$10 anycast:(Maybe Anycast) 21 | workchain_id:int8 address:bits256 = MsgAddressInt; 22 | addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9) 23 | workchain_id:int32 address:(bits addr_len) = MsgAddressInt; 24 | _ _:MsgAddressInt = MsgAddress; 25 | _ _:MsgAddressExt = MsgAddress; 26 | 27 | var_uint$_ {n:#} len:(#< n) value:(uint (len * 8)) 28 | = VarUInteger n; 29 | var_int$_ {n:#} len:(#< n) value:(int (len * 8)) 30 | = VarInteger n; 31 | nanograms$_ amount:(VarUInteger 16) = Grams; 32 | 33 | _ grams:Grams = Coins; 34 | 35 | 36 | //======================================================================= 37 | // TEP - 74 38 | // https://github.com/ton-blockchain/TEPs/blob/master/text/0074-jettons-standard.md 39 | 40 | transfer#0f8a7ea5 41 | query_id:uint64 42 | amount:Coins 43 | destination:MsgAddress 44 | response_destination:MsgAddress 45 | custom_payload:(Maybe ^Cell) 46 | forward_ton_amount:Coins 47 | forward_payload:(Either Cell ^Cell) 48 | = JettonMsg; 49 | 50 | burn#595f07bc 51 | query_id:uint64 52 | amount:Coins 53 | response_destination:MsgAddress 54 | custom_payload:(Maybe ^Cell) 55 | = JettonMsg; 56 | 57 | transfer_notification#7362d09c 58 | query_id:uint64 59 | amount:Coins 60 | sender:MsgAddress 61 | forward_payload:(Either Cell ^Cell) 62 | = JettonOutMsg; 63 | 64 | excesses#d53276db query_id:uint64 = InternalMsgBody; 65 | 66 | 67 | internal_transfer#178d4519 68 | query_id:uint64 69 | amount:Coins 70 | from:MsgAddress 71 | response_address:MsgAddress 72 | forward_ton_amount:Coins 73 | forward_payload:(Either Cell ^Cell) 74 | = JettonInternalTransfer; 75 | 76 | burn_notification#7bdd97de 77 | query_id:uint64 78 | amount:Coins 79 | sender:MsgAddress 80 | response_destination:MsgAddress 81 | = JettonMinterMsg; 82 | 83 | _ _:JettonMsg = InternalMsgBody; 84 | _ _:JettonOutMsg = InternalMsgBody; 85 | _ _:JettonInternalTransfer = InternalMsgBody; 86 | 87 | //======================================================================= 88 | // TEP - 89 89 | // https://github.com/ton-blockchain/TEPs/blob/master/text/0089-jetton-wallet-discovery.md 90 | 91 | provide_wallet_address#2c76b973 92 | query_id:uint64 93 | owner_address:MsgAddress 94 | include_address:Bool 95 | = JettonMinterMsg; 96 | 97 | take_wallet_address#d1735400 98 | query_id:uint64 99 | wallet_address:MsgAddress 100 | owner_address:(Maybe ^MsgAddress) 101 | = JettonMinterOutMsg; 102 | 103 | 104 | _ _:JettonMinterMsg = InternalMsgBody; 105 | _ _:JettonMinterOutMsg = InternalMsgBody; 106 | 107 | //======================================================================= 108 | // Stable 109 | 110 | top_up#d372158c 111 | query_id:uint64 112 | = InternalMsgBody; 113 | 114 | set_status#eed236d3 115 | query_id:uint64 116 | status:uint4 117 | = JettonMsg; 118 | 119 | mint#642b7d07 120 | query_id:uint64 121 | to_address:MsgAddressInt 122 | ton_amount:Coins 123 | master_msg:^JettonInternalTransfer 124 | = JettonMinterMsg; 125 | 126 | change_admin#6501f354 127 | query_id:uint64 128 | new_admin_address:MsgAddress 129 | = JettonMinterMsg; 130 | 131 | claim_admin#fb88e119 132 | query_id:uint64 133 | = JettonMinterMsg; 134 | 135 | call_to#235caf52 136 | query_id:uint64 137 | to_address:MsgAddressInt 138 | ton_amount:Coins 139 | master_msg:^JettonMsg 140 | = JettonMinterMsg; 141 | 142 | upgrade#2508d66a 143 | query_id:uint64 144 | new_data:^Cell 145 | new_code:^Cell 146 | = JettonMinterMsg; 147 | 148 | change_metadata_uri#cb862902 149 | query_id:uint64 150 | metadata:Cell 151 | = JettonMinterMsg; -------------------------------------------------------------------------------- /contracts/op-codes.fc: -------------------------------------------------------------------------------- 1 | ;; common 2 | 3 | const op::transfer = 0xf8a7ea5; 4 | const op::transfer_notification = 0x7362d09c; 5 | const op::internal_transfer = 0x178d4519; 6 | const op::excesses = 0xd53276db; 7 | const op::burn = 0x595f07bc; 8 | const op::burn_notification = 0x7bdd97de; 9 | 10 | const op::provide_wallet_address = 0x2c76b973; 11 | const op::take_wallet_address = 0xd1735400; 12 | 13 | const op::top_up = 0xd372158c; 14 | 15 | const error::invalid_op = 72; 16 | const error::wrong_op = 0xffff; 17 | const error::not_owner = 73; 18 | const error::not_valid_wallet = 74; 19 | const error::wrong_workchain = 333; 20 | 21 | ;; jetton-minter 22 | 23 | const op::mint = 0x642b7d07; 24 | const op::change_admin = 0x6501f354; 25 | const op::claim_admin = 0xfb88e119; 26 | const op::upgrade = 0x2508d66a; 27 | const op::call_to = 0x235caf52; 28 | const op::change_metadata_uri = 0xcb862902; 29 | 30 | ;; jetton-wallet 31 | 32 | const op::set_status = 0xeed236d3; 33 | 34 | const error::contract_locked = 45; 35 | const error::balance_error = 47; 36 | const error::not_enough_gas = 48; 37 | const error::invalid_message = 49; 38 | -------------------------------------------------------------------------------- /contracts/stdlib.fc: -------------------------------------------------------------------------------- 1 | ;; Standard library for funC 2 | ;; 3 | 4 | {- 5 | This file is part of TON FunC Standard Library. 6 | 7 | FunC Standard Library is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU Lesser General Public License as published by 9 | the Free Software Foundation, either version 2 of the License, or 10 | (at your option) any later version. 11 | 12 | FunC Standard Library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU Lesser General Public License for more details. 16 | 17 | -} 18 | 19 | {- 20 | # Tuple manipulation primitives 21 | The names and the types are mostly self-explaining. 22 | See [polymorhism with forall](https://ton.org/docs/#/func/functions?id=polymorphism-with-forall) 23 | for more info on the polymorphic functions. 24 | 25 | Note that currently values of atomic type `tuple` can't be cast to composite tuple type (e.g. `[int, cell]`) 26 | and vise versa. 27 | -} 28 | 29 | {- 30 | # Lisp-style lists 31 | 32 | Lists can be represented as nested 2-elements tuples. 33 | Empty list is conventionally represented as TVM `null` value (it can be obtained by calling [null()]). 34 | For example, tuple `(1, (2, (3, null)))` represents list `[1, 2, 3]`. Elements of a list can be of different types. 35 | -} 36 | 37 | ;;; Adds an element to the beginning of lisp-style list. 38 | forall X -> tuple cons(X head, tuple tail) asm "CONS"; 39 | 40 | ;;; Extracts the head and the tail of lisp-style list. 41 | forall X -> (X, tuple) uncons(tuple list) asm "UNCONS"; 42 | 43 | ;;; Extracts the tail and the head of lisp-style list. 44 | forall X -> (tuple, X) list_next(tuple list) asm(-> 1 0) "UNCONS"; 45 | 46 | ;;; Returns the head of lisp-style list. 47 | forall X -> X car(tuple list) asm "CAR"; 48 | 49 | ;;; Returns the tail of lisp-style list. 50 | tuple cdr(tuple list) asm "CDR"; 51 | 52 | ;;; Creates tuple with zero elements. 53 | tuple empty_tuple() asm "NIL"; 54 | 55 | ;;; Appends a value `x` to a `Tuple t = (x1, ..., xn)`, but only if the resulting `Tuple t' = (x1, ..., xn, x)` 56 | ;;; is of length at most 255. Otherwise throws a type check exception. 57 | forall X -> tuple tpush(tuple t, X value) asm "TPUSH"; 58 | forall X -> (tuple, ()) ~tpush(tuple t, X value) asm "TPUSH"; 59 | 60 | ;;; Creates a tuple of length one with given argument as element. 61 | forall X -> [X] single(X x) asm "SINGLE"; 62 | 63 | ;;; Unpacks a tuple of length one 64 | forall X -> X unsingle([X] t) asm "UNSINGLE"; 65 | 66 | ;;; Creates a tuple of length two with given arguments as elements. 67 | forall X, Y -> [X, Y] pair(X x, Y y) asm "PAIR"; 68 | 69 | ;;; Unpacks a tuple of length two 70 | forall X, Y -> (X, Y) unpair([X, Y] t) asm "UNPAIR"; 71 | 72 | ;;; Creates a tuple of length three with given arguments as elements. 73 | forall X, Y, Z -> [X, Y, Z] triple(X x, Y y, Z z) asm "TRIPLE"; 74 | 75 | ;;; Unpacks a tuple of length three 76 | forall X, Y, Z -> (X, Y, Z) untriple([X, Y, Z] t) asm "UNTRIPLE"; 77 | 78 | ;;; Creates a tuple of length four with given arguments as elements. 79 | forall X, Y, Z, W -> [X, Y, Z, W] tuple4(X x, Y y, Z z, W w) asm "4 TUPLE"; 80 | 81 | ;;; Unpacks a tuple of length four 82 | forall X, Y, Z, W -> (X, Y, Z, W) untuple4([X, Y, Z, W] t) asm "4 UNTUPLE"; 83 | 84 | ;;; Returns the first element of a tuple (with unknown element types). 85 | forall X -> X first(tuple t) asm "FIRST"; 86 | 87 | ;;; Returns the second element of a tuple (with unknown element types). 88 | forall X -> X second(tuple t) asm "SECOND"; 89 | 90 | ;;; Returns the third element of a tuple (with unknown element types). 91 | forall X -> X third(tuple t) asm "THIRD"; 92 | 93 | ;;; Returns the fourth element of a tuple (with unknown element types). 94 | forall X -> X fourth(tuple t) asm "3 INDEX"; 95 | 96 | ;;; Returns the first element of a pair tuple. 97 | forall X, Y -> X pair_first([X, Y] p) asm "FIRST"; 98 | 99 | ;;; Returns the second element of a pair tuple. 100 | forall X, Y -> Y pair_second([X, Y] p) asm "SECOND"; 101 | 102 | ;;; Returns the first element of a triple tuple. 103 | forall X, Y, Z -> X triple_first([X, Y, Z] p) asm "FIRST"; 104 | 105 | ;;; Returns the second element of a triple tuple. 106 | forall X, Y, Z -> Y triple_second([X, Y, Z] p) asm "SECOND"; 107 | 108 | ;;; Returns the third element of a triple tuple. 109 | forall X, Y, Z -> Z triple_third([X, Y, Z] p) asm "THIRD"; 110 | 111 | 112 | ;;; Push null element (casted to given type) 113 | ;;; By the TVM type `Null` FunC represents absence of a value of some atomic type. 114 | ;;; So `null` can actually have any atomic type. 115 | forall X -> X null() asm "PUSHNULL"; 116 | 117 | ;;; Moves a variable [x] to the top of the stack 118 | forall X -> (X, ()) ~impure_touch(X x) impure asm "NOP"; 119 | 120 | 121 | 122 | ;;; Returns the current Unix time as an Integer 123 | int now() asm "NOW"; 124 | 125 | ;;; Returns the internal address of the current smart contract as a Slice with a `MsgAddressInt`. 126 | ;;; If necessary, it can be parsed further using primitives such as [parse_std_addr]. 127 | slice my_address() asm "MYADDR"; 128 | 129 | ;;; Returns the balance of the smart contract as a tuple consisting of an int 130 | ;;; (balance in nanotoncoins) and a `cell` 131 | ;;; (a dictionary with 32-bit keys representing the balance of "extra currencies") 132 | ;;; at the start of Computation Phase. 133 | ;;; Note that RAW primitives such as [send_raw_message] do not update this field. 134 | [int, cell] get_balance() asm "BALANCE"; 135 | 136 | ;;; Returns the logical time of the current transaction. 137 | int cur_lt() asm "LTIME"; 138 | 139 | ;;; Returns the starting logical time of the current block. 140 | int block_lt() asm "BLOCKLT"; 141 | 142 | ;;; Computes the representation hash of a `cell` [c] and returns it as a 256-bit unsigned integer `x`. 143 | ;;; Useful for signing and checking signatures of arbitrary entities represented by a tree of cells. 144 | int cell_hash(cell c) asm "HASHCU"; 145 | 146 | ;;; Computes the hash of a `slice s` and returns it as a 256-bit unsigned integer `x`. 147 | ;;; The result is the same as if an ordinary cell containing only data and references from `s` had been created 148 | ;;; and its hash computed by [cell_hash]. 149 | int slice_hash(slice s) asm "HASHSU"; 150 | 151 | ;;; Computes sha256 of the data bits of `slice` [s]. If the bit length of `s` is not divisible by eight, 152 | ;;; throws a cell underflow exception. The hash value is returned as a 256-bit unsigned integer `x`. 153 | int string_hash(slice s) asm "SHA256U"; 154 | 155 | {- 156 | # Signature checks 157 | -} 158 | 159 | ;;; Checks the Ed25519-`signature` of a `hash` (a 256-bit unsigned integer, usually computed as the hash of some data) 160 | ;;; using [public_key] (also represented by a 256-bit unsigned integer). 161 | ;;; The signature must contain at least 512 data bits; only the first 512 bits are used. 162 | ;;; The result is `−1` if the signature is valid, `0` otherwise. 163 | ;;; Note that `CHKSIGNU` creates a 256-bit slice with the hash and calls `CHKSIGNS`. 164 | ;;; That is, if [hash] is computed as the hash of some data, these data are hashed twice, 165 | ;;; the second hashing occurring inside `CHKSIGNS`. 166 | int check_signature(int hash, slice signature, int public_key) asm "CHKSIGNU"; 167 | 168 | ;;; Checks whether [signature] is a valid Ed25519-signature of the data portion of `slice data` using `public_key`, 169 | ;;; similarly to [check_signature]. 170 | ;;; If the bit length of [data] is not divisible by eight, throws a cell underflow exception. 171 | ;;; The verification of Ed25519 signatures is the standard one, 172 | ;;; with sha256 used to reduce [data] to the 256-bit number that is actually signed. 173 | int check_data_signature(slice data, slice signature, int public_key) asm "CHKSIGNS"; 174 | 175 | {--- 176 | # Computation of boc size 177 | The primitives below may be useful for computing storage fees of user-provided data. 178 | -} 179 | 180 | ;;; Returns `(x, y, z, -1)` or `(null, null, null, 0)`. 181 | ;;; Recursively computes the count of distinct cells `x`, data bits `y`, and cell references `z` 182 | ;;; in the DAG rooted at `cell` [c], effectively returning the total storage used by this DAG taking into account 183 | ;;; the identification of equal cells. 184 | ;;; The values of `x`, `y`, and `z` are computed by a depth-first traversal of this DAG, 185 | ;;; with a hash table of visited cell hashes used to prevent visits of already-visited cells. 186 | ;;; The total count of visited cells `x` cannot exceed non-negative [max_cells]; 187 | ;;; otherwise the computation is aborted before visiting the `(max_cells + 1)`-st cell and 188 | ;;; a zero flag is returned to indicate failure. If [c] is `null`, returns `x = y = z = 0`. 189 | (int, int, int) compute_data_size(cell c, int max_cells) impure asm "CDATASIZE"; 190 | 191 | ;;; Similar to [compute_data_size?], but accepting a `slice` [s] instead of a `cell`. 192 | ;;; The returned value of `x` does not take into account the cell that contains the `slice` [s] itself; 193 | ;;; however, the data bits and the cell references of [s] are accounted for in `y` and `z`. 194 | (int, int, int) slice_compute_data_size(slice s, int max_cells) impure asm "SDATASIZE"; 195 | 196 | ;;; A non-quiet version of [compute_data_size?] that throws a cell overflow exception (`8`) on failure. 197 | (int, int, int, int) compute_data_size?(cell c, int max_cells) asm "CDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; 198 | 199 | ;;; A non-quiet version of [slice_compute_data_size?] that throws a cell overflow exception (8) on failure. 200 | (int, int, int, int) slice_compute_data_size?(cell c, int max_cells) asm "SDATASIZEQ NULLSWAPIFNOT2 NULLSWAPIFNOT"; 201 | 202 | ;;; Throws an exception with exit_code excno if cond is not 0 (commented since implemented in compilator) 203 | ;; () throw_if(int excno, int cond) impure asm "THROWARGIF"; 204 | 205 | {-- 206 | # Debug primitives 207 | Only works for local TVM execution with debug level verbosity 208 | -} 209 | ;;; Dumps the stack (at most the top 255 values) and shows the total stack depth. 210 | () dump_stack() impure asm "DUMPSTK"; 211 | 212 | {- 213 | # Persistent storage save and load 214 | -} 215 | 216 | ;;; Returns the persistent contract storage cell. It can be parsed or modified with slice and builder primitives later. 217 | cell get_data() asm "c4 PUSH"; 218 | 219 | ;;; Sets `cell` [c] as persistent contract data. You can update persistent contract storage with this primitive. 220 | () set_data(cell c) impure asm "c4 POP"; 221 | 222 | {- 223 | # Continuation primitives 224 | -} 225 | ;;; Usually `c3` has a continuation initialized by the whole code of the contract. It is used for function calls. 226 | ;;; The primitive returns the current value of `c3`. 227 | cont get_c3() impure asm "c3 PUSH"; 228 | 229 | ;;; Updates the current value of `c3`. Usually, it is used for updating smart contract code in run-time. 230 | ;;; Note that after execution of this primitive the current code 231 | ;;; (and the stack of recursive function calls) won't change, 232 | ;;; but any other function call will use a function from the new code. 233 | () set_c3(cont c) impure asm "c3 POP"; 234 | 235 | ;;; Transforms a `slice` [s] into a simple ordinary continuation `c`, with `c.code = s` and an empty stack and savelist. 236 | cont bless(slice s) impure asm "BLESS"; 237 | 238 | {--- 239 | # Gas related primitives 240 | -} 241 | 242 | ;;; Sets current gas limit `gl` to its maximal allowed value `gm`, and resets the gas credit `gc` to zero, 243 | ;;; decreasing the value of `gr` by `gc` in the process. 244 | ;;; In other words, the current smart contract agrees to buy some gas to finish the current transaction. 245 | ;;; This action is required to process external messages, which bring no value (hence no gas) with themselves. 246 | ;;; 247 | ;;; For more details check [accept_message effects](https://ton.org/docs/#/smart-contracts/accept). 248 | () accept_message() impure asm "ACCEPT"; 249 | 250 | ;;; Sets current gas limit `gl` to the minimum of limit and `gm`, and resets the gas credit `gc` to zero. 251 | ;;; If the gas consumed so far (including the present instruction) exceeds the resulting value of `gl`, 252 | ;;; an (unhandled) out of gas exception is thrown before setting new gas limits. 253 | ;;; Notice that [set_gas_limit] with an argument `limit ≥ 2^63 − 1` is equivalent to [accept_message]. 254 | () set_gas_limit(int limit) impure asm "SETGASLIMIT"; 255 | 256 | ;;; Commits the current state of registers `c4` (“persistent data”) and `c5` (“actions”) 257 | ;;; so that the current execution is considered “successful” with the saved values even if an exception 258 | ;;; in Computation Phase is thrown later. 259 | () commit() impure asm "COMMIT"; 260 | 261 | ;;; Not implemented 262 | ;;; Computes the amount of gas that can be bought for `amount` nanoTONs, 263 | ;;; and sets `gl` accordingly in the same way as [set_gas_limit]. 264 | ;;() buy_gas(int amount) impure asm "BUYGAS"; 265 | 266 | ;;; Computes the minimum of two integers [x] and [y]. 267 | int min(int x, int y) asm "MIN"; 268 | 269 | ;;; Computes the maximum of two integers [x] and [y]. 270 | int max(int x, int y) asm "MAX"; 271 | 272 | ;;; Sorts two integers. 273 | (int, int) minmax(int x, int y) asm "MINMAX"; 274 | 275 | ;;; Computes the absolute value of an integer [x]. 276 | int abs(int x) asm "ABS"; 277 | 278 | {- 279 | # Slice primitives 280 | 281 | It is said that a primitive _loads_ some data, 282 | if it returns the data and the remainder of the slice 283 | (so it can also be used as [modifying method](https://ton.org/docs/#/func/statements?id=modifying-methods)). 284 | 285 | It is said that a primitive _preloads_ some data, if it returns only the data 286 | (it can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods)). 287 | 288 | Unless otherwise stated, loading and preloading primitives read the data from a prefix of the slice. 289 | -} 290 | 291 | 292 | ;;; Converts a `cell` [c] into a `slice`. Notice that [c] must be either an ordinary cell, 293 | ;;; or an exotic cell (see [TVM.pdf](https://ton-blockchain.github.io/docs/tvm.pdf), 3.1.2) 294 | ;;; which is automatically loaded to yield an ordinary cell `c'`, converted into a `slice` afterwards. 295 | slice begin_parse(cell c) asm "CTOS"; 296 | 297 | ;;; Checks if [s] is empty. If not, throws an exception. 298 | () end_parse(slice s) impure asm "ENDS"; 299 | 300 | ;;; Loads the first reference from the slice. 301 | (slice, cell) load_ref(slice s) asm(-> 1 0) "LDREF"; 302 | 303 | ;;; Preloads the first reference from the slice. 304 | cell preload_ref(slice s) asm "PLDREF"; 305 | 306 | {- Functions below are commented because are implemented on compilator level for optimisation -} 307 | 308 | ;;; Loads a signed [len]-bit integer from a slice [s]. 309 | ;; (slice, int) ~load_int(slice s, int len) asm(s len -> 1 0) "LDIX"; 310 | 311 | ;;; Loads an unsigned [len]-bit integer from a slice [s]. 312 | ;; (slice, int) ~load_uint(slice s, int len) asm( -> 1 0) "LDUX"; 313 | 314 | ;;; Preloads a signed [len]-bit integer from a slice [s]. 315 | ;; int preload_int(slice s, int len) asm "PLDIX"; 316 | 317 | ;;; Preloads an unsigned [len]-bit integer from a slice [s]. 318 | ;; int preload_uint(slice s, int len) asm "PLDUX"; 319 | 320 | ;;; Loads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`. 321 | ;; (slice, slice) load_bits(slice s, int len) asm(s len -> 1 0) "LDSLICEX"; 322 | 323 | ;;; Preloads the first `0 ≤ len ≤ 1023` bits from slice [s] into a separate `slice s''`. 324 | ;; slice preload_bits(slice s, int len) asm "PLDSLICEX"; 325 | 326 | ;;; Loads serialized amount of TonCoins (any unsigned integer up to `2^128 - 1`). 327 | (slice, int) load_grams(slice s) asm(-> 1 0) "LDGRAMS"; 328 | (slice, int) load_coins(slice s) asm(-> 1 0) "LDVARUINT16"; 329 | 330 | ;;; Returns all but the first `0 ≤ len ≤ 1023` bits of `slice` [s]. 331 | slice skip_bits(slice s, int len) asm "SDSKIPFIRST"; 332 | (slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST"; 333 | 334 | ;;; Returns the first `0 ≤ len ≤ 1023` bits of `slice` [s]. 335 | slice first_bits(slice s, int len) asm "SDCUTFIRST"; 336 | 337 | ;;; Returns all but the last `0 ≤ len ≤ 1023` bits of `slice` [s]. 338 | slice skip_last_bits(slice s, int len) asm "SDSKIPLAST"; 339 | (slice, ()) ~skip_last_bits(slice s, int len) asm "SDSKIPLAST"; 340 | 341 | ;;; Returns the last `0 ≤ len ≤ 1023` bits of `slice` [s]. 342 | slice slice_last(slice s, int len) asm "SDCUTLAST"; 343 | 344 | ;;; Loads a dictionary `D` (HashMapE) from `slice` [s]. 345 | ;;; (returns `null` if `nothing` constructor is used). 346 | (slice, cell) load_dict(slice s) asm(-> 1 0) "LDDICT"; 347 | 348 | ;;; Preloads a dictionary `D` from `slice` [s]. 349 | cell preload_dict(slice s) asm "PLDDICT"; 350 | 351 | ;;; Loads a dictionary as [load_dict], but returns only the remainder of the slice. 352 | slice skip_dict(slice s) asm "SKIPDICT"; 353 | (slice, ()) ~skip_dict(slice s) asm "SKIPDICT"; 354 | 355 | ;;; Loads (Maybe ^Cell) from `slice` [s]. 356 | ;;; In other words loads 1 bit and if it is true 357 | ;;; loads first ref and return it with slice remainder 358 | ;;; otherwise returns `null` and slice remainder 359 | (slice, cell) load_maybe_ref(slice s) asm(-> 1 0) "LDOPTREF"; 360 | 361 | ;;; Preloads (Maybe ^Cell) from `slice` [s]. 362 | cell preload_maybe_ref(slice s) asm "PLDOPTREF"; 363 | 364 | 365 | ;;; Returns the depth of `cell` [c]. 366 | ;;; If [c] has no references, then return `0`; 367 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [c]. 368 | ;;; If [c] is a `null` instead of a cell, returns zero. 369 | int cell_depth(cell c) asm "CDEPTH"; 370 | 371 | 372 | {- 373 | # Slice size primitives 374 | -} 375 | 376 | ;;; Returns the number of references in `slice` [s]. 377 | int slice_refs(slice s) asm "SREFS"; 378 | 379 | ;;; Returns the number of data bits in `slice` [s]. 380 | int slice_bits(slice s) asm "SBITS"; 381 | 382 | ;;; Returns both the number of data bits and the number of references in `slice` [s]. 383 | (int, int) slice_bits_refs(slice s) asm "SBITREFS"; 384 | 385 | ;;; Checks whether a `slice` [s] is empty (i.e., contains no bits of data and no cell references). 386 | int slice_empty?(slice s) asm "SEMPTY"; 387 | 388 | ;;; Checks whether `slice` [s] has no bits of data. 389 | int slice_data_empty?(slice s) asm "SDEMPTY"; 390 | 391 | ;;; Checks whether `slice` [s] has no references. 392 | int slice_refs_empty?(slice s) asm "SREMPTY"; 393 | 394 | ;;; Returns the depth of `slice` [s]. 395 | ;;; If [s] has no references, then returns `0`; 396 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [s]. 397 | int slice_depth(slice s) asm "SDEPTH"; 398 | 399 | {- 400 | # Builder size primitives 401 | -} 402 | 403 | ;;; Returns the number of cell references already stored in `builder` [b] 404 | int builder_refs(builder b) asm "BREFS"; 405 | 406 | ;;; Returns the number of data bits already stored in `builder` [b]. 407 | int builder_bits(builder b) asm "BBITS"; 408 | 409 | ;;; Returns the depth of `builder` [b]. 410 | ;;; If no cell references are stored in [b], then returns 0; 411 | ;;; otherwise the returned value is one plus the maximum of depths of cells referred to from [b]. 412 | int builder_depth(builder b) asm "BDEPTH"; 413 | 414 | {- 415 | # Builder primitives 416 | It is said that a primitive _stores_ a value `x` into a builder `b` 417 | if it returns a modified version of the builder `b'` with the value `x` stored at the end of it. 418 | It can be used as [non-modifying method](https://ton.org/docs/#/func/statements?id=non-modifying-methods). 419 | 420 | All the primitives below first check whether there is enough space in the `builder`, 421 | and only then check the range of the value being serialized. 422 | -} 423 | 424 | ;;; Creates a new empty `builder`. 425 | builder begin_cell() asm "NEWC"; 426 | 427 | ;;; Converts a `builder` into an ordinary `cell`. 428 | cell end_cell(builder b) asm "ENDC"; 429 | 430 | ;;; Stores a reference to `cell` [c] into `builder` [b]. 431 | builder store_ref(builder b, cell c) asm(c b) "STREF"; 432 | 433 | ;;; Stores an unsigned [len]-bit integer `x` into `b` for `0 ≤ len ≤ 256`. 434 | ;; builder store_uint(builder b, int x, int len) asm(x b len) "STUX"; 435 | 436 | ;;; Stores a signed [len]-bit integer `x` into `b` for` 0 ≤ len ≤ 257`. 437 | ;; builder store_int(builder b, int x, int len) asm(x b len) "STIX"; 438 | 439 | 440 | ;;; Stores `slice` [s] into `builder` [b] 441 | builder store_slice(builder b, slice s) asm "STSLICER"; 442 | 443 | ;;; Stores (serializes) an integer [x] in the range `0..2^128 − 1` into `builder` [b]. 444 | ;;; The serialization of [x] consists of a 4-bit unsigned big-endian integer `l`, 445 | ;;; which is the smallest integer `l ≥ 0`, such that `x < 2^8l`, 446 | ;;; followed by an `8l`-bit unsigned big-endian representation of [x]. 447 | ;;; If [x] does not belong to the supported range, a range check exception is thrown. 448 | ;;; 449 | ;;; Store amounts of TonCoins to the builder as VarUInteger 16 450 | builder store_grams(builder b, int x) asm "STGRAMS"; 451 | builder store_coins(builder b, int x) asm "STVARUINT16"; 452 | 453 | ;;; Stores dictionary `D` represented by `cell` [c] or `null` into `builder` [b]. 454 | ;;; In other words, stores a `1`-bit and a reference to [c] if [c] is not `null` and `0`-bit otherwise. 455 | builder store_dict(builder b, cell c) asm(c b) "STDICT"; 456 | 457 | ;;; Stores (Maybe ^Cell) to builder: 458 | ;;; if cell is null store 1 zero bit 459 | ;;; otherwise store 1 true bit and ref to cell 460 | builder store_maybe_ref(builder b, cell c) asm(c b) "STOPTREF"; 461 | 462 | 463 | {- 464 | # Address manipulation primitives 465 | The address manipulation primitives listed below serialize and deserialize values according to the following TL-B scheme: 466 | ```TL-B 467 | addr_none$00 = MsgAddressExt; 468 | addr_extern$01 len:(## 8) external_address:(bits len) 469 | = MsgAddressExt; 470 | anycast_info$_ depth:(#<= 30) { depth >= 1 } 471 | rewrite_pfx:(bits depth) = Anycast; 472 | addr_std$10 anycast:(Maybe Anycast) 473 | workchain_id:int8 address:bits256 = MsgAddressInt; 474 | addr_var$11 anycast:(Maybe Anycast) addr_len:(## 9) 475 | workchain_id:int32 address:(bits addr_len) = MsgAddressInt; 476 | _ _:MsgAddressInt = MsgAddress; 477 | _ _:MsgAddressExt = MsgAddress; 478 | 479 | int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool 480 | src:MsgAddress dest:MsgAddressInt 481 | value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams 482 | created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; 483 | ext_out_msg_info$11 src:MsgAddress dest:MsgAddressExt 484 | created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed; 485 | ``` 486 | A deserialized `MsgAddress` is represented by a tuple `t` as follows: 487 | 488 | - `addr_none` is represented by `t = (0)`, 489 | i.e., a tuple containing exactly one integer equal to zero. 490 | - `addr_extern` is represented by `t = (1, s)`, 491 | where slice `s` contains the field `external_address`. In other words, ` 492 | t` is a pair (a tuple consisting of two entries), containing an integer equal to one and slice `s`. 493 | - `addr_std` is represented by `t = (2, u, x, s)`, 494 | where `u` is either a `null` (if `anycast` is absent) or a slice `s'` containing `rewrite_pfx` (if anycast is present). 495 | Next, integer `x` is the `workchain_id`, and slice `s` contains the address. 496 | - `addr_var` is represented by `t = (3, u, x, s)`, 497 | where `u`, `x`, and `s` have the same meaning as for `addr_std`. 498 | -} 499 | 500 | ;;; Loads from slice [s] the only prefix that is a valid `MsgAddress`, 501 | ;;; and returns both this prefix `s'` and the remainder `s''` of [s] as slices. 502 | (slice, slice) load_msg_addr(slice s) asm(-> 1 0) "LDMSGADDR"; 503 | 504 | ;;; Decomposes slice [s] containing a valid `MsgAddress` into a `tuple t` with separate fields of this `MsgAddress`. 505 | ;;; If [s] is not a valid `MsgAddress`, a cell deserialization exception is thrown. 506 | tuple parse_addr(slice s) asm "PARSEMSGADDR"; 507 | 508 | ;;; Parses slice [s] containing a valid `MsgAddressInt` (usually a `msg_addr_std`), 509 | ;;; applies rewriting from the anycast (if present) to the same-length prefix of the address, 510 | ;;; and returns both the workchain and the 256-bit address as integers. 511 | ;;; If the address is not 256-bit, or if [s] is not a valid serialization of `MsgAddressInt`, 512 | ;;; throws a cell deserialization exception. 513 | (int, int) parse_std_addr(slice s) asm "REWRITESTDADDR"; 514 | 515 | ;;; A variant of [parse_std_addr] that returns the (rewritten) address as a slice [s], 516 | ;;; even if it is not exactly 256 bit long (represented by a `msg_addr_var`). 517 | (int, slice) parse_var_addr(slice s) asm "REWRITEVARADDR"; 518 | 519 | {- 520 | # Dictionary primitives 521 | -} 522 | 523 | 524 | ;;; Sets the value associated with [key_len]-bit key signed index in dictionary [dict] to [value] (cell), 525 | ;;; and returns the resulting dictionary. 526 | cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; 527 | (cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; 528 | 529 | ;;; Sets the value associated with [key_len]-bit key unsigned index in dictionary [dict] to [value] (cell), 530 | ;;; and returns the resulting dictionary. 531 | cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; 532 | (cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; 533 | 534 | cell idict_get_ref(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETOPTREF"; 535 | (cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF" "NULLSWAPIFNOT"; 536 | (cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF" "NULLSWAPIFNOT"; 537 | (cell, cell) idict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETGETOPTREF"; 538 | (cell, cell) udict_set_get_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETGETOPTREF"; 539 | (cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL"; 540 | (cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL"; 541 | (slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT"; 542 | (slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT"; 543 | (cell, slice, int) idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; 544 | (cell, slice, int) udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; 545 | (cell, (slice, int)) ~idict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDELGET" "NULLSWAPIFNOT"; 546 | (cell, (slice, int)) ~udict_delete_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDELGET" "NULLSWAPIFNOT"; 547 | cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; 548 | (cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; 549 | cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; 550 | (cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; 551 | cell dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; 552 | (cell, ()) ~dict_set(cell dict, int key_len, slice index, slice value) asm(value index dict key_len) "DICTSET"; 553 | (cell, int) udict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUADD"; 554 | (cell, int) udict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUREPLACE"; 555 | (cell, int) idict_add?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIADD"; 556 | (cell, int) idict_replace?(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTIREPLACE"; 557 | cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; 558 | (cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; 559 | cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; 560 | (cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; 561 | cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; 562 | (cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; 563 | (cell, int) udict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUADDB"; 564 | (cell, int) udict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUREPLACEB"; 565 | (cell, int) idict_add_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIADDB"; 566 | (cell, int) idict_replace_builder?(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTIREPLACEB"; 567 | (cell, int, slice, int) udict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; 568 | (cell, (int, slice, int)) ~udict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMIN" "NULLSWAPIFNOT2"; 569 | (cell, int, slice, int) idict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; 570 | (cell, (int, slice, int)) ~idict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMIN" "NULLSWAPIFNOT2"; 571 | (cell, slice, slice, int) dict_delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; 572 | (cell, (slice, slice, int)) ~dict::delete_get_min(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMIN" "NULLSWAPIFNOT2"; 573 | (cell, int, slice, int) udict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; 574 | (cell, (int, slice, int)) ~udict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTUREMMAX" "NULLSWAPIFNOT2"; 575 | (cell, int, slice, int) idict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; 576 | (cell, (int, slice, int)) ~idict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTIREMMAX" "NULLSWAPIFNOT2"; 577 | (cell, slice, slice, int) dict_delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; 578 | (cell, (slice, slice, int)) ~dict::delete_get_max(cell dict, int key_len) asm(-> 0 2 1 3) "DICTREMMAX" "NULLSWAPIFNOT2"; 579 | (int, slice, int) udict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMIN" "NULLSWAPIFNOT2"; 580 | (int, slice, int) udict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAX" "NULLSWAPIFNOT2"; 581 | (int, cell, int) udict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMINREF" "NULLSWAPIFNOT2"; 582 | (int, cell, int) udict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTUMAXREF" "NULLSWAPIFNOT2"; 583 | (int, slice, int) idict_get_min?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMIN" "NULLSWAPIFNOT2"; 584 | (int, slice, int) idict_get_max?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAX" "NULLSWAPIFNOT2"; 585 | (int, cell, int) idict_get_min_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMINREF" "NULLSWAPIFNOT2"; 586 | (int, cell, int) idict_get_max_ref?(cell dict, int key_len) asm (-> 1 0 2) "DICTIMAXREF" "NULLSWAPIFNOT2"; 587 | (int, slice, int) udict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXT" "NULLSWAPIFNOT2"; 588 | (int, slice, int) udict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETNEXTEQ" "NULLSWAPIFNOT2"; 589 | (int, slice, int) udict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREV" "NULLSWAPIFNOT2"; 590 | (int, slice, int) udict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTUGETPREVEQ" "NULLSWAPIFNOT2"; 591 | (int, slice, int) idict_get_next?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXT" "NULLSWAPIFNOT2"; 592 | (int, slice, int) idict_get_nexteq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETNEXTEQ" "NULLSWAPIFNOT2"; 593 | (int, slice, int) idict_get_prev?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREV" "NULLSWAPIFNOT2"; 594 | (int, slice, int) idict_get_preveq?(cell dict, int key_len, int pivot) asm(pivot dict key_len -> 1 0 2) "DICTIGETPREVEQ" "NULLSWAPIFNOT2"; 595 | 596 | ;;; Creates an empty dictionary, which is actually a null value. Equivalent to PUSHNULL 597 | cell new_dict() asm "NEWDICT"; 598 | ;;; Checks whether a dictionary is empty. Equivalent to cell_null?. 599 | int dict_empty?(cell c) asm "DICTEMPTY"; 600 | 601 | 602 | {- Prefix dictionary primitives -} 603 | (slice, slice, slice, int) pfxdict_get?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTGETQ" "NULLSWAPIFNOT2"; 604 | (cell, int) pfxdict_set?(cell dict, int key_len, slice key, slice value) asm(value key dict key_len) "PFXDICTSET"; 605 | (cell, int) pfxdict_delete?(cell dict, int key_len, slice key) asm(key dict key_len) "PFXDICTDEL"; 606 | 607 | ;;; Returns the value of the global configuration parameter with integer index `i` as a `cell` or `null` value. 608 | cell config_param(int x) asm "CONFIGOPTPARAM"; 609 | ;;; Checks whether c is a null. Note, that FunC also has polymorphic null? built-in. 610 | int cell_null?(cell c) asm "ISNULL"; 611 | 612 | ;;; Creates an output action which would reserve exactly amount nanotoncoins (if mode = 0), at most amount nanotoncoins (if mode = 2), or all but amount nanotoncoins (if mode = 1 or mode = 3), from the remaining balance of the account. It is roughly equivalent to creating an outbound message carrying amount nanotoncoins (or b − amount nanotoncoins, where b is the remaining balance) to oneself, so that the subsequent output actions would not be able to spend more money than the remainder. Bit +2 in mode means that the external action does not fail if the specified amount cannot be reserved; instead, all remaining balance is reserved. Bit +8 in mode means `amount <- -amount` before performing any further actions. Bit +4 in mode means that amount is increased by the original balance of the current account (before the compute phase), including all extra currencies, before performing any other checks and actions. Currently, amount must be a non-negative integer, and mode must be in the range 0..15. 613 | () raw_reserve(int amount, int mode) impure asm "RAWRESERVE"; 614 | ;;; Similar to raw_reserve, but also accepts a dictionary extra_amount (represented by a cell or null) with extra currencies. In this way currencies other than TonCoin can be reserved. 615 | () raw_reserve_extra(int amount, cell extra_amount, int mode) impure asm "RAWRESERVEX"; 616 | ;;; Sends a raw message contained in msg, which should contain a correctly serialized object Message X, with the only exception that the source address is allowed to have dummy value addr_none (to be automatically replaced with the current smart contract address), and ihr_fee, fwd_fee, created_lt and created_at fields can have arbitrary values (to be rewritten with correct values during the action phase of the current transaction). Integer parameter mode contains the flags. Currently mode = 0 is used for ordinary messages; mode = 128 is used for messages that are to carry all the remaining balance of the current smart contract (instead of the value originally indicated in the message); mode = 64 is used for messages that carry all the remaining value of the inbound message in addition to the value initially indicated in the new message (if bit 0 is not set, the gas fees are deducted from this amount); mode' = mode + 1 means that the sender wants to pay transfer fees separately; mode' = mode + 2 means that any errors arising while processing this message during the action phase should be ignored. Finally, mode' = mode + 32 means that the current account must be destroyed if its resulting balance is zero. This flag is usually employed together with +128. 617 | () send_raw_message(cell msg, int mode) impure asm "SENDRAWMSG"; 618 | ;;; Creates an output action that would change this smart contract code to that given by cell new_code. Notice that this change will take effect only after the successful termination of the current run of the smart contract 619 | () set_code(cell new_code) impure asm "SETCODE"; 620 | 621 | ;;; Generates a new pseudo-random unsigned 256-bit integer x. The algorithm is as follows: if r is the old value of the random seed, considered as a 32-byte array (by constructing the big-endian representation of an unsigned 256-bit integer), then its sha512(r) is computed; the first 32 bytes of this hash are stored as the new value r' of the random seed, and the remaining 32 bytes are returned as the next random value x. 622 | int random() impure asm "RANDU256"; 623 | ;;; Generates a new pseudo-random integer z in the range 0..range−1 (or range..−1, if range < 0). More precisely, an unsigned random value x is generated as in random; then z := x * range / 2^256 is computed. 624 | int rand(int range) impure asm "RAND"; 625 | ;;; Returns the current random seed as an unsigned 256-bit Integer. 626 | int get_seed() impure asm "RANDSEED"; 627 | ;;; Sets the random seed to unsigned 256-bit seed. 628 | () set_seed(int x) impure asm "SETRAND"; 629 | ;;; Mixes unsigned 256-bit integer x into the random seed r by setting the random seed to sha256 of the concatenation of two 32-byte strings: the first with the big-endian representation of the old seed r, and the second with the big-endian representation of x. 630 | () randomize(int x) impure asm "ADDRAND"; 631 | ;;; Equivalent to randomize(cur_lt());. 632 | () randomize_lt() impure asm "LTIME" "ADDRAND"; 633 | 634 | ;;; Checks whether the data parts of two slices coinside 635 | int equal_slices_bits(slice a, slice b) asm "SDEQ"; 636 | ;;; Checks whether b is a null. Note, that FunC also has polymorphic null? built-in. 637 | int builder_null?(builder b) asm "ISNULL"; 638 | ;;; Concatenates two builders 639 | builder store_builder(builder to, builder from) asm "STBR"; 640 | 641 | ;; CUSTOM: 642 | 643 | ;; TVM UPGRADE 2023-07 https://docs.ton.org/learn/tvm-instructions/tvm-upgrade-2023-07 644 | ;; In mainnet since 20 Dec 2023 https://t.me/tonblockchain/226 645 | 646 | ;;; Retrieves code of smart-contract from c7 647 | cell my_code() asm "MYCODE"; 648 | 649 | ;;; Creates an output action and returns a fee for creating a message. Mode has the same effect as in the case of SENDRAWMSG 650 | int send_message(cell msg, int mode) impure asm "SENDMSG"; 651 | 652 | int gas_consumed() asm "GASCONSUMED"; 653 | 654 | ;; TVM V6 https://github.com/ton-blockchain/ton/blob/testnet/doc/GlobalVersions.md#version-6 655 | 656 | int get_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEE"; 657 | int get_storage_fee(int workchain, int seconds, int bits, int cells) asm(cells bits seconds workchain) "GETSTORAGEFEE"; 658 | int get_forward_fee(int workchain, int bits, int cells) asm(cells bits workchain) "GETFORWARDFEE"; 659 | int get_precompiled_gas_consumption() asm "GETPRECOMPILEDGAS"; 660 | 661 | int get_simple_compute_fee(int workchain, int gas_used) asm(gas_used workchain) "GETGASFEESIMPLE"; 662 | int get_simple_forward_fee(int workchain, int bits, int cells) asm(cells bits workchain) "GETFORWARDFEESIMPLE"; 663 | int get_original_fwd_fee(int workchain, int fwd_fee) asm(fwd_fee workchain) "GETORIGINALFWDFEE"; 664 | int my_storage_due() asm "DUEPAYMENT"; 665 | 666 | tuple get_fee_cofigs() asm "UNPACKEDCONFIGTUPLE"; 667 | 668 | ;; BASIC 669 | 670 | const int TRUE = -1; 671 | const int FALSE = 0; 672 | 673 | const int MASTERCHAIN = -1; 674 | const int BASECHAIN = 0; 675 | 676 | ;;; skip (Maybe ^Cell) from `slice` [s]. 677 | (slice, ()) ~skip_maybe_ref(slice s) asm "SKIPOPTREF"; 678 | 679 | (slice, int) ~load_bool(slice s) inline { 680 | return s.load_int(1); 681 | } 682 | 683 | builder store_bool(builder b, int value) inline { 684 | return b.store_int(value, 1); 685 | } 686 | 687 | ;; ADDRESS NONE 688 | ;; addr_none$00 = MsgAddressExt; https://github.com/ton-blockchain/ton/blob/8a9ff339927b22b72819c5125428b70c406da631/crypto/block/block.tlb#L100 689 | 690 | builder store_address_none(builder b) inline { 691 | return b.store_uint(0, 2); 692 | } 693 | 694 | slice address_none() asm " ; 18 | export type FullFees = ReturnType; 19 | 20 | export class StorageStats { 21 | bits: bigint; 22 | cells: bigint; 23 | 24 | constructor(bits?: number | bigint, cells?: number | bigint) { 25 | this.bits = bits !== undefined ? BigInt(bits) : 0n; 26 | this.cells = cells !== undefined ? BigInt(cells) : 0n; 27 | } 28 | add(...stats: StorageStats[]) { 29 | let cells = this.cells, bits = this.bits; 30 | for (let stat of stats) { 31 | bits += stat.bits; 32 | cells += stat.cells; 33 | } 34 | return new StorageStats(bits, cells); 35 | } 36 | sub(...stats: StorageStats[]) { 37 | let cells = this.cells, bits = this.bits; 38 | for (let stat of stats) { 39 | bits -= stat.bits; 40 | cells -= stat.cells; 41 | } 42 | return new StorageStats(bits, cells); 43 | } 44 | addBits(bits: number | bigint) { 45 | return new StorageStats(this.bits + BigInt(bits), this.cells); 46 | } 47 | subBits(bits: number | bigint) { 48 | return new StorageStats(this.bits - BigInt(bits), this.cells); 49 | } 50 | addCells(cells: number | bigint) { 51 | return new StorageStats(this.bits, this.cells + BigInt(cells)); 52 | } 53 | subCells(cells: number | bigint) { 54 | return new StorageStats(this.bits, this.cells - BigInt(cells)); 55 | } 56 | 57 | toString() : string { 58 | return JSON.stringify({ 59 | bits: this.bits.toString(), 60 | cells: this.cells.toString() 61 | }); 62 | } 63 | } 64 | 65 | export function computedGeneric(transaction: T) { 66 | if(transaction.description.type !== "generic") 67 | throw("Expected generic transactionaction"); 68 | if(transaction.description.computePhase.type !== "vm") 69 | throw("Compute phase expected") 70 | return transaction.description.computePhase; 71 | } 72 | 73 | export function storageGeneric(transaction: T) { 74 | if(transaction.description.type !== "generic") 75 | throw("Expected generic transactionaction"); 76 | const storagePhase = transaction.description.storagePhase; 77 | if(storagePhase === null || storagePhase === undefined) 78 | throw("Storage phase expected") 79 | return storagePhase; 80 | } 81 | 82 | function shr16ceil(src: bigint) { 83 | let rem = src % BigInt(65536); 84 | let res = src / 65536n; // >> BigInt(16); 85 | if (rem != BigInt(0)) { 86 | res += BigInt(1); 87 | } 88 | return res; 89 | } 90 | 91 | export function collectCellStats(cell: Cell, visited:Array, skipRoot: boolean = false): StorageStats { 92 | let bits = skipRoot ? 0n : BigInt(cell.bits.length); 93 | let cells = skipRoot ? 0n : 1n; 94 | let hash = cell.hash().toString(); 95 | if (visited.includes(hash)) { 96 | // We should not account for current cell data if visited 97 | return new StorageStats(); 98 | } 99 | else { 100 | visited.push(hash); 101 | } 102 | for (let ref of cell.refs) { 103 | let r = collectCellStats(ref, visited); 104 | cells += r.cells; 105 | bits += r.bits; 106 | } 107 | return new StorageStats(bits, cells); 108 | } 109 | 110 | export function getGasPrices(configRaw: Cell, workchain: 0 | -1): GasPrices { 111 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); 112 | 113 | const ds = config.get(21 + workchain)!.beginParse(); 114 | if(ds.loadUint(8) !== 0xd1) { 115 | throw new Error("Invalid flat gas prices tag!"); 116 | } 117 | 118 | const flat_gas_limit = ds.loadUintBig(64); 119 | const flat_gas_price = ds.loadUintBig(64); 120 | 121 | if(ds.loadUint(8) !== 0xde) { 122 | throw new Error("Invalid gas prices tag!"); 123 | } 124 | return { 125 | flat_gas_limit, 126 | flat_gas_price, 127 | gas_price: ds.preloadUintBig(64) 128 | }; 129 | } 130 | 131 | export function setGasPrice(configRaw: Cell, prices: GasPrices, workchain: 0 | -1) : Cell { 132 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); 133 | const idx = 21 + workchain; 134 | const ds = config.get(idx)!; 135 | const tail = ds.beginParse().skip(8 + 64 + 64 + 8 + 64); 136 | 137 | const newPrices = beginCell().storeUint(0xd1, 8) 138 | .storeUint(prices.flat_gas_limit, 64) 139 | .storeUint(prices.flat_gas_price, 64) 140 | .storeUint(0xde, 8) 141 | .storeUint(prices.gas_price, 64) 142 | .storeSlice(tail) 143 | .endCell(); 144 | config.set(idx, newPrices); 145 | 146 | return beginCell().storeDictDirect(config).endCell(); 147 | } 148 | 149 | export const storageValue : DictionaryValue = { 150 | serialize: (src, builder) => { 151 | builder.storeUint(0xcc, 8) 152 | .storeUint(src.utime_sice, 32) 153 | .storeUint(src.bit_price_ps, 64) 154 | .storeUint(src.cell_price_ps, 64) 155 | .storeUint(src.mc_bit_price_ps, 64) 156 | .storeUint(src.mc_cell_price_ps, 64) 157 | }, 158 | parse: (src) => { 159 | return { 160 | utime_sice: src.skip(8).loadUint(32), 161 | bit_price_ps: src.loadUintBig(64), 162 | cell_price_ps: src.loadUintBig(64), 163 | mc_bit_price_ps: src.loadUintBig(64), 164 | mc_cell_price_ps: src.loadUintBig(64) 165 | }; 166 | } 167 | }; 168 | 169 | export function getStoragePrices(configRaw: Cell) { 170 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); 171 | const storageData = Dictionary.loadDirect(Dictionary.Keys.Uint(32),storageValue, config.get(18)!); 172 | const values = storageData.values(); 173 | 174 | return values[values.length - 1]; 175 | } 176 | export function calcStorageFee(prices: StorageValue, stats: StorageStats, duration: bigint) { 177 | return shr16ceil((stats.bits * prices.bit_price_ps + stats.cells * prices.cell_price_ps) * duration) 178 | } 179 | export function setStoragePrices(configRaw: Cell, prices: StorageValue) { 180 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); 181 | const storageData = Dictionary.loadDirect(Dictionary.Keys.Uint(32),storageValue, config.get(18)!); 182 | storageData.set(storageData.values().length - 1, prices); 183 | config.set(18, beginCell().storeDictDirect(storageData).endCell()); 184 | return beginCell().storeDictDirect(config).endCell(); 185 | } 186 | 187 | export function computeGasFee(prices: GasPrices, gas: bigint): bigint { 188 | if(gas <= prices.flat_gas_limit) { 189 | return prices.flat_gas_price; 190 | } 191 | return prices.flat_gas_price + prices.gas_price * (gas - prices.flat_gas_limit) / 65536n 192 | } 193 | 194 | export function computeDefaultForwardFee(msgPrices: MsgPrices) { 195 | return msgPrices.lumpPrice - ((msgPrices.lumpPrice * msgPrices.firstFrac) >> BigInt(16)); 196 | } 197 | 198 | export function computeCellForwardFees(msgPrices: MsgPrices, msg: Cell) { 199 | let storageStats = collectCellStats(msg, [], true); 200 | return computeFwdFees(msgPrices, storageStats.cells, storageStats.bits); 201 | } 202 | export function computeMessageForwardFees(msgPrices: MsgPrices, msg: Message) { 203 | // let msg = loadMessageRelaxed(cell.beginParse()); 204 | let storageStats = new StorageStats(); 205 | 206 | if( msg.info.type !== "internal") { 207 | throw Error("Helper intended for internal messages"); 208 | } 209 | const defaultFwd = computeDefaultForwardFee(msgPrices); 210 | // If message forward fee matches default than msg cell is flat 211 | if(msg.info.forwardFee == defaultFwd) { 212 | return {fees: msgPrices.lumpPrice, res : defaultFwd, remaining: defaultFwd, stats: storageStats}; 213 | } 214 | let visited : Array = []; 215 | // Init 216 | if (msg.init) { 217 | let addBits = 5n; // Minimal additional bits 218 | let refCount = 0; 219 | if(msg.init.splitDepth) { 220 | addBits += 5n; 221 | } 222 | if(msg.init.libraries) { 223 | refCount++; 224 | storageStats = storageStats.add(collectCellStats(beginCell().storeDictDirect(msg.init.libraries).endCell(), visited, true)); 225 | } 226 | if(msg.init.code) { 227 | refCount++; 228 | storageStats = storageStats.add(collectCellStats(msg.init.code, visited)) 229 | } 230 | if(msg.init.data) { 231 | refCount++; 232 | storageStats = storageStats.add(collectCellStats(msg.init.data, visited)); 233 | } 234 | if(refCount >= 2) { //https://github.com/ton-blockchain/ton/blob/51baec48a02e5ba0106b0565410d2c2fd4665157/crypto/block/transaction.cpp#L2079 235 | storageStats.cells++; 236 | storageStats.bits += addBits; 237 | } 238 | } 239 | const lumpBits = BigInt(msg.body.bits.length); 240 | const bodyStats = collectCellStats(msg.body,visited, true); 241 | storageStats = storageStats.add(bodyStats); 242 | 243 | // NOTE: Extra currencies are ignored for now 244 | let fees = computeFwdFeesVerbose(msgPrices, BigInt(storageStats.cells), BigInt(storageStats.bits)); 245 | // Meeh 246 | if(fees.remaining < msg.info.forwardFee) { 247 | // console.log(`Remaining ${fees.remaining} < ${msg.info.forwardFee} lump bits:${lumpBits}`); 248 | storageStats = storageStats.addCells(1).addBits(lumpBits); 249 | fees = computeFwdFeesVerbose(msgPrices, storageStats.cells, storageStats.bits); 250 | } 251 | if(fees.remaining != msg.info.forwardFee) { 252 | console.log("Result fees:", fees); 253 | console.log(msg); 254 | console.log(fees.remaining); 255 | throw(new Error("Something went wrong in fee calcuation!")); 256 | } 257 | return {fees, stats: storageStats}; 258 | } 259 | 260 | export const configParseMsgPrices = (sc: Slice) => { 261 | 262 | let magic = sc.loadUint(8); 263 | 264 | if(magic != 0xea) { 265 | throw Error("Invalid message prices magic number!"); 266 | } 267 | return { 268 | lumpPrice:sc.loadUintBig(64), 269 | bitPrice: sc.loadUintBig(64), 270 | cellPrice: sc.loadUintBig(64), 271 | ihrPriceFactor: sc.loadUintBig(32), 272 | firstFrac: sc.loadUintBig(16), 273 | nextFrac: sc.loadUintBig(16) 274 | }; 275 | } 276 | 277 | export const setMsgPrices = (configRaw: Cell, prices: MsgPrices, workchain: 0 | -1) => { 278 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); 279 | 280 | const priceCell = beginCell().storeUint(0xea, 8) 281 | .storeUint(prices.lumpPrice, 64) 282 | .storeUint(prices.bitPrice, 64) 283 | .storeUint(prices.cellPrice, 64) 284 | .storeUint(prices.ihrPriceFactor, 32) 285 | .storeUint(prices.firstFrac, 16) 286 | .storeUint(prices.nextFrac, 16) 287 | .endCell(); 288 | config.set(25 + workchain, priceCell); 289 | 290 | return beginCell().storeDictDirect(config).endCell(); 291 | } 292 | 293 | export const getMsgPrices = (configRaw: Cell, workchain: 0 | -1 ) => { 294 | 295 | const config = configRaw.beginParse().loadDictDirect(Dictionary.Keys.Int(32), Dictionary.Values.Cell()); 296 | 297 | const prices = config.get(25 + workchain); 298 | 299 | if(prices === undefined) { 300 | throw Error("No prices defined in config"); 301 | } 302 | 303 | return configParseMsgPrices(prices.beginParse()); 304 | } 305 | 306 | export function computeFwdFees(msgPrices: MsgPrices, cells: bigint, bits: bigint) { 307 | return msgPrices.lumpPrice + (shr16ceil((msgPrices.bitPrice * bits) 308 | + (msgPrices.cellPrice * cells)) 309 | ); 310 | } 311 | 312 | export function computeFwdFeesVerbose(msgPrices: MsgPrices, cells: bigint | number, bits: bigint | number) { 313 | const fees = computeFwdFees(msgPrices, BigInt(cells), BigInt(bits)); 314 | 315 | const res = (fees * msgPrices.firstFrac) >> 16n; 316 | return { 317 | total: fees, 318 | res, 319 | remaining: fees - res 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ton-stable", 3 | "description": "", 4 | "version": "0.1.0", 5 | "scripts": { 6 | "build": "npx blueprint build", 7 | "test": "jest", 8 | "deploy": "npx blueprint run", 9 | "prettier": "npx prettier --write '{test,contracts,build}/**/*.{ts,js,json}'" 10 | }, 11 | "devDependencies": { 12 | "@ton/blueprint": "^0.15.0", 13 | "@ton/core": "^0.54.0", 14 | "@ton/crypto": "^3.2.0", 15 | "@ton/sandbox": "0.16.0-tvmbeta.3", 16 | "@ton/test-utils": "^0.4.2", 17 | "@ton/ton": "^13.10.0", 18 | "@types/jest": "^29.5.4", 19 | "@types/node": "^20.2.5", 20 | "dotenv": "^16.0.0", 21 | "jest": "^29.6.3", 22 | "prettier": "^2.8.6", 23 | "ts-jest": "^29.0.5", 24 | "ts-node": "^10.9.1", 25 | "typescript": "^4.9.5" 26 | }, 27 | "overrides": { 28 | "@ton-community/func-js-bin": "0.4.5-tvmbeta.3", 29 | "@ton-community/func-js": "0.6.3-tvmbeta.3", 30 | "@ton-community/sandbox": "0.16.0-tvmbeta.3" 31 | }, 32 | "prettier": { 33 | "printWidth": 180 34 | }, 35 | "mocha": { 36 | "require": [ 37 | "chai", 38 | "ts-node/register" 39 | ], 40 | "timeout": 20000 41 | }, 42 | "engines": { 43 | "node": ">=16.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /sandbox_tests/StateInit.spec.ts: -------------------------------------------------------------------------------- 1 | import { Blockchain, SandboxContract, TreasuryContract, internal, BlockchainSnapshot, SendMessageResult, defaultConfigSeqno, BlockchainTransaction } from '@ton/sandbox'; 2 | import { Cell, toNano, beginCell, Address, Transaction, TransactionComputeVm, TransactionStoragePhase, storeAccountStorage, Sender, Dictionary, storeMessage, fromNano, DictionaryValue, storeStateInit } from '@ton/core'; 3 | import { jettonContentToCell, JettonMinter, jettonMinterConfigToCell, JettonMinterContent, LockType } from '../wrappers/JettonMinter'; 4 | import { JettonWallet } from '../wrappers/JettonWallet'; 5 | import { compile } from '@ton/blueprint'; 6 | import '@ton/test-utils'; 7 | import { collectCellStats } from '../gasUtils'; 8 | import { Op, Errors } from '../wrappers/JettonConstants'; 9 | 10 | let blockchain: Blockchain; 11 | let deployer: SandboxContract; 12 | let jettonMinter:SandboxContract; 13 | let minter_code: Cell; 14 | let wallet_code: Cell; 15 | let jwallet_code_raw: Cell; 16 | let jwallet_code: Cell; 17 | let userWallet: (address: Address) => Promise>; 18 | 19 | describe('State init tests', () => { 20 | beforeAll(async () => { 21 | blockchain = await Blockchain.create(); 22 | deployer = await blockchain.treasury('deployer'); 23 | jwallet_code_raw = await compile('JettonWallet'); 24 | minter_code = await compile('JettonMinter'); 25 | 26 | //jwallet_code is library 27 | const _libs = Dictionary.empty(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell()); 28 | _libs.set(BigInt(`0x${jwallet_code_raw.hash().toString('hex')}`), jwallet_code_raw); 29 | const libs = beginCell().storeDictDirect(_libs).endCell(); 30 | blockchain.libs = libs; 31 | let lib_prep = beginCell().storeUint(2,8).storeBuffer(jwallet_code_raw.hash()).endCell(); 32 | jwallet_code = new Cell({ exotic:true, bits: lib_prep.bits, refs:lib_prep.refs}); 33 | 34 | console.log('jetton minter code hash = ', minter_code.hash().toString('hex')); 35 | console.log('jetton wallet code hash = ', jwallet_code.hash().toString('hex')); 36 | 37 | jettonMinter = blockchain.openContract( 38 | JettonMinter.createFromConfig( 39 | { 40 | admin: deployer.address, 41 | wallet_code: jwallet_code, 42 | jetton_content: jettonContentToCell({uri: "https://ton.org/"}) 43 | }, 44 | minter_code)); 45 | 46 | userWallet = async (address:Address) => blockchain.openContract( 47 | JettonWallet.createFromAddress( 48 | await jettonMinter.getWalletAddress(address) 49 | ) 50 | ); 51 | 52 | }); 53 | it('should deploy', async () => { 54 | 55 | //await blockchain.setVerbosityForAddress(jettonMinter.address, {blockchainLogs:true, vmLogs: 'vm_logs'}); 56 | const deployResult = await jettonMinter.sendDeploy(deployer.getSender(), toNano('10')); 57 | 58 | expect(deployResult.transactions).toHaveTransaction({ 59 | from: deployer.address, 60 | to: jettonMinter.address, 61 | deploy: true, 62 | }); 63 | // Make sure it didn't bounce 64 | expect(deployResult.transactions).not.toHaveTransaction({ 65 | on: deployer.address, 66 | from: jettonMinter.address, 67 | inMessageBounced: true 68 | }); 69 | }); 70 | it('should mint max jetton walue', async () => { 71 | const maxValue = (2n ** 120n) - 1n; 72 | const deployerWallet = await userWallet(deployer.address); 73 | const res = await jettonMinter.sendMint(deployer.getSender(), 74 | deployer.address, 75 | maxValue, 76 | null, null, null); 77 | expect(res.transactions).toHaveTransaction({ 78 | on: deployerWallet.address, 79 | op: Op.internal_transfer, 80 | success: true, 81 | }); 82 | 83 | const curBalance = await deployerWallet.getJettonBalance(); 84 | expect(curBalance).toEqual(maxValue); 85 | const smc = await blockchain.getContract(deployerWallet.address); 86 | if(smc.accountState === undefined) 87 | throw new Error("Can't access wallet account state"); 88 | if(smc.accountState.type !== "active") 89 | throw new Error("Wallet account is not active"); 90 | if(smc.account.account === undefined || smc.account.account === null) 91 | throw new Error("Can't access wallet account!"); 92 | console.log("Jetton wallet max storage stats:", smc.account.account.storageStats.used); 93 | const state = smc.accountState.state; 94 | const stateCell = beginCell().store(storeStateInit(state)).endCell(); 95 | console.log("State init stats:", collectCellStats(stateCell, [])); 96 | }); 97 | }); 98 | 99 | -------------------------------------------------------------------------------- /sandbox_tests/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Address, toNano, Cell} from "@ton/core"; 3 | 4 | export const randomAddress = (wc: number = 0) => { 5 | const buf = Buffer.alloc(32); 6 | for (let i = 0; i < buf.length; i++) { 7 | buf[i] = Math.floor(Math.random() * 256); 8 | } 9 | return new Address(wc, buf); 10 | }; 11 | 12 | export const differentAddress = (old: Address) => { 13 | let newAddr: Address; 14 | do { 15 | newAddr = randomAddress(old.workChain); 16 | } while(newAddr.equals(old)); 17 | 18 | return newAddr; 19 | } 20 | 21 | const getRandom = (min:number, max:number) => { 22 | return Math.random() * (max - min) + min; 23 | } 24 | 25 | export const getRandomInt = (min: number, max: number) => { 26 | return Math.round(getRandom(min, max)); 27 | } 28 | 29 | export const getRandomTon = (min:number, max:number): bigint => { 30 | return toNano(getRandom(min, max).toFixed(9)); 31 | } 32 | 33 | export type InternalTransfer = { 34 | from: Address | null, 35 | response: Address | null, 36 | amount: bigint, 37 | forwardAmount: bigint, 38 | payload: Cell | null 39 | }; 40 | export type JettonTransfer = { 41 | to: Address, 42 | response_address: Address | null, 43 | amount: bigint, 44 | custom_payload: Cell | null, 45 | forward_amount: bigint, 46 | forward_payload: Cell | null 47 | } 48 | 49 | export const parseTransfer = (body: Cell) => { 50 | const ts = body.beginParse().skip(64 + 32); 51 | return { 52 | amount: ts.loadCoins(), 53 | to: ts.loadAddress(), 54 | response_address: ts.loadAddressAny(), 55 | custom_payload: ts.loadMaybeRef(), 56 | forward_amount: ts.loadCoins(), 57 | forward_payload: ts.loadMaybeRef() 58 | } 59 | } 60 | export const parseInternalTransfer = (body: Cell) => { 61 | 62 | const ts = body.beginParse().skip(64 + 32); 63 | 64 | return { 65 | amount: ts.loadCoins(), 66 | from: ts.loadAddressAny(), 67 | response: ts.loadAddressAny(), 68 | forwardAmount: ts.loadCoins(), 69 | payload: ts.loadMaybeRef() 70 | }; 71 | }; 72 | type JettonTransferNotification = { 73 | amount: bigint, 74 | from: Address | null, 75 | payload: Cell | null 76 | } 77 | export const parseTransferNotification = (body: Cell) => { 78 | const bs = body.beginParse().skip(64 + 32); 79 | return { 80 | amount: bs.loadCoins(), 81 | from: bs.loadAddressAny(), 82 | payload: bs.loadMaybeRef() 83 | } 84 | } 85 | 86 | type JettonBurnNotification = { 87 | amount: bigint, 88 | from: Address, 89 | response_address: Address | null, 90 | } 91 | export const parseBurnNotification = (body: Cell) => { 92 | const ds = body.beginParse().skip(64 + 32); 93 | const res = { 94 | amount: ds.loadCoins(), 95 | from: ds.loadAddress(), 96 | response_address: ds.loadAddressAny(), 97 | }; 98 | 99 | return res; 100 | } 101 | 102 | const testPartial = (cmp: any, match: any) => { 103 | for (let key in match) { 104 | if(!(key in cmp)) { 105 | throw Error(`Unknown key ${key} in ${cmp}`); 106 | } 107 | 108 | if(match[key] instanceof Address) { 109 | if(!(cmp[key] instanceof Address)) { 110 | return false 111 | } 112 | if(!(match[key] as Address).equals(cmp[key])) { 113 | return false 114 | } 115 | } 116 | else if(match[key] instanceof Cell) { 117 | if(!(cmp[key] instanceof Cell)) { 118 | return false; 119 | } 120 | if(!(match[key] as Cell).equals(cmp[key])) { 121 | return false; 122 | } 123 | } 124 | else if(match[key] !== cmp[key]){ 125 | return false; 126 | } 127 | } 128 | return true; 129 | } 130 | export const testJettonBurnNotification = (body: Cell, match: Partial) => { 131 | const res= parseBurnNotification(body); 132 | return testPartial(res, match); 133 | } 134 | 135 | export const testJettonTransfer = (body: Cell, match: Partial) => { 136 | const res = parseTransfer(body); 137 | return testPartial(res, match); 138 | } 139 | export const testJettonInternalTransfer = (body: Cell, match: Partial) => { 140 | const res = parseInternalTransfer(body); 141 | return testPartial(res, match); 142 | }; 143 | export const testJettonNotification = (body: Cell, match: Partial) => { 144 | const res = parseTransferNotification(body); 145 | return testPartial(res, match); 146 | } 147 | -------------------------------------------------------------------------------- /scripts/JettonMinterChecker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addressToString, 3 | assert, 4 | base64toCell, 5 | equalsMsgAddresses, 6 | formatAddressAndUrl, 7 | parseContentCell, 8 | sendToIndex 9 | } from "../wrappers/ui-utils"; 10 | import {Address, Cell, fromNano, OpenedContract} from "@ton/core"; 11 | import {JettonMinter, parseJettonMinterData} from "../wrappers/JettonMinter"; 12 | import {NetworkProvider, UIProvider} from "@ton/blueprint"; 13 | import {fromUnits} from "./units"; 14 | 15 | export const checkJettonMinter = async ( 16 | jettonMinterAddress: { 17 | isBounceable: boolean, 18 | isTestOnly: boolean, 19 | address: Address 20 | }, 21 | jettonMinterCode: Cell, 22 | jettonWalletCode: Cell, 23 | provider: NetworkProvider, 24 | ui: UIProvider, 25 | isTestnet: boolean, 26 | silent: boolean 27 | ) => { 28 | 29 | const write = (message: string) => { 30 | if (!silent) { 31 | ui.write(message); 32 | } 33 | } 34 | 35 | // Account State and Data 36 | 37 | const result = await sendToIndex('account', {address: addressToString(jettonMinterAddress)}, provider); 38 | write('Contract status: ' + result.status); 39 | 40 | assert(result.status === 'active', "Contract not active", ui); 41 | 42 | if (base64toCell(result.code).equals(jettonMinterCode)) { 43 | write('The contract code matches the jetton-minter code from this repository'); 44 | } else { 45 | throw new Error('The contract code DOES NOT match the jetton-minter code from this repository'); 46 | } 47 | 48 | write('Toncoin balance on jetton-minter: ' + fromNano(result.balance) + ' TON'); 49 | 50 | const data = base64toCell(result.data); 51 | const parsedData = parseJettonMinterData(data); 52 | 53 | if (parsedData.wallet_code.equals(jettonWalletCode)) { 54 | write('The jetton-wallet code matches the jetton-wallet code from this repository'); 55 | } else { 56 | throw new Error('The jetton-wallet DOES NOT match the jetton-wallet code from this repository'); 57 | } 58 | 59 | const metadataUrl: string = (parsedData.jetton_content as Cell).beginParse().loadStringTail(); 60 | 61 | // Get-methods 62 | 63 | const jettonMinterContract: OpenedContract = provider.open(JettonMinter.createFromAddress(jettonMinterAddress.address)); 64 | const getData = await jettonMinterContract.getJettonData(); 65 | 66 | assert(getData.totalSupply === parsedData.supply, "Total supply doesn't match", ui); 67 | assert(getData.adminAddress.equals(parsedData.admin), "Admin address doesn't match", ui); 68 | 69 | let decimals: number; 70 | const parsedContent = await parseContentCell(getData.content); 71 | if (parsedContent instanceof String) { 72 | throw new Error('content not HashMap'); 73 | } else { 74 | const contentMap: any = parsedContent; 75 | console.assert(contentMap['uri'], metadataUrl, "Metadata URL doesn't match"); 76 | const decimalsString = contentMap['decimals']; 77 | decimals = parseInt(decimalsString); 78 | if (isNaN(decimals)) { 79 | throw new Error('invalid decimals'); 80 | } 81 | } 82 | 83 | assert(getData.walletCode.equals(parsedData.wallet_code), "Jetton-wallet code doesn't match", ui); 84 | 85 | const getNextAdminAddress = await jettonMinterContract.getNextAdminAddress(); 86 | console.assert(equalsMsgAddresses(getNextAdminAddress, parsedData.transfer_admin), "Next admin address doesn't match"); 87 | 88 | // StateInit 89 | 90 | const jettonMinterContract2 = JettonMinter.createFromConfig({ 91 | admin: parsedData.admin, 92 | wallet_code: jettonWalletCode, 93 | jetton_content: { 94 | uri: metadataUrl 95 | } 96 | }, jettonMinterCode) 97 | 98 | if (jettonMinterContract2.address.equals(jettonMinterAddress.address)) { 99 | write('StateInit matches'); 100 | } 101 | 102 | // Print 103 | 104 | write('Decimals: ' + decimals); 105 | write('Total Supply: ' + fromUnits(parsedData.supply, decimals)); 106 | write('Mintable: ' + getData.mintable); 107 | write(`Metadata URL: "${metadataUrl}"`); 108 | write('Current admin address: ' + (await formatAddressAndUrl(parsedData.admin, provider, isTestnet))); 109 | const nextAdminAddress = parsedData.transfer_admin; 110 | if (!nextAdminAddress) { 111 | write('Next admin address: null'); 112 | } else { 113 | write('Next admin address: ' + (await formatAddressAndUrl(nextAdminAddress, provider, isTestnet))); 114 | } 115 | 116 | return { 117 | jettonMinterContract, 118 | adminAddress: parsedData.admin, 119 | nextAdminAddress: parsedData.transfer_admin, 120 | decimals 121 | } 122 | } -------------------------------------------------------------------------------- /scripts/JettonWalletChecker.ts: -------------------------------------------------------------------------------- 1 | import {Address, Cell, fromNano, OpenedContract} from "@ton/core"; 2 | import {NetworkProvider, UIProvider} from "@ton/blueprint"; 3 | import { 4 | addressToString, 5 | assert, 6 | base64toCell, 7 | formatAddressAndUrl, 8 | lockTypeToName, 9 | parseContentCell, 10 | sendToIndex 11 | } from "../wrappers/ui-utils"; 12 | import {JettonWallet, parseJettonWalletData} from "../wrappers/JettonWallet"; 13 | import {intToLockType, JettonMinter} from "../wrappers/JettonMinter"; 14 | import {fromUnits} from "./units"; 15 | 16 | export const checkJettonWallet = async ( 17 | jettonWalletAddress: { 18 | isBounceable: boolean, 19 | isTestOnly: boolean, 20 | address: Address 21 | }, 22 | jettonMinterCode: Cell, 23 | jettonWalletCode: Cell, 24 | provider: NetworkProvider, 25 | ui: UIProvider, 26 | isTestnet: boolean, 27 | silent: boolean 28 | ) => { 29 | 30 | const write = (message: string) => { 31 | if (!silent) { 32 | ui.write(message); 33 | } 34 | } 35 | 36 | // Account State and Data 37 | 38 | const result = await sendToIndex('account', {address: addressToString(jettonWalletAddress)}, provider); 39 | write('Contract status: ' + result.status); 40 | 41 | assert(result.status === 'active', "Contract not active", ui); 42 | 43 | if (base64toCell(result.code).equals(jettonWalletCode)) { 44 | write('The contract code matches the jetton-wallet code from this repository'); 45 | } else { 46 | throw new Error('The contract code DOES NOT match the jetton-wallet code from this repository'); 47 | } 48 | 49 | write('Toncoin balance on jetton-wallet: ' + fromNano(result.balance) + ' TON'); 50 | 51 | const data = base64toCell(result.data); 52 | const parsedData = parseJettonWalletData(data); 53 | 54 | // Check in jetton-minter 55 | 56 | const jettonMinterContract: OpenedContract = provider.open(JettonMinter.createFromAddress(parsedData.jettonMasterAddress)); 57 | const jettonWalletAddress2 = await jettonMinterContract.getWalletAddress(parsedData.ownerAddress); 58 | assert(jettonWalletAddress2.equals(jettonWalletAddress.address), "fake jetton-minter", ui); 59 | 60 | 61 | const {content} = await jettonMinterContract.getJettonData(); 62 | let decimals: number; 63 | const parsedContent = await parseContentCell(content); 64 | if (parsedContent instanceof String) { 65 | throw new Error('content not HashMap'); 66 | } else { 67 | const contentMap: any = parsedContent; 68 | const decimalsString = contentMap['decimals']; 69 | decimals = parseInt(decimalsString); 70 | if (isNaN(decimals)) { 71 | throw new Error('invalid decimals'); 72 | } 73 | } 74 | 75 | // Get-methods 76 | 77 | const jettonWalletContract: OpenedContract = provider.open(JettonWallet.createFromAddress(jettonWalletAddress.address)); 78 | const getData = await jettonWalletContract.getWalletData(); 79 | 80 | assert(getData.balance === parsedData.balance, "Balance doesn't match", ui); 81 | assert(getData.owner.equals(parsedData.ownerAddress), "Owner address doesn't match", ui); 82 | assert(getData.minter.equals(parsedData.jettonMasterAddress), "Jetton master address doesn't match", ui); 83 | assert(getData.wallet_code.equals(jettonWalletCode), "Jetton wallet code doesn't match", ui); 84 | 85 | assert((await jettonWalletContract.getWalletStatus()) === parsedData.status, "Jetton wallet status doesn't match", ui); 86 | 87 | // StateInit 88 | 89 | const jettonWalletContract2 = JettonWallet.createFromConfig({ 90 | ownerAddress: parsedData.ownerAddress, 91 | jettonMasterAddress: parsedData.jettonMasterAddress 92 | }, jettonWalletCode); 93 | 94 | if (jettonWalletContract2.address.equals(jettonWalletAddress.address)) { 95 | write('StateInit matches'); 96 | } 97 | 98 | // Print 99 | 100 | write('Jetton-wallet status: ' + lockTypeToName(intToLockType(parsedData.status))); 101 | write('Balance: ' + fromUnits(parsedData.balance, decimals)); 102 | write('Owner address: ' + (await formatAddressAndUrl(parsedData.ownerAddress, provider, isTestnet))); 103 | write('Jetton-minter address: ' + (await formatAddressAndUrl(parsedData.jettonMasterAddress, provider, isTestnet))); 104 | 105 | 106 | return { 107 | jettonWalletContract, 108 | jettonBalance: parsedData.balance 109 | } 110 | } -------------------------------------------------------------------------------- /scripts/changeAdmin.ts: -------------------------------------------------------------------------------- 1 | import {compile, NetworkProvider} from '@ton/blueprint'; 2 | import { 3 | addressToString, 4 | jettonWalletCodeFromLibrary, 5 | promptBool, 6 | promptUserFriendlyAddress 7 | } from "../wrappers/ui-utils"; 8 | import {checkJettonMinter} from "./JettonMinterChecker"; 9 | 10 | export async function run(provider: NetworkProvider) { 11 | const isTestnet = provider.network() !== 'mainnet'; 12 | 13 | const ui = provider.ui(); 14 | 15 | const jettonMinterCode = await compile('JettonMinter'); 16 | const jettonWalletCodeRaw = await compile('JettonWallet'); 17 | const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw); 18 | 19 | const jettonMinterAddress = await promptUserFriendlyAddress("Enter the address of the jetton minter", ui, isTestnet); 20 | 21 | try { 22 | const { 23 | jettonMinterContract, 24 | adminAddress 25 | } = await checkJettonMinter(jettonMinterAddress, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, true); 26 | 27 | if (!provider.sender().address!.equals(adminAddress)) { 28 | ui.write('You are not admin of this jetton minter'); 29 | return; 30 | } 31 | 32 | const newAdminAddress = await promptUserFriendlyAddress("Enter new admin address", ui, isTestnet); 33 | 34 | if (!(await promptBool(`Change admin to to ${addressToString(newAdminAddress)}?`, ['yes', 'no'], ui))) { 35 | return; 36 | } 37 | 38 | await jettonMinterContract.sendChangeAdmin(provider.sender(), newAdminAddress.address); 39 | 40 | ui.write('Transaction sent'); 41 | 42 | } catch (e: any) { 43 | ui.write(e.message); 44 | return; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /scripts/changeMetadataUrl.ts: -------------------------------------------------------------------------------- 1 | import {compile, NetworkProvider} from '@ton/blueprint'; 2 | import {jettonWalletCodeFromLibrary, promptBool, promptUrl, promptUserFriendlyAddress} from "../wrappers/ui-utils"; 3 | import {checkJettonMinter} from "./JettonMinterChecker"; 4 | 5 | export async function run(provider: NetworkProvider) { 6 | const isTestnet = provider.network() !== 'mainnet'; 7 | 8 | const ui = provider.ui(); 9 | 10 | const jettonMinterCode = await compile('JettonMinter'); 11 | const jettonWalletCodeRaw = await compile('JettonWallet'); 12 | const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw); 13 | 14 | const jettonMinterAddress = await promptUserFriendlyAddress("Enter the address of the jetton minter", ui, isTestnet); 15 | 16 | try { 17 | const { 18 | jettonMinterContract, 19 | adminAddress 20 | } = await checkJettonMinter(jettonMinterAddress, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, true); 21 | 22 | if (!provider.sender().address!.equals(adminAddress)) { 23 | ui.write('You are not admin of this jetton minter'); 24 | return; 25 | } 26 | 27 | // e.g "https://bridge.ton.org/token/1/0x111111111117dC0aa78b770fA6A738034120C302.json" 28 | const jettonMetadataUri = await promptUrl("Enter jetton metadata uri (https://jettonowner.com/jetton.json)", ui) 29 | 30 | if (!(await promptBool(`Change metadata url to "${jettonMetadataUri}"?`, ['yes', 'no'], ui))) { 31 | return; 32 | } 33 | 34 | await jettonMinterContract.sendChangeContent(provider.sender(), { 35 | uri: jettonMetadataUri 36 | }); 37 | 38 | ui.write('Transaction sent'); 39 | 40 | } catch (e: any) { 41 | ui.write(e.message); 42 | return; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts/claimAdmin.ts: -------------------------------------------------------------------------------- 1 | import {compile, NetworkProvider} from '@ton/blueprint'; 2 | import {jettonWalletCodeFromLibrary, promptBool, promptUserFriendlyAddress} from "../wrappers/ui-utils"; 3 | import {checkJettonMinter} from "./JettonMinterChecker"; 4 | 5 | export async function run(provider: NetworkProvider) { 6 | const isTestnet = provider.network() !== 'mainnet'; 7 | 8 | const ui = provider.ui(); 9 | 10 | const jettonMinterCode = await compile('JettonMinter'); 11 | const jettonWalletCodeRaw = await compile('JettonWallet'); 12 | const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw); 13 | 14 | const jettonMinterAddress = await promptUserFriendlyAddress("Enter the address of the jetton minter", ui, isTestnet); 15 | 16 | try { 17 | const { 18 | jettonMinterContract, 19 | nextAdminAddress 20 | } = await checkJettonMinter(jettonMinterAddress, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, true); 21 | 22 | if (!nextAdminAddress || !provider.sender().address!.equals(nextAdminAddress)) { 23 | ui.write('You are not new admin of this jetton minter'); 24 | return; 25 | } 26 | 27 | if (!(await promptBool(`Claim admin?`, ['yes', 'no'], ui))) { 28 | return; 29 | } 30 | 31 | await jettonMinterContract.sendClaimAdmin(provider.sender()); 32 | 33 | ui.write('Transaction sent'); 34 | 35 | } catch (e: any) { 36 | ui.write(e.message); 37 | return; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scripts/deployJettonMinter.ts: -------------------------------------------------------------------------------- 1 | import {toNano} from '@ton/core'; 2 | import {JettonMinter} from '../wrappers/JettonMinter'; 3 | import {compile, NetworkProvider} from '@ton/blueprint'; 4 | import {jettonWalletCodeFromLibrary, promptUrl, promptUserFriendlyAddress} from "../wrappers/ui-utils"; 5 | 6 | export async function run(provider: NetworkProvider) { 7 | const isTestnet = provider.network() !== 'mainnet'; 8 | 9 | const ui = provider.ui(); 10 | const jettonWalletCodeRaw = await compile('JettonWallet'); 11 | 12 | const adminAddress = await promptUserFriendlyAddress("Enter the address of the jetton owner (admin):", ui, isTestnet); 13 | 14 | // e.g "https://bridge.ton.org/token/1/0x111111111117dC0aa78b770fA6A738034120C302.json" 15 | const jettonMetadataUri = await promptUrl("Enter jetton metadata uri (https://jettonowner.com/jetton.json)", ui) 16 | 17 | const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw); 18 | 19 | const minter = provider.open(JettonMinter.createFromConfig({ 20 | admin: adminAddress.address, 21 | wallet_code: jettonWalletCode, 22 | jetton_content: {uri: jettonMetadataUri} 23 | }, 24 | await compile('JettonMinter'))); 25 | 26 | await minter.sendDeploy(provider.sender(), toNano("1.5")); // send 1.5 TON 27 | } 28 | -------------------------------------------------------------------------------- /scripts/deployLibrary.ts: -------------------------------------------------------------------------------- 1 | import {Librarian} from '../wrappers/Librarian'; 2 | import {compile, NetworkProvider} from '@ton/blueprint'; 3 | import {promptToncoin} from "../wrappers/ui-utils"; 4 | 5 | export async function run(provider: NetworkProvider) { 6 | const ui = provider.ui(); 7 | ui.write("This jetton contract uses the jetton-wallet code from library. This reduces network fees when operating with the jetton."); 8 | ui.write("Librarian is the contract that stores the library."); 9 | ui.write("If someone is already storing this jetton-wallet library on the blockchain - you don't need to deploy librarian."); 10 | const jettonWalletCodeRaw = await compile('JettonWallet'); 11 | const librarianCode = await compile('Librarian'); 12 | 13 | const tonAmount = await promptToncoin("Enter Toncoin amount to deploy librarian. Some of Toncoins will reserved on the contract to pay storage fees. Excess will be returned.", ui); 14 | const librarian = provider.open(Librarian.createFromConfig({code: jettonWalletCodeRaw}, librarianCode)); 15 | await librarian.sendDeploy(provider.sender(), tonAmount); 16 | } 17 | -------------------------------------------------------------------------------- /scripts/forceBurn.ts: -------------------------------------------------------------------------------- 1 | import {compile, NetworkProvider} from '@ton/blueprint'; 2 | import { 3 | addressToString, 4 | jettonWalletCodeFromLibrary, 5 | promptAmount, 6 | promptBool, 7 | promptUserFriendlyAddress 8 | } from "../wrappers/ui-utils"; 9 | import {checkJettonMinter} from "./JettonMinterChecker"; 10 | import {checkJettonWallet} from "./JettonWalletChecker"; 11 | import {fromUnits} from "./units"; 12 | 13 | export async function run(provider: NetworkProvider) { 14 | const isTestnet = provider.network() !== 'mainnet'; 15 | 16 | const ui = provider.ui(); 17 | 18 | const jettonMinterCode = await compile('JettonMinter'); 19 | const jettonWalletCodeRaw = await compile('JettonWallet'); 20 | const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw); 21 | 22 | const jettonMinterAddress = await promptUserFriendlyAddress("Enter the address of the jetton minter", ui, isTestnet); 23 | 24 | try { 25 | const { 26 | jettonMinterContract, 27 | adminAddress, 28 | decimals 29 | } = await checkJettonMinter(jettonMinterAddress, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, true); 30 | 31 | const fromAddress = await promptUserFriendlyAddress("Please enter user address to burn from:", ui, isTestnet); 32 | const fromJettonWalletAddress = await jettonMinterContract.getWalletAddress(fromAddress.address); 33 | 34 | const {jettonBalance} = await checkJettonWallet({ 35 | address: fromJettonWalletAddress, 36 | isBounceable: true, 37 | isTestOnly: isTestnet 38 | }, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, true); 39 | 40 | if (!provider.sender().address!.equals(adminAddress)) { 41 | ui.write('You are not admin of this jetton minter'); 42 | return; 43 | } 44 | 45 | const amount = await promptAmount("Enter jetton amount to burn", decimals, ui); 46 | 47 | if (jettonBalance < amount) { 48 | ui.write(`This user have only ${fromUnits(jettonBalance, decimals)}`); 49 | return; 50 | } 51 | 52 | if (!(await promptBool(`Burn ${fromUnits(amount, decimals)} from ${addressToString(fromAddress)}?`, ['yes', 'no'], ui))) { 53 | return; 54 | } 55 | 56 | await jettonMinterContract.sendForceBurn(provider.sender(), 57 | amount, 58 | fromAddress.address, 59 | null); 60 | 61 | ui.write('Transaction sent'); 62 | 63 | } catch (e: any) { 64 | ui.write(e.message); 65 | return; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scripts/forceTransfer.ts: -------------------------------------------------------------------------------- 1 | import {compile, NetworkProvider} from '@ton/blueprint'; 2 | import { 3 | addressToString, 4 | jettonWalletCodeFromLibrary, 5 | promptAmount, 6 | promptBool, 7 | promptUserFriendlyAddress 8 | } from "../wrappers/ui-utils"; 9 | import {checkJettonMinter} from "./JettonMinterChecker"; 10 | import {checkJettonWallet} from "./JettonWalletChecker"; 11 | import {fromUnits} from "./units"; 12 | 13 | export async function run(provider: NetworkProvider) { 14 | const isTestnet = provider.network() !== 'mainnet'; 15 | 16 | const ui = provider.ui(); 17 | 18 | const jettonMinterCode = await compile('JettonMinter'); 19 | const jettonWalletCodeRaw = await compile('JettonWallet'); 20 | const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw); 21 | 22 | const jettonMinterAddress = await promptUserFriendlyAddress("Enter the address of the jetton minter", ui, isTestnet); 23 | 24 | try { 25 | const {jettonMinterContract, adminAddress, decimals} = await checkJettonMinter(jettonMinterAddress, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, true); 26 | 27 | const fromAddress = await promptUserFriendlyAddress("Please enter user address to transfer from:", ui, isTestnet); 28 | const fromJettonWalletAddress = await jettonMinterContract.getWalletAddress(fromAddress.address); 29 | 30 | const {jettonBalance} = await checkJettonWallet({address: fromJettonWalletAddress, isBounceable: true, isTestOnly: isTestnet}, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, true); 31 | 32 | if (!provider.sender().address!.equals(adminAddress)) { 33 | ui.write('You are not admin of this jetton minter'); 34 | return; 35 | } 36 | 37 | const amount = await promptAmount("Enter jetton amount to transfer", decimals, ui); 38 | 39 | 40 | if (jettonBalance < amount) { 41 | ui.write(`This user have only ${fromUnits(jettonBalance, decimals)}`); 42 | return; 43 | } 44 | 45 | const toAddress = await promptUserFriendlyAddress("Please enter user address to transfer to:", ui, isTestnet); 46 | 47 | if (!(await promptBool(`Transfer ${fromUnits(amount, decimals)} from ${addressToString(fromAddress)} to ${addressToString(toAddress)}?`, ['yes', 'no'], ui))) { 48 | return; 49 | } 50 | 51 | await jettonMinterContract.sendForceTransfer(provider.sender(), 52 | amount, 53 | toAddress.address, 54 | fromAddress.address, 55 | null, 56 | 0n, 57 | null); 58 | 59 | ui.write('Transaction sent'); 60 | 61 | } catch (e: any) { 62 | ui.write(e.message); 63 | return; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /scripts/mint.ts: -------------------------------------------------------------------------------- 1 | import {compile, NetworkProvider} from '@ton/blueprint'; 2 | import { 3 | addressToString, 4 | jettonWalletCodeFromLibrary, 5 | promptAmount, 6 | promptBool, 7 | promptUserFriendlyAddress 8 | } from "../wrappers/ui-utils"; 9 | import {checkJettonMinter} from "./JettonMinterChecker"; 10 | import {fromUnits} from "./units"; 11 | 12 | export async function run(provider: NetworkProvider) { 13 | const isTestnet = provider.network() !== 'mainnet'; 14 | 15 | const ui = provider.ui(); 16 | 17 | const jettonMinterCode = await compile('JettonMinter'); 18 | const jettonWalletCodeRaw = await compile('JettonWallet'); 19 | const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw); 20 | 21 | const jettonMinterAddress = await promptUserFriendlyAddress("Enter the address of the jetton minter", ui, isTestnet); 22 | 23 | try { 24 | const { 25 | jettonMinterContract, 26 | adminAddress, 27 | decimals 28 | } = await checkJettonMinter(jettonMinterAddress, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, true); 29 | 30 | if (!provider.sender().address!.equals(adminAddress)) { 31 | ui.write('You are not admin of this jetton minter'); 32 | return; 33 | } 34 | 35 | const amount = await promptAmount("Enter jetton amount to mint", decimals, ui); 36 | 37 | const destinationAddress = await promptUserFriendlyAddress("Enter destination user address to mint", ui, isTestnet); 38 | 39 | if (!(await promptBool(`Mint ${fromUnits(amount, decimals)} to ${addressToString(destinationAddress)}?`, ['yes', 'no'], ui))) { 40 | return; 41 | } 42 | 43 | await jettonMinterContract.sendMint(provider.sender(), 44 | destinationAddress.address, 45 | amount); 46 | 47 | ui.write('Transaction sent'); 48 | 49 | } catch (e: any) { 50 | ui.write(e.message); 51 | return; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /scripts/setStatus.ts: -------------------------------------------------------------------------------- 1 | import {compile, NetworkProvider} from '@ton/blueprint'; 2 | import { 3 | addressToString, 4 | jettonWalletCodeFromLibrary, 5 | lockTypeToName, 6 | promptBool, 7 | promptLockType, 8 | promptUserFriendlyAddress 9 | } from "../wrappers/ui-utils"; 10 | import {checkJettonMinter} from "./JettonMinterChecker"; 11 | import {checkJettonWallet} from "./JettonWalletChecker"; 12 | import {LOCK_TYPES, LockType} from "../wrappers/JettonMinter"; 13 | 14 | export async function run(provider: NetworkProvider) { 15 | const isTestnet = provider.network() !== 'mainnet'; 16 | 17 | const ui = provider.ui(); 18 | 19 | const jettonMinterCode = await compile('JettonMinter'); 20 | const jettonWalletCodeRaw = await compile('JettonWallet'); 21 | const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw); 22 | 23 | const jettonMinterAddress = await promptUserFriendlyAddress("Enter the address of the jetton minter", ui, isTestnet); 24 | 25 | try { 26 | const {jettonMinterContract} = await checkJettonMinter(jettonMinterAddress, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, true); 27 | 28 | const userAddress = await promptUserFriendlyAddress("Please enter user address:", ui, isTestnet); 29 | const fromJettonWalletAddress = await jettonMinterContract.getWalletAddress(userAddress.address); 30 | const {jettonBalance} = await checkJettonWallet({ 31 | address: fromJettonWalletAddress, 32 | isBounceable: true, 33 | isTestOnly: isTestnet 34 | }, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, true); 35 | 36 | 37 | LOCK_TYPES.forEach(lockType => { 38 | ui.write(lockType + ' - ' + lockTypeToName(lockType as LockType)); 39 | }); 40 | 41 | const newStatus = await promptLockType(`Enter new status (${LOCK_TYPES.join(', ')})`, ui); 42 | 43 | if (!(await promptBool(`Set status ${newStatus} to ${addressToString(userAddress)}?`, ['yes', 'no'], ui))) { 44 | return; 45 | } 46 | 47 | await jettonMinterContract.sendLockWallet(provider.sender(), 48 | userAddress.address, 49 | newStatus 50 | ); 51 | 52 | ui.write('Transaction sent'); 53 | 54 | } catch (e: any) { 55 | ui.write(e.message); 56 | return; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/showJettonMinterInfo.ts: -------------------------------------------------------------------------------- 1 | import {compile, NetworkProvider} from '@ton/blueprint'; 2 | import {jettonWalletCodeFromLibrary, promptUserFriendlyAddress} from "../wrappers/ui-utils"; 3 | import {checkJettonMinter} from "./JettonMinterChecker"; 4 | 5 | export async function run(provider: NetworkProvider) { 6 | const isTestnet = provider.network() !== 'mainnet'; 7 | 8 | const ui = provider.ui(); 9 | 10 | const jettonMinterCode = await compile('JettonMinter'); 11 | const jettonWalletCodeRaw = await compile('JettonWallet'); 12 | const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw); 13 | 14 | const jettonMinterAddress = await promptUserFriendlyAddress("Enter the address of the jetton minter", ui, isTestnet); 15 | 16 | try { 17 | await checkJettonMinter(jettonMinterAddress, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, false); 18 | } catch (e: any) { 19 | ui.write(e.message); 20 | return; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scripts/showJettonWalletInfo.ts: -------------------------------------------------------------------------------- 1 | import {compile, NetworkProvider} from '@ton/blueprint'; 2 | import {jettonWalletCodeFromLibrary, promptUserFriendlyAddress} from "../wrappers/ui-utils"; 3 | import {checkJettonWallet} from "./JettonWalletChecker"; 4 | 5 | export async function run(provider: NetworkProvider) { 6 | const isTestnet = provider.network() !== 'mainnet'; 7 | 8 | const ui = provider.ui(); 9 | 10 | const jettonMinterCode = await compile('JettonMinter'); 11 | const jettonWalletCodeRaw = await compile('JettonWallet'); 12 | const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw); 13 | 14 | const jettonWalletAddress = await promptUserFriendlyAddress("Enter the address of the jetton wallet", ui, isTestnet); 15 | 16 | try { 17 | await checkJettonWallet(jettonWalletAddress, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, false); 18 | } catch (e: any) { 19 | ui.write(e.message); 20 | return; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scripts/topUp.ts: -------------------------------------------------------------------------------- 1 | import {compile, NetworkProvider} from '@ton/blueprint'; 2 | import {jettonWalletCodeFromLibrary, promptBool, promptToncoin, promptUserFriendlyAddress} from "../wrappers/ui-utils"; 3 | import {checkJettonMinter} from "./JettonMinterChecker"; 4 | import {fromNano} from "@ton/core"; 5 | 6 | export async function run(provider: NetworkProvider) { 7 | const isTestnet = provider.network() !== 'mainnet'; 8 | 9 | const ui = provider.ui(); 10 | 11 | const jettonMinterCode = await compile('JettonMinter'); 12 | const jettonWalletCodeRaw = await compile('JettonWallet'); 13 | const jettonWalletCode = jettonWalletCodeFromLibrary(jettonWalletCodeRaw); 14 | 15 | const jettonMinterAddress = await promptUserFriendlyAddress("Enter the address of the jetton minter", ui, isTestnet); 16 | 17 | try { 18 | const {jettonMinterContract} = await checkJettonMinter(jettonMinterAddress, jettonMinterCode, jettonWalletCode, provider, ui, isTestnet, true); 19 | 20 | const tonAmount = await promptToncoin("Enter Toncoin amount to top-up jetton-minter Toncoins balance.", ui); 21 | 22 | if (!(await promptBool(`${fromNano(tonAmount)} TON top-up ?`, ['yes', 'no'], ui))) { 23 | return; 24 | } 25 | 26 | await jettonMinterContract.sendTopUp(provider.sender(), tonAmount); 27 | 28 | ui.write('Transaction sent'); 29 | 30 | } catch (e: any) { 31 | ui.write(e.message); 32 | return; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scripts/units.ts: -------------------------------------------------------------------------------- 1 | 2 | function getMultiplier(decimals: number): bigint { 3 | let x = 1n; 4 | for (let i = 0; i < decimals; i++) { 5 | x *= 10n; 6 | } 7 | return x; 8 | } 9 | 10 | export function toUnits(src: string | bigint, decimals: number): bigint { 11 | const MULTIPLIER = getMultiplier(decimals); 12 | 13 | if (typeof src === 'bigint') { 14 | return src * MULTIPLIER; 15 | } else { 16 | 17 | // Check sign 18 | let neg = false; 19 | while (src.startsWith('-')) { 20 | neg = !neg; 21 | src = src.slice(1); 22 | } 23 | 24 | // Split string 25 | if (src === '.') { 26 | throw Error('Invalid number'); 27 | } 28 | let parts = src.split('.'); 29 | if (parts.length > 2) { 30 | throw Error('Invalid number'); 31 | } 32 | 33 | // Prepare parts 34 | let whole = parts[0]; 35 | let frac = parts[1]; 36 | if (!whole) { 37 | whole = '0'; 38 | } 39 | if (!frac) { 40 | frac = '0'; 41 | } 42 | if (frac.length > decimals) { 43 | throw Error('Invalid number'); 44 | } 45 | while (frac.length < decimals) { 46 | frac += '0'; 47 | } 48 | 49 | // Convert 50 | let r = BigInt(whole) * MULTIPLIER + BigInt(frac); 51 | if (neg) { 52 | r = -r; 53 | } 54 | return r; 55 | } 56 | } 57 | 58 | export function fromUnits(src: bigint | string, decimals: number): string { 59 | const MULTIPLIER = getMultiplier(decimals); 60 | 61 | let v = BigInt(src); 62 | let neg = false; 63 | if (v < 0) { 64 | neg = true; 65 | v = -v; 66 | } 67 | 68 | // Convert fraction 69 | let frac = v % MULTIPLIER; 70 | let facStr = frac.toString(); 71 | while (facStr.length < decimals) { 72 | facStr = '0' + facStr; 73 | } 74 | facStr = facStr.match(/^([0-9]*[1-9]|0)(0*)/)![1]; 75 | 76 | // Convert whole 77 | let whole = v / MULTIPLIER; 78 | let wholeStr = whole.toString(); 79 | 80 | // Value 81 | let value = `${wholeStr}${facStr === '0' ? '' : `.${facStr}`}`; 82 | if (neg) { 83 | value = '-' + value; 84 | } 85 | 86 | return value; 87 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /wrappers/JettonConstants.ts: -------------------------------------------------------------------------------- 1 | export abstract class Op { 2 | static transfer = 0xf8a7ea5; 3 | static transfer_notification = 0x7362d09c; 4 | static internal_transfer = 0x178d4519; 5 | static excesses = 0xd53276db; 6 | static burn = 0x595f07bc; 7 | static burn_notification = 0x7bdd97de; 8 | 9 | static provide_wallet_address = 0x2c76b973; 10 | static take_wallet_address = 0xd1735400; 11 | static mint = 0x642b7d07; 12 | static change_admin = 0x6501f354; 13 | static claim_admin = 0xfb88e119; 14 | static upgrade = 0x2508d66a; 15 | static call_to = 0x235caf52; 16 | static top_up = 0xd372158c; 17 | static change_metadata_url = 0xcb862902; 18 | static set_status = 0xeed236d3; 19 | } 20 | 21 | export abstract class Errors { 22 | static invalid_op = 72; 23 | static wrong_op = 0xffff; 24 | static not_owner = 73; 25 | static not_valid_wallet = 74; 26 | static wrong_workchain = 333; 27 | 28 | static contract_locked = 45; 29 | static balance_error = 47; 30 | static not_enough_gas = 48; 31 | static invalid_mesage = 49; 32 | static discovery_fee_not_matched = 75; 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /wrappers/JettonMinter.compile.ts: -------------------------------------------------------------------------------- 1 | import { CompilerConfig } from '@ton/blueprint'; 2 | 3 | export const compile: CompilerConfig = { 4 | targets: ['contracts/jetton-minter.fc'], 5 | }; 6 | -------------------------------------------------------------------------------- /wrappers/JettonMinter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | beginCell, 4 | Cell, 5 | Contract, 6 | contractAddress, 7 | ContractProvider, 8 | Sender, 9 | SendMode, Slice, 10 | toNano 11 | } from '@ton/core'; 12 | import {JettonWallet} from './JettonWallet'; 13 | import {Op} from './JettonConstants'; 14 | 15 | export type JettonMinterContent = { 16 | uri: string 17 | }; 18 | export type JettonMinterConfig = { 19 | admin: Address, 20 | wallet_code: Cell, 21 | jetton_content: Cell | JettonMinterContent 22 | }; 23 | export type JettonMinterConfigFull = { 24 | supply: bigint, 25 | admin: Address, 26 | //Makes no sense to update transfer admin. ...Or is it? 27 | transfer_admin: Address | null, 28 | wallet_code: Cell, 29 | jetton_content: Cell | JettonMinterContent 30 | } 31 | 32 | export type LockType = 'unlock' | 'out' | 'in' | 'full'; 33 | 34 | export const LOCK_TYPES = ['unlock', 'out', 'in', 'full']; 35 | 36 | export const lockTypeToInt = (lockType: LockType): number => { 37 | switch (lockType) { 38 | case 'unlock': 39 | return 0; 40 | case 'out': 41 | return 1; 42 | case 'in': 43 | return 2; 44 | case 'full': 45 | return 3; 46 | default: 47 | throw new Error("Invalid argument!"); 48 | } 49 | } 50 | 51 | export const intToLockType = (lockType: number): LockType => { 52 | switch (lockType) { 53 | case 0: 54 | return 'unlock'; 55 | case 1: 56 | return 'out'; 57 | case 2: 58 | return 'in'; 59 | case 3: 60 | return 'full'; 61 | default: 62 | throw new Error("Invalid argument!"); 63 | } 64 | } 65 | 66 | export function endParse(slice: Slice) { 67 | if (slice.remainingBits > 0 || slice.remainingRefs > 0) { 68 | throw new Error('remaining bits in data'); 69 | } 70 | } 71 | 72 | export function jettonMinterConfigCellToConfig(config: Cell): JettonMinterConfigFull { 73 | const sc = config.beginParse() 74 | const parsed: JettonMinterConfigFull = { 75 | supply: sc.loadCoins(), 76 | admin: sc.loadAddress(), 77 | transfer_admin: sc.loadMaybeAddress(), 78 | wallet_code: sc.loadRef(), 79 | jetton_content: sc.loadRef() 80 | }; 81 | endParse(sc); 82 | return parsed; 83 | } 84 | 85 | export function parseJettonMinterData(data: Cell): JettonMinterConfigFull { 86 | return jettonMinterConfigCellToConfig(data); 87 | } 88 | 89 | export function jettonMinterConfigFullToCell(config: JettonMinterConfigFull): Cell { 90 | const content = config.jetton_content instanceof Cell ? config.jetton_content : jettonContentToCell(config.jetton_content); 91 | return beginCell() 92 | .storeCoins(config.supply) 93 | .storeAddress(config.admin) 94 | .storeAddress(config.transfer_admin) 95 | .storeRef(config.wallet_code) 96 | .storeRef(content) 97 | .endCell() 98 | } 99 | 100 | export function jettonMinterConfigToCell(config: JettonMinterConfig): Cell { 101 | const content = config.jetton_content instanceof Cell ? config.jetton_content : jettonContentToCell(config.jetton_content); 102 | return beginCell() 103 | .storeCoins(0) 104 | .storeAddress(config.admin) 105 | .storeAddress(null) // Transfer admin address 106 | .storeRef(config.wallet_code) 107 | .storeRef(content) 108 | .endCell(); 109 | } 110 | 111 | export function jettonContentToCell(content: JettonMinterContent) { 112 | return beginCell() 113 | .storeStringRefTail(content.uri) //Snake logic under the hood 114 | .endCell(); 115 | } 116 | 117 | export class JettonMinter implements Contract { 118 | constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) { 119 | } 120 | 121 | static createFromAddress(address: Address) { 122 | return new JettonMinter(address); 123 | } 124 | 125 | static createFromConfig(config: JettonMinterConfig, code: Cell, workchain = 0) { 126 | const data = jettonMinterConfigToCell(config); 127 | const init = {code, data}; 128 | return new JettonMinter(contractAddress(workchain, init), init); 129 | } 130 | 131 | async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { 132 | await provider.internal(via, { 133 | value, 134 | sendMode: SendMode.PAY_GAS_SEPARATELY, 135 | body: beginCell().storeUint(Op.top_up, 32).storeUint(0, 64).endCell(), 136 | }); 137 | } 138 | 139 | static mintMessage(to: Address, jetton_amount: bigint, from?: Address | null, response?: Address | null, customPayload?: Cell | null, forward_ton_amount: bigint = 0n, total_ton_amount: bigint = 0n) { 140 | const mintMsg = beginCell().storeUint(Op.internal_transfer, 32) 141 | .storeUint(0, 64) 142 | .storeCoins(jetton_amount) 143 | .storeAddress(from) 144 | .storeAddress(response) 145 | .storeCoins(forward_ton_amount) 146 | .storeMaybeRef(customPayload) 147 | .endCell(); 148 | return beginCell().storeUint(Op.mint, 32).storeUint(0, 64) // op, queryId 149 | .storeAddress(to) 150 | .storeCoins(total_ton_amount) 151 | .storeRef(mintMsg) 152 | .endCell(); 153 | } 154 | 155 | static parseMintInternalMessage(slice: Slice) { 156 | const op = slice.loadUint(32); 157 | if (op !== Op.internal_transfer) throw new Error('Invalid op'); 158 | const queryId = slice.loadUint(64); 159 | const jettonAmount = slice.loadCoins(); 160 | const fromAddress = slice.loadAddress(); 161 | const responseAddress = slice.loadAddress(); 162 | const forwardTonAmount = slice.loadCoins(); 163 | const customPayload = slice.loadMaybeRef(); 164 | endParse(slice); 165 | return { 166 | queryId, 167 | jettonAmount, 168 | fromAddress, 169 | responseAddress, 170 | forwardTonAmount, 171 | customPayload 172 | } 173 | } 174 | 175 | static parseMintMessage(slice: Slice) { 176 | const op = slice.loadUint(32); 177 | if (op !== Op.mint) throw new Error('Invalid op'); 178 | const queryId = slice.loadUint(64); 179 | const toAddress = slice.loadAddress(); 180 | const tonAmount = slice.loadCoins(); 181 | const mintMsg = slice.loadRef(); 182 | endParse(slice); 183 | return { 184 | queryId, 185 | toAddress, 186 | tonAmount, 187 | internalMessage: this.parseMintInternalMessage(mintMsg.beginParse()) 188 | } 189 | } 190 | 191 | async sendMint(provider: ContractProvider, 192 | via: Sender, 193 | to: Address, 194 | jetton_amount: bigint, 195 | from?: Address | null, 196 | response_addr?: Address | null, 197 | customPayload?: Cell | null, 198 | forward_ton_amount: bigint = toNano('0.05'), total_ton_amount: bigint = toNano('0.1')) { 199 | await provider.internal(via, { 200 | sendMode: SendMode.PAY_GAS_SEPARATELY, 201 | body: JettonMinter.mintMessage(to, jetton_amount, from, response_addr, customPayload, forward_ton_amount, total_ton_amount), 202 | value: total_ton_amount, 203 | }); 204 | } 205 | 206 | /* provide_wallet_address#2c76b973 query_id:uint64 owner_address:MsgAddress include_address:Bool = InternalMsgBody; 207 | */ 208 | static discoveryMessage(owner: Address, include_address: boolean) { 209 | return beginCell().storeUint(Op.provide_wallet_address, 32).storeUint(0, 64) // op, queryId 210 | .storeAddress(owner).storeBit(include_address) 211 | .endCell(); 212 | } 213 | 214 | async sendDiscovery(provider: ContractProvider, via: Sender, owner: Address, include_address: boolean, value: bigint = toNano('0.1')) { 215 | await provider.internal(via, { 216 | sendMode: SendMode.PAY_GAS_SEPARATELY, 217 | body: JettonMinter.discoveryMessage(owner, include_address), 218 | value: value, 219 | }); 220 | } 221 | 222 | static topUpMessage() { 223 | return beginCell().storeUint(Op.top_up, 32).storeUint(0, 64) // op, queryId 224 | .endCell(); 225 | } 226 | 227 | static parseTopUp(slice: Slice) { 228 | const op = slice.loadUint(32); 229 | if (op !== Op.top_up) throw new Error('Invalid op'); 230 | const queryId = slice.loadUint(64); 231 | endParse(slice); 232 | return { 233 | queryId, 234 | } 235 | } 236 | 237 | async sendTopUp(provider: ContractProvider, via: Sender, value: bigint = toNano('0.1')) { 238 | await provider.internal(via, { 239 | sendMode: SendMode.PAY_GAS_SEPARATELY, 240 | body: JettonMinter.topUpMessage(), 241 | value: value, 242 | }); 243 | } 244 | 245 | static changeAdminMessage(newOwner: Address) { 246 | return beginCell().storeUint(Op.change_admin, 32).storeUint(0, 64) // op, queryId 247 | .storeAddress(newOwner) 248 | .endCell(); 249 | } 250 | 251 | static parseChangeAdmin(slice: Slice) { 252 | const op = slice.loadUint(32); 253 | if (op !== Op.change_admin) throw new Error('Invalid op'); 254 | const queryId = slice.loadUint(64); 255 | const newAdminAddress = slice.loadAddress(); 256 | endParse(slice); 257 | return { 258 | queryId, 259 | newAdminAddress 260 | } 261 | } 262 | 263 | async sendChangeAdmin(provider: ContractProvider, via: Sender, newOwner: Address) { 264 | await provider.internal(via, { 265 | sendMode: SendMode.PAY_GAS_SEPARATELY, 266 | body: JettonMinter.changeAdminMessage(newOwner), 267 | value: toNano("0.1"), 268 | }); 269 | } 270 | 271 | static claimAdminMessage(query_id: bigint = 0n) { 272 | return beginCell().storeUint(Op.claim_admin, 32).storeUint(query_id, 64).endCell(); 273 | } 274 | 275 | static parseClaimAdmin(slice: Slice) { 276 | const op = slice.loadUint(32); 277 | if (op !== Op.claim_admin) throw new Error('Invalid op'); 278 | const queryId = slice.loadUint(64); 279 | endParse(slice); 280 | return { 281 | queryId 282 | } 283 | } 284 | 285 | async sendClaimAdmin(provider: ContractProvider, via: Sender, query_id: bigint = 0n) { 286 | await provider.internal(via, { 287 | sendMode: SendMode.PAY_GAS_SEPARATELY, 288 | body: JettonMinter.claimAdminMessage(query_id), 289 | value: toNano('0.1') 290 | }) 291 | } 292 | 293 | static changeContentMessage(content: Cell | JettonMinterContent) { 294 | const contentString = content instanceof Cell ? content.beginParse().loadStringTail() : content.uri; 295 | return beginCell().storeUint(Op.change_metadata_url, 32).storeUint(0, 64) // op, queryId 296 | .storeStringTail(contentString) 297 | .endCell(); 298 | } 299 | 300 | static parseChangeContent(slice: Slice) { 301 | const op = slice.loadUint(32); 302 | if (op !== Op.change_metadata_url) throw new Error('Invalid op'); 303 | const queryId = slice.loadUint(64); 304 | const newMetadataUrl = slice.loadStringTail(); 305 | endParse(slice); 306 | return { 307 | queryId, 308 | newMetadataUrl 309 | } 310 | } 311 | 312 | async sendChangeContent(provider: ContractProvider, via: Sender, content: Cell | JettonMinterContent) { 313 | await provider.internal(via, { 314 | sendMode: SendMode.PAY_GAS_SEPARATELY, 315 | body: JettonMinter.changeContentMessage(content), 316 | value: toNano("0.1"), 317 | }); 318 | } 319 | 320 | static lockWalletMessage(lock_address: Address, lock: number, amount: bigint, query_id: bigint | number = 0) { 321 | return beginCell().storeUint(Op.call_to, 32).storeUint(query_id, 64) 322 | .storeAddress(lock_address) 323 | .storeCoins(amount) 324 | .storeRef(beginCell().storeUint(Op.set_status, 32).storeUint(query_id, 64).storeUint(lock, 4).endCell()) 325 | .endCell(); 326 | } 327 | 328 | static parseSetStatus(slice: Slice) { 329 | const op = slice.loadUint(32); 330 | if (op !== Op.set_status) throw new Error('Invalid op'); 331 | const queryId = slice.loadUint(64); 332 | const newStatus = slice.loadUint(4); 333 | endParse(slice); 334 | return { 335 | queryId, 336 | newStatus 337 | } 338 | } 339 | 340 | static parseCallTo(slice: Slice, refPrser: (slice: Slice) => any) { 341 | const op = slice.loadUint(32); 342 | if (op !== Op.call_to) throw new Error('Invalid op'); 343 | const queryId = slice.loadUint(64); 344 | const toAddress = slice.loadAddress(); 345 | const tonAmount = slice.loadCoins(); 346 | const ref = slice.loadRef(); 347 | endParse(slice); 348 | return { 349 | queryId, 350 | toAddress, 351 | tonAmount, 352 | action: refPrser(ref.beginParse()) 353 | } 354 | } 355 | 356 | async sendLockWallet(provider: ContractProvider, via: Sender, lock_address: Address, lock: LockType, amount: bigint = toNano('0.1'), query_id: bigint | number = 0) { 357 | const lockCmd: number = lockTypeToInt(lock); 358 | 359 | await provider.internal(via, { 360 | sendMode: SendMode.PAY_GAS_SEPARATELY, 361 | body: JettonMinter.lockWalletMessage(lock_address, lockCmd, amount, query_id), 362 | value: amount + toNano('0.1') 363 | }); 364 | } 365 | 366 | static forceTransferMessage(transfer_amount: bigint, 367 | to: Address, 368 | from: Address, 369 | custom_payload: Cell | null, 370 | forward_amount: bigint = 0n, 371 | forward_payload: Cell | null, 372 | value: bigint = toNano('0.1'), 373 | query_id: bigint = 0n) { 374 | 375 | const transferMessage = JettonWallet.transferMessage(transfer_amount, 376 | to, 377 | to, 378 | custom_payload, 379 | forward_amount, 380 | forward_payload); 381 | return beginCell().storeUint(Op.call_to, 32).storeUint(query_id, 64) 382 | .storeAddress(from) 383 | .storeCoins(value) 384 | .storeRef(transferMessage) 385 | .endCell(); 386 | } 387 | 388 | static parseTransfer(slice: Slice) { 389 | const op = slice.loadUint(32); 390 | if (op !== Op.transfer) throw new Error('Invalid op'); 391 | const queryId = slice.loadUint(64); 392 | const jettonAmount = slice.loadCoins(); 393 | const toAddress = slice.loadAddress(); 394 | const responseAddress = slice.loadAddress(); 395 | const customPayload = slice.loadMaybeRef(); 396 | const forwardTonAmount = slice.loadCoins(); 397 | const inRef = slice.loadBit(); 398 | const forwardPayload = inRef ? slice.loadRef().beginParse() : slice; 399 | return { 400 | queryId, 401 | jettonAmount, 402 | toAddress, 403 | responseAddress, 404 | customPayload, 405 | forwardTonAmount, 406 | forwardPayload 407 | } 408 | } 409 | 410 | async sendForceTransfer(provider: ContractProvider, 411 | via: Sender, 412 | transfer_amount: bigint, 413 | to: Address, 414 | from: Address, 415 | custom_payload: Cell | null, 416 | forward_amount: bigint = 0n, 417 | forward_payload: Cell | null, 418 | value: bigint = toNano('0.1'), 419 | query_id: bigint = 0n) { 420 | await provider.internal(via, { 421 | sendMode: SendMode.PAY_GAS_SEPARATELY, 422 | body: JettonMinter.forceTransferMessage(transfer_amount, 423 | to, from, 424 | custom_payload, 425 | forward_amount, 426 | forward_payload, 427 | value, query_id), 428 | value: value + toNano('0.1') 429 | }); 430 | } 431 | 432 | static forceBurnMessage(burn_amount: bigint, 433 | to: Address, 434 | response: Address | null, 435 | value: bigint = toNano('0.1'), 436 | query_id: bigint | number = 0) { 437 | 438 | return beginCell().storeUint(Op.call_to, 32).storeUint(query_id, 64) 439 | .storeAddress(to) 440 | .storeCoins(value) 441 | .storeRef(JettonWallet.burnMessage(burn_amount, response, null)) 442 | .endCell() 443 | } 444 | 445 | static parseBurn(slice: Slice) { 446 | const op = slice.loadUint(32); 447 | if (op !== Op.burn) throw new Error('Invalid op'); 448 | const queryId = slice.loadUint(64); 449 | const jettonAmount = slice.loadCoins(); 450 | const responseAddress = slice.loadAddress(); 451 | const customPayload = slice.loadMaybeRef(); 452 | endParse(slice); 453 | return { 454 | queryId, 455 | jettonAmount, 456 | responseAddress, 457 | customPayload, 458 | } 459 | } 460 | async sendForceBurn(provider: ContractProvider, 461 | via: Sender, 462 | burn_amount: bigint, 463 | address: Address, 464 | response: Address | null, 465 | value: bigint = toNano('0.1'), 466 | query_id: bigint | number = 0) { 467 | 468 | await provider.internal(via, { 469 | sendMode: SendMode.PAY_GAS_SEPARATELY, 470 | body: JettonMinter.forceBurnMessage(burn_amount, address, response, value, query_id), 471 | value: value + toNano('0.1') 472 | }); 473 | } 474 | 475 | static upgradeMessage(new_code: Cell, new_data: Cell, query_id: bigint | number = 0) { 476 | return beginCell().storeUint(Op.upgrade, 32).storeUint(query_id, 64) 477 | .storeRef(new_data) 478 | .storeRef(new_code) 479 | .endCell(); 480 | } 481 | 482 | static parseUpgrade(slice: Slice) { 483 | const op = slice.loadUint(32); 484 | if (op !== Op.upgrade) throw new Error('Invalid op'); 485 | const queryId = slice.loadUint(64); 486 | const newData = slice.loadRef(); 487 | const newCode = slice.loadRef(); 488 | endParse(slice); 489 | return { 490 | queryId, 491 | newData, 492 | newCode 493 | } 494 | } 495 | 496 | async sendUpgrade(provider: ContractProvider, via: Sender, new_code: Cell, new_data: Cell, value: bigint = toNano('0.1'), query_id: bigint | number = 0) { 497 | await provider.internal(via, { 498 | sendMode: SendMode.PAY_GAS_SEPARATELY, 499 | body: JettonMinter.upgradeMessage(new_code, new_data, query_id), 500 | value 501 | }); 502 | } 503 | 504 | async getWalletAddress(provider: ContractProvider, owner: Address): Promise
{ 505 | const res = await provider.get('get_wallet_address', [{ 506 | type: 'slice', 507 | cell: beginCell().storeAddress(owner).endCell() 508 | }]) 509 | return res.stack.readAddress() 510 | } 511 | 512 | async getJettonData(provider: ContractProvider) { 513 | let res = await provider.get('get_jetton_data', []); 514 | let totalSupply = res.stack.readBigNumber(); 515 | let mintable = res.stack.readBoolean(); 516 | let adminAddress = res.stack.readAddress(); 517 | let content = res.stack.readCell(); 518 | let walletCode = res.stack.readCell(); 519 | return { 520 | totalSupply, 521 | mintable, 522 | adminAddress, 523 | content, 524 | walletCode, 525 | }; 526 | } 527 | 528 | async getTotalSupply(provider: ContractProvider) { 529 | let res = await this.getJettonData(provider); 530 | return res.totalSupply; 531 | } 532 | 533 | async getAdminAddress(provider: ContractProvider) { 534 | let res = await this.getJettonData(provider); 535 | return res.adminAddress; 536 | } 537 | 538 | async getContent(provider: ContractProvider) { 539 | let res = await this.getJettonData(provider); 540 | return res.content; 541 | } 542 | 543 | async getNextAdminAddress(provider: ContractProvider) { 544 | const res = await provider.get('get_next_admin_address', []); 545 | return res.stack.readAddressOpt(); 546 | } 547 | } 548 | -------------------------------------------------------------------------------- /wrappers/JettonWallet.compile.ts: -------------------------------------------------------------------------------- 1 | import { CompilerConfig } from '@ton/blueprint'; 2 | 3 | export const compile: CompilerConfig = { 4 | targets: ['contracts/jetton-wallet.fc'], 5 | }; 6 | -------------------------------------------------------------------------------- /wrappers/JettonWallet.ts: -------------------------------------------------------------------------------- 1 | import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode, toNano } from '@ton/core'; 2 | import { Op } from './JettonConstants'; 3 | import {endParse} from "./JettonMinter"; 4 | 5 | export type JettonWalletConfig = { 6 | ownerAddress: Address, 7 | jettonMasterAddress: Address 8 | }; 9 | 10 | export function jettonWalletConfigToCell(config: JettonWalletConfig): Cell { 11 | return beginCell() 12 | .storeUint(0, 4) // status 13 | .storeCoins(0) // jetton balance 14 | .storeAddress(config.ownerAddress) 15 | .storeAddress(config.jettonMasterAddress) 16 | .endCell(); 17 | } 18 | 19 | export function parseJettonWalletData(data: Cell) { 20 | const sc = data.beginParse() 21 | const parsed = { 22 | status: sc.loadUint(4), 23 | balance: sc.loadCoins(), 24 | ownerAddress: sc.loadAddress(), 25 | jettonMasterAddress: sc.loadAddress(), 26 | }; 27 | endParse(sc); 28 | return parsed; 29 | } 30 | 31 | export class JettonWallet implements Contract { 32 | constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {} 33 | 34 | static createFromAddress(address: Address) { 35 | return new JettonWallet(address); 36 | } 37 | 38 | static createFromConfig(config: JettonWalletConfig, code: Cell, workchain = 0) { 39 | const data = jettonWalletConfigToCell(config); 40 | const init = { code, data }; 41 | return new JettonWallet(contractAddress(workchain, init), init); 42 | } 43 | 44 | async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { 45 | await provider.internal(via, { 46 | value, 47 | sendMode: SendMode.PAY_GAS_SEPARATELY, 48 | body: beginCell().endCell(), 49 | }); 50 | } 51 | 52 | async getWalletData(provider: ContractProvider) { 53 | let { stack } = await provider.get('get_wallet_data', []); 54 | return { 55 | balance: stack.readBigNumber(), 56 | owner: stack.readAddress(), 57 | minter: stack.readAddress(), 58 | wallet_code: stack.readCell() 59 | } 60 | } 61 | async getJettonBalance(provider: ContractProvider) { 62 | let state = await provider.getState(); 63 | if (state.state.type !== 'active') { 64 | return 0n; 65 | } 66 | let res = await provider.get('get_wallet_data', []); 67 | return res.stack.readBigNumber(); 68 | } 69 | async getWalletStatus(provider: ContractProvider) { 70 | let state = await provider.getState(); 71 | if (state.state.type !== 'active') { 72 | return 0; 73 | } 74 | let res = await provider.get('get_status', []); 75 | return res.stack.readNumber(); 76 | } 77 | static transferMessage(jetton_amount: bigint, to: Address, 78 | responseAddress:Address | null, 79 | customPayload: Cell | null, 80 | forward_ton_amount: bigint, 81 | forwardPayload: Cell | null) { 82 | 83 | return beginCell().storeUint(Op.transfer, 32).storeUint(0, 64) // op, queryId 84 | .storeCoins(jetton_amount) 85 | .storeAddress(to) 86 | .storeAddress(responseAddress) 87 | .storeMaybeRef(customPayload) 88 | .storeCoins(forward_ton_amount) 89 | .storeMaybeRef(forwardPayload) 90 | .endCell(); 91 | } 92 | async sendTransfer(provider: ContractProvider, via: Sender, 93 | value: bigint, 94 | jetton_amount: bigint, to: Address, 95 | responseAddress:Address, 96 | customPayload: Cell | null, 97 | forward_ton_amount: bigint, 98 | forwardPayload: Cell | null) { 99 | await provider.internal(via, { 100 | sendMode: SendMode.PAY_GAS_SEPARATELY, 101 | body: JettonWallet.transferMessage(jetton_amount, to, responseAddress, customPayload, forward_ton_amount, forwardPayload), 102 | value:value 103 | }); 104 | 105 | } 106 | /* 107 | burn#595f07bc query_id:uint64 amount:(VarUInteger 16) 108 | response_destination:MsgAddress custom_payload:(Maybe ^Cell) 109 | = InternalMsgBody; 110 | */ 111 | static burnMessage(jetton_amount: bigint, 112 | responseAddress:Address | null, 113 | customPayload: Cell | null) { 114 | return beginCell().storeUint(Op.burn, 32).storeUint(0, 64) // op, queryId 115 | .storeCoins(jetton_amount).storeAddress(responseAddress) 116 | .storeMaybeRef(customPayload) 117 | .endCell(); 118 | } 119 | 120 | async sendBurn(provider: ContractProvider, via: Sender, value: bigint, 121 | jetton_amount: bigint, 122 | responseAddress:Address | null, 123 | customPayload: Cell | null) { 124 | await provider.internal(via, { 125 | sendMode: SendMode.PAY_GAS_SEPARATELY, 126 | body: JettonWallet.burnMessage(jetton_amount, responseAddress, customPayload), 127 | value:value 128 | }); 129 | 130 | } 131 | /* 132 | withdraw_tons#107c49ef query_id:uint64 = InternalMsgBody; 133 | */ 134 | static withdrawTonsMessage() { 135 | return beginCell().storeUint(0x6d8e5e3c, 32).storeUint(0, 64) // op, queryId 136 | .endCell(); 137 | } 138 | 139 | async sendWithdrawTons(provider: ContractProvider, via: Sender) { 140 | await provider.internal(via, { 141 | sendMode: SendMode.PAY_GAS_SEPARATELY, 142 | body: JettonWallet.withdrawTonsMessage(), 143 | value:toNano('0.1') 144 | }); 145 | 146 | } 147 | /* 148 | withdraw_jettons#10 query_id:uint64 wallet:MsgAddressInt amount:Coins = InternalMsgBody; 149 | */ 150 | static withdrawJettonsMessage(from:Address, amount:bigint) { 151 | return beginCell().storeUint(0x768a50b2, 32).storeUint(0, 64) // op, queryId 152 | .storeAddress(from) 153 | .storeCoins(amount) 154 | .storeMaybeRef(null) 155 | .endCell(); 156 | } 157 | 158 | async sendWithdrawJettons(provider: ContractProvider, via: Sender, from:Address, amount:bigint) { 159 | await provider.internal(via, { 160 | sendMode: SendMode.PAY_GAS_SEPARATELY, 161 | body: JettonWallet.withdrawJettonsMessage(from, amount), 162 | value:toNano('0.1') 163 | }); 164 | 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /wrappers/Librarian.compile.ts: -------------------------------------------------------------------------------- 1 | import { CompilerConfig } from '@ton/blueprint'; 2 | 3 | export const compile:CompilerConfig = { 4 | targets: ['contracts/helpers/librarian.func'] 5 | }; 6 | -------------------------------------------------------------------------------- /wrappers/Librarian.ts: -------------------------------------------------------------------------------- 1 | import { Address, toNano, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode, Message, storeMessage } from '@ton/core'; 2 | 3 | 4 | export type LibrarianConfig = { 5 | code: Cell; 6 | }; 7 | 8 | export function librarianConfigToCell(config: LibrarianConfig): Cell { 9 | return config.code; 10 | } 11 | export class Librarian implements Contract { 12 | constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {} 13 | 14 | async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { 15 | await provider.internal(via, { 16 | value, 17 | sendMode: SendMode.PAY_GAS_SEPARATELY 18 | }); 19 | } 20 | 21 | static createFromAddress(address: Address) { 22 | return new Librarian(address); 23 | } 24 | 25 | static createFromConfig(config: LibrarianConfig, code: Cell, workchain = -1) { 26 | const data = librarianConfigToCell(config); 27 | const init = { code, data }; 28 | return new Librarian(contractAddress(workchain, init), init); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /wrappers/ui-utils.ts: -------------------------------------------------------------------------------- 1 | import {NetworkProvider, sleep, UIProvider} from '@ton/blueprint'; 2 | import {Address, beginCell, Builder, Cell, Dictionary, DictionaryValue, Slice} from "@ton/core"; 3 | import {sha256} from 'ton-crypto'; 4 | import {TonClient4} from "@ton/ton"; 5 | import {base64Decode} from "@ton/sandbox/dist/utils/base64"; 6 | import {LOCK_TYPES, LockType} from "./JettonMinter"; 7 | import {toUnits} from "../scripts/units"; 8 | 9 | export const defaultJettonKeys = ["uri", "name", "description", "image", "image_data", "symbol", "decimals", "amount_style"]; 10 | export const defaultNftKeys = ["uri", "name", "description", "image", "image_data"]; 11 | 12 | export const promptBool = async (prompt: string, options: [string, string], ui: UIProvider, choice: boolean = false) => { 13 | let yes = false; 14 | let no = false; 15 | let opts = options.map(o => o.toLowerCase()); 16 | 17 | do { 18 | let res = (choice ? await ui.choose(prompt, options, (c: string) => c) : await ui.input(`${prompt}(${options[0]}/${options[1]})`)).toLowerCase(); 19 | yes = res == opts[0] 20 | if (!yes) 21 | no = res == opts[1]; 22 | } while (!(yes || no)); 23 | 24 | return yes; 25 | } 26 | 27 | export const promptAddress = async (prompt: string, provider: UIProvider, fallback?: Address) => { 28 | let promptFinal = fallback ? prompt.replace(/:$/, '') + `(default:${fallback}):` : prompt; 29 | do { 30 | let testAddr = (await provider.input(promptFinal)).replace(/^\s+|\s+$/g, ''); 31 | try { 32 | return testAddr == "" && fallback ? fallback : Address.parse(testAddr); 33 | } catch (e) { 34 | provider.write(testAddr + " is not valid!\n"); 35 | prompt = "Please try again:"; 36 | } 37 | } while (true); 38 | 39 | }; 40 | 41 | export const promptToncoin = async (prompt: string, provider: UIProvider) => { 42 | return promptAmount(prompt, 9, provider); 43 | } 44 | 45 | export const promptAmount = async (prompt: string, decimals: number, provider: UIProvider) => { 46 | let resAmount: bigint; 47 | do { 48 | const inputAmount = await provider.input(prompt); 49 | try { 50 | resAmount = toUnits(inputAmount, decimals); 51 | 52 | if (resAmount <= 0) { 53 | throw new Error("Please enter positive number"); 54 | } 55 | 56 | return resAmount; 57 | } catch (e: any) { 58 | provider.write(e.message); 59 | } 60 | } while (true); 61 | } 62 | 63 | export const getLastBlock = async (provider: NetworkProvider) => { 64 | return (await (provider.api() as TonClient4).getLastBlock()).last.seqno; 65 | } 66 | export const getAccountLastTx = async (provider: NetworkProvider, address: Address) => { 67 | const res = await (provider.api() as TonClient4).getAccountLite(await getLastBlock(provider), address); 68 | if (res.account.last == null) 69 | throw (Error("Contract is not active")); 70 | return res.account.last.lt; 71 | } 72 | export const waitForTransaction = async (provider: NetworkProvider, address: Address, curTx: string | null, maxRetry: number, interval: number = 1000) => { 73 | let done = false; 74 | let count = 0; 75 | const ui = provider.ui(); 76 | 77 | do { 78 | const lastBlock = await getLastBlock(provider); 79 | ui.write(`Awaiting transaction completion (${++count}/${maxRetry})`); 80 | await sleep(interval); 81 | const curState = await (provider.api() as TonClient4).getAccountLite(lastBlock, address); 82 | if (curState.account.last !== null) { 83 | done = curState.account.last.lt !== curTx; 84 | } 85 | } while (!done && count < maxRetry); 86 | return done; 87 | } 88 | 89 | const keysToHashMap = async (keys: string[]) => { 90 | let keyMap: { [key: string]: bigint } = {}; 91 | for (let i = 0; i < keys.length; i++) { 92 | keyMap[keys[i]] = BigInt("0x" + (await sha256(keys[i])).toString('hex')); 93 | } 94 | } 95 | 96 | const contentValue: DictionaryValue = { 97 | serialize: (src: string, builder: Builder) => { 98 | builder.storeRef(beginCell().storeUint(0, 8).storeStringTail(src).endCell()); 99 | }, 100 | parse: (src: Slice) => { 101 | const sc = src.loadRef().beginParse(); 102 | const prefix = sc.loadUint(8); 103 | if (prefix == 0) { 104 | return sc.loadStringTail(); 105 | } else if (prefix == 1) { 106 | // Not really tested, but feels like it should work 107 | const chunkDict = Dictionary.loadDirect(Dictionary.Keys.Uint(32), Dictionary.Values.Cell(), sc); 108 | return chunkDict.values().map(x => x.beginParse().loadStringTail()).join(''); 109 | } else { 110 | throw (Error(`Prefix ${prefix} is not supported yet`)); 111 | } 112 | } 113 | }; 114 | 115 | export const parseContentCell = async (content: Cell) => { 116 | const cs = content.beginParse(); 117 | const contentType = cs.loadUint(8); 118 | if (contentType == 1) { 119 | const noData = cs.remainingBits == 0; 120 | if (noData && cs.remainingRefs == 0) { 121 | throw new Error("No data in content cell!"); 122 | } else { 123 | const contentUrl = noData ? cs.loadStringRefTail() : cs.loadStringTail(); 124 | return contentUrl; 125 | } 126 | } else if (contentType == 0) { 127 | let contentKeys: string[]; 128 | const contentDict = Dictionary.load(Dictionary.Keys.BigUint(256), contentValue, cs); 129 | const contentMap: { [key: string]: string } = {}; 130 | 131 | for (const name of defaultJettonKeys) { 132 | // I know we should pre-compute hashed keys for known values... just not today. 133 | const dictKey = BigInt("0x" + (await sha256(name)).toString('hex')) 134 | const dictValue = contentDict.get(dictKey); 135 | if (dictValue !== undefined) { 136 | contentMap[name] = dictValue; 137 | } 138 | } 139 | return contentMap; 140 | } else { 141 | throw new Error(`Unknown content format indicator:${contentType}\n`); 142 | } 143 | } 144 | 145 | export const displayContentCell = async (contentCell: Cell, ui: UIProvider, jetton: boolean = true, additional?: string[]) => { 146 | const content = await parseContentCell(contentCell); 147 | 148 | if (content instanceof String) { 149 | ui.write(`Content metadata url:${content}\n`); 150 | } else { 151 | ui.write(`Content:${JSON.stringify(content, null, 2)}`); 152 | } 153 | } 154 | 155 | export const promptUrl = async (prompt: string, ui: UIProvider) => { 156 | let retry = false; 157 | let input = ""; 158 | let res = ""; 159 | 160 | do { 161 | input = await ui.input(prompt); 162 | try { 163 | let testUrl = new URL(input); 164 | res = testUrl.toString(); 165 | retry = false; 166 | } catch (e) { 167 | ui.write(input + " doesn't look like a valid url:\n" + e); 168 | retry = !(await promptBool('Use anyway?(y/n)', ['y', 'n'], ui)); 169 | } 170 | } while (retry); 171 | return input; 172 | } 173 | 174 | export const explorerUrl = (address: string, isTestnet: boolean) => { 175 | return (isTestnet ? 'https://testnet.tonscan.org/address/' : 'https://tonscan.org/address/') + address; 176 | } 177 | 178 | export const promptUserFriendlyAddress = async (prompt: string, provider: UIProvider, isTestnet: boolean) => { 179 | do { 180 | const s = await provider.input(prompt); 181 | if (Address.isFriendly(s)) { 182 | const address = Address.parseFriendly(s); 183 | if (address.isTestOnly && !isTestnet) { 184 | provider.write("Please enter mainnet address"); 185 | prompt = "Please try again:"; 186 | } else { 187 | return address; 188 | } 189 | } else { 190 | provider.write(s + " is not valid!\n"); 191 | prompt = "Please try again:"; 192 | } 193 | } while (true); 194 | } 195 | 196 | export const lockTypeToName = (lockType: LockType): string => { 197 | switch (lockType) { 198 | case 'unlock': 199 | return "Unlocked"; 200 | case 'out': 201 | return "Can't send"; 202 | case 'in': 203 | return "Can't receive"; 204 | case 'full': 205 | return "Can't send and receive"; 206 | default: 207 | throw new Error("Invalid argument!"); 208 | } 209 | } 210 | 211 | export const promptLockType = async (prompt: string, provider: UIProvider): Promise => { 212 | do { 213 | const s = await provider.input(prompt); 214 | if (LOCK_TYPES.indexOf(s) === -1) { 215 | provider.write(s + " is not valid!\n"); 216 | } else { 217 | return s as LockType; 218 | } 219 | } while (true); 220 | } 221 | 222 | export const addressToString = (address: { 223 | isBounceable: boolean, 224 | isTestOnly: boolean, 225 | address: Address 226 | }) => { 227 | return address.address.toString({ 228 | bounceable: address.isBounceable, 229 | testOnly: address.isTestOnly 230 | }) 231 | } 232 | 233 | export const base64toCell = (base64: string) => { 234 | const bytes = base64Decode(base64); 235 | const buffer = Buffer.from(bytes); 236 | return Cell.fromBoc(buffer)[0]; 237 | } 238 | 239 | export const equalsMsgAddresses = (a: Address | null, b: Address | null) => { 240 | if (!a) return !b; 241 | if (!b) return !a; 242 | return a.equals(b); 243 | } 244 | 245 | export const sendToIndex = async (method: string, params: any, provider: NetworkProvider) => { 246 | const isTestnet = provider.network() !== 'mainnet'; 247 | const mainnetRpc = 'https://toncenter.com/api/v3/'; 248 | const testnetRpc = 'https://testnet.toncenter.com/api/v3/'; 249 | const rpc = isTestnet ? testnetRpc : mainnetRpc; 250 | 251 | const apiKey = (provider.api() as any).api.parameters.apiKey!; // todo: provider.api().parameters.apiKey is undefined 252 | 253 | const headers = { 254 | 'Content-Type': 'application/json', 255 | 'X-API-Key': apiKey 256 | }; 257 | 258 | const response = await fetch(rpc + method + '?' + new URLSearchParams(params), { 259 | method: 'GET', 260 | headers: headers, 261 | }); 262 | return response.json(); 263 | } 264 | 265 | export const getAddressFormat = async (address: Address, provider: NetworkProvider, isTestnet: boolean) => { 266 | const result = await sendToIndex('wallet', {address: address}, provider); 267 | 268 | const nonBounceable = (result.status === "uninit") || (result.wallet_type && result.wallet_type.startsWith('wallet')); 269 | 270 | return { 271 | isBounceable: !nonBounceable, 272 | isTestOnly: isTestnet, 273 | address 274 | } 275 | } 276 | 277 | export const formatAddressAndUrl = async (address: Address, provider: NetworkProvider, isTestnet: boolean) => { 278 | const f = await getAddressFormat(address, provider, isTestnet); 279 | const addressString = addressToString(f); 280 | return addressString + ' ' + explorerUrl(addressString, isTestnet); 281 | } 282 | 283 | export const jettonWalletCodeFromLibrary = (jettonWalletCodeRaw: Cell) => { 284 | // https://docs.ton.org/tvm.pdf, page 30 285 | // Library reference cell — Always has level 0, and contains 8+256 data bits, including its 8-bit type integer 2 286 | // and the representation hash Hash(c) of the library cell being referred to. When loaded, a library 287 | // reference cell may be transparently replaced by the cell it refers to, if found in the current library context. 288 | 289 | const libraryReferenceCell = beginCell().storeUint(2, 8).storeBuffer(jettonWalletCodeRaw.hash()).endCell(); 290 | 291 | return new Cell({exotic: true, bits: libraryReferenceCell.bits, refs: libraryReferenceCell.refs}); 292 | } 293 | 294 | export const assert = (condition: boolean, error: string, ui: UIProvider) => { 295 | if (!condition) { 296 | ui.write(error); 297 | throw new Error(); 298 | } 299 | } --------------------------------------------------------------------------------