Locazia: release

This commit is contained in:
user 2024-03-01 22:35:26 +03:00
commit 540ab55120
18 changed files with 6801 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
.saved_wallet
build
.idea

26
README.md Normal file
View File

@ -0,0 +1,26 @@
# WalletV3CR3
## Project structure
- `contracts` - source code of all the smart contracts of the project and their dependencies.
- `wrappers` - wrapper classes (implementing `Contract` from ton-core) for the contracts, including any [de]serialization primitives and compilation functions.
- `tests` - tests for the contracts.
- `scripts` - scripts used by the project, mainly the deployment scripts.
## How to use
### Build
`npx blueprint build` or `yarn blueprint build`
### Test
`npx blueprint test` or `yarn blueprint test`
### Deploy or run another script
`npx blueprint run` or `yarn blueprint run`
### Add a new contract
`npx blueprint create ContractName` or `yarn blueprint create ContractName`

230
contracts/imports/stdlib.fc Normal file
View File

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

117
contracts/wallet.fc Normal file
View File

@ -0,0 +1,117 @@
;; Wallet V3 Custom R3
;; (c) 2024 oscux
;; TL-B scheme:
;; storage#_ seqno:uint32 public_key:uint256 subwallet_id:uint32 trusted_hashpart:uint256 = Storage;
;;
;; new_internal#0000aac1 send_mode:uint8 message:^InternalMessage = SpendRequest;
;; upgrade#0000aaa0 new_code:^Cell new_data:^Cell = UpgradeRequest;
#include "imports/stdlib.fc";
const int op::new_internal = 0xAAC1;
const int op::upgrade = 0xAAA0;
const int error::not_allowed = 105;
const int error::expired = 108;
() execute_command(slice request, int sudo?) impure {
int op = request~load_uint(32);
if (op == op::new_internal) {
int mode = request~load_uint(8);
cell message = request~load_ref();
send_raw_message(message, mode);
return ();
}
if (op == op::upgrade) {
throw_unless(error::not_allowed, sudo?);
cell new_code = request~load_ref();
cell new_data = request~load_ref();
set_code(new_code);
set_c3(new_code.begin_parse().bless());
set_data(new_data);
;; but next commands executes with old code
return ();
}
if (op == 0) {
return ();
}
throw(0xffff);
}
() execute_commands(slice requests, int sudo?) impure {
while (requests.slice_refs() > 0) {
execute_command(requests~load_ref().begin_parse(), sudo?);
}
}
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
slice cs = in_msg_full.begin_parse();
cs~skip_bits(4);
slice sender_address = cs~load_msg_addr();
(_, int sender_hashpart) = parse_std_addr(sender_address);
slice ds = get_data().begin_parse();
ds~skip_bits(32 + 256 + 32);
int trusted_hashpart = ds~load_uint(256);
if (sender_hashpart == trusted_hashpart) {
throw_if(error::not_allowed, trusted_hashpart == 0);
execute_commands(in_msg_body~load_ref().begin_parse(), true);
}
return ();
}
() recv_external(slice in_msg) impure {
slice signature = in_msg~load_bits(512);
slice cs = in_msg;
(int subwallet_id, int valid_until, int msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32));
throw_if(error::expired, valid_until <= now());
slice ds = get_data().begin_parse();
(int stored_seqno, int public_key, int stored_subwallet, int trusted_hashpart) = (ds~load_uint(32), ds~load_uint(256), ds~load_uint(32), ds~load_uint(256));
throw_unless(error::not_allowed, subwallet_id == stored_subwallet);
throw_unless(error::not_allowed, msg_seqno == stored_seqno);
throw_unless(error::not_allowed, check_signature(slice_hash(in_msg), signature, public_key));
accept_message();
cs~touch();
int exit_code = 0;
try {
execute_commands(in_msg~load_ref().begin_parse(), trusted_hashpart == 0 ? true : false);
} catch (x, n) {
exit_code = n;
}
set_data(
begin_cell()
.store_uint(stored_seqno + 1, 32)
.store_uint(public_key, 256)
.store_uint(stored_subwallet, 32)
.store_uint(trusted_hashpart, 256)
.store_uint(exit_code, 16)
.end_cell()
);
return ();
}
;; Get methods
int seqno() method_id {
slice ds = get_data().begin_parse();
return ds~load_uint(32);
}
int get_public_key() method_id {
slice ds = get_data().begin_parse();
ds~skip_bits(32);
return ds~load_uint(256);
}
int get_trusted_hashpart() method_id {
slice ds = get_data().begin_parse();
ds~skip_bits(32 + 256 + 32);
return ds~load_uint(256);
}
int prev_exit_code() method_id {
slice ds = get_data().begin_parse();
ds~skip_bits(32 + 256 + 32 + 256);
return ds~load_uint(16);
}

9
jest.config.ts Normal file
View File

@ -0,0 +1,9 @@
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
};
export default config;

5758
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "WalletV3CR3",
"version": "0.0.1",
"scripts": {
"start": "blueprint run",
"build": "blueprint build",
"test": "jest --verbose"
},
"devDependencies": {
"@ton/blueprint": "^0.16.0",
"@ton/sandbox": "^0.15.0",
"@ton/test-utils": "^0.4.2",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.20",
"jest": "^29.7.0",
"prettier": "^3.2.5",
"@ton/ton": "^13.11.0",
"@ton/core": "~0",
"@ton/crypto": "^3.2.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}

0
scripts/__init__.py Normal file
View File

101
scripts/_auth.py Normal file
View File

@ -0,0 +1,101 @@
import json
import os
from getpass import getpass
from tonsdk.contract.wallet import Wallets
from tonsdk.crypto import mnemonic_to_wallet_key
from tonsdk.utils import Address
from _core import unpack_wallet, encrypt_data, decrypt_data
def mnemonic_new(subwallet_id: int=0):
while True:
mnemonic, public_key, private_key, _ = Wallets.create('v3r2', 0)
wallet = unpack_wallet({
'mnemonic': mnemonic,
'public_key': public_key.hex(),
'secret_key': private_key.hex(),
'subwallet_id': subwallet_id,
'custom_address': None,
'is_testnet': False,
})
ugly = False
for wallet_address in [
wallet.address.to_string(1, 1, 0),
wallet.address.to_string(1, 1, 1)
]:
if ('_' in wallet_address or '-' in wallet_address):
ugly = True
if not ugly:
return mnemonic
def get_or_create_wallet_settings():
wallet_settings_file = os.environ.get('ENCRYPTED_WALLET_FILE', '.saved_wallet')
# Check if the .saved_wallet file exists
if os.path.exists(wallet_settings_file):
# If the file exists, read and decrypt its contents
with open(wallet_settings_file, 'rb') as file:
_content = file.read()
hashpart = _content[:32].hex()
_content = _content[32:]
print(f"=== Saved wallet: {Address('0:' + hashpart).to_string(1, 1, 0)}")
password = getpass("Enter the password to decrypt the wallet: ")
try:
wallet_settings = decrypt_data(_content, password.encode("UTF-8"))
wallet_settings = json.loads(wallet_settings.decode())
except (ValueError, KeyError):
print("Incorrect password or corrupted data. Please try again.")
return get_or_create_wallet_settings()
print(f"=== Mainnet: {not wallet_settings.get('is_testnet', True)}")
return wallet_settings
else:
# If the file doesn't exist, prompt the user for input
try:
subwallet_id = int(input("Enter the subwallet_id (default is 0): ") or 0)
assert subwallet_id >= 0, "Subwallet ID must be a positive integer"
mnemonic = input("Enter the wallet mnemonic (24 words, separated by spaces): ")
if mnemonic == 'new':
mnemonic = ' '.join(mnemonic_new(subwallet_id=subwallet_id))
assert len(mnemonic.split()) == 24, "Mnemonic must be 24 words long"
public_key, secret_key = mnemonic_to_wallet_key(mnemonic.split())
custom_address = input("Enter a custom address (if available): ")
custom_address = Address(custom_address) if custom_address else None
password = getpass("Enter a password to protect the wallet: ")
assert len(password) > 3, "Password must be at least 4 characters long"
is_testnet = input("Is this a testnet wallet? (yes/NO): ").lower() == 'yes'
except KeyboardInterrupt:
os._exit(1)
except BaseException as e:
print(f"Error: {e}")
print("Please try again")
return get_or_create_wallet_settings()
# Create a dictionary with the user input
wallet_settings = {
'mnemonic': mnemonic,
'public_key': public_key.hex(),
'secret_key': secret_key.hex(),
'subwallet_id': subwallet_id,
'custom_address': custom_address.to_string(1, 1, 1) if custom_address else None,
'is_testnet': is_testnet,
}
print(f"=== Mainnet: {not is_testnet}")
wallet_address = unpack_wallet(wallet_settings).address.to_string(0, 0, 0)
# Encrypt and save the data to the file
encrypted_wallet = encrypt_data(json.dumps(wallet_settings).encode("UTF-8"), password.encode("UTF-8"))
with open(wallet_settings_file, 'wb') as file:
file.write(bytes.fromhex(wallet_address.split(':')[1]) + encrypted_wallet)
# Recursively call the function to read data from the file
return get_or_create_wallet_settings()

181
scripts/_core.py Normal file
View File

@ -0,0 +1,181 @@
import os
import pickle
from decimal import Decimal
from time import sleep
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
from tonsdk.boc import begin_cell
from tonsdk.contract import Contract
from tonsdk.contract.token.ft import JettonWallet
from tonsdk.contract.token.nft import NFTItem
from tonsdk.utils import Address
from toncenter import TonCenter
from wallet_contract import WalletV3CR3
ACTION_TYPES_DESC = {
1: "Send TON",
2: "Send Jetton",
3: "Send NFT",
4: "Update wallet contract",
7: "New decentralized note",
}
def unpack_wallet(wallet_settings):
kwargs = {}
if wallet_settings['custom_address']:
kwargs['address'] = Address(wallet_settings['custom_address'])
wallet = WalletV3CR3(
public_key=bytes.fromhex(wallet_settings['public_key']),
private_key=bytes.fromhex(wallet_settings['secret_key']),
subwallet_id=wallet_settings['subwallet_id'],
trusted_hashpart=0,
**kwargs
)
return wallet
def generate_key(password, salt, iterations=100000):
key = PBKDF2(password, salt, dkLen=32, count=iterations)
return key
def encrypt_data(data: bytes, password: bytes) -> bytes:
salt = os.urandom(16)
key = generate_key(password, salt)
cipher = AES.new(key, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(data)
return pickle.dumps([salt, ciphertext, tag, cipher.nonce])
def decrypt_data(data: bytes, password: bytes) -> bytes:
salt, ciphertext, tag, nonce = pickle.loads(data)
key = generate_key(password, salt)
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
return plaintext
async def perform_action(wallet_settings, _commands):
wallet = unpack_wallet(wallet_settings)
toncenter = TonCenter(testnet=wallet_settings.get("is_testnet", True))
async def get_wallet_seqno():
result = await toncenter.run_get_method(wallet.address.to_string(1, 1, 1), 'seqno')
if result.get('exit_code', -1) != 0:
seqno = 0
else:
seqno = int(result['stack'][0][1], 16)
sleep(1)
return seqno
seqno = -100
_new_deploy = False
while seqno < 0:
seqno = await get_wallet_seqno()
if seqno < 1:
_new_deploy = True
result = await toncenter.send_boc(
wallet.create_external_message(
begin_cell()
.store_cell(wallet.create_signing_message(0, 60))
.store_ref(begin_cell().end_cell())
.end_cell(), 0
)['message'].to_boc(False)
)
print("Init transaction broadcast result:", result)
sleep(7)
if _new_deploy:
seqno += 1
print(f"=== Current seqno: {seqno} ===")
assert input("Broadcast transaction? (yes/NO): ").lower() == 'yes'
signing_message = begin_cell().store_cell(wallet.create_signing_message(seqno, 60))
signing_message = signing_message.store_ref(_commands)
query = wallet.create_external_message(
signing_message.end_cell(), seqno, False
)
print("Raw signed transaction:", query['message'].to_boc(False).hex())
result = await toncenter.send_boc(query['message'].to_boc(False))
print("Transaction broadcast result:", result)
def serialize_command(action, response_address: Address = None):
if action['type'] == 1:
return (
begin_cell()
.store_uint(0xAAC1, 32)
.store_uint(action['args'].get('send_mode', 1), 8)
.store_ref(
Contract.create_common_msg_info(
Contract.create_internal_message_header(
Address(action['args']['destination']), Decimal(action['args']['amount'])
), action['args'].get('state_init'), action['args'].get('payload_cell')
)
)
.end_cell()
)
elif action['type'] == 2:
return (
begin_cell()
.store_uint(0xAAC1, 32)
.store_uint(action['args'].get('send_mode', 1), 8)
.store_ref(
Contract.create_common_msg_info(
Contract.create_internal_message_header(
Address(action['args']['jetton_wallet']), Decimal(1e8)
), None, JettonWallet().create_transfer_body(
Address(action['args']['recipient']), action['args']['amount'],
forward_amount=1e7, forward_payload=(
(bytes(4) + action['args']['comment']) if action['args'].get('comment') else None
),
response_address=response_address
)
)
)
.end_cell()
)
elif action['type'] == 3:
return (
begin_cell()
.store_uint(0xAAC1, 32)
.store_uint(action['args'].get('send_mode', 1), 8)
.store_ref(
Contract.create_common_msg_info(
Contract.create_internal_message_header(
Address(action['args']['nft_address']), Decimal(1e8)
), None, NFTItem().create_transfer_body(
Address(action['args']['recipient']), response_address=response_address,
forward_amount = 1e7
)
)
)
.end_cell()
)
elif action['type'] == 4:
return (
begin_cell()
.store_uint(0xAAA0, 32)
.store_ref(action['args']['new_code'])
.store_ref(action['args']['new_data'])
.end_cell()
)
raise Exception('Unsupported action type')
def print_actions(actions, prefix='\n' + "=== {actions_count} actions selected"):
if not actions: return
print(prefix.format(actions_count=len(actions)))
for action in actions:
print(f"Action {ACTION_TYPES_DESC[action['type']]}: {action['args']}")
print("===")

127
scripts/manage.py Normal file
View File

@ -0,0 +1,127 @@
from asyncio import run
from tonsdk.boc import Cell, begin_cell
from tonsdk.utils import Address
from _auth import get_or_create_wallet_settings
from _core import unpack_wallet, serialize_command, perform_action, print_actions
async def main():
print('\n' * 30)
wallet_settings = get_or_create_wallet_settings()
wallet = unpack_wallet(wallet_settings)
print(f"=== Successfully loaded wallet {wallet.address.to_string(1, 1, 0)}")
# print(wallet_settings)
actions = []
while len(actions) < 4:
print_actions(actions)
print("Available actions:")
print("1) Send TON")
print("2) Send Jetton")
print("3) Send NFT")
print("4) Update wallet contract")
print("5) Export mnemonic phrase")
print("7) New decentralized note")
print("8) Read decentralized note")
print("0) View actions and exit | send transactions")
choice = input("Select an action (1/2/3/4/0): ")
if choice == "0":
break
elif choice in {"5"}:
print("=== Mnemonic phrase:")
print(wallet_settings['mnemonic'])
elif choice in {"1", "2", "3", "4", "7"}:
action_type = int(choice)
action = {"type": action_type, "args": []}
if action_type == 1:
destination = input("Enter the destination address: ")
Address(destination).to_string(1, 1, 1)
amount = float(input("Enter the amount in TON: "))
comment = input("Enter a comment (or leave it empty): ")
if comment:
raw_payload = (
begin_cell()
.store_uint(0, 32)
.store_bytes(comment.encode())
.end_cell()
)
else:
raw_payload = input("Enter raw_payload in HEX (or leave it empty): ")
try:
raw_payload = Cell.one_from_boc(raw_payload)
except:
raw_payload = None
action["args"] = {
"destination": destination,
"amount": int(amount * 10 ** 9),
"payload_cell": raw_payload
}
elif action_type == 2:
token_contract = input("Enter the token contract address: ")
recipient = input("Enter the recipient address: ")
Address(token_contract).to_string(1, 1, 1)
Address(recipient).to_string(1, 1, 1)
amount = float(input("Enter the amount in tokens: "))
decimals = int(input("Enter token decimals: "))
amount = int(amount * 10 ** decimals)
comment = input("Enter a comment: ")
action["args"] = {
"token_contract": token_contract,
"recipient": recipient,
"amount": amount,
"comment": comment
}
elif action_type == 3:
nft_address = input("Enter the NFT address: ")
recipient = input("Enter the recipient address: ")
Address(nft_address).to_string(1, 1, 1)
Address(recipient).to_string(1, 1, 1)
action["args"] = {
"nft_address": nft_address,
"recipient": recipient
}
elif action_type == 4:
hex_code = input("Enter the smart contract code in HEX: ")
hex_data = input("Enter smart contract data in HEX: ")
action["args"] = {
'new_code': Cell.one_from_boc(hex_code),
'new_data': Cell.one_from_boc(hex_data)
}
elif action_type == 7:
note_key = input("Enter the note key: ")
print(f"""[?] Please, enter plain text & input 0 for write the note:""")
plain_text = ""
while True:
new_input = input()
if new_input == '0':
break
plain_text += new_input + '\n'
action['args'] = {
'note_key': note_key.strip(),
'plain_text': plain_text.strip(),
'private_key': bytes.fromhex(wallet_settings['secret_key'])
}
actions.append(action)
if not actions:
print("No actions selected")
return
_commands = begin_cell()
for action in actions:
_commands = _commands.store_ref(
serialize_command(action, response_address=wallet.address)
)
await perform_action(wallet_settings, _commands.end_cell())
if __name__ == "__main__":
run(main())

3
scripts/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
httpx==0.25.0
tonsdk==1.0.13
pycryptodome==3.19.0

59
scripts/toncenter.py Normal file
View File

@ -0,0 +1,59 @@
import os
from base64 import b64encode
from httpx import AsyncClient
class TonCenter:
def __init__(self, api_key: str = None, testnet: bool = False):
self.host = os.getenv("TONCENTER_HOST", "https://toncenter.com/api/v1/")
if not (self.host[-1] == '/'):
self.host += '/'
self.api_key = api_key
async def request(self, method: str, endpoint: str, *args, **kwargs) -> dict:
async with AsyncClient() as client:
response = await client.request(method, f"{self.host}{endpoint}", *args, **kwargs)
if response.status_code != 200:
raise Exception(f'Error while TONCENTER request {endpoint}: {response.text}')
return response.json()
async def send_boc(self, src: bytes):
return await self.request(
"POST", 'sendBoc',
json={'boc': b64encode(src).decode()}
)
async def get_account(self, addr: str):
return (await self.request(
"GET", 'getAddressInformation',
params={'address': addr}
)).get('result', {})
async def get_seqno(self, addr: str):
return (await self.request(
"GET", 'getWalletInformation',
json={'address': addr}
)).get('result', {}).get('seqno', 0)
async def run_get_method(self, addr, method, stack=[]):
return (await self.request(
"POST", 'runGetMethod', json={
'address': addr,
'method': method,
'stack': stack if type(stack) == list else []
}
)).get('result', {})
async def get_transactions(
self, addr: str, limit: int = 100, lt: str = None, hash: str = None,
offset: int = 0, to_lt: str = None
):
return (await self.request(
"GET", 'getTransactions', params={
'address': addr,
}
)).get('result', {})

View File

@ -0,0 +1,75 @@
from decimal import Decimal
from time import time
from tonsdk.boc import Cell, begin_cell
from tonsdk.contract import Contract
from tonsdk.contract.wallet import WalletContract
from tonsdk.utils import Address
WALLET_V3_CR3_CODE_HEX = 'b5ee9c7241021001000162000114ff00f4a413f4bcf2c80b01020120020f02014803080202ce0407020120050600510ccc741d35c87e900c3e910c7b513420405035c874ffcc19aea6f0003cb41a750c341ffc00a456f8a000730074c7c860802ab06ea65b0874c1f50c007ec0380860802aa82ea384cc407cb81a75350c087ec100743b47bb54fb55380c0c7000372103fcbc20002349521d74ac2009801d401d022f00101e85b8020120090c0201200a0b0019bb39ced44d08020d721d3ff3080011b8c97ed44d0d31f3080201580d0e001bb71e3da89a1020481ae43a61e610001bb49f3da89a1020281ae43a7fe61000f6f28308d71820d31fd31fd31f3001f823bbf2d06ced44d0d31fd3ffd31fd3ff305151baf2e0695132baf2e06924f901541066f910f2e069f8007054715226ed44ed45ed479131ed67ed65ed64747fed118e1104d430d023c000917f9170e2f002045023ed41edf101f2ff04a4c8cb1f13cbffcb1fcbffcb0fc9ed542675fc7e'
class WalletV3CR3(WalletContract):
def __init__(self, **kwargs):
kwargs['code'] = Cell.one_from_boc(WALLET_V3_CR3_CODE_HEX)
kwargs['subwallet_id'] = kwargs.get('subwallet_id', 0)
super().__init__(**kwargs)
def create_data_cell(self):
return (
begin_cell()
.store_uint(0, 32)
.store_bytes(self.options['public_key'])
.store_uint(self.options['subwallet_id'], 32)
.store_uint(0, 256)
.end_cell()
)
def create_signing_message(self, seqno=None, timeout=60) -> Cell:
seqno = seqno or 0
return begin_cell().store_uint(self.options['subwallet_id'], 32).store_uint(int(time() + timeout), 32).store_uint(seqno, 32).end_cell()
def create_transfer_message(self, recipients_list: list, seqno: int, timeout=60, dummy_signature=False) -> dict:
signing_message = begin_cell().store_cell(self.create_signing_message(seqno=seqno, timeout=timeout))
_commands = begin_cell()
for i, recipient in enumerate(recipients_list):
if not recipient: continue
payload_cell = Cell()
if recipient.get('payload'):
if type(recipient['payload']) == str:
if len(recipient['payload']) > 0:
payload_cell.bits.write_uint(0, 32)
payload_cell.bits.write_string(recipient['payload'])
elif hasattr(recipient['payload'], 'refs'):
payload_cell = recipient['payload']
else:
payload_cell.bits.write_bytes(recipient['payload'])
order_header = Contract.create_internal_message_header(
Address(recipient['address']), Decimal(recipient['amount'])
)
order = Contract.create_common_msg_info(
order_header, recipient.get('state_init'), payload_cell
)
_commands = _commands.store_ref(
begin_cell()
.store_uint(0xAAC1, 32)
.store_uint8(recipient.get('send_mode', 0))
.store_ref(order).end_cell()
)
signing_message = signing_message.store_ref(_commands.end_cell())
return self.create_external_message(signing_message.end_cell(), seqno, dummy_signature)
def create_upgrade_message(self, new_code: Cell, new_data: Cell) -> dict:
signing_message = begin_cell().store_cell(self.create_signing_message())
_commands = begin_cell()
_commands = _commands.store_ref(
begin_cell()
.store_uint(0xAAA0, 32)
.store_ref(new_code)
.store_ref(new_data)
.end_cell()
)
signing_message = signing_message.store_ref(_commands.end_cell())
return self.create_external_message(signing_message.end_cell(), 0, True)

39
tests/WalletV3.spec.ts Normal file
View File

@ -0,0 +1,39 @@
import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox';
import { Cell, toNano } from '@ton/core';
import { WalletV3 } from '../wrappers/WalletV3';
import '@ton/test-utils';
import { compile } from '@ton/blueprint';
describe('WalletV3', () => {
let code: Cell;
beforeAll(async () => {
code = await compile('WalletV3');
});
let blockchain: Blockchain;
let deployer: SandboxContract<TreasuryContract>;
let walletV3: SandboxContract<WalletV3>;
beforeEach(async () => {
blockchain = await Blockchain.create();
walletV3 = blockchain.openContract(WalletV3.createFromConfig({}, code));
deployer = await blockchain.treasury('deployer');
const deployResult = await walletV3.sendDeploy(deployer.getSender(), toNano('0.05'));
expect(deployResult.transactions).toHaveTransaction({
from: deployer.address,
to: walletV3.address,
deploy: true,
success: true,
});
});
it('should deploy', async () => {
// the check is done inside beforeEach
// blockchain and walletV3 are ready to use
});
});

12
tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2020",
"outDir": "dist",
"module": "commonjs",
"declaration": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

View File

@ -0,0 +1,6 @@
import { CompilerConfig } from '@ton/blueprint';
export const compile: CompilerConfig = {
lang: 'func',
targets: ['contracts/wallet.fc'],
};

29
wrappers/WalletV3.ts Normal file
View File

@ -0,0 +1,29 @@
import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode } from '@ton/core';
export type WalletV3Config = {};
export function walletV3ConfigToCell(config: WalletV3Config): Cell {
return beginCell().endCell();
}
export class WalletV3 implements Contract {
constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {}
static createFromAddress(address: Address) {
return new WalletV3(address);
}
static createFromConfig(config: WalletV3Config, code: Cell, workchain = 0) {
const data = walletV3ConfigToCell(config);
const init = { code, data };
return new WalletV3(contractAddress(workchain, init), init);
}
async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) {
await provider.internal(via, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell().endCell(),
});
}
}