'
158 |
159 | key_id = input(f'Used key ({possible_keys_s}): ')
160 | job = input_address(f'{b}Job address: {nb}')
161 | stake = int(1e9 * float(input('Staked value (TON): ')))
162 | desc_text = input('Offer description: ')
163 |
164 | job_data = Cell.one_from_boc(b16decode(
165 | load_account(job.to_string())['data'].upper()
166 | ))
167 | assert job_data.begin_parse().load_uint(2) == 0
168 | job_hash = int.from_bytes(job_data.bytes_hash(), 'big') & ((1 << 160) - 1)
169 |
170 | post_offer(job, stake, desc_text, job_hash, keyring, key_id)
171 | elif command == 'or':
172 | revoke_offer(input_address(f'{b}Offer address: {nb}').to_string(True, True, True))
173 | else:
174 | print(f'{b}not implemented:{nb} {repr(command)}')
--------------------------------------------------------------------------------
/bot/cli/polyfills.py:
--------------------------------------------------------------------------------
1 | from tonsdk.boc import Cell
2 | from .tslice import Slice
3 |
4 | Cell.begin_parse = lambda self: Slice(self)
5 |
--------------------------------------------------------------------------------
/bot/cli/signing.py:
--------------------------------------------------------------------------------
1 | from .colors import nh, h, nb, b, ns, s
2 |
3 | from base64 import b16decode, b64encode, urlsafe_b64encode
4 | from getpass import getpass
5 | import time
6 |
7 | from tonsdk.contract.wallet import Wallets
8 | from tonsdk.contract import Contract
9 | from tonsdk.utils import Address
10 | from tonsdk.boc import Cell
11 | import tonsdk.crypto
12 | import nacl.signing
13 | # TODO: move `requests` out of functions/modules where secret keys are accessed
14 | import requests
15 |
16 |
17 |
18 | def retrieve_keypair(auth_way: str):
19 | if auth_way == 'm':
20 | while True:
21 | mnemonic = getpass(f'{b}Your wallet mnemonic (not echoed):{nb} ').split()
22 | if tonsdk.crypto.mnemonic_is_valid(mnemonic): break
23 |
24 | use_anyway = input(f'{b}Entered mnemonic is invalid. Use it anyway?{nb} [y/n] ').lower()
25 | if use_anyway == 'y': break
26 |
27 | _, secret_key = tonsdk.crypto.mnemonic_to_wallet_key(mnemonic)
28 | secret_key = secret_key[:32]
29 | del mnemonic
30 | elif auth_way == 's':
31 | while True:
32 | secret_hex = getpass(f'{b}Your secret key in HEX (not echoed):{nb} ').upper()
33 | if not set('0123456789ABCDEF').issuperset(secret_hex):
34 | print('Invalid characters met')
35 | elif len(secret_hex) != 64:
36 | print('Invalid key length')
37 | else:
38 | break
39 |
40 | secret_key = b16decode(secret_hex)
41 | del secret_hex
42 | else:
43 | raise Exception('unsupported auth way for retrieving keypair')
44 |
45 | secret_key_obj = nacl.signing.SigningKey(secret_key)
46 | public_key = secret_key_obj.verify_key.encode()
47 | secret_key = secret_key_obj.encode() + public_key # making secret key 64-byte
48 | return public_key, secret_key
49 |
50 |
51 | def retrieve_auth_wallet(auth_way: str, plugin_only=False):
52 | public_key, secret_key = retrieve_keypair(auth_way)
53 |
54 | WALLET_V = ['v4r2'] if plugin_only else ['v3r1', 'v3r2', 'v4r2']
55 | WALLET_PROMPT = 'Enter wallet version (' + b + '/'.join(WALLET_V) + nb + '): '
56 | while (wallet_ver := input(WALLET_PROMPT).lower()) not in WALLET_V: pass
57 |
58 | wallet_class = Wallets.ALL[wallet_ver]
59 | return wallet_class(public_key=public_key, private_key=secret_key)
60 |
61 |
62 | # orders: list[tuple[to_addr, state_init, payload, amount]]
63 | def sign_multitransfer_body(wallet: Contract, seqno: int,
64 | orders: list[tuple[Address,Cell,Cell,int]]) -> Cell:
65 | assert len(orders) <= 4
66 |
67 | send_mode = 3
68 | signing_message = wallet.create_signing_message(seqno)
69 |
70 | for (to_addr, state_init, payload, amount) in orders:
71 | order_header = Contract.create_internal_message_header(to_addr, amount)
72 | order = Contract.create_common_msg_info(order_header, state_init, payload)
73 | signing_message.bits.write_uint8(send_mode)
74 | signing_message.refs.append(order)
75 |
76 | return wallet.create_external_message(signing_message, seqno)['message']
77 |
78 |
79 | def sign_for_sending(orders: list[tuple[Address,Cell,Cell,int]],
80 | description: str, auth_way=None, wallet=None) -> Cell:
81 | print(f'{h}Sending messages for purpose of{nh}', repr(description))
82 |
83 | sum_value = 0
84 | for (dest, state_init, message, value_nton) in orders:
85 | init_flag = f'{b}[deploy]{nb}' if state_init else ''
86 |
87 | print('===')
88 | print(f'{h}Destination:{nh}', dest.to_string(True, True, True), init_flag)
89 | print(f'{h}TON amount: {nh}', value_nton / 1e9)
90 | print(f'{h}Message BOC:{nh}', b64encode(message.to_boc(False)).decode('ascii'))
91 | sum_value += value_nton
92 |
93 | print('===')
94 | print(f'{h}Total TON: {nh} {sum_value / 1e9}')
95 | print()
96 |
97 | if not auth_way:
98 | WAY_PROMPT = f'Send via mnemonic [{h}m{nh}]/wallet seed [{h}s{nh}]/ton link [{h}t{nh}]? '
99 | while (auth_way := input(WAY_PROMPT).lower()) not in ('m', 's', 't'): pass
100 |
101 | if auth_way == 't':
102 | print('\nTransfer links:')
103 |
104 | for (dest, state_init, message, value_nton) in orders:
105 | addr = dest.to_string(True, True, True)
106 | boc = urlsafe_b64encode(message.to_boc(False)).decode('ascii')
107 | link = f'ton://transfer/{addr}?bin={boc}&amount={value_nton}'
108 | if state_init:
109 | link += '&init='
110 | link += urlsafe_b64encode(state_init.to_boc(False)).decode('ascii')
111 |
112 | print(f'{b}{link}{nb}')
113 |
114 | return None
115 |
116 | if not wallet:
117 | wallet = retrieve_auth_wallet(auth_way)
118 | addr = wallet.address.to_string(True, True, True)
119 |
120 | print('Ready to do transfer from', addr)
121 |
122 | while (confirm := input(f'{h}Confirm? [y/n] {nh}').lower()) not in ('y', 'n'):
123 | pass
124 |
125 | if confirm == 'n': return None
126 |
127 | link = f'https://tonapi.io/v1/wallet/getSeqno?account={addr}'
128 | seqno = requests.get(link).json().get('seqno', 0)
129 |
130 | return sign_multitransfer_body(wallet, seqno, orders)
131 |
132 |
133 | def sign_send(orders: list[tuple[Address,Cell,Cell,int]],
134 | description: str, auth_way=None, wallet=None):
135 | signed_msg = sign_for_sending(orders, description, auth_way, wallet)
136 | if signed_msg:
137 | requests.post('https://tonapi.io/v1/send/boc', json={
138 | 'boc': b64encode(signed_msg.to_boc(False)).decode('ascii')
139 | })
140 |
141 |
142 | def sign_plugin(plugin_init: Cell, value_nton: int,
143 | description: str, wallet=None) -> Cell:
144 | print(f'{h}Attempting to install plugin{nh}', repr(description))
145 | print(f'{h}Init BOC: {nh}', b64encode(plugin_init.to_boc(False)).decode('ascii'))
146 | print(f'{h}TON amount: {nh}', value_nton / 1e9)
147 | print()
148 |
149 | if not wallet:
150 | WAY_PROMPT = f'Install via mnemonic [{h}m{nh}]/wallet seed [{h}s{nh}]? '
151 | while (auth_way := input(WAY_PROMPT).lower()) not in ('m', 's'): pass
152 | wallet = retrieve_auth_wallet(auth_way, plugin_only=True)
153 | addr = wallet.address.to_string(True, True, True)
154 |
155 | print('Ready to install plugin to', addr)
156 |
157 | while (confirm := input(f'{h}Confirm? [y/n] {nh}').lower()) not in ('y', 'n'):
158 | pass
159 |
160 | if confirm == 'n': return None
161 |
162 | link = f'https://tonapi.io/v1/wallet/getSeqno?account={addr}'
163 | seqno = requests.get(link).json().get('seqno', 0)
164 |
165 | msg_body = wallet.create_signing_message(seqno, without_op=True)
166 | msg_body.bits.write_uint(1, 8) # deploy + install plugin
167 | msg_body.bits.write_int(0, 8) # workchain 0
168 | msg_body.bits.write_coins(value_nton) # initial plugin balance
169 | msg_body.refs.append(plugin_init)
170 | msg_body.refs.append(Cell())
171 |
172 | return wallet.create_external_message(msg_body, seqno)['message']
173 |
174 |
175 | def sign_install_plugin(plugin_init: Cell, value_nton: int,
176 | description: str, wallet=None) -> Cell:
177 | signed_msg = sign_plugin(plugin_init, value_nton, description, wallet)
178 | if signed_msg:
179 | requests.post('https://tonapi.io/v1/send/boc', json={
180 | 'boc': b64encode(signed_msg.to_boc(False)).decode('ascii')
181 | })
182 |
183 |
184 | def sign_unplug(plugin: str, description: str, wallet=None) -> Cell:
185 | print(f'{h}Attempting to remove plugin{nh}', repr(description))
186 | print(f'{h}Address:{nh}', plugin)
187 | print()
188 |
189 | if not wallet:
190 | WAY_PROMPT = f'Remove via mnemonic [{h}m{nh}]/wallet seed [{h}s{nh}]? '
191 | while (auth_way := input(WAY_PROMPT).lower()) not in ('m', 's'): pass
192 | wallet = retrieve_auth_wallet(auth_way, plugin_only=True)
193 | addr = wallet.address.to_string(True, True, True)
194 |
195 | print('Ready to remove plugin from', addr)
196 |
197 | while (confirm := input(f'{h}Confirm? [y/n] {nh}').lower()) not in ('y', 'n'):
198 | pass
199 |
200 | if confirm == 'n': return None
201 |
202 | link = f'https://tonapi.io/v1/wallet/getSeqno?account={addr}'
203 | seqno = requests.get(link).json().get('seqno', 0)
204 |
205 | plugin_addr = Address(plugin)
206 |
207 | msg_body = wallet.create_signing_message(seqno, without_op=True)
208 | msg_body.bits.write_uint(3, 8) # remove plugin
209 | msg_body.bits.write_int(plugin_addr.wc, 8)
210 | msg_body.bits.write_bytes(plugin_addr.hash_part)
211 | msg_body.bits.write_coins(5*10**7) # value to send for destroying
212 | msg_body.bits.write_uint(int(time.time() * 1000), 64)
213 |
214 | return wallet.create_external_message(msg_body, seqno)['message']
215 |
216 |
217 | def sign_uninstall_plugin(plugin: str, description: str, wallet=None) -> Cell:
218 | signed_msg = sign_unplug(plugin, description, wallet)
219 | if signed_msg:
220 | requests.post('https://tonapi.io/v1/send/boc', json={
221 | 'boc': b64encode(signed_msg.to_boc(False)).decode('ascii')
222 | })
223 |
224 |
--------------------------------------------------------------------------------
/bot/cli/tslice.py:
--------------------------------------------------------------------------------
1 | # Modified version of _slice.py from fork of tonsdk library:
2 | # https://github.com/devdaoteam/tonsdk/blob/e3d6451e50a46d984e7fec1c52d1d32290781da5/tonsdk/boc/_slice.py
3 | # Original code is licensed under Apache-2.0 License.
4 |
5 | import bitarray
6 |
7 | from tonsdk.boc import Cell
8 | from tonsdk.utils import Address
9 |
10 |
11 |
12 | class Slice:
13 | '''Slice like an analog of slice in FunC. Used only for reading.'''
14 | def __init__(self, cell: Cell):
15 | self.bits = bitarray.bitarray()
16 | self.bits.frombytes(cell.bits.array)
17 | self.bits = self.bits[:cell.bits.cursor]
18 | self.refs = cell.refs
19 | self.ref_offset = 0
20 |
21 | def __len__(self):
22 | return len(self.bits)
23 |
24 | def __repr__(self):
25 | return hex(int(self.bits.to01(), 2))[2:].upper()
26 |
27 | def is_empty(self) -> bool:
28 | return len(self.bits) == 0
29 |
30 | def end_parse(self):
31 | '''Throws an exception if the slice is not empty.'''
32 | if not self.is_empty() or self.ref_offset != len(self.refs):
33 | raise Exception('Upon .end_parse(), slice is not empty.')
34 |
35 | def load_bit(self) -> int:
36 | '''Loads single bit from the slice.'''
37 | bit = self.bits[0]
38 | del self.bits[0]
39 | return bit
40 |
41 | def preload_bit(self) -> int:
42 | return self.bits[0]
43 |
44 | def load_bits(self, bit_count: int) -> bitarray.bitarray:
45 | bits = self.bits[:bit_count]
46 | del self.bits[:bit_count]
47 | return bits
48 |
49 | def preload_bits(self, bit_count: int) -> bitarray.bitarray:
50 | return self.bits[:bit_count]
51 |
52 | def skip_bits(self, bit_count: int):
53 | del self.bits[:bit_count]
54 |
55 | def load_uint(self, bit_length: int) -> int:
56 | value = self.bits[:bit_length]
57 | del self.bits[:bit_length]
58 | return int(value.to01(), 2)
59 |
60 | def preload_uint(self, bit_length: int) -> int:
61 | value = self.bits[:bit_length]
62 | return int(value.to01(), 2)
63 |
64 | def load_bytes(self, bytes_count: int) -> bytes:
65 | length = bytes_count * 8
66 | value = self.bits[:length]
67 | del self.bits[:length]
68 | return value.tobytes()
69 |
70 | def load_int(self, bit_length: int) -> int:
71 | if bit_length == 1:
72 | # if num is -1 then bit is 1. if 0 then 0. see _bit_string.py
73 | return - self.load_bit()
74 | else:
75 | is_negative = self.load_bit()
76 | value = self.load_uint(bit_length - 1)
77 | if is_negative == 1:
78 | # ones complement
79 | return - (2 ** (bit_length - 1) - value)
80 | else:
81 | return value
82 |
83 | def preload_int(self, bit_length: int) -> int:
84 | tmp = self.bits
85 | value = self.load_int(bit_length)
86 | self.bits = tmp
87 | return value
88 |
89 | def load_msg_addr(self) -> Address:
90 | '''Loads contract address from the slice.
91 | May return None if there is a zero-address.'''
92 | # TODO: support for external addresses
93 | if self.load_uint(2) == 0:
94 | return None
95 | self.load_bit() # anycast
96 | workchain_id = hex(self.load_int(8))[2:]
97 | hashpart = hex(self.load_uint(256))[2:].zfill(64)
98 | return Address(workchain_id + ':' + hashpart)
99 |
100 | def load_coins(self) -> int:
101 | '''Loads an amount of coins from the slice. Returns nanocoins.'''
102 | length = self.load_uint(4)
103 | if length == 0: # 0 in length means 0 coins
104 | return 0
105 | else:
106 | return self.load_uint(length * 8)
107 |
108 | def load_grams(self) -> int:
109 | '''Loads an amount of coins from the slice. Returns nanocoins.'''
110 | return self.load_coins()
111 |
112 | def load_string(self, length: int = 0) -> str:
113 | '''Loads string from the slice.
114 | If length is 0, then loads string until the end of the slice.'''
115 | if length == 0:
116 | length = len(self.bits) // 8
117 | return self.load_bytes(length).decode('utf-8')
118 |
119 | def load_ref(self) -> Cell:
120 | '''Loads next reference cell from the slice.'''
121 | ref = self.refs[self.ref_offset]
122 | self.ref_offset += 1
123 | return ref
124 |
125 | def preload_ref(self) -> Cell:
126 | return self.refs[self.ref_offset]
127 |
128 | def load_dict(self) -> Cell:
129 | '''Loads dictionary like a Cell from the slice.
130 | Returns None if the dictionary was null().'''
131 | not_null = self.load_bit()
132 | if not_null:
133 | return self.load_ref()
134 | else:
135 | return None
136 |
137 | def preload_dict(self) -> Cell:
138 | not_null = self.preload_bit()
139 | if not_null:
140 | return self.preload_ref()
141 | else:
142 | return None
143 |
144 | def skip_dict(self):
145 | self.load_dict()
146 |
--------------------------------------------------------------------------------
/bot/install_libs.py:
--------------------------------------------------------------------------------
1 | import shlex
2 | import sys
3 | import os
4 |
5 | interpreter = sys.executable.replace('pythonw', 'python')
6 | pip_prefix = shlex.quote(interpreter).replace("'", '"') + ' -m pip install '
7 |
8 | try:
9 | import portalocker
10 | except:
11 | os.system(pip_prefix + 'portalocker')
12 |
13 | try:
14 | import requests
15 | except:
16 | os.system(pip_prefix + 'requests')
17 |
18 | try:
19 | import tonsdk
20 | except:
21 | os.system(pip_prefix + 'tonsdk bitstring==3.1.9')
22 |
23 | try:
24 | import nacl
25 | except:
26 | os.system(pip_prefix + 'pynacl')
27 |
--------------------------------------------------------------------------------
/bot/keyutils.py:
--------------------------------------------------------------------------------
1 | from utils import KEY_GENERATOR_SALT
2 | from base64 import b16encode
3 | import nacl.signing
4 | import hashlib
5 |
6 | class KeyCustodialUtils:
7 | @staticmethod
8 | def get_keypair_for_user(chat_id):
9 | user_secret_uid = (KEY_GENERATOR_SALT + str(chat_id) + KEY_GENERATOR_SALT).encode('utf-8')
10 | secret_bytes = hashlib.sha256(user_secret_uid).digest()
11 |
12 | secret_key_obj = nacl.signing.SigningKey(secret_bytes)
13 | public_bytes = secret_key_obj.verify_key.encode()
14 |
15 | public_key_armored = 'pub:ed25519:vk:' + b16encode(public_bytes).decode('ascii')
16 | secret_key_armored = 'prv:ed25519:sk:' + b16encode(secret_bytes).decode('ascii')
17 | key_id = hashlib.sha256(public_key_armored.encode('ascii')).hexdigest()[::8]
18 |
19 | return {
20 | 'public': public_bytes,
21 | 'secret': secret_bytes,
22 | 'key_id': key_id,
23 | 'public_armored': public_key_armored,
24 | 'secret_armored': secret_key_armored
25 | }
26 |
--------------------------------------------------------------------------------
/bot/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # encoding: utf-8
3 |
4 | from collections import deque
5 | import traceback
6 | import logging
7 | import html
8 | import time
9 | import os
10 |
11 |
12 | # Checking all third-party libraries are installed before proceeding.
13 | import install_libs
14 |
15 | import portalocker
16 |
17 | from states import StartState, SentinelState, donation_middleware
18 | from stateful import MultiuserStateMachine
19 | from backends import TelegramBackend
20 |
21 |
22 | # Setting logging configuration before `test.py` is launched.
23 | logging.basicConfig(format='%(asctime)s [%(levelname)s] %(message)s',
24 | datefmt='%d.%m.%Y %H:%M:%S',
25 | filename=os.path.abspath(__file__+'/../bot.log'),
26 | level=logging.DEBUG)
27 |
28 |
29 | # import test
30 |
31 |
32 | LOCK_PATH = os.path.abspath(__file__ + '/../.lock')
33 |
34 |
35 | def load_multiuser_machine():
36 | try:
37 | with open(os.path.abspath(__file__ + '/../bot.json')) as f:
38 | return MultiuserStateMachine.load(f.read(), StartState)
39 | except FileNotFoundError:
40 | return MultiuserStateMachine(StartState)
41 |
42 |
43 | try:
44 | # Waiting for old bot instance to close.
45 |
46 | logging.info('Bot is waiting for older instance to close.')
47 | time.sleep(1)
48 |
49 | messages = deque()
50 |
51 | with portalocker.Lock(LOCK_PATH, 'w') as _:
52 | logging.info('Bot started.')
53 | backend = TelegramBackend()
54 | machine = load_multiuser_machine()
55 | machine.interceptors.append(donation_middleware)
56 |
57 | while not machine.state_is(SentinelState):
58 | if not len(messages) and machine.needs_message():
59 | for e in backend.receive_all_new_messages():
60 | logging.info(f'Putting message into queue: {str(e)[:40]}')
61 | messages.append(e)
62 |
63 | if not machine.needs_message():
64 | machine.next(backend, None)
65 | elif len(messages):
66 | msg = messages.popleft()
67 |
68 | logging.info(f'Processing message from queue: {msg}')
69 | machine.next(backend, msg)
70 |
71 | time.sleep(2)
72 |
73 | except KeyboardInterrupt:
74 | logging.warning(backend.send_message(1463706336, 'Stopped by KeyboardInterrupt'))
75 | except:
76 | logging.error('Started sending crash log')
77 |
78 | remaining_messages = list(messages)
79 |
80 | if 'machine' not in globals(): machine = None
81 | if 'remaining_messages' not in globals(): remaining_messages = None
82 |
83 | crash_log = f'''
84 | BOT CRASHED
85 | Unparsed messages: {html.escape(repr(remaining_messages))}
86 |
87 | Machine state: {html.escape(repr(machine))}
88 |
89 | {html.escape(traceback.format_exc())}
90 | '''.strip()
91 |
92 | logging.error(crash_log)
93 | logging.warning(backend.send_message(1463706336, crash_log))
94 | finally:
95 | logging.info('Bot has stopped.')
96 |
97 | with open(os.path.abspath(__file__ + '/../bot.json'), 'w') as f:
98 | f.write(repr(machine))
99 |
--------------------------------------------------------------------------------
/bot/persistence.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 |
5 | class PersistentValue:
6 | def __init__(self, filename, default):
7 | path_s = os.path.sep
8 |
9 | self_dir = __file__ + path_s + '..' + path_s
10 | self.path = os.path.abspath(self_dir + filename)
11 |
12 | try:
13 | with open(self.path, 'r') as f:
14 | self.value = json.load(f)
15 | except:
16 | self.value = default
17 |
18 | def get(self):
19 | return self.value
20 |
21 | def set(self, value):
22 | self.value = value
23 | self.flush()
24 |
25 | def flush(self):
26 | with open(self.path, 'w') as f:
27 | json.dump(self.value, f)
28 |
29 | def set_max(self, value):
30 | if value > self.value:
31 | self.set(value)
32 |
--------------------------------------------------------------------------------
/bot/stateful.py:
--------------------------------------------------------------------------------
1 | import json
2 | import abc
3 |
4 |
5 | class IState(abc.ABC):
6 | registered_states = {}
7 |
8 | @abc.abstractmethod
9 | def needs_message(self): pass
10 |
11 | @abc.abstractmethod
12 | def enter_state(self, message_info, reply, send_callback): pass
13 |
14 | @abc.abstractmethod
15 | def run(self, message_info, reply, send_callback): pass
16 |
17 | @staticmethod
18 | def load(state_repr):
19 | state_name, state_value = state_repr.split(':', 1)
20 | return IState.registered_states[state_name].load(state_value)
21 |
22 |
23 | def RegisterState(state_class):
24 | IState.registered_states[state_class.__name__] = state_class
25 | return state_class
26 |
27 |
28 | def load_chat_id(message_info):
29 | if 'inline_query' in message_info:
30 | return message_info['inline_query']['from']['id']
31 | elif 'chosen_inline_result' in message_info:
32 | return message_info['chosen_inline_result']['from']['id']
33 | elif 'message' in message_info:
34 | return message_info['message'].get('chat', {}).get('id', -1)
35 | else:
36 | # unreachable
37 | return -2
38 |
39 |
40 | class UserStateMachine:
41 | def __init__(self, start_state):
42 | self.state = start_state
43 |
44 | def __repr__(self):
45 | return self.state.__class__.__name__ + ':' + repr(self.state)
46 |
47 | def next(self, backend, message_info):
48 | '''
49 | typeof message_info =
50 | {'inline_query': {'id': str, 'from': User, 'query': str, 'offset': str}} |
51 | {'message': {'message_id': int, 'from': User?, 'date': int, 'chat': Chat,
52 | 'reply_to_message': Message, 'text': str?}};
53 | '''
54 |
55 | if self.state.needs_message() and not message_info:
56 | return
57 |
58 | chat_id = load_chat_id(message_info)
59 |
60 | if 'message' in message_info:
61 | incoming_id = message_info['message']['message_id']
62 | def reply(reply_text, **kw):
63 | backend.send_message(chat_id, reply_text, reply=incoming_id, **kw)
64 | self.state = self.state.run(message_info, reply, backend.send_message)
65 | elif 'chosen_inline_result' in message_info:
66 | def reply(reply_text, **kw):
67 | backend.send_message(chat_id, reply_text, **kw)
68 | self.state = self.state.run(message_info, reply, backend.send_message)
69 | elif 'inline_query' in message_info:
70 | incoming_id = message_info['inline_query']['id']
71 | def reply_inline(results, button, **kw):
72 | backend.respond_inline_query(incoming_id, results, button, **kw)
73 | self.state = self.state.run(message_info, reply_inline, backend.send_message)
74 |
75 |
76 | def state_is(self, state_class):
77 | return isinstance(self.state, state_class)
78 |
79 | @staticmethod
80 | def load(machine_repr):
81 | self = UserStateMachine(None)
82 | self.state = IState.load(machine_repr)
83 | return self
84 |
85 |
86 | class MultiuserStateMachine:
87 | def __init__(self, start_state_class):
88 | self.start_state_class = start_state_class
89 | self.interceptors = []
90 | self.users = {}
91 |
92 | def __repr__(self):
93 | return json.dumps({str(chat_id): repr(machine)
94 | for (chat_id, machine) in self.users.items()})
95 |
96 | @staticmethod
97 | def load(users_repr, start_state_class):
98 | self = MultiuserStateMachine(start_state_class)
99 | self.users = {int(chat_id): UserStateMachine.load(machine_repr)
100 | for (chat_id, machine_repr) in json.loads(users_repr).items()}
101 | return self
102 |
103 | def next(self, backend, message_info):
104 | '''
105 | typeof message_info =
106 | {'inline_query': {'id': str, 'from': User, 'query': str, 'offset': str}} |
107 | {'message': {'message_id': int, 'from': User?, 'date': int, 'chat': Chat,
108 | 'reply_to_message': Message, 'text': str?}};
109 | '''
110 |
111 | if not message_info:
112 | for chat_id, machine in self.users.items():
113 | machine.next(backend, message_info)
114 | else:
115 | if any(intercept(backend, message_info) for intercept in self.interceptors):
116 | return
117 |
118 | chat_id = load_chat_id(message_info)
119 |
120 | if chat_id not in self.users:
121 | self.users[chat_id] = UserStateMachine(self.start_state_class())
122 |
123 | self.users[chat_id].next(backend, message_info)
124 |
125 | def needs_message(self):
126 | return all(machine.state.needs_message() for machine in self.users.values())
127 |
128 | def state_is(self, state_class):
129 | return any(machine.state_is(state_class) for machine in self.users.values())
130 |
--------------------------------------------------------------------------------
/bot/states.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import logging
3 | import secrets
4 | import html
5 | import json
6 | import time
7 | import os
8 |
9 | from stateful import IState, MultiuserStateMachine, RegisterState, load_chat_id
10 | from keyutils import KeyCustodialUtils
11 | from textutils import JobPostUtils
12 |
13 | import cli.polyfills
14 | from cli.dnsresolver import resolve_to_userfriendly, TONDNSResolutionError
15 |
16 |
17 | def flatten(arr):
18 | for row in arr:
19 | yield from row
20 |
21 |
22 | #VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
23 | '''
24 | typeof message_info =
25 | {'inline_query': {'id': str, 'from': User, 'query': str, 'offset': str}} |
26 | {'message': {'message_id': int, 'from': User?, 'date': int, 'chat': Chat,
27 | 'reply_to_message': Message, 'text': str?}};
28 | '''
29 | #^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30 |
31 |
32 | @RegisterState
33 | class StartState(IState):
34 | '''
35 | Welcome state, where everyone not yet known to bot starts.
36 | '''
37 |
38 | def __init__(self, settings=None): self.settings = settings or {}
39 | def needs_message(self): return True
40 | def enter_state(self, message_info, reply, send_callback): pass
41 |
42 | @staticmethod
43 | def load(state_repr): return StartState(json.loads(state_repr))
44 | def __repr__(self): return json.dumps(self.settings)
45 |
46 | def run(self, message_info, reply, send_callback):
47 | chat_id = load_chat_id(message_info)
48 | keypair = KeyCustodialUtils.get_keypair_for_user(chat_id)
49 |
50 | if 'message' in message_info:
51 | text = message_info['message'].get('text', '')
52 | if text == '/setwallet' or text == '/start set-wallet':
53 | new_state = SetWalletState(self.settings)
54 | new_state.enter_state(message_info, reply, send_callback)
55 | return new_state
56 | if text == '/me' or text == '/start me':
57 | wallet_text = f'Wallet: {self.settings.get("address","")}
\n'
58 | pkey_text = f'Public key - ID {keypair["key_id"]}: {keypair["public_armored"]}
'
59 | reply(wallet_text + pkey_text, keyboard=[['Show secret key'], ['/setwallet']])
60 | if text == 'Show secret key' or text == '/showsecret':
61 | chat_id = message_info['message']['chat']['id']
62 | keypair = KeyCustodialUtils.get_keypair_for_user(chat_id)
63 | reply('%s
' % keypair['secret_armored'])
64 |
65 | return self
66 | elif 'chosen_inline_result' in message_info:
67 | reply(JobPostUtils.format_deploy_links(
68 | message_info['chosen_inline_result']['query'],
69 | self.settings['address'],
70 | keypair['public']
71 | ))
72 | return self
73 |
74 | if 'address' not in self.settings:
75 | reply([], {'text': 'Set wallet address', 'start_parameter': 'set-wallet'})
76 | elif message_info['inline_query']['chat_type'] in ('group', 'supergroup'):
77 | reply([], {'text': 'Usage in groups is locked', 'start_parameter': 'me'})
78 | else:
79 | reply(JobPostUtils.format_article_list(
80 | message_info['inline_query']['query'],
81 | self.settings['address'],
82 | keypair['public']
83 | ), None)
84 |
85 | chat_id = message_info['inline_query']['from']['id']
86 | # send_callback(chat_id, 'You need to deploy the job.')
87 |
88 | return self
89 |
90 |
91 | @RegisterState
92 | class SetWalletState(IState):
93 | def __init__(self, settings=None): self.settings = settings or {}
94 | def needs_message(self): return True
95 | def enter_state(self, message_info, reply, send_callback):
96 | reply('Provide your TON wallet address in any format.')
97 |
98 | @staticmethod
99 | def load(state_repr): return StartState(json.loads(state_repr))
100 | def __repr__(self): return json.dumps(self.settings)
101 |
102 | def run(self, message_info, reply, send_callback):
103 | if 'message' in message_info:
104 | text = message_info['message'].get('text', '')
105 | try:
106 | self.settings['address'] = resolve_to_userfriendly(text)
107 | reply(f'Wallet address set to {self.settings["address"]}.', custom={
108 | 'reply_markup': {'inline_keyboard': [[{
109 | 'text': 'Back to original chat',
110 | 'switch_inline_query': ''
111 | }]]}
112 | })
113 | return StartState(self.settings)
114 | except TONDNSResolutionError as e:
115 | reply('Error when resolving address: ' + str(e) + '. Please, try again.')
116 | except Exception as e:
117 | reply(repr(e))
118 | else:
119 | reply([], {'text': 'Set wallet address', 'start_parameter': 'set-wallet'})
120 | return self
121 |
122 |
123 | @RegisterState
124 | class SentinelState(IState):
125 | '''
126 | State where bot needs to shutdown.
127 | '''
128 | def __init__(self, previous_state=None):
129 | logging.debug(f'State just before bot shutdown: {previous_state}')
130 | self.previous_state = previous_state
131 |
132 | def needs_message(self):
133 | return False
134 |
135 | def enter_state(self, message_info, reply, send_callback):
136 | reply('Stopping.')
137 |
138 | def run(self, message_info, reply, send_callback):
139 | return self
140 |
141 | @staticmethod
142 | def load(state_repr):
143 | logging.info(f'Loading SentinelState: {state_repr}')
144 | if state_repr != 'None': return IState.load(state_repr) # state just before SentinelState
145 | return SentinelState()
146 | def __repr__(self):
147 | if not self.previous_state: return ''
148 | return self.previous_state.__class__.__name__ + ':' + repr(self.previous_state)
149 |
150 |
151 | def format_donation_msg():
152 | return '''
153 | TON, mainnet: EQCyoez1VF4HbNNq5Rbqfr3zKuoAjKorhK-YZr7LIIiVrSD7
154 | ton://transfer/EQCyoez1VF4HbNNq5Rbqfr3zKuoAjKorhK-YZr7LIIiVrSD7
155 | '''.strip()
156 |
157 |
158 | def donation_middleware(backend, in_msg_full):
159 | if 'message' not in in_msg_full: return
160 | in_msg_full = in_msg_full['message']
161 | in_msg_body = in_msg_full.get('text', '')
162 | sender = in_msg_full['from']['id']
163 | lt = in_msg_full['message_id']
164 |
165 | if in_msg_body == '/donate':
166 | backend.send_message(sender, format_donation_msg(), reply=lt)
167 | return True
168 | elif in_msg_body == '/stopkb':
169 | raise KeyboardInterrupt
170 | elif in_msg_body == '//restart':
171 | os.startfile(__file__.replace('states.py', 'main.py'))
172 | raise KeyboardInterrupt
173 | else:
174 | return False
175 |
--------------------------------------------------------------------------------
/bot/textutils.py:
--------------------------------------------------------------------------------
1 | from cli.jobs import analytic_msg, job_state_init, JOB_NOTIFICATIONS
2 | from cli.bcutils import encode_text
3 | from tonsdk.utils import Address
4 | from tonsdk.boc import Cell
5 |
6 | from base64 import urlsafe_b64encode, b16encode
7 | import secrets
8 |
9 |
10 | def part_escape_html(text: str) -> str:
11 | return text.replace('<', '<').replace('>', '>')
12 |
13 |
14 | def ton_link(destination: Address, init: Cell, body: Cell, value: int) -> str:
15 | addr = destination.to_string(True, True, True)
16 | boc = urlsafe_b64encode(body.to_boc(False)).decode('ascii')
17 | link = f'ton://transfer/{addr}?bin={boc}&amount={value}'
18 | if init:
19 | link += '&init='
20 | link += urlsafe_b64encode(init.to_boc(False)).decode('ascii')
21 | return link
22 |
23 |
24 | class JobPostUtils:
25 | @staticmethod
26 | def parse_amount_title_description(message: str) -> tuple[int, str, str]:
27 | # message is like '20 [ton] the job. text'
28 | # or '20ton the job. text'
29 |
30 | words = message.strip().split(' ', 2)
31 | if len(words) < 2: raise Exception('insufficient words')
32 |
33 | price = words[0].lower()
34 | if price.endswith('ton'):
35 | price = price.removesuffix('ton')
36 | elif words[1].lower() == 'ton': # removing TON suffix
37 | words.pop(1)
38 |
39 | if '$' in price: raise Exception('$ denomination is not supported')
40 |
41 | amount = int(float(price) * 1e9)
42 | if amount < 10**8: raise Exception('too small job price')
43 |
44 | text = ' '.join(words[1:])
45 |
46 | if '.' in text:
47 | title, description = text.split('.', 1)
48 | return amount, title.strip(), description.strip()
49 | else:
50 | return amount, 'Untitled', text.strip()
51 |
52 | @staticmethod
53 | def create_address_deploylinks(value: int, text: str, my_address: str,
54 | public_key: bytes) -> tuple[str, list[str]]:
55 | description_cell = encode_text(text)
56 | key_int = int.from_bytes(public_key, 'big')
57 |
58 | state_init = job_state_init(my_address, value, description_cell, key_int)
59 | addr = Address('0:' + b16encode(state_init.bytes_hash()).decode('ascii'))
60 | am = analytic_msg(addr, value, description_cell, key_int)
61 |
62 | jobs = Address(JOB_NOTIFICATIONS)
63 | jobs.is_bounceable = False
64 |
65 | return addr.to_string(True, True, True), [
66 | ton_link(addr, state_init, Cell(), value),
67 | ton_link(jobs, None, am, 5*10**7)
68 | ]
69 |
70 | @staticmethod
71 | def format_article_list(message: str, my_address: str, public_key: bytes) -> list:
72 | try:
73 | amount, title, description = JobPostUtils.parse_amount_title_description(message)
74 |
75 | job_text = f'# {title}\n\n{description}'
76 | job_addr, _ = JobPostUtils.create_address_deploylinks(amount, job_text, my_address, public_key)
77 |
78 | article_text = f'''
79 | {part_escape_html(title)}
80 | worth {amount/1e9:.2f} TON, by {my_address}
81 |
82 | {part_escape_html(description)}
83 |
84 | Job address: {job_addr}
85 | '''.replace(' ' * 12, '').strip()
86 |
87 | return [{
88 | 'type': 'article',
89 | 'id': secrets.token_urlsafe(),
90 | 'title': f'Create job "{part_escape_html(title)}" worth {amount/1e9:.2f} TON',
91 | 'input_message_content': {
92 | 'message_text': article_text, 'parse_mode': 'html'
93 | }
94 | }]
95 | except:
96 | return []
97 |
98 | @staticmethod
99 | def format_deploy_links(message: str, my_address: str, public_key: bytes) -> str:
100 | try:
101 | amount, title, description = JobPostUtils.parse_amount_title_description(message)
102 |
103 | job_text = f'# {title}\n\n{description}'
104 | _, links = JobPostUtils.create_address_deploylinks(amount, job_text, my_address, public_key)
105 |
106 | deploy, notify = links
107 |
108 | return (f'Job "{part_escape_html(title)}" worth {amount/1e9:.2f} TON\n' +
109 | f'[Deploy] + ' +
110 | f'[notify others about job contract]')
111 | except Exception as e:
112 | return 'Error when creating deploy links: ' + repr(e)
113 |
--------------------------------------------------------------------------------
/bot/tg.py:
--------------------------------------------------------------------------------
1 | __all__ = ['check_bot_ok', 'send', 'yield_messages']
2 |
3 |
4 | import traceback
5 | import logging
6 |
7 | import requests
8 |
9 | from persistence import PersistentValue
10 | from utils import TOKEN
11 |
12 |
13 | url = 'https://api.telegram.org/bot%s/' % TOKEN
14 | update_id = PersistentValue('update_id.txt', default=0)
15 |
16 |
17 | def check_bot_ok():
18 | assert requests.get(url + 'getMe').json()['ok']
19 |
20 |
21 | def yield_messages():
22 | '''
23 | Generator yielding new messages sent from users to Telegram bot.
24 | '''
25 |
26 | try:
27 | updates = requests.get(url + 'getUpdates', params={
28 | 'offset': update_id.get() + 1,
29 | 'timeout': 15
30 | }, timeout=20).json()
31 | except requests.exceptions.ReadTimeout:
32 | return
33 | except Exception as e:
34 | logging.error(repr(e))
35 | return
36 |
37 | if not updates['ok']:
38 | logging.error('Request failed: ' + updates['description'])
39 | return
40 |
41 | for update in updates['result']:
42 | update_id.set_max(update['update_id'])
43 |
44 | '''
45 | typeof update =
46 | {'inline_query': {'id': str, 'from': User, 'query': str, 'offset': str}} |
47 | {'message': {'message_id': int, 'from': User?, 'date': int, 'chat': Chat,
48 | 'reply_to_message': Message, 'text': str?}};
49 | '''
50 |
51 | if 'message' in update or 'inline_query' in update or 'chosen_inline_result' in update:
52 | logging.debug(update)
53 | yield update
54 |
55 |
56 | def respond_inline_query(query_id, results, button):
57 | msg_params = {
58 | 'inline_query_id': query_id,
59 | 'results': results,
60 | 'is_personal': True,
61 | 'cache_time': 40
62 | }
63 | if button: msg_params['button'] = button
64 |
65 | logging.warning(msg_params)
66 | try:
67 | result = requests.post(url + 'answerInlineQuery', json=msg_params).json()
68 | except:
69 | result = {'ok': False, 'description': traceback.format_exc()}
70 |
71 | if not result.get('ok', False):
72 | logging.warning('Inline responding failed ' + str(result))
73 |
74 | return result
75 |
76 |
77 | def send(chat, text, reply=None, keyboard=None, parse_mode='html', custom={}):
78 | '''
79 | Function sending message `text` via Telegram bot to user specified by `chat`.
80 | If `reply` is specified, the sent message is reply to message with specified ID.
81 | `keyboard` must be either None or List[List[str]] - buttons' text.
82 | `parse_mode` is forwarded to Telegram as-is and changes text handling.
83 | '''
84 |
85 | msg_params = {'chat_id': chat, 'text': text, 'parse_mode': parse_mode}
86 |
87 | if reply:
88 | msg_params['reply_to_message_id'] = reply
89 |
90 | if keyboard:
91 | msg_params['reply_markup'] = {
92 | 'keyboard': keyboard,
93 | 'one_time_keyboard': True
94 | }
95 | else:
96 | msg_params['reply_markup'] = {'remove_keyboard': True}
97 |
98 | msg_params.update(custom)
99 |
100 | try:
101 | result = requests.post(url + 'sendMessage', json=msg_params).json()
102 | except:
103 | result = {'ok': False, 'description': traceback.format_exc()}
104 |
105 | if not result.get('ok', False):
106 | logging.warning('Sending message failed ' + str(result))
107 |
108 | return result
109 |
--------------------------------------------------------------------------------
/cli/about.py:
--------------------------------------------------------------------------------
1 | from .colors import nh, h, nb, b, ns, s
2 |
3 | PROMPT_SINGLE = f'''{b}{"="*80}{ns}
4 | Jobs {h}jl{nh}: list {h}jp{nh}: post {h}ji{nh}: info {h}jv{nh}: load+verify {h}jr{nh}: revoke {h}jd{nh}: delegate
5 | Offers {h}ol{nh}: list {h}op{nh}: post {h}oi{nh}: info {h}ov{nh}: load+verify {h}or{nh}: revoke
6 | Contracts {s}cu: unseal{ns}{h}{nh} {h}ct{nh}: talk {h}cn{nh}: negotiate
7 | General {h} h{nh}: help {h} u{nh}: update {h} q{nh}: quit {h} d{nh}: donate
8 | Keys {h}kl{nh}: list {h}ke{nh}: export {h}kn{nh}: new {h}ki{nh}: import
9 | {b}{"="*80}{ns}
10 | '''
11 |
12 | PROMPT = f'{b}ratelance> {nb}'
13 |
14 | ABOUT = f'''
15 | Repository: {h}https://github.com/ProgramCrafter/ratelance/{nh}
16 | TG channel: {h}https://t.me/ratelance{nh}
17 | '''.strip()
18 |
--------------------------------------------------------------------------------
/cli/assets/contract-job.boc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProgramCrafter/ratelance/2de56f4f96acf0f9e1e5b0e60ae0ad846ba50170/cli/assets/contract-job.boc
--------------------------------------------------------------------------------
/cli/assets/contract-offer.boc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProgramCrafter/ratelance/2de56f4f96acf0f9e1e5b0e60ae0ad846ba50170/cli/assets/contract-offer.boc
--------------------------------------------------------------------------------
/cli/bcutils.py:
--------------------------------------------------------------------------------
1 | from tonsdk.boc import Builder, Cell
2 | from tonsdk.utils import Address
3 | import requests
4 |
5 |
6 |
7 | def input_address(prompt):
8 | while True:
9 | try:
10 | return Address(input(prompt))
11 | except KeyboardInterrupt:
12 | raise
13 | except Exception:
14 | pass
15 |
16 |
17 | def input_long(prompt):
18 | s = ''
19 | while True:
20 | t = input(prompt)
21 | if not t: return s
22 |
23 | prompt = '> '
24 | s += t + '\n'
25 |
26 |
27 | def load_transactions(address, start_lt):
28 | return requests.get(
29 | f'https://tonapi.io/v1/blockchain/getTransactions?account={address}&limit=100'
30 | ).json()['transactions']
31 |
32 |
33 | def load_account(address):
34 | return requests.get(
35 | f'https://tonapi.io/v1/blockchain/getAccount?account={address}'
36 | ).json()
37 |
38 |
39 | def decode_text(cell: Cell) -> str:
40 | a = ''
41 | s = cell.begin_parse()
42 | if s.is_empty(): return ''
43 | if s.preload_uint(32) == 0: s.skip_bits(32)
44 | a += s.load_bytes(len(s) // 8).decode('utf-8')
45 | while s.refs:
46 | s = s.load_ref().begin_parse()
47 | a += s.load_bytes(len(s) // 8).decode('utf-8')
48 | return a
49 |
50 |
51 | def encode_text(a) -> Cell:
52 | if isinstance(a, str):
53 | s = a.encode('utf-8')
54 | else:
55 | s = a
56 |
57 | b = Builder()
58 | b.store_bytes(s[:127])
59 | if len(s) > 127:
60 | b.store_ref(encode_text(s[127:]))
61 | return b.end_cell()
62 |
63 |
64 | def shorten_escape(s: str, indent=2) -> str:
65 | s = s.replace('\x1b', '').replace('\r', '')
66 | lines = s.split('\n')
67 | if len(lines) > 4:
68 | lines = lines[:3] + ['...']
69 | if lines:
70 | lines = [lines[0]] + [' ' * indent + line for line in lines[1:]]
71 | return '\n'.join(lines)
72 |
--------------------------------------------------------------------------------
/cli/colors.py:
--------------------------------------------------------------------------------
1 | # ANSI escape sequences
2 |
3 | # highlight (green)
4 | nh = '\x1b[37m'
5 | h = '\x1b[32m'
6 |
7 | # strikethrough (or invisible)
8 | ns = '\x1b[37m'
9 | s = '\x1b[30m'
10 |
11 | # blue
12 | nb = '\x1b[37m'
13 | b = '\x1b[36m'
14 |
--------------------------------------------------------------------------------
/cli/contracts.py:
--------------------------------------------------------------------------------
1 | from .bcutils import input_address, load_account, load_transactions
2 | from .colors import h, nh, b, nb
3 | from .signing import sign_send
4 |
5 | from tonsdk.boc import Builder, Cell
6 | from tonsdk.utils import Address
7 | import nacl.signing
8 |
9 | from base64 import b16decode, b16encode, b64decode
10 | import hashlib
11 |
12 |
13 |
14 | def serialize_signed_data(job: Address, bottom: int, upper: int) -> bytes:
15 | b = Builder()
16 | b.store_bytes(b16decode('FFFF726C3A3A6A6F623A3A7630'))
17 | b.store_uint(0, 5)
18 | b.store_address(job)
19 | b.store_uint(bottom, 64)
20 | b.store_uint(upper, 64)
21 | return b.end_cell().bytes_hash()
22 |
23 |
24 | def sign_pay_proposal(job: Address, bottom: int, upper: int, role: int, key_id: str, keyring) -> Cell:
25 | key = keyring.keys_info[key_id]
26 | secret_key_obj = nacl.signing.SigningKey(key['secret'])
27 | to_sign = serialize_signed_data(job, bottom, upper)
28 | signature = secret_key_obj.sign(to_sign)[:512//8]
29 | # print(signature, len(signature))
30 |
31 | b = Builder()
32 | b.store_uint(role, 2)
33 | b.store_bytes(signature)
34 | b.store_uint(bottom, 64)
35 | b.store_uint(upper, 64)
36 | return b.end_cell()
37 |
38 |
39 | def double_sign_proposal(job: Address, bottom: int, upper: int, key_id1: str, key_id2: str, keyring, role1=0, role2=1) -> Cell:
40 | b = Builder()
41 | b.store_uint(0x000000BB, 32)
42 | b.store_ref(sign_pay_proposal(job, bottom, upper, role1, key_id1, keyring))
43 | b.store_ref(sign_pay_proposal(job, bottom, upper, role2, key_id2, keyring))
44 | return b.end_cell()
45 |
46 |
47 | def upsign_proposal(job: Address, bottom: int, upper: int, key_id: str, keyring, role: int, proposal: Cell) -> Cell:
48 | b = Builder()
49 | b.store_uint(0x000000BB, 32)
50 | b.store_ref(proposal)
51 | b.store_ref(sign_pay_proposal(job, bottom, upper, role, key_id, keyring))
52 | return b.end_cell()
53 |
54 |
55 | def close_job_with(job: Address, proposal: Cell):
56 | sign_send([
57 | (job, None, proposal, 5*10**7)
58 | ], 'closing job ' + job.to_string(True, True, True))
59 |
60 |
61 | def load_job_keys_triple(job: Address) -> (bytes, bytes, bytes):
62 | acc = load_account(job.to_string(True, True, True))
63 | if acc['status'] != 'active':
64 | raise Exception('contract ' + acc['status'])
65 |
66 | d = Cell.one_from_boc(b16decode(acc['data'].upper())).begin_parse()
67 | flag = d.load_uint(2)
68 | if flag != 2:
69 | raise Exception('job is not delegated')
70 |
71 | d.load_msg_addr()
72 | d.load_msg_addr()
73 | d.skip_bits(64)
74 | d.load_ref()
75 | d.load_ref()
76 | keys = d.load_ref().begin_parse()
77 | d.end_parse()
78 |
79 | p = keys.load_bytes(32)
80 | w = keys.load_bytes(32)
81 | r = keys.load_bytes(32)
82 | keys.end_parse()
83 | return (p, w, r)
84 |
85 |
86 | def check_intersect(limits_a: tuple[int, int], proposal_plus_limits: tuple[Cell, int, int]) -> bool:
87 | return max(limits_a[0], proposal_plus_limits[1]) <= min(limits_a[1], proposal_plus_limits[2])
88 |
89 |
90 | def check_negotiate_suggestions(job: Address, p: bytes, w: bytes, r: bytes, skip_role: int):
91 | for tx in load_transactions(job.to_string(True, True, True), 0):
92 | try:
93 | body = Cell.one_from_boc(b64decode(tx['in_msg']['msg_data'])).begin_parse()
94 | if body.preload_uint(32) != 0x4bed4ee8: continue
95 |
96 | proposal_cell = body.load_ref()
97 | proposal_part = proposal_cell.begin_parse()
98 | role = proposal_part.load_uint(2)
99 | if role == 3:
100 | print('* someone claims that TON validators have voted; check TON config param 1652841508')
101 | continue
102 | if role == skip_role:
103 | # this can be our own negotiation message
104 | continue
105 |
106 | check_key_bytes = ((p, w, r))[role]
107 | check_key_obj = nacl.signing.VerifyKey(check_key_bytes)
108 |
109 | signature = proposal_part.load_bytes(512 // 8)
110 | pay_bottom = proposal_part.load_uint(64)
111 | pay_upper = proposal_part.load_uint(64)
112 |
113 | check_key_obj.verify(serialize_signed_data(job, pay_bottom, pay_upper),
114 | signature)
115 |
116 | yield [proposal_cell, pay_bottom, pay_upper]
117 | except Exception as exc:
118 | print(f'{b}Error while processing contract payment negotiation:{nb}', repr(exc))
119 |
120 |
121 | def is_key_available(i: int, key: bytes, keyring) -> bool:
122 | key_hex = b16encode(key).decode('ascii')
123 | key_armored = 'pub:ed25519:vk:' + key_hex
124 | key_id = hashlib.sha256(key_armored.encode('ascii')).hexdigest()[::8]
125 | if key_id in keyring.keys_info:
126 | return (i, key_id)
127 | return None
128 |
129 |
130 | def check_available_roles_keyids(p: bytes, w: bytes, r: bytes, keyring):
131 | return list(filter(None, [is_key_available(i, k, keyring) for i,k in enumerate((p,w,r))]))
132 |
133 |
134 | def process_contract_cmd(command: str, keyring):
135 | if command == 'ct':
136 | print(f'{h}not implemented:{nh} \'ct\'')
137 | elif command == 'cn':
138 | job = input_address(f'{b}Job address:{nb} ')
139 | (poster_key, worker_key, ratelance_key) = load_job_keys_triple(job)
140 | available_keys = check_available_roles_keyids(poster_key, worker_key, ratelance_key, keyring)
141 |
142 | if len(available_keys) > 1:
143 | print('We have keys of two sides. Proceeding to contract closing.')
144 | upper = int(float(input(f'{b}TON to give to freelancer (upper limit):{nb} ')) * 1e9)
145 | bottom = int(float(input(f'{b}TON to give to freelancer (bottom limit):{nb} ')) * 1e9)
146 |
147 | role1, key1 = available_keys[0]
148 | role2, key2 = available_keys[1]
149 |
150 | close_job_with(job, double_sign_proposal(
151 | job, bottom, upper, key1, key2, keyring, role1, role2
152 | ))
153 | else:
154 | print('We have key of single party. Looking for negotiation requests.')
155 | skip_role = available_keys[0][0]
156 |
157 | # (proposal_cell, pay_bottom, pay_upper)
158 | negotiations = list(check_negotiate_suggestions(job, poster_key, worker_key, ratelance_key, skip_role))
159 |
160 | if negotiations:
161 | min_fl = min(n[1] for n in negotiations) / 1e9
162 | max_fl = max(n[2] for n in negotiations) / 1e9
163 | print(f'There are suggestions to give {min_fl}..{max_fl} TON to freelancer.')
164 | upper = int(float(input(f'{b}TON you want to give to freelancer (upper limit):{nb} ')) * 1e9)
165 | bottom = int(float(input(f'{b}TON you want to give to freelancer (bottom limit):{nb} ')) * 1e9)
166 |
167 | for n in negotiations:
168 | if check_intersect((bottom, upper), n):
169 | print('Found matching suggestion. Building message with two signatures...')
170 | p = upsign_proposal(job, bottom, upper, available_keys[0][1], keyring, skip_role, n[0])
171 | close_job_with(job, p)
172 | return
173 | else:
174 | print('No matching suggestion. Proceeding to sending negotiation request to other party.')
175 | else:
176 | upper = int(float(input(f'{b}TON you want to give to freelancer (upper limit):{nb} ')) * 1e9)
177 | bottom = int(float(input(f'{b}TON you want to give to freelancer (bottom limit):{nb} ')) * 1e9)
178 |
179 | p = sign_pay_proposal(job, bottom, upper, skip_role, available_keys[0][1], keyring)
180 | msg = Builder()
181 | msg.store_uint(0x4bed4ee8, 32)
182 | msg.store_ref(p)
183 | sign_send([
184 | (job, None, msg.end_cell(), 2)
185 | ], 'negotiating payment for job ' + job.to_string(True, True, True))
186 |
187 | else:
188 | print(f'{b}not implemented:{nb} {repr(command)}')
189 |
--------------------------------------------------------------------------------
/cli/jobs.py:
--------------------------------------------------------------------------------
1 | from .bcutils import decode_text, encode_text, load_transactions, shorten_escape, input_address, load_account, input_long
2 | from .signing import retrieve_auth_wallet, sign_send
3 | from .colors import h, nh, b, nb
4 | from .keyring import Keyring
5 |
6 | from base64 import b64decode, b16decode, b16encode
7 | import traceback
8 | import hashlib
9 |
10 | from tonsdk.boc import Builder, Cell
11 | from tonsdk.utils import Address
12 | from .tslice import Slice
13 |
14 |
15 |
16 | JOB_NOTIFICATIONS = 'EQA__RATELANCE_______________________________JvN'
17 |
18 |
19 | def job_data_init(poster: str, value: int, desc: Cell, key: int) -> Cell:
20 | di = Builder()
21 | di.store_uint(0, 2)
22 | di.store_address(Address(poster))
23 | di.store_uint(value, 64)
24 | di.store_ref(desc)
25 | di.store_uint(key, 256)
26 | return di.end_cell()
27 |
28 |
29 | def job_state_init(poster: str, value: int, desc: Cell, key: int) -> Cell:
30 | with open(__file__ + '/../assets/contract-job.boc', 'rb') as f:
31 | code = Cell.one_from_boc(f.read())
32 |
33 | si = Builder()
34 | si.store_uint(6, 5)
35 | si.store_ref(code)
36 | si.store_ref(job_data_init(poster, value, desc, key))
37 | return si.end_cell()
38 |
39 |
40 | def analytic_msg(job: Address, value: int, desc: Cell, key: int) -> Cell:
41 | am = Builder()
42 | am.store_uint(0x130850fc, 32)
43 | am.store_address(job)
44 | am.store_uint(value, 64)
45 | am.store_ref(desc)
46 | am.store_uint(key, 256)
47 | return am.end_cell()
48 |
49 |
50 | def load_jobs(start_lt=None, custom_notif_addr=None):
51 | if start_lt: raise Exception('loading specific jobs not supported')
52 |
53 | notif = custom_notif_addr or JOB_NOTIFICATIONS
54 | for tx in load_transactions(notif, start_lt=start_lt):
55 | try:
56 | body = Cell.one_from_boc(b64decode(tx['in_msg']['msg_data'])).begin_parse()
57 |
58 | if body.preload_uint(32) != 0x130850fc:
59 | # legacy job notification [without TL-B tag]
60 | assert tx['hash'] in (
61 | '155ccfdc282660413ada2c1b71ecbd5935db7afd40c82d7b36b8502fea064b8a',
62 | 'f2c76d3ea82e6147887320a71b359553f99cb176a521d63081facfb80a183dbf',
63 | )
64 | else:
65 | body.load_uint(32)
66 |
67 | job = body.load_msg_addr()
68 | poster = Address(tx['in_msg']['source']['address']).to_string(True, True, True)
69 | value = body.load_uint(64)
70 | desc = body.load_ref()
71 | desc_text = decode_text(desc)
72 | poster_key = body.load_uint(256)
73 |
74 | # TODO: skip notifications with value < 0.05 TON
75 |
76 | if job.hash_part != job_state_init(poster, value, desc, poster_key).bytes_hash():
77 | print(f'{b}Found notification with invalid job address:{nb}', job.to_string())
78 | print(f'* {h}poster: {nh}{poster}')
79 | print(f'* {h}description: {nh}{shorten_escape(desc_text)}')
80 | else:
81 | yield (job.to_string(True, True, True), poster, value, desc_text)
82 | except Exception as exc:
83 | print(f'{b}Failure while processing notification:{nb}', repr(exc))
84 |
85 |
86 | def show_jobs(start_lt=None, custom_notif_addr=None, validate_jobs=False):
87 | if validate_jobs: raise Exception('validating jobs not supported')
88 |
89 | for (job, poster, value, desc) in load_jobs(start_lt, custom_notif_addr):
90 | jid = job[40:]
91 | print(f'Order [{h}{jid}{nh}] {job}')
92 | print(f'- {h}posted by{nh} {poster}')
93 | print(f'- {h}promising{nh} {value/1e9} TON, {h}staked{nh} ')
94 | print('-', shorten_escape(desc))
95 |
96 |
97 | def post_job(value: int, stake: int, desc_text: str, keyring: Keyring, key_id: str):
98 | print(f'\n{h}Creating new job{nh}', repr(desc_text))
99 |
100 | if not key_id.strip() and len(keyring.keys_info) == 1:
101 | key_id = list(keyring.keys_info.keys())[0]
102 |
103 | key_info = keyring.keys_info[key_id]
104 | assert key_info['key_id'] == key_id
105 | public_key = int.from_bytes(key_info['public'], 'big')
106 |
107 | WAY_PROMPT = f'Send via mnemonic [{h}m{nh}]/wallet seed [{h}s{nh}]/ton link [{h}t{nh}]? '
108 | while (auth_way := input(WAY_PROMPT).lower()) not in ('m', 's', 't'): pass
109 |
110 | if auth_way == 't':
111 | wallet = None
112 | poster = input_address(f'{b}Your address: {nb}')
113 | else:
114 | wallet = retrieve_auth_wallet(auth_way)
115 | poster = wallet.address.to_string(True, True, True)
116 |
117 | desc = encode_text(desc_text)
118 | si = job_state_init(poster, value, desc, public_key)
119 | addr = Address('0:' + b16encode(si.bytes_hash()).decode('ascii'))
120 | am = analytic_msg(addr, value, desc, public_key)
121 |
122 | jobs = Address(JOB_NOTIFICATIONS)
123 | jobs.is_bounceable = False
124 |
125 | print()
126 | sign_send([
127 | (addr, si, Cell(), stake),
128 | (jobs, None, am, 5*10**7),
129 | ], 'creating job', auth_way, wallet)
130 |
131 |
132 | def delegate_job(job: str, offer_addr: str):
133 | msg = Builder()
134 | msg.store_uint(0x000000AC, 32)
135 | msg.store_ref(Cell())
136 | msg.store_address(Address(offer_addr))
137 |
138 | sign_send([(Address(job), None, msg.end_cell(), 10**9)], 'delegating job')
139 |
140 |
141 | def public_key_desc(key: bytes, keyring) -> str:
142 | key_hex = b16encode(key).decode('ascii')
143 | key_armored = 'pub:ed25519:vk:' + key_hex
144 | key_id = hashlib.sha256(key_armored.encode('ascii')).hexdigest()[::8]
145 | not_present = 'not ' if key_id not in keyring.keys_info else ''
146 | return f'{key_hex} ({key_id}, {h}{not_present}present{nh} in local keyring)'
147 |
148 |
149 | def show_job(job: str, keyring):
150 | acc = load_account(job)
151 |
152 | if acc['status'] != 'active':
153 | print('* contract', acc['status'])
154 | return
155 |
156 | d = Cell.one_from_boc(b16decode(acc['data'].upper())).begin_parse()
157 | flag = d.load_uint(2)
158 |
159 | if flag == 0:
160 | print(f'* job {h}waiting for offers{nh}')
161 | print(f'- {h}posted by {nh}', d.load_msg_addr().to_string(True, True, True))
162 | print(f'- {h}promising {nh}', d.load_uint(64) / 1e9, 'TON')
163 | print(f'- {h}public key{nh}', public_key_desc(d.load_bytes(32), keyring))
164 |
165 | j_desc_text = decode_text(d.load_ref())
166 | print(f'- {h}job descr {nh}', shorten_escape(j_desc_text, indent=12))
167 |
168 | d.end_parse()
169 | elif flag == 1:
170 | print(f'* job {h}locked on offer{nh}', d.load_msg_addr().to_string(True, True, True))
171 | print(f'- {h}posted by {nh}', d.load_msg_addr().to_string(True, True, True))
172 | print(f'- {h}promising {nh}', d.load_uint(64) / 1e9, 'TON')
173 | print(f'- {h}public key{nh}', public_key_desc(d.load_bytes(32), keyring))
174 |
175 | j_desc_text = decode_text(d.load_ref())
176 | print(f'- {h}job descr {nh}', shorten_escape(j_desc_text, indent=12))
177 |
178 | d.end_parse()
179 | elif flag == 2:
180 | print(f'* {h}taken{nh} job')
181 | print(f'- {h}posted by {nh}', d.load_msg_addr().to_string(True, True, True))
182 | print(f'- {h}worker {nh}', d.load_msg_addr().to_string(True, True, True))
183 | print(f'- {h}promising {nh}', d.load_uint(64) / 1e9, 'TON')
184 |
185 | j_desc_text = decode_text(d.load_ref())
186 | print(f'- {h}job descr {nh}', shorten_escape(j_desc_text, indent=13))
187 | o_desc_text = decode_text(d.load_ref())
188 | print(f'- {h}offer descr{nh}', shorten_escape(o_desc_text, indent=13))
189 |
190 | keys = d.load_ref().begin_parse()
191 | print(f'- {h} poster key{nh}', public_key_desc(keys.load_bytes(32), keyring))
192 | print(f'- {h} worker key{nh}', public_key_desc(keys.load_bytes(32), keyring))
193 | print(f'- {h}Ratelance key{nh}', public_key_desc(keys.load_bytes(32), keyring))
194 |
195 | keys.end_parse()
196 | d.end_parse()
197 | else:
198 | print('* broken job contract')
199 |
200 |
201 | def process_jobs_cmd(command, keyring):
202 | if command == 'jl':
203 | show_jobs()
204 | elif command == 'jp':
205 | possible_keys = list(keyring.keys_info.keys())
206 | if len(possible_keys) > 3:
207 | possible_keys_s = '/'.join(possible_keys[:3]) + '/...'
208 | else:
209 | possible_keys_s = '/'.join(possible_keys) or ''
210 |
211 | key_id = input(f'Used key ({possible_keys_s}): ')
212 | value = int(1e9 * float(input('Promised job value (TON): ')))
213 | stake = int(1e9 * float(input('Send stake (TON): ')))
214 | desc_text = input_long('Job description: ')
215 |
216 | post_job(value, stake, desc_text, keyring, key_id)
217 | elif command == 'jd':
218 | job = input_address('Job address: ')
219 | offer = input_address('Offer address: ')
220 |
221 | delegate_job(job, offer)
222 | elif command == 'ji':
223 | try:
224 | show_job(input_address('Job address: ').to_string(True, True, True), keyring)
225 | except Exception as exc:
226 | print(f'{b}Invalid job:{nb}', repr(exc))
227 | else:
228 | print(f'{b}not implemented:{nb} {repr(command)}')
--------------------------------------------------------------------------------
/cli/keyring.py:
--------------------------------------------------------------------------------
1 | from .colors import nh, h, nb, b, ns, s
2 |
3 | from base64 import b16encode as hex_encode_b2b
4 | from base64 import b16decode
5 | import traceback
6 | import hashlib
7 | import time
8 | import os
9 |
10 | import nacl.signing
11 | import nacl.secret
12 |
13 |
14 |
15 | def b16encode(v):
16 | return hex_encode_b2b(v).decode('ascii')
17 |
18 | class Keyring:
19 | def __init__(self, path=None):
20 | self.path = path or self.generate_keyring_path()
21 | self.keys_info = {}
22 |
23 | def __enter__(self):
24 | try:
25 | with open(self.path, 'r') as f:
26 | self.parse_keys_from(f)
27 | except IOError:
28 | open(self.path, 'x').close()
29 | return self
30 |
31 | def __exit__(self, exc_type, exc_val, exc_trace):
32 | try:
33 | with open(self.path, 'r') as f:
34 | self.parse_keys_from(f)
35 | except IOError:
36 | pass
37 |
38 | self.flush_keys()
39 |
40 | if exc_val:
41 | raise
42 |
43 | def generate_keyring_path(self):
44 | appdata = os.environ['LOCALAPPDATA']
45 | return os.path.abspath(appdata + '/ratelance-private-keys.dat')
46 |
47 | def flush_keys(self):
48 | with open(self.path, 'w') as f:
49 | self.write_keys_to(f)
50 |
51 | def parse_keys_from(self, file):
52 | for line in file:
53 | version, public_key, secret_key, name = line.strip().split(' ', 3)
54 | assert version == 'v0.0.2'
55 | assert public_key.startswith('pub:ed25519:vk:')
56 | assert secret_key.startswith('prv:ed25519:sk:')
57 |
58 | key_id = hashlib.sha256(public_key.encode('ascii')).hexdigest()[::8]
59 |
60 | self.keys_info[key_id] = {
61 | 'public': b16decode(public_key.removeprefix('pub:ed25519:vk:')),
62 | 'secret': b16decode(secret_key.removeprefix('prv:ed25519:sk:')),
63 | 'key_id': key_id,
64 | 'name': name
65 | }
66 |
67 | def write_keys_to(self, file):
68 | for (key_id, key_info) in self.keys_info.items():
69 | public_key = 'pub:ed25519:vk:' + b16encode(key_info['public'])
70 | secret_key = 'prv:ed25519:sk:' + b16encode(key_info['secret'])
71 |
72 | print('v0.0.2', public_key, secret_key, key_info['name'],
73 | file=file, flush=True)
74 |
75 | def add_key(self, secret_bytes, name):
76 | secret_key_obj = nacl.signing.SigningKey(secret_bytes)
77 | public_bytes = secret_key_obj.verify_key.encode()
78 |
79 | public_key_armored = 'pub:ed25519:vk:' + b16encode(public_bytes)
80 | key_id = hashlib.sha256(public_key_armored.encode('ascii')).hexdigest()[::8]
81 |
82 | self.keys_info[key_id] = {
83 | 'public': public_bytes,
84 | 'secret': secret_bytes,
85 | 'key_id': key_id,
86 | 'name': name
87 | }
88 | self.flush_keys()
89 | return self.keys_info[key_id]
90 |
91 | def generate_new_key(self):
92 | return self.add_key(
93 | nacl.secret.random(32),
94 | f'''key@[{time.strftime('%Y.%m.%d %H:%M:%S')}]'''
95 | )
96 |
97 |
98 | def process_keyring_cmd(command, keyring):
99 | if command == 'kl':
100 | print('Keys list:')
101 |
102 | for (key_id, key_info) in keyring.keys_info.items():
103 | print(f'- {b}{key_id}{nb} -', repr(key_info['name']))
104 |
105 | elif command == 'ke':
106 | key_id = input(f' {h}Key ID:{nh} ')
107 |
108 | try:
109 | key_info = keyring.keys_info[key_id]
110 | assert key_info['key_id'] == key_id
111 | print('- ID: ', key_id)
112 | print('- Name: ', key_info['name'])
113 | print('- Public key:', b16encode(key_info['public']))
114 | print('- Secret key:', b16encode(key_info['secret']))
115 | except:
116 | traceback.print_exc()
117 |
118 | elif command == 'ki':
119 | secret = input(f' {h}HEX secret key:{nh} ')
120 | name = input(f' {h}Key name:{nh} ')
121 |
122 | try:
123 | secret_bytes = b16decode(secret.upper())
124 | assert len(secret_bytes) == 32
125 | keyring.add_key(secret_bytes, name)
126 | except:
127 | traceback.print_exc()
128 |
129 | elif command == 'kn':
130 | key_info = keyring.generate_new_key()
131 | print(f'{h}Created key{nh}', key_info['key_id'], repr(key_info['name']))
132 |
133 | else:
134 | print(f'{b}not implemented:{nb} {repr(command)}')
135 |
--------------------------------------------------------------------------------
/cli/offers.py:
--------------------------------------------------------------------------------
1 | from .bcutils import decode_text, encode_text, load_transactions, shorten_escape, input_address, load_account
2 | from .signing import retrieve_auth_wallet, sign_send, sign_install_plugin, sign_uninstall_plugin
3 | from .colors import h, nh, b, nb
4 | from .keyring import Keyring
5 |
6 | from base64 import b64decode, b16decode, b16encode
7 | import traceback
8 | import time
9 |
10 | from tonsdk.boc import Builder, Cell
11 | from tonsdk.utils import Address
12 | from .tslice import Slice
13 |
14 |
15 |
16 | def offer_data_init(job: str, worker: Address, stake: int, desc: Cell,
17 | key: int, short_job_hash: int) -> Cell:
18 | di = Builder()
19 | di.store_uint(0, 2)
20 | di.store_address(Address(job))
21 | di.store_address(worker)
22 | di.store_uint(stake, 64)
23 | di.store_ref(desc)
24 | di.store_uint(key, 256)
25 | di.store_uint(short_job_hash, 160)
26 | return di.end_cell()
27 |
28 |
29 | def offer_state_init(job: str, worker: Address, stake: int, desc: Cell,
30 | key: int, short_job_hash: int) -> Cell:
31 | with open(__file__ + '/../assets/contract-offer.boc', 'rb') as f:
32 | code = Cell.one_from_boc(f.read())
33 |
34 | si = Builder()
35 | si.store_uint(6, 5)
36 | si.store_ref(code)
37 | si.store_ref(offer_data_init(job, worker, stake, desc, key, short_job_hash))
38 | return si.end_cell()
39 |
40 |
41 | def analytic_msg(offer: Address, stake: int, desc: Cell,
42 | key: int, short_job_hash: int) -> Cell:
43 | am = Builder()
44 | am.store_uint(0x18ceb1bf, 32)
45 | am.store_address(offer)
46 | am.store_uint(stake, 64)
47 | am.store_ref(desc)
48 | am.store_uint(key, 256)
49 | am.store_uint(short_job_hash, 160)
50 | return am.end_cell()
51 |
52 |
53 | def load_offers(job: str, start_lt=None):
54 | if start_lt: raise Exception('loading specific offers not supported')
55 |
56 | job_data = Cell.one_from_boc(b16decode(load_account(job)['data'].upper()))
57 | job_hash = int.from_bytes(job_data.bytes_hash(), 'big') & ((1 << 160) - 1)
58 |
59 | if job_data.begin_parse().load_uint(2) != 0:
60 | print(f'{b}[job not in unlocked state]{nb}')
61 |
62 | for tx in load_transactions(job, start_lt=start_lt):
63 | try:
64 | if tx['in_msg']['value'] != 1: continue
65 |
66 | worker = Address(tx['in_msg']['source']['address'])
67 | body = Cell.one_from_boc(b64decode(tx['in_msg']['msg_data'])).begin_parse()
68 |
69 | if body.preload_uint(32) not in (0x18ceb1bf, 0x4bed4ee8):
70 | print(f'\n{b}* legacy offer notification [without TL-B tag]{nb}')
71 | assert tx['hash'] in (
72 | '4109a9bf38f7376acdb013ff95e33ebb5112c00ebd9d93a348361522400b5783',
73 | '8fb9cb7532a8d6a710106d1c346f99bdd22a2d74480858956ecc19a02e1dfd8d',
74 | 'a598c47a792ceccb9e26b2cd5cc73c7a6024dae12f24ae20747182966b407b65',
75 | )
76 | else:
77 | body.skip_bits(32)
78 |
79 | offer = body.load_msg_addr()
80 | stake = body.load_uint(64)
81 | desc = body.load_ref()
82 | desc_text = decode_text(desc)
83 | key = body.load_uint(256)
84 | shjh = body.load_uint(160)
85 |
86 | hash_up_to_date = shjh == job_hash
87 |
88 | if offer.hash_part != offer_state_init(job, worker, stake, desc, key, shjh).bytes_hash():
89 | print(f'{b}Found notification with invalid offer address:{nb}', offer.to_string())
90 | print(f'* {h}worker: {nh}{worker.to_string()}')
91 | print(f'* {h}description: {nh}{repr(desc_text)}')
92 | else:
93 | yield (offer.to_string(True, True, True), worker.to_string(True, True, True),
94 | stake, desc_text, hash_up_to_date)
95 | except Exception as exc:
96 | print(f'{b}Failure while processing notification:{nb}', repr(exc))
97 |
98 |
99 | def show_offers(job: str, start_lt=None, validate_offers=False):
100 | if validate_offers: raise Exception('validating offers not supported')
101 | # requires checking whether contract exists and is attached as plugin to worker's wallet
102 |
103 | for (offer, worker, stake, desc, hash_up_to_date) in load_offers(job, start_lt):
104 | oid = offer[40:]
105 | print(f'\nOffer [{h}{oid}{nh}] {offer}')
106 | print(f'- {h}posted by{nh} {worker}')
107 | print(f'- {h}staked{nh} {stake/1e9} TON, {h}really available{nh} ')
108 | print(f'- {h}hash{nh} {"" if hash_up_to_date else "not "}up to date')
109 | print('-', shorten_escape(desc))
110 |
111 |
112 | def post_offer(job: str, stake: int, desc_text: str, shjh: int, keyring: Keyring, key_id: str):
113 | print(f'\n{h}Creating new offer{nh}', repr(desc_text))
114 | print('You will have to use v4 wallet, because offer is a plugin.')
115 |
116 | if not key_id.strip() and len(keyring.keys_info) == 1:
117 | key_id = list(keyring.keys_info.keys())[0]
118 |
119 | key_info = keyring.keys_info[key_id]
120 | assert key_info['key_id'] == key_id
121 | public_key = int.from_bytes(key_info['public'], 'big')
122 |
123 | WAY_PROMPT = f'Send via mnemonic [{h}m{nh}]/wallet seed [{h}s{nh}]? '
124 | while (auth_way := input(WAY_PROMPT).lower()) not in ('m', 's'): pass
125 |
126 | wallet = retrieve_auth_wallet(auth_way, plugin_only=True)
127 | worker = wallet.address.to_string(True, True, True)
128 |
129 | desc = encode_text(desc_text)
130 | si = offer_state_init(job, wallet.address, stake, desc, public_key, shjh)
131 | addr = Address('0:' + b16encode(si.bytes_hash()).decode('ascii'))
132 | am = analytic_msg(addr, stake, desc, public_key, shjh)
133 |
134 | print()
135 | sign_install_plugin(si, 5*10**7, 'creating offer', wallet)
136 | print('... please, wait for blockchain update (30s) ...')
137 | time.sleep(30)
138 | print()
139 | sign_send([(job, None, am, 1)], 'notifying job poster', auth_way, wallet)
140 |
141 |
142 | def revoke_offer(offer: str):
143 | sign_uninstall_plugin(offer, 'revoking offer')
144 |
145 |
146 | def process_offers_cmd(command, keyring):
147 | if command == 'ol':
148 | # TODO: support job IDs instead of addresses
149 | job = input_address(f'{b}Job address: {nb}')
150 |
151 | show_offers(job.to_string())
152 | elif command == 'op':
153 | possible_keys = list(keyring.keys_info.keys())
154 | if len(possible_keys) > 3:
155 | possible_keys_s = '/'.join(possible_keys[:3]) + '/...'
156 | else:
157 | possible_keys_s = '/'.join(possible_keys) or ''
158 |
159 | key_id = input(f'Used key ({possible_keys_s}): ')
160 | job = input_address(f'{b}Job address: {nb}')
161 | stake = int(1e9 * float(input('Staked value (TON): ')))
162 | desc_text = input('Offer description: ')
163 |
164 | job_data = Cell.one_from_boc(b16decode(
165 | load_account(job.to_string())['data'].upper()
166 | ))
167 | assert job_data.begin_parse().load_uint(2) == 0
168 | job_hash = int.from_bytes(job_data.bytes_hash(), 'big') & ((1 << 160) - 1)
169 |
170 | post_offer(job, stake, desc_text, job_hash, keyring, key_id)
171 | elif command == 'or':
172 | revoke_offer(input_address(f'{b}Offer address: {nb}').to_string(True, True, True))
173 | else:
174 | print(f'{b}not implemented:{nb} {repr(command)}')
--------------------------------------------------------------------------------
/cli/polyfills.py:
--------------------------------------------------------------------------------
1 | from tonsdk.boc import Cell
2 | from .tslice import Slice
3 |
4 | Cell.begin_parse = lambda self: Slice(self)
5 |
--------------------------------------------------------------------------------
/cli/signing.py:
--------------------------------------------------------------------------------
1 | from .colors import nh, h, nb, b, ns, s
2 |
3 | from base64 import b16decode, b64encode, urlsafe_b64encode
4 | from getpass import getpass
5 | import time
6 |
7 | from tonsdk.contract.wallet import Wallets
8 | from tonsdk.contract import Contract
9 | from tonsdk.utils import Address
10 | from tonsdk.boc import Cell
11 | import tonsdk.crypto
12 | import nacl.signing
13 | # TODO: move `requests` out of functions/modules where secret keys are accessed
14 | import requests
15 |
16 |
17 |
18 | def retrieve_keypair(auth_way: str):
19 | if auth_way == 'm':
20 | while True:
21 | mnemonic = getpass(f'{b}Your wallet mnemonic (not echoed):{nb} ').split()
22 | if tonsdk.crypto.mnemonic_is_valid(mnemonic): break
23 |
24 | use_anyway = input(f'{b}Entered mnemonic is invalid. Use it anyway?{nb} [y/n] ').lower()
25 | if use_anyway == 'y': break
26 |
27 | _, secret_key = tonsdk.crypto.mnemonic_to_wallet_key(mnemonic)
28 | secret_key = secret_key[:32]
29 | del mnemonic
30 | elif auth_way == 's':
31 | while True:
32 | secret_hex = getpass(f'{b}Your secret key in HEX (not echoed):{nb} ').upper()
33 | if not set('0123456789ABCDEF').issuperset(secret_hex):
34 | print('Invalid characters met')
35 | elif len(secret_hex) != 64:
36 | print('Invalid key length')
37 | else:
38 | break
39 |
40 | secret_key = b16decode(secret_hex)
41 | del secret_hex
42 | else:
43 | raise Exception('unsupported auth way for retrieving keypair')
44 |
45 | secret_key_obj = nacl.signing.SigningKey(secret_key)
46 | public_key = secret_key_obj.verify_key.encode()
47 | secret_key = secret_key_obj.encode() + public_key # making secret key 64-byte
48 | return public_key, secret_key
49 |
50 |
51 | def retrieve_auth_wallet(auth_way: str, plugin_only=False):
52 | public_key, secret_key = retrieve_keypair(auth_way)
53 |
54 | WALLET_V = ['v4r2'] if plugin_only else ['v3r1', 'v3r2', 'v4r2']
55 | WALLET_PROMPT = 'Enter wallet version (' + b + '/'.join(WALLET_V) + nb + '): '
56 | while (wallet_ver := input(WALLET_PROMPT).lower()) not in WALLET_V: pass
57 |
58 | wallet_class = Wallets.ALL[wallet_ver]
59 | return wallet_class(public_key=public_key, private_key=secret_key)
60 |
61 |
62 | # orders: list[tuple[to_addr, state_init, payload, amount]]
63 | def sign_multitransfer_body(wallet: Contract, seqno: int,
64 | orders: list[tuple[Address,Cell,Cell,int]]) -> Cell:
65 | assert len(orders) <= 4
66 |
67 | send_mode = 3
68 | signing_message = wallet.create_signing_message(seqno)
69 |
70 | for (to_addr, state_init, payload, amount) in orders:
71 | order_header = Contract.create_internal_message_header(to_addr, amount)
72 | order = Contract.create_common_msg_info(order_header, state_init, payload)
73 | signing_message.bits.write_uint8(send_mode)
74 | signing_message.refs.append(order)
75 |
76 | return wallet.create_external_message(signing_message, seqno)['message']
77 |
78 |
79 | def sign_for_sending(orders: list[tuple[Address,Cell,Cell,int]],
80 | description: str, auth_way=None, wallet=None) -> Cell:
81 | print(f'{h}Sending messages for purpose of{nh}', repr(description))
82 |
83 | sum_value = 0
84 | for (dest, state_init, message, value_nton) in orders:
85 | init_flag = f'{b}[deploy]{nb}' if state_init else ''
86 |
87 | print('===')
88 | print(f'{h}Destination:{nh}', dest.to_string(True, True, True), init_flag)
89 | print(f'{h}TON amount: {nh}', value_nton / 1e9)
90 | print(f'{h}Message BOC:{nh}', b64encode(message.to_boc(False)).decode('ascii'))
91 | sum_value += value_nton
92 |
93 | print('===')
94 | print(f'{h}Total TON: {nh} {sum_value / 1e9}')
95 | print()
96 |
97 | if not auth_way:
98 | WAY_PROMPT = f'Send via mnemonic [{h}m{nh}]/wallet seed [{h}s{nh}]/ton link [{h}t{nh}]? '
99 | while (auth_way := input(WAY_PROMPT).lower()) not in ('m', 's', 't'): pass
100 |
101 | if auth_way == 't':
102 | print('\nTransfer links:')
103 |
104 | for (dest, state_init, message, value_nton) in orders:
105 | addr = dest.to_string(True, True, True)
106 | boc = urlsafe_b64encode(message.to_boc(False)).decode('ascii')
107 | link = f'ton://transfer/{addr}?bin={boc}&amount={value_nton}'
108 | if state_init:
109 | link += '&init='
110 | link += urlsafe_b64encode(state_init.to_boc(False)).decode('ascii')
111 |
112 | print(f'{b}{link}{nb}')
113 |
114 | return None
115 |
116 | if not wallet:
117 | wallet = retrieve_auth_wallet(auth_way)
118 | addr = wallet.address.to_string(True, True, True)
119 |
120 | print('Ready to do transfer from', addr)
121 |
122 | while (confirm := input(f'{h}Confirm? [y/n] {nh}').lower()) not in ('y', 'n'):
123 | pass
124 |
125 | if confirm == 'n': return None
126 |
127 | link = f'https://tonapi.io/v1/wallet/getSeqno?account={addr}'
128 | seqno = requests.get(link).json().get('seqno', 0)
129 |
130 | return sign_multitransfer_body(wallet, seqno, orders)
131 |
132 |
133 | def sign_send(orders: list[tuple[Address,Cell,Cell,int]],
134 | description: str, auth_way=None, wallet=None):
135 | signed_msg = sign_for_sending(orders, description, auth_way, wallet)
136 | if signed_msg:
137 | requests.post('https://tonapi.io/v1/send/boc', json={
138 | 'boc': b64encode(signed_msg.to_boc(False)).decode('ascii')
139 | })
140 |
141 |
142 | def sign_plugin(plugin_init: Cell, value_nton: int,
143 | description: str, wallet=None) -> Cell:
144 | print(f'{h}Attempting to install plugin{nh}', repr(description))
145 | print(f'{h}Init BOC: {nh}', b64encode(plugin_init.to_boc(False)).decode('ascii'))
146 | print(f'{h}TON amount: {nh}', value_nton / 1e9)
147 | print()
148 |
149 | if not wallet:
150 | WAY_PROMPT = f'Install via mnemonic [{h}m{nh}]/wallet seed [{h}s{nh}]? '
151 | while (auth_way := input(WAY_PROMPT).lower()) not in ('m', 's'): pass
152 | wallet = retrieve_auth_wallet(auth_way, plugin_only=True)
153 | addr = wallet.address.to_string(True, True, True)
154 |
155 | print('Ready to install plugin to', addr)
156 |
157 | while (confirm := input(f'{h}Confirm? [y/n] {nh}').lower()) not in ('y', 'n'):
158 | pass
159 |
160 | if confirm == 'n': return None
161 |
162 | link = f'https://tonapi.io/v1/wallet/getSeqno?account={addr}'
163 | seqno = requests.get(link).json().get('seqno', 0)
164 |
165 | msg_body = wallet.create_signing_message(seqno, without_op=True)
166 | msg_body.bits.write_uint(1, 8) # deploy + install plugin
167 | msg_body.bits.write_int(0, 8) # workchain 0
168 | msg_body.bits.write_coins(value_nton) # initial plugin balance
169 | msg_body.refs.append(plugin_init)
170 | msg_body.refs.append(Cell())
171 |
172 | return wallet.create_external_message(msg_body, seqno)['message']
173 |
174 |
175 | def sign_install_plugin(plugin_init: Cell, value_nton: int,
176 | description: str, wallet=None) -> Cell:
177 | signed_msg = sign_plugin(plugin_init, value_nton, description, wallet)
178 | if signed_msg:
179 | requests.post('https://tonapi.io/v1/send/boc', json={
180 | 'boc': b64encode(signed_msg.to_boc(False)).decode('ascii')
181 | })
182 |
183 |
184 | def sign_unplug(plugin: str, description: str, wallet=None) -> Cell:
185 | print(f'{h}Attempting to remove plugin{nh}', repr(description))
186 | print(f'{h}Address:{nh}', plugin)
187 | print()
188 |
189 | if not wallet:
190 | WAY_PROMPT = f'Remove via mnemonic [{h}m{nh}]/wallet seed [{h}s{nh}]? '
191 | while (auth_way := input(WAY_PROMPT).lower()) not in ('m', 's'): pass
192 | wallet = retrieve_auth_wallet(auth_way, plugin_only=True)
193 | addr = wallet.address.to_string(True, True, True)
194 |
195 | print('Ready to remove plugin from', addr)
196 |
197 | while (confirm := input(f'{h}Confirm? [y/n] {nh}').lower()) not in ('y', 'n'):
198 | pass
199 |
200 | if confirm == 'n': return None
201 |
202 | link = f'https://tonapi.io/v1/wallet/getSeqno?account={addr}'
203 | seqno = requests.get(link).json().get('seqno', 0)
204 |
205 | plugin_addr = Address(plugin)
206 |
207 | msg_body = wallet.create_signing_message(seqno, without_op=True)
208 | msg_body.bits.write_uint(3, 8) # remove plugin
209 | msg_body.bits.write_int(plugin_addr.wc, 8)
210 | msg_body.bits.write_bytes(plugin_addr.hash_part)
211 | msg_body.bits.write_coins(5*10**7) # value to send for destroying
212 | msg_body.bits.write_uint(int(time.time() * 1000), 64)
213 |
214 | return wallet.create_external_message(msg_body, seqno)['message']
215 |
216 |
217 | def sign_uninstall_plugin(plugin: str, description: str, wallet=None) -> Cell:
218 | signed_msg = sign_unplug(plugin, description, wallet)
219 | if signed_msg:
220 | requests.post('https://tonapi.io/v1/send/boc', json={
221 | 'boc': b64encode(signed_msg.to_boc(False)).decode('ascii')
222 | })
223 |
224 |
--------------------------------------------------------------------------------
/cli/tslice.py:
--------------------------------------------------------------------------------
1 | # Modified version of _slice.py from fork of tonsdk library:
2 | # https://github.com/devdaoteam/tonsdk/blob/e3d6451e50a46d984e7fec1c52d1d32290781da5/tonsdk/boc/_slice.py
3 | # Original code is licensed under Apache-2.0 License.
4 |
5 | import bitarray
6 |
7 | from tonsdk.boc import Cell
8 | from tonsdk.utils import Address
9 |
10 |
11 |
12 | class Slice:
13 | '''Slice like an analog of slice in FunC. Used only for reading.'''
14 | def __init__(self, cell: Cell):
15 | self.bits = bitarray.bitarray()
16 | self.bits.frombytes(cell.bits.array)
17 | self.bits = self.bits[:cell.bits.cursor]
18 | self.refs = cell.refs
19 | self.ref_offset = 0
20 |
21 | def __len__(self):
22 | return len(self.bits)
23 |
24 | def __repr__(self):
25 | return hex(int(self.bits.to01(), 2))[2:].upper()
26 |
27 | def is_empty(self) -> bool:
28 | return len(self.bits) == 0
29 |
30 | def end_parse(self):
31 | '''Throws an exception if the slice is not empty.'''
32 | if not self.is_empty() or self.ref_offset != len(self.refs):
33 | raise Exception('Upon .end_parse(), slice is not empty.')
34 |
35 | def load_bit(self) -> int:
36 | '''Loads single bit from the slice.'''
37 | bit = self.bits[0]
38 | del self.bits[0]
39 | return bit
40 |
41 | def preload_bit(self) -> int:
42 | return self.bits[0]
43 |
44 | def load_bits(self, bit_count: int) -> bitarray.bitarray:
45 | bits = self.bits[:bit_count]
46 | del self.bits[:bit_count]
47 | return bits
48 |
49 | def preload_bits(self, bit_count: int) -> bitarray.bitarray:
50 | return self.bits[:bit_count]
51 |
52 | def skip_bits(self, bit_count: int):
53 | del self.bits[:bit_count]
54 |
55 | def load_uint(self, bit_length: int) -> int:
56 | value = self.bits[:bit_length]
57 | del self.bits[:bit_length]
58 | return int(value.to01(), 2)
59 |
60 | def preload_uint(self, bit_length: int) -> int:
61 | value = self.bits[:bit_length]
62 | return int(value.to01(), 2)
63 |
64 | def load_bytes(self, bytes_count: int) -> bytes:
65 | length = bytes_count * 8
66 | value = self.bits[:length]
67 | del self.bits[:length]
68 | return value.tobytes()
69 |
70 | def load_int(self, bit_length: int) -> int:
71 | if bit_length == 1:
72 | # if num is -1 then bit is 1. if 0 then 0. see _bit_string.py
73 | return - self.load_bit()
74 | else:
75 | is_negative = self.load_bit()
76 | value = self.load_uint(bit_length - 1)
77 | if is_negative == 1:
78 | # ones complement
79 | return - (2 ** (bit_length - 1) - value)
80 | else:
81 | return value
82 |
83 | def preload_int(self, bit_length: int) -> int:
84 | tmp = self.bits
85 | value = self.load_int(bit_length)
86 | self.bits = tmp
87 | return value
88 |
89 | def load_msg_addr(self) -> Address:
90 | '''Loads contract address from the slice.
91 | May return None if there is a zero-address.'''
92 | # TODO: support for external addresses
93 | if self.load_uint(2) == 0:
94 | return None
95 | self.load_bit() # anycast
96 | workchain_id = hex(self.load_int(8))[2:]
97 | hashpart = hex(self.load_uint(256))[2:].zfill(64)
98 | return Address(workchain_id + ':' + hashpart)
99 |
100 | def load_coins(self) -> int:
101 | '''Loads an amount of coins from the slice. Returns nanocoins.'''
102 | length = self.load_uint(4)
103 | if length == 0: # 0 in length means 0 coins
104 | return 0
105 | else:
106 | return self.load_uint(length * 8)
107 |
108 | def load_grams(self) -> int:
109 | '''Loads an amount of coins from the slice. Returns nanocoins.'''
110 | return self.load_coins()
111 |
112 | def load_string(self, length: int = 0) -> str:
113 | '''Loads string from the slice.
114 | If length is 0, then loads string until the end of the slice.'''
115 | if length == 0:
116 | length = len(self.bits) // 8
117 | return self.load_bytes(length).decode('utf-8')
118 |
119 | def load_ref(self) -> Cell:
120 | '''Loads next reference cell from the slice.'''
121 | ref = self.refs[self.ref_offset]
122 | self.ref_offset += 1
123 | return ref
124 |
125 | def preload_ref(self) -> Cell:
126 | return self.refs[self.ref_offset]
127 |
128 | def load_dict(self) -> Cell:
129 | '''Loads dictionary like a Cell from the slice.
130 | Returns None if the dictionary was null().'''
131 | not_null = self.load_bit()
132 | if not_null:
133 | return self.load_ref()
134 | else:
135 | return None
136 |
137 | def preload_dict(self) -> Cell:
138 | not_null = self.preload_bit()
139 | if not_null:
140 | return self.preload_ref()
141 | else:
142 | return None
143 |
144 | def skip_dict(self):
145 | self.load_dict()
146 |
--------------------------------------------------------------------------------
/cli_main.py:
--------------------------------------------------------------------------------
1 | from cli.keyring import Keyring, process_keyring_cmd
2 | from cli.about import PROMPT_SINGLE, PROMPT, ABOUT
3 | from cli.contracts import process_contract_cmd
4 | from cli.offers import process_offers_cmd
5 | from cli.jobs import process_jobs_cmd
6 | from cli.signing import sign_send
7 | from cli.colors import b, nb
8 | import cli.polyfills
9 |
10 | import traceback
11 | import os
12 |
13 | from tonsdk.utils import Address
14 | from tonsdk.boc import Cell
15 | import requests
16 |
17 |
18 |
19 | def main():
20 | keys = Keyring()
21 |
22 | while True:
23 | print(PROMPT_SINGLE)
24 |
25 | try:
26 | while not (command := input(PROMPT).lower().strip()):
27 | pass
28 | except KeyboardInterrupt:
29 | command = 'q'
30 |
31 | try:
32 | if not command:
33 | pass
34 | elif command == 'h':
35 | print(ABOUT)
36 | elif command == 'q':
37 | break
38 | elif command == 'd':
39 | donate_addr = 'EQCyoez1VF4HbNNq5Rbqfr3zKuoAjKorhK-YZr7LIIiVrSD7'
40 | sign_send([
41 | (Address(donate_addr), None, Cell(), 5*10**7),
42 | ], 'donate')
43 | elif command[0] == 'k':
44 | with keys: process_keyring_cmd(command, keys)
45 | elif command[0] == 'j':
46 | with keys: process_jobs_cmd(command, keys)
47 | elif command[0] == 'o':
48 | with keys: process_offers_cmd(command, keys)
49 | elif command[0] == 'c':
50 | with keys: process_contract_cmd(command, keys)
51 | else:
52 | print(f'{b}not implemented:{nb} {repr(command)}')
53 | except KeyboardInterrupt:
54 | print(f'{nb}\noperation cancelled')
55 |
56 | print('\n')
57 |
58 | try:
59 | if __name__ == '__main__':
60 | os.system('')
61 | main()
62 | except:
63 | traceback.print_exc()
64 | input('...')
65 |
--------------------------------------------------------------------------------
/contracts/build.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | import base64
3 | import shutil
4 | import sys
5 | import os
6 |
7 | FIFT_LIBS_LIST = 'Fift Asm AsmTests TonUtil Lists Color'.split(' ')
8 | CONTRACTS_LIST = 'contract-job contract-offer'.split(' ')
9 |
10 | ap = os.path.abspath
11 |
12 | base_path = ap(__file__ + '/../')
13 | fift_path = os.environ['FIFTPATH']
14 |
15 | print('====== Starting build ====================')
16 |
17 | os.system('cls') # clears screen + enables escape sequences
18 |
19 | for fift_lib in FIFT_LIBS_LIST:
20 | shutil.copy(ap(f'{fift_path}toncli/lib/fift-libs/{fift_lib}.fif'),
21 | ap(f'{base_path}/{fift_lib}.fif'))
22 |
23 | print('====== Loaded libs for toncli ============')
24 |
25 | with open(ap(base_path + '/fift/exotic.fif')) as f:
26 | exotic_patch = f.read()
27 |
28 | with open(ap(base_path + '/Asm.fif'), 'a') as f: f.write(exotic_patch)
29 | with open(ap(base_path + '/AsmTests.fif'), 'a') as f: f.write(exotic_patch)
30 |
31 | print('====== Patched Fift libraries ============')
32 |
33 | os.chdir(base_path)
34 | os.system('toncli run_tests >toncli.log 2>toncli.err')
35 | os.system('python show-log.py')
36 |
37 | print('====== Ran tests =========================')
38 |
39 | os.system('toncli build >nul 2>nul')
40 |
41 | print('====== Built contract in prod mode =======')
42 |
43 | for contract in CONTRACTS_LIST:
44 | with open(ap(base_path + f'/build/{contract}.fif'), 'a') as f:
45 | f.write(f'\nboc>B "build/boc/{contract}.boc" B>file')
46 | os.system(f'toncli fift run build/{contract}.fif')
47 |
48 | with open(ap(f'build/boc/{contract}.boc'), 'rb') as rf:
49 | boc = rf.read()
50 | print(f'====== BOC of {repr(contract)} is {len(boc)} B')
51 | with open(ap(f'build/boc/{contract}.hex'), 'wb') as wf:
52 | wf.write(base64.b16encode(boc))
53 |
54 | print(f'====== Saved {repr(contract)} in BOC and HEX representation')
55 |
56 | if '--noclean' not in sys.argv:
57 | for fift_lib in FIFT_LIBS_LIST:
58 | os.remove(ap(f'{base_path}/{fift_lib}.fif'))
59 |
60 | print('====== Deleted Fift libs =================')
61 |
--------------------------------------------------------------------------------
/contracts/build/boc/contract-job.boc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProgramCrafter/ratelance/2de56f4f96acf0f9e1e5b0e60ae0ad846ba50170/contracts/build/boc/contract-job.boc
--------------------------------------------------------------------------------
/contracts/build/boc/contract-job.hex:
--------------------------------------------------------------------------------
1 | B5EE9C7201021501000382000114FF00F4A413F4BCF2C80B0102016202030202CB04050201200D0E0247D0835D2704838C2007434C0C05C6C007E900C00B4C7FB513434C048700038C3E103FCBC206070023A11876A27C80424FD8408058AC658FE5CFC001D23133218100EBBA8E5E018100ACBA8E535213C709F2E0C80282103B9ACA00BEF2E0C7D4318040706D92F012245520705530C859DA21C97180100591789170E215B1C8CB055003CF1601FA0212CB6ACCC901FB0071C8CB010259CF1601CF16C9ED54DB31925F04E2E30D0802B63620C0018EAB30218100ADBA8E21308100D4BA01B18E1501FA4059C705F2E0C870C8CB0101CF16C9ED54DB31915BE2E30D8EA7333301C0028E9C8100BBBA8E9301FA40FA4072D748D003D401D001D74CD05114915BE2925F03E2E2090A006C5B6C1221C709F2E0C8830601708BE4A6F622064657374726F7965642E8708010C8CB055004CF1658FA0212CB8A01CF16C901FB00DB3100CC6C2102FA40FA40D33FD45045C705F2E0C804FA40D472C8CB01071046103550445076CF165004CF1612CB3F01CF1401CF14C85003CF1601CF168D0808CEB4C769ADE5E6E837239313EE5118E7BD11EDAB2B541C6FED19BA23FDBDBEE0CF16C9CF14C9ED54DB3101F601D30121C0038E255B821062845C24F833206EF2D0C8F8280181010BF40A6FA1F2E0C8736D02D33FD33F301023998308D718D33FD33F30E223C3038E3723AA07158307D724531470C88BDFFFF726C3A3A6A6F623A3A76308CF16CB04F828CF1612CB3FCB3FC9F90001D70BFF4130F910F2E0C602923233E21250360B01FE01D30121C0038E255B821062845C24F833206EF2D0C8F8280181010BF40A6FA1F2E0C8736D02D33FD33F301023998308D718D33FD33F30E223C3038E3723AA07158307D724531470C88BDFFFF726C3A3A6A6F623A3A76308CF16CB04F828CF1612CB3FCB3FC9F90001D70BFF4130F910F2E0C602923233E2125ABAF2D0C8590C00C4B6095044B6085133BCF2D0C87140138BE4A6F6220636F6D706C65746564218708010C8CB055004CF1658FA0212CB8A01CF16C901FB00830601708BD4A6F622066696E69736865642E8708010C8CB055004CF1658FA0212CB8A01CF16C901FB00DB310201580F1002012011120011B614BDA89A1AE160300021B6355DA89A1A6020327F48063BDF48061002016A13140017B946DED44D072D721FA403080027AEB876A268698080C9FD2018EF7D2018E99F9840000FAF4076A2686BA640
--------------------------------------------------------------------------------
/contracts/build/boc/contract-offer.boc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProgramCrafter/ratelance/2de56f4f96acf0f9e1e5b0e60ae0ad846ba50170/contracts/build/boc/contract-offer.boc
--------------------------------------------------------------------------------
/contracts/build/boc/contract-offer.hex:
--------------------------------------------------------------------------------
1 | B5EE9C7201020C010001D6000114FF00F4A413F4BCF2C80B010202CE02030247D0835D2704838C2007434C0C05C6C007E900C00B4C7FB513434C048700038C3E103FCBC20405020120080901E6313322FA40FA40D33FD4D6FFD70B9F6C2125821064737472BA8E56058100B1BA8E4B5026C705F2E0C801D70B9F58BAF2E0C88040717092F016103541401316C859DA21C97180100591789170E215B1C8CB055003CF1601FA0212CB6ACCC901FB0071C8CB0101CF16C9ED54DB31925F07E2E30D0601D032C0018EDF20FA40FA405217C705F2E0C8048E4E31018210F06C7567BA8E3F028040D72183067022D74C03D6FF3044306F0392F01A1034705530C859DA21C97180100591789170E215B1C8CB055003CF1601FA0212CB6ACCC901FB00DB31925F03E2E30D925F04E207006E10265F0621C705F2E0C8830601708BE4F66666572207265766F6B65642E8708010C8CB055004CF1658FA0212CB8A01CF16C901FB00DB31007E6C22328040706D92F01C10341035705530C859DA21C97180100591789170E215B1C8CB055003CF1601FA0212CB6ACCC901FB0070C8CB0101CF16C9ED54DB31002BBD0420E0D8EACEB1963FF04A03967E03F404E00396010201200A0B002569BC8E0402B540132C7D633C58073C50073C5A000FD1840806A00E58FC
--------------------------------------------------------------------------------
/contracts/fift/config-validators.boc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProgramCrafter/ratelance/2de56f4f96acf0f9e1e5b0e60ae0ad846ba50170/contracts/fift/config-validators.boc
--------------------------------------------------------------------------------
/contracts/fift/exotic.fif:
--------------------------------------------------------------------------------
1 |
2 | x{CF23} @Defop ENDXC
3 | x{FEEF10} @Defop GASLIMITSTEMP
4 | x{FEEF13} @Defop RESETLOADEDCELLS
5 |
6 | B B>boc ref, b> [-x *] [-n|-b] [-t] [-B ] [-C ] []" +cr +tab
17 | +"Creates a request to advanced wallet created by new-wallet-v3.fif, with private key loaded from file .pk "
18 | +"and address from .addr, and saves it into .boc ('wallet-query.boc' by default)"
19 | disable-digit-options generic-help-setopt
20 | "n" "--no-bounce" { false =: allow-bounce } short-long-option
21 | "Clears bounce flag" option-help
22 | "b" "--force-bounce" { true =: force-bounce } short-long-option
23 | "Forces bounce flag" option-help
24 | "x" "--extra" { $>xcc extra-cc+! } short-long-option-arg
25 | "Indicates the amount of extra currencies to be transfered" option-help
26 | "t" "--timeout" { parse-int =: timeout } short-long-option-arg
27 | "Sets expiration timeout in seconds (" timeout (.) $+ +" by default)" option-help
28 | "B" "--body" { =: body-fift-file } short-long-option-arg
29 | "Sets the payload of the transfer message" option-help
30 | "C" "--comment" { =: comment } short-long-option-arg
31 | "Sets the comment to be sent in the transfer message" option-help
32 | "m" "--mode" { parse-int =: send-mode } short-long-option-arg
33 | "Sets transfer mode (0..255) for SENDRAWMSG (" send-mode (.) $+ +" by default)"
34 | option-help
35 | "h" "--help" { usage } short-long-option
36 | "Shows a help message" option-help
37 | parse-options
38 |
39 | $# dup 5 < swap 6 > or ' usage if
40 | 6 :$1..n
41 |
42 | true constant bounce
43 | $1 =: file-base
44 | $2 bounce parse-load-address force-bounce or allow-bounce and =: bounce 2=: dest_addr
45 | $3 parse-int =: subwallet_id
46 | $4 parse-int =: seqno
47 | $5 $>cc extra-cc+! extra-currencies @ 2=: amount
48 | $6 "wallet-query" replace-if-null =: savefile
49 | subwallet_id (.) 1 ' $+ does : +subwallet
50 |
51 | file-base +subwallet +".addr" dup file-exists? { drop file-base +".addr" } ifnot
52 | load-address
53 | 2dup 2constant wallet_addr
54 | ."INFO: 👀 Source wallet address = " 2dup 6 .Addr cr
55 | file-base +".pk" load-keypair nip constant wallet_pk
56 |
57 | def? body-fift-file { @' body-fift-file include } { comment simple-transfer-body } cond
58 | constant body-cell
59 |
60 | ."INFO: 👋 Send " dest_addr 2dup bounce 7 + .Addr ." = "
61 | ."subwallet_id=0x" subwallet_id x.
62 | ."seqno=0x" seqno x. ."bounce=" bounce . cr
63 | ."INFO: 🧟 Body of transfer message is " body-cell
69 |
70 |
71 |
72 | dup hashu wallet_pk ed25519_sign_uint
73 |
75 |
76 | 2 boc+>B
77 |
78 | saveboc // it is defined in build/cli.fif (or /tmp if in script mode)
79 | // it is project-specific lib from tlcli thats specify needed locations
80 |
--------------------------------------------------------------------------------
/contracts/func/job-contract.fc:
--------------------------------------------------------------------------------
1 | #include "func/stdlib-ext.fc";
2 | #include "func/opcodes.fc";
3 | #include "func/tlb.fc";
4 |
5 | (int, int, int) parse_proposal_check_sig(slice v, slice keys) impure inline {
6 | (int tag, slice sig, int lb, int ub) = job::parse_proposal_with_tag(v);
7 |
8 | if (tag != 3) {
9 | slice key = keys.extract_part(tag * 256, 256);
10 | throw_unless(198,
11 | check_signature(cell_hash(job::wrap_for_signing(lb, ub)),
12 | sig,
13 | key.preload_uint(256)));
14 | }
15 |
16 | return (tag, lb, ub);
17 | }
18 |
19 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
20 |
21 | int get_job_state() method_id {
22 | return get_data().begin_parse().preload_uint(2);
23 | }
24 | slice waiting_message_from() method_id {
25 | slice own_data = get_data().begin_parse();
26 | own_data~skip_bits(2);
27 | return own_data~load_msg_addr();
28 | }
29 | cell get_job_description() method_id {
30 | return get_data().begin_parse().preload_ref();
31 | }
32 | int get_job_value() method_id {
33 | slice own_data = get_data().begin_parse();
34 | int tag = own_data~load_uint(2);
35 | if (tag) { own_data~load_msg_addr(); }
36 | own_data~load_msg_addr();
37 | return own_data~load_uint(64);
38 | }
39 | slice get_job_poster() method_id {
40 | slice own_data = get_data().begin_parse();
41 | int tag = own_data~load_uint(2);
42 | if (tag) { own_data~load_msg_addr(); }
43 | return own_data~load_msg_addr();
44 | }
45 |
46 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
47 |
48 | ;; Incoming messages:
49 | ;; CUS-1. poster [empty]
50 | ;; CUS-1,2-REV. poster op::cancel_job
51 | ;; CUS-4. offer [insufficient value to even notice it]
52 | ;; CUS-5. poster op::lock_on_offer
53 | ;; CUS-7-ERR. offer op::refuse_collapse [bounced]
54 | ;; CUS-9-OK. offer op::lock_success
55 | ;; CUS-9-ERR. offer op::lock_failed
56 | ;; CUS-12. anyone op::finish_job
57 |
58 | () recv_internal(int msg_value, cell in_msg, slice in_msg_body) {
59 | terminate_if(in_msg_body.slice_bits() < 32);
60 |
61 | (int bounced, slice sender) = in_msg.load_bounced_sender();
62 |
63 | int op = in_msg_body~load_uint(32);
64 |
65 | slice own_data = get_data().begin_parse();
66 | int tag = own_data~load_uint(2);
67 |
68 | if (tag == job::tag::unlocked) {
69 | if (op == op::cancel_job) {
70 | throw_unless(200, own_data.starts_with(sender));
71 | send_text(mode::destroy_into(), sender, 0, "Job destroyed.");
72 | terminate();
73 | } elseif (op == op::lock_on_offer) {
74 | throw_unless(200, own_data.starts_with(sender));
75 | throw_unless(199, msg_value >= TON); ;; this will be returned eventually
76 |
77 | (_, slice offer_addr) = job::ld_msg_lock_on(in_msg_body);
78 | send_to(mode::forward_value(), offer_addr, 0, null(),
79 | job::st_msg_collapse);
80 | set_data(begin_cell()
81 | .store_uint(job::tag::locked_on_offer, 2)
82 | .job::st_locked(offer_addr, own_data)
83 | .end_cell());
84 | terminate();
85 | }
86 | } elseif (tag == job::tag::locked_on_offer) {
87 | if (op == op::lock_success) {
88 | ;; throw_unless(200, own_data.starts_with(sender));
89 | (slice offer, slice poster, int value, cell poster_desc, slice poster_key)
90 | = job::ld_locked(own_data);
91 | throw_unless(200, equal_slices(offer, sender));
92 |
93 | (slice worker, cell worker_desc, slice worker_key)
94 | = job::ld_msg_lock_success(in_msg_body);
95 |
96 | set_data(begin_cell()
97 | .store_uint(job::tag::locked_working, 2)
98 | .job::st_working(poster, worker, value, poster_desc, worker_desc,
99 | poster_key, worker_key)
100 | .end_cell());
101 | terminate();
102 | } elseif ((op == op::lock_failed) | bounced) {
103 | ;; throw_unless(200, own_data.starts_with(sender));
104 | throw_unless(200, equal_slices(sender, own_data~load_msg_addr()));
105 | set_data(begin_cell()
106 | .store_uint(job::tag::unlocked, 2)
107 | .job::st_unlocked(own_data)
108 | .end_cell());
109 | terminate();
110 | }
111 | } elseif (tag == job::tag::locked_working) {
112 | if (op == op::finish_job) {
113 | (slice poster, slice worker, cell keys) = job::ld_working_main(own_data);
114 | slice keys = keys.begin_parse();
115 |
116 | (slice a, slice b) = job::parse_finish_message(in_msg_body);
117 | (int tag_a, int al, int au) = parse_proposal_check_sig(a, keys);
118 | (int tag_b, int bl, int bu) = parse_proposal_check_sig(b, keys);
119 | throw_if(200, tag_a == tag_b);
120 |
121 | ;; we are not checking sender, as this code is guarded by signatures
122 |
123 | int nton_min = max(al, bl);
124 | int nton_max = min(au, bu);
125 | throw_if(200, nton_min > nton_max);
126 |
127 | send_text(mode::pay_transfer(), worker, nton_max, "Job completed!");
128 | send_text(mode::destroy_into(), poster, 0, "Job finished.");
129 | terminate();
130 | }
131 | }
132 |
133 | throw(0xFFFF);
134 | }
135 |
--------------------------------------------------------------------------------
/contracts/func/offer-contract.fc:
--------------------------------------------------------------------------------
1 | #include "func/stdlib-ext.fc";
2 | #include "func/opcodes.fc";
3 | #include "func/tlb.fc";
4 |
5 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
6 |
7 | ;; Incoming messages:
8 | ;; CUS-3. worker [empty]
9 | ;; CUS-3,4-REV. worker op::destruct
10 | ;; CUS-6. job op::collapse
11 | ;; CUS-8-OK. worker op::payment_ok
12 | ;; CUS-8-ERR. worker 0xFFFFFFFF
13 |
14 | () recv_internal(cell in_msg, slice in_msg_body) {
15 | terminate_if(in_msg_body.slice_bits() < 32);
16 |
17 | (int bounced, slice sender) = load_bounced_sender(in_msg);
18 |
19 | int op = in_msg_body~load_uint(32);
20 |
21 | slice own_data = get_data().begin_parse();
22 | int tag = own_data~load_uint(2);
23 |
24 | if (tag == offer::tag::unlocked) {
25 | (slice job, slice worker, int stake, cell desc, slice worker_key,
26 | int job_hash) = offer::ld_unlocked(own_data);
27 |
28 | if (op == op::destruct) {
29 | throw_unless(200, equal_slices(worker, sender));
30 |
31 | send_text(mode::destroy_into(), sender, 0, "Offer revoked.");
32 | terminate();
33 | } elseif (op == op::collapse) {
34 | throw_unless(200, equal_slices(job, sender));
35 | throw_unless(200, offer::ld_msg_collapse(in_msg_body) == job_hash);
36 |
37 | send_to_rbounceable(mode::forward_value(), 1, worker, 0, stake,
38 | offer::st_msg_take_stake);
39 | set_data(begin_cell()
40 | .store_uint(offer::tag::locked, 2)
41 | .offer::st_locked(own_data)
42 | .end_cell());
43 | terminate();
44 | }
45 | } elseif (tag == offer::tag::locked) {
46 | slice full_own_data = own_data;
47 | slice job = own_data~load_msg_addr();
48 | slice worker = own_data~load_msg_addr();
49 |
50 | throw_unless(200, equal_slices(worker, sender));
51 |
52 | if (bounced) {
53 | send_to(mode::forward_value(), job, 0, null(), offer::st_msg_lock_failed);
54 | set_data(begin_cell()
55 | .store_uint(offer::tag::unlocked, 2)
56 | .store_slice(full_own_data)
57 | .end_cell());
58 | terminate();
59 | } elseif (op == op::payment_ok) {
60 | own_data~skip_bits(64);
61 |
62 | send_to(mode::destroy_into(), job, 0, triple(
63 | worker,
64 | own_data.preload_ref(),
65 | own_data~load_bits(256)
66 | ), offer::st_msg_lock_success);
67 | terminate();
68 | }
69 | }
70 |
71 | throw(0xFFFF);
72 | }
73 |
--------------------------------------------------------------------------------
/contracts/func/opcodes.fc:
--------------------------------------------------------------------------------
1 | ;; ---- Job contract states --------------------------------------------------
2 |
3 | const int job::tag::unlocked = 0;
4 | const int job::tag::locked_on_offer = 1;
5 | const int job::tag::locked_working = 2;
6 |
7 | const int offer::tag::unlocked = 0;
8 | const int offer::tag::locked = 1;
9 |
10 | ;; ---- Offer contract opcodes -----------------------------------------------
11 |
12 | const int op::destruct = 0x64737472;
13 | const int op::payment_request = 0x706c7567;
14 | const int op::payment_ok = 0x80000000 | op::payment_request;
15 | const int op::excesses = 0xd53276db;
16 | const int op::subscription = 0x73756273;
17 |
18 | ;; ---- Job contract opcodes -------------------------------------------------
19 |
20 | const int op::update_job = 168; ;; 156 + zlib.crc32(b'op::update_job') % 100
21 | const int op::lock_on_offer = 172; ;; 156 + zlib.crc32(b'op::lock_on_offer') % 100
22 | const int op::collapse = 177; ;; 156 + zlib.crc32(b'op::collapse') % 100
23 | const int op::lock_success = 173; ;; 156 + zlib.crc32(b'op::lock_success') % 100
24 | const int op::lock_failed = 212; ;; 156 + zlib.crc32(b'op::lock_failed') % 100
25 | const int op::cancel_job = 235; ;; 156 + zlib.crc32(b'op::cancel_job') % 100
26 | const int op::finish_job = 187; ;; 156 + zlib.crc32(b'op::finish_job') % 100
27 |
28 | ;; ---- Errors (common) ------------------------------------------------------
29 |
30 | const int err::invalid_sender = 171; ;; 100 + zlib.crc32(b'err::invalid_sender') % 100
31 | const int err::invariant_failed = 172; ;; 100 + zlib.crc32(b'err::invariant_failed') % 100
32 | const int err::insufficient_stake = 179; ;; 100 + zlib.crc32(b'err::insufficient_stake') % 100
33 | const int err::low_job_value = 127; ;; 100 + zlib.crc32(b'err::low_job_value') % 100
34 |
35 | ;; ---- TON utility constants ------------------------------------------------
36 |
37 | const int TON = 1000 * 1000 * 1000;
38 |
39 | const int DEVELOPMENT = 1;
40 |
41 | (int, ()) mode::forward_value() inline { return (64, ()); }
42 | (int, ()) mode::pay_transfer() inline { return (1, ()); }
43 | (int, ()) mode::destroy_into() inline { return DEVELOPMENT ? (128, ()) : (160, ()); }
44 |
45 | ;; ---- Addresses (common) ---------------------------------------------------
46 |
47 | const slice job_analytic_address = "EQA__RATELANCE_______________________________JvN"a;
48 |
49 | const slice ratelance_public_key = "233AD31DA6B7979BA0DC8E4C4FB944639EF447B6ACAD5071BFB466E88FF6F6FB"s;
50 |
--------------------------------------------------------------------------------
/contracts/func/stdlib-ext.fc:
--------------------------------------------------------------------------------
1 | (builder, ()) ~store_slice(builder, slice) asm "STSLICER";
2 | (builder, ()) ~store_coins(builder a, int b) asm "STGRAMS";
3 | (builder, ()) ~store_ref(builder, cell) asm "STREFR";
4 |
5 | (slice, int) dict_get?(cell dict, int key_len, slice index)
6 | asm(index dict key_len) "DICTGET" "NULLSWAPIFNOT";
7 |
8 | slice extract_part(slice v, int offset, int len) asm "SDSUBSTR";
9 | cell load_nth_ref(slice v, int offset) asm "PLDREFVAR";
10 |
11 | int starts_with(slice, slice) asm "SDPFXREV";
12 | int equal_slices(slice, slice) asm "SDEQ";
13 | int first_bit(slice) asm "SDFIRST";
14 | int tuple_length(tuple) asm "TLEN";
15 |
16 | forall X -> int is_null(X) asm "ISNULL";
17 |
18 | (int, slice) load_bounced_sender(cell in_msg) inline {
19 | slice in_msg = in_msg.begin_parse();
20 | int b = in_msg~load_uint(4) & 1;
21 | return (b, in_msg~load_msg_addr());
22 | }
23 |
24 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
25 |
26 | ;; termination primitives not cleaning stack
27 |
28 | () terminate() impure asm "RETALT";
29 | () terminate_if(int) impure asm "IFRETALT";
30 |
31 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
32 |
33 | forall ADDARGS ->
34 | () send_to_rbounceable((int, ()) packed_mode, int bounce, slice dest, int value,
35 | ADDARGS a, ((builder, ADDARGS) -> builder) m)
36 | impure inline {
37 | (int mode, _) = packed_mode;
38 |
39 | send_raw_message(begin_cell()
40 | .store_uint(0x10 | (bounce ? 8 : 0), 6)
41 | .store_slice(dest)
42 | .store_coins(value)
43 | .store_uint(1, 107)
44 | .store_ref(m(begin_cell(), a).end_cell())
45 | .end_cell(), mode);
46 | }
47 | forall ADDARGS ->
48 | () send_to((int, ()) packed_mode, slice dest, int value, ADDARGS a,
49 | ((builder, ADDARGS) -> builder) m) impure inline {
50 | return send_to_rbounceable(packed_mode, 0, dest, value, a, m);
51 | }
52 | () send_text((int, ()) packed_mode, slice dest, int value, slice body)
53 | impure inline {
54 | (int mode, _) = packed_mode;
55 |
56 | send_raw_message(begin_cell()
57 | .store_uint(0x10, 6)
58 | .store_slice(dest)
59 | .store_coins(value)
60 | .store_uint(0, 107 + 32)
61 | .store_slice(body)
62 | .end_cell(), mode);
63 | }
64 |
--------------------------------------------------------------------------------
/contracts/func/tlb.fc:
--------------------------------------------------------------------------------
1 | #include "stdlib-ext.fc";
2 | #include "opcodes.fc";
3 |
4 | ;; CUS-1. Serialized by user.
5 | (slice, int) job::ld_tag(slice v) inline {
6 | return v.load_uint(2);
7 | }
8 | (slice, int, cell, slice) job::ld_unlocked(slice v) inline {
9 | return (v~load_msg_addr(), v~load_uint(64), v~load_ref(), v);
10 | }
11 |
12 | ;; CUS-2. Serialized by user. Deserialized by user.
13 |
14 | ;; CUS-1,2-REV. Serialized by user. No deserialization needed.
15 |
16 | ;; CUS-3. Serialized by user.
17 | (slice, int) offer::ld_tag(slice v) inline {
18 | return v.load_uint(2);
19 | }
20 | (slice, slice, int, cell, slice, int) offer::ld_unlocked(slice v) inline {
21 | return (v~load_msg_addr(), v~load_msg_addr(), v~load_uint(64), v~load_ref(),
22 | v~load_bits(256), v.preload_uint(160));
23 | }
24 |
25 | ;; CUS-4. Serialized by user. Deserialized by user.
26 |
27 | ;; CUS-3,4-REV. Serialized by wallet. No deserialization needed.
28 |
29 | ;; CUS-5. Sent by user.
30 | (cell, slice) job::ld_msg_lock_on(slice v) inline {
31 | return (v~load_ref(), v);
32 | }
33 |
34 | ;; CUS-6.
35 | builder job::st_msg_collapse(builder v, cell add_args) inline {
36 | int order_hash = cell_hash(get_data());
37 | order_hash &= (1 << 160) - 1;
38 | v~store_uint(op::collapse, 32); v~store_uint(order_hash, 160);
39 | return v;
40 | }
41 | builder job::st_locked(builder v, slice offer, slice unlocked) inline {
42 | v~store_slice(offer); v~store_slice(unlocked);
43 | return v;
44 | }
45 | (slice, slice, int, cell, slice) job::ld_locked(slice v) inline {
46 | return (v~load_msg_addr(), v~load_msg_addr(), v~load_uint(64),
47 | v~load_ref(), v);
48 | }
49 | int offer::ld_msg_collapse(slice v) inline {
50 | return v.preload_uint(160);
51 | }
52 |
53 | ;; CUS-7.
54 | builder offer::st_msg_take_stake(builder v, int stake) inline {
55 | v~store_uint(0x706c7567, 32); v~store_uint(cur_lt(), 64);
56 | v~store_coins(stake); v~store_uint(0, 1);
57 | return v;
58 | }
59 | builder offer::st_locked(builder v, slice unlocked) inline {
60 | v~store_slice(unlocked);
61 | return v;
62 | }
63 | (slice, slice, int, cell, slice, slice) offer::ld_locked(slice v) inline {
64 | return (v~load_msg_addr(), v~load_msg_addr(), v~load_uint(64), v~load_ref(),
65 | v~load_bits(256), v);
66 | }
67 |
68 | ;; CUS-8. Serialized by wallet. Needs no parsing other than opcode check.
69 |
70 | ;; CUS-9-OK.
71 | builder offer::st_msg_unplug(builder v, () add_args) inline {
72 | v~store_uint(0x64737472, 32); v~store_uint(cur_lt(), 64);
73 | return v;
74 | }
75 | builder offer::st_msg_lock_success(builder v, [slice, cell, slice] add_args)
76 | inline {
77 | (slice worker, cell desc, slice worker_key) = untriple(add_args);
78 | v~store_uint(op::lock_success, 32); v~store_slice(worker);
79 | v~store_ref(desc); v~store_slice(worker_key);
80 | return v;
81 | }
82 | (slice, cell, slice) job::ld_msg_lock_success(slice v) inline {
83 | return (v~load_msg_addr(), v~load_ref(), v);
84 | }
85 |
86 | ;; CUS-9-ERR.
87 | builder offer::st_msg_lock_failed(builder v, cell add_args) inline {
88 | v~store_uint(op::lock_failed, 32); return v;
89 | }
90 | builder offer::st_unlocked(builder v, slice job, slice worker, int stake,
91 | cell desc, slice worker_key, int short_job_hash) inline {
92 | v~store_slice(job); v~store_slice(worker); v~store_uint(stake, 64);
93 | v~store_ref(desc); v~store_slice(worker_key);
94 | v~store_uint(short_job_hash, 160);
95 | return v;
96 | }
97 | builder job::st_unlocked(builder v, slice locked_without_offer) inline {
98 | v~store_slice(locked_without_offer);
99 | return v;
100 | }
101 |
102 | ;; CUS-10.
103 | builder job::st_working(builder v, slice poster, slice worker, int value,
104 | cell poster_desc, cell worker_desc, slice poster_key, slice worker_key)
105 | inline {
106 | v~store_slice(poster); v~store_slice(worker); v~store_uint(value, 64);
107 | v~store_ref(poster_desc); v~store_ref(worker_desc);
108 | v~store_ref(begin_cell()
109 | .store_slice(poster_key)
110 | .store_slice(worker_key)
111 | .store_slice(ratelance_public_key)
112 | .end_cell());
113 | return v;
114 | }
115 | (slice, slice, cell) job::ld_working_main(slice v) inline {
116 | return (v~load_msg_addr(), v~load_msg_addr(), v.load_nth_ref(2));
117 | }
118 |
119 | ;; CUS-11. Serialized by user.
120 | cell job::wrap_for_signing(int worker_ton_min, int worker_ton_max) inline {
121 | return begin_cell()
122 | .store_slice("FFFF726C3A3A6A6F623A3A7630"s)
123 | .store_uint(0, 5)
124 | .store_slice(my_address())
125 | .store_uint(worker_ton_min, 64)
126 | .store_uint(worker_ton_max, 64)
127 | .end_cell();
128 | }
129 | (int, slice, int, int) job::parse_proposal_with_tag(slice v) impure inline {
130 | int tag = v~load_uint(2);
131 | if (tag == 3) {
132 | cell c = config_param(1652841508);
133 | throw_if(200, cell_null?(c));
134 | (v, int success) = c.dict_get?(267, my_address());
135 | throw_unless(200, success);
136 | return (3, null(), v~load_uint(64), v~load_uint(64));
137 | }
138 | return (tag, v~load_bits(512), v~load_uint(64), v~load_uint(64));
139 | }
140 |
141 | ;; CUS-12. Serialized by user.
142 | (slice, slice) job::parse_finish_message(slice v) impure inline {
143 | return (v~load_ref().begin_parse(), v.preload_ref().begin_parse());
144 | }
145 |
--------------------------------------------------------------------------------
/contracts/project.yaml:
--------------------------------------------------------------------------------
1 | contract-job:
2 | func:
3 | - func/job-contract.fc
4 | tests:
5 | - tests/tests-job.fc
6 |
7 | contract-offer:
8 | func:
9 | - func/offer-contract.fc
10 | tests:
11 | - tests/tests-offer.fc
12 |
13 |
--------------------------------------------------------------------------------
/contracts/show-log.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 |
4 | os.system('')
5 |
6 | ostream = sys.stderr.detach()
7 | ostream.write(b'\x1b[37m')
8 |
9 | need_load_err = True
10 |
11 | exit_codes = {
12 | '2': 'ERR_STACK_UNDERFLOW',
13 | '3': 'ERR_STACK_OVERFLOW',
14 | '4': 'ERR_INTEGER_OVERFLOW',
15 | '5': 'ERR_INTEGER_OUT_OF_RANGE',
16 | '6': 'ERR_INVALID_OPCODE',
17 | '7': 'ERR_TYPE_CHECK',
18 | '8': 'ERR_CELL_OVERFLOW',
19 | '9': 'ERR_CELL_UNDERFLOW',
20 | '10': 'ERR_DICT',
21 | '13': 'ERR_OUT_OF_GAS',
22 | '32': 'ERR_INVALID_ACTION_LIST',
23 | '34': 'ERR_ACTION_UNSUPPORTED',
24 | '37': 'ERR_NOT_ENOUGH_TON',
25 | '38': 'ERR_NOT_ENOUGH_CURRENCY'
26 | }
27 |
28 | with open(__file__ + '/../toncli.log', 'r', encoding='utf-8') as f:
29 | debug_line_tostr = False
30 |
31 | for line in f:
32 | if not line.strip(): continue
33 | if '_io.TextIO' in line: continue
34 | if 'detached' in line: continue
35 |
36 | need_load_err = False
37 |
38 | for d in (31, 32, 36, 0):
39 | line = line.replace('\x1b[%dm' % d, '')
40 | if '[ 3]' in line:
41 | continue
42 | line = ('\x1b[36m' +
43 | line.removeprefix('[ 3][t 0]')
44 | .replace('9223372036854775807', 'UMAX')
45 | .replace('[vm.cpp:558]', '[vm558]') + '\x1b[37m')
46 |
47 | line = line.removeprefix('INFO: ')
48 |
49 | for (code, desc) in exit_codes.items():
50 | c2 = int(code) + 200
51 | line = line.replace(f'code: [{code}]', f'code: [{code} | {desc}]')
52 | line = line.replace(f'code: [{c2}]', f'code: [{c2} | CALLEE_{desc}]')
53 |
54 | if line.strip() == '#DEBUG#: s0 = 4445':
55 | debug_line_tostr = True
56 | continue
57 | elif line.startswith('#DEBUG#') and debug_line_tostr:
58 | debug_line_tostr = False
59 | try:
60 | n = int(line.removeprefix('#DEBUG#: s0 = ').rstrip())
61 | s = ''
62 | while n:
63 | s = s + chr(n % 256)
64 | n //= 256
65 | line = '\x1b[36m DEBUG : ' + s[::-1] + '\x1b[37m\n'
66 | except:
67 | pass
68 |
69 | if 'Test' in line:
70 | color = '\x1b[37m'
71 | if 'SUCCESS' in line: color = '\x1b[32m'
72 | if 'FAIL' in line: color = '\x1b[33m'
73 |
74 | line = (line
75 | .replace('[SUCCESS] Test', 'OK,')
76 | .replace('[FAIL]', 'RE')
77 | .replace(' Total gas used (including testing code)', ', gas usage')
78 | )
79 |
80 | gas_usage = int(line[line.rfind('[')+1:line.rfind(']')])
81 | ton_usage = gas_usage * 10**-6
82 | if '_get]' in line:
83 | ton_s = ' (offchain request)'
84 | elif '_transfer]' in line:
85 | ton_s = ' (money transfer)'
86 | elif 'init_contract]' in line:
87 | ton_s = ' (one-time initialization)'
88 | elif ton_usage > 0.01:
89 | ton_s = '\x1b[33m (%.5f TON == %.2f req/TON)' % (ton_usage, 1/ton_usage)
90 | else:
91 | ton_s = '\x1b[32m (%.5f TON == %.2f req/TON)' % (ton_usage, 1/ton_usage)
92 |
93 | line = color + line.rstrip().replace('__test_', '', 1).replace('status: ', '', 1) + ton_s + '\x1b[37m\n'
94 |
95 | ostream.write(line.replace('\r', '').encode('utf-8'))
96 |
97 | red_border = '\n\x1b[41m\x1b[30m \x1b[40m\x1b[37m\n'
98 |
99 | with open(__file__ + '/../toncli.err', 'r', encoding='utf-8') as f:
100 | skipping_traceback = False
101 |
102 | for line in f:
103 | if '--- Logging error ---' in line: continue
104 |
105 | if line.startswith('Traceback'):
106 | skipping_traceback = True
107 | elif line.startswith('Arguments') or line.startswith('subprocess'):
108 | skipping_traceback = False
109 | continue
110 |
111 | if skipping_traceback: continue
112 |
113 | if red_border:
114 | ostream.write(red_border.encode('utf-8'))
115 | red_border = ''
116 |
117 | if 'func/' in line or 'func\\' in line:
118 | path, remainder = line.split(': ', 1)
119 | ostream.write(('\x1b[33m%s: \x1b[37m%s' % (path, remainder)).encode('utf-8'))
120 | else:
121 | ostream.write(line.encode('utf-8'))
122 |
--------------------------------------------------------------------------------
/contracts/tests/tests-job.fc:
--------------------------------------------------------------------------------
1 | forall F, A, R -> (int, int, R) gas::invoke_nowrap(F fun, A args) impure asm
2 | "{" ;; <- this function saves gas-remaining to 11-th element of c7's config
3 | "c7 PUSH DUP FIRST" ;; (c7, config)
4 | "GASLIMITSTEMP SWAP DROP" ;; (c7, config, gas_remaining)
5 | "11 SETINDEX 0 SETINDEX c7 POP" ;; <- new c7 saved
6 | "} : save-gas-remaining"
7 |
8 | "{" ;; <- assumes 'save-gas-remaining' was called before
9 | "GASLIMITSTEMP SWAP DROP"
10 | "11 GETPARAM SWAP SUB"
11 | ;; we additionally executed [GASLIMITSTEMP, SWAP, DROP, SETINDEX, SETINDEX, POP],
12 | ;; before actually calling 'EXECUTE'
13 | ;; so we need to subtract ( 18 + 18 + 26 + (12+26) + (1+26) + 18)
14 | ;; from consumed gas value
15 | "145 PUSHINT SUB"
16 | "} : compute-gas-used"
17 |
18 | "c7 PUSH DUP FIRST c0 PUSH" ;; (fun, args, c7, config, c0)
19 | "12 SETINDEXQ 0 SETINDEX" ;; (fun, args, c7)
20 | "c7 POP" ;; (fun, args)
21 |
22 | "NEWC ENDC c5 POP" ;; clear actions cell
23 | "RESETLOADEDCELLS" ;; <- make sure first cell load cost 100 gas, not 25
24 | "255 PUSHINT EXPLODEVAR" ;; (fun, arg_1, arg_2, ..., arg_n, n)
25 | "DUP INC ROLLX" ;; (arg_1, arg_2, ..., arg_n, n, fun)
26 | "<{"
27 | "<{" ;; <- normal execution
28 | "<{"
29 | "compute-gas-used" ;; <- execution terminated via jump to c1
30 | "DEPTH DEC ROLLREVX"
31 | "DEPTH DEC TUPLEVAR"
32 | "ZERO ROTREV" ;; (exit_code, gas_used, [res...])
33 | "c7 PUSH FIRST 12 INDEX" ;; (exit_code, gas_used, [res...], ret_to)
34 | "JMPX"
35 | "}> PUSHCONT SWAP c1 SETCONTCTR"
36 | "save-gas-remaining"
37 | "EXECUTE"
38 | "compute-gas-used" ;; <- it is important to call it just after EXECUTE, so we don't count additional commands
39 | "DEPTH DEC ROLLREVX" ;; (gas_used, res_1, res_2, ..., res_k)
40 | "DEPTH DEC TUPLEVAR" ;; (gas_used, [res_1, res_2, ..., res_k])
41 | "ZERO ROTREV" ;; (exit_code = 0, gas_used, [res..])
42 | "}> PUSHCONT"
43 | "<{" ;; <- exception handler
44 | "compute-gas-used" ;; (exception_info, exit_code, gas_used)
45 | "ROT DROP NIL" ;; (exit_code, gas_used, [])
46 | "}> PUSHCONT"
47 | "TRY"
48 | "}> PUSHCONT" ;; (args.., n, fun, func_with_exception_wrapper)
49 | "ROT INC -1 PUSHINT" ;; (args.., fun, func_with_exception_wrapper, n + 1, -1)
50 | "CALLXVARARGS" ;; (exit_code, gas_used, [res..])
51 | ;
52 |
53 | forall F, A, R -> (int, int, R) gas::invoke_method_full(F fun, A args) impure {
54 | ;; no inlining
55 | return gas::invoke_nowrap(fun, args);
56 | }
57 |
58 | forall F, A, R -> (int, R) gas::invoke_method(F fun, A args) impure inline {
59 | (int exit_code, int gas_used, R return_values) = gas::invoke_method_full(fun, args);
60 | throw_if(exit_code, (exit_code != 0) & (exit_code != 1));
61 | return (gas_used, return_values);
62 | }
63 |
64 | forall F, A -> int invoke_gas(F fun, A args) impure inline {
65 | (int exit_code, int gas_used, _) = gas::invoke_method_full(fun, args);
66 |
67 | ;; ~dump(4445); ~dump("gas::invoke_method_full exit"u);
68 |
69 | throw_if(exit_code, (exit_code != 0) & (exit_code != 1));
70 | return gas_used;
71 | }
72 |
73 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
74 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
75 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
76 |
77 | ;; job_unlocked$00 poster:MsgAddressInt value:uint64 desc:^.. poster_key:uint256
78 | cell init_job() asm
79 | "b{00}"
80 | "b{100} x{003b819131c4c44a0caa0aefe707735ecca910875fe4c948a88c39be0edbf5d204}"
81 | "x{000000003b9aca00}"
82 | "x{0000000054657374206A6F62} |_"
83 | "B priv>pub B, b> c PUSHREF";
85 | cell init_job_locked() asm
86 | "b{01}"
87 | "b{100} x{007da879d948524500877828304cfbdb4c43085f57c9fc49cc9fdfe6dc3bd1b490}"
88 | "b{100} x{003b819131c4c44a0caa0aefe707735ecca910875fe4c948a88c39be0edbf5d204}"
89 | "x{000000003b9aca00}"
90 | "x{0000000054657374206A6F62} |_"
91 | "B priv>pub B, b> c PUSHREF";
93 | cell init_job_working() asm
94 | "b{10}"
95 | "b{100} x{003b819131c4c44a0caa0aefe707735ecca910875fe4c948a88c39be0edbf5d204}"
96 | "b{100} x{008be1e8c90f9d4f44895721ab629051a8ae6e345730d84ca9a7d403abbf563897}"
97 | "x{000000003b9aca00}"
98 | "x{0000000054657374206A6F62} |_"
99 |
100 | "x{0000000054657374206A6F62} |_"
101 |
102 | "B priv>pub B, b> B priv>pub B, b> c PUSHREF";
106 |
107 | slice offer_addr() asm
108 | "b{100} x{007da879d948524500877828304cfbdb4c43085f57c9fc49cc9fdfe6dc3bd1b490}"
109 | "|+ PUSHSLICE";
110 | slice worker_addr_desc_key() asm
111 | "b{100} x{008be1e8c90f9d4f44895721ab629051a8ae6e345730d84ca9a7d403abbf563897}"
112 | "x{0000000054657374206A6F62} |_"
113 | "B priv>pub B, b> B ed25519_sign B ed25519_sign 1);
223 | }
224 |
225 | () __test_job_unlock_on_failure() {
226 | set_data(init_job_locked());
227 | invoke_gas(recv_internal, [1, build_offer_pfx().end_cell(),
228 | begin_cell().store_uint(op::lock_failed, 32).end_cell().begin_parse()]);
229 |
230 | throw_unless(140, is_null(parse_c5()));
231 | throw_unless(141, cell_hash(get_data()) == cell_hash(init_job()));
232 | }
233 |
234 | () __test_job_unlock_on_bounce() {
235 | set_data(init_job_locked());
236 | invoke_gas(recv_internal, [1, build_offer_pfx_bounced().end_cell(),
237 | begin_cell().store_uint(0xFFFFFFFF, 32).end_cell().begin_parse()]);
238 |
239 | throw_unless(150, is_null(parse_c5()));
240 | throw_unless(151, cell_hash(get_data()) == cell_hash(init_job()));
241 | }
242 |
243 | () __test_job_lockup_worker_stake() {
244 | set_data(init_job_locked());
245 | invoke_gas(recv_internal, [1, build_offer_pfx().end_cell(),
246 | begin_cell()
247 | .store_uint(op::lock_success, 32)
248 | .store_slice(worker_addr_desc_key())
249 | .end_cell()
250 | .begin_parse()]);
251 |
252 | throw_unless(160, is_null(parse_c5()));
253 | throw_unless(161, cell_hash(get_data()) == cell_hash(init_job_working()));
254 | }
255 |
256 | () __test_job_work_complete() {
257 | set_data(init_job_working());
258 | invoke_gas(recv_internal, [1, build_offer_pfx().end_cell(),
259 | begin_cell()
260 | .store_uint(op::finish_job, 32)
261 | .store_ref(proposal_poster())
262 | .store_ref(proposal_worker())
263 | .end_cell()
264 | .begin_parse()]);
265 |
266 | tuple actions = parse_c5();
267 | throw_if(170, is_null(actions));
268 | throw_unless(171, tuple_length(actions) == 2);
269 | }
270 |
--------------------------------------------------------------------------------
/contracts/tests/tests-offer.fc:
--------------------------------------------------------------------------------
1 | forall F, A, R -> (int, int, R) gas::invoke_nowrap(F fun, A args) impure asm
2 | "{" ;; <- this function saves gas-remaining to 11-th element of c7's config
3 | "c7 PUSH DUP FIRST" ;; (c7, config)
4 | "GASLIMITSTEMP SWAP DROP" ;; (c7, config, gas_remaining)
5 | "11 SETINDEX 0 SETINDEX c7 POP" ;; <- new c7 saved
6 | "} : save-gas-remaining"
7 |
8 | "{" ;; <- assumes 'save-gas-remaining' was called before
9 | "GASLIMITSTEMP SWAP DROP"
10 | "11 GETPARAM SWAP SUB"
11 | ;; we additionally executed [GASLIMITSTEMP, SWAP, DROP, SETINDEX, SETINDEX, POP],
12 | ;; before actually calling 'EXECUTE'
13 | ;; so we need to subtract ( 18 + 18 + 26 + (12+26) + (1+26) + 18)
14 | ;; from consumed gas value
15 | "145 PUSHINT SUB"
16 | "} : compute-gas-used"
17 |
18 | "c7 PUSH DUP FIRST c0 PUSH" ;; (fun, args, c7, config, c0)
19 | "12 SETINDEXQ 0 SETINDEX" ;; (fun, args, c7)
20 | "c7 POP" ;; (fun, args)
21 |
22 | "NEWC ENDC c5 POP" ;; clear actions cell
23 | "RESETLOADEDCELLS" ;; <- make sure first cell load cost 100 gas, not 25
24 | "255 PUSHINT EXPLODEVAR" ;; (fun, arg_1, arg_2, ..., arg_n, n)
25 | "DUP INC ROLLX" ;; (arg_1, arg_2, ..., arg_n, n, fun)
26 | "<{"
27 | "<{" ;; <- normal execution
28 | "<{"
29 | "compute-gas-used" ;; <- execution terminated via jump to c1
30 | "DEPTH DEC ROLLREVX"
31 | "DEPTH DEC TUPLEVAR"
32 | "ZERO ROTREV" ;; (exit_code, gas_used, [res...])
33 | "c7 PUSH FIRST 12 INDEX" ;; (exit_code, gas_used, [res...], ret_to)
34 | "JMPX"
35 | "}> PUSHCONT SWAP c1 SETCONTCTR"
36 | "save-gas-remaining"
37 | "EXECUTE"
38 | "compute-gas-used" ;; <- it is important to call it just after EXECUTE, so we don't count additional commands
39 | "DEPTH DEC ROLLREVX" ;; (gas_used, res_1, res_2, ..., res_k)
40 | "DEPTH DEC TUPLEVAR" ;; (gas_used, [res_1, res_2, ..., res_k])
41 | "ZERO ROTREV" ;; (exit_code = 0, gas_used, [res..])
42 | "}> PUSHCONT"
43 | "<{" ;; <- exception handler
44 | "compute-gas-used" ;; (exception_info, exit_code, gas_used)
45 | "ROT DROP NIL" ;; (exit_code, gas_used, [])
46 | "}> PUSHCONT"
47 | "TRY"
48 | "}> PUSHCONT" ;; (args.., n, fun, func_with_exception_wrapper)
49 | "ROT INC -1 PUSHINT" ;; (args.., fun, func_with_exception_wrapper, n + 1, -1)
50 | "CALLXVARARGS" ;; (exit_code, gas_used, [res..])
51 | ;
52 |
53 | forall F, A, R -> (int, int, R) gas::invoke_method_full(F fun, A args) impure {
54 | ;; no inlining
55 | return gas::invoke_nowrap(fun, args);
56 | }
57 |
58 | forall F, A, R -> (int, R) gas::invoke_method(F fun, A args) impure method_id {
59 | (int exit_code, int gas_used, R return_values) = gas::invoke_method_full(fun, args);
60 | throw_if(exit_code, (exit_code != 0) & (exit_code != 1));
61 | return (gas_used, return_values);
62 | }
63 |
64 | forall F, A -> int invoke_gas(F fun, A args) impure method_id {
65 | (int exit_code, int gas_used, _) = gas::invoke_method_full(fun, args);
66 | throw_if(exit_code, (exit_code != 0) & (exit_code != 1));
67 | return gas_used;
68 | }
69 |
70 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
71 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
72 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
73 |
74 | () __fift_init() impure method_id asm
75 | "{"
76 | "b{00}"
77 | "b{100} x{003b819131c4c44a0caa0aefe707735ecca910875fe4c948a88c39be0edbf5d204}"
78 | "x{000000003b9aca00}"
79 | "x{0000000054657374206A6F62} |_"
80 | "B priv>pub B, b> c hash 1 160 << 1- and // job hash"
82 | "} : job-hash";
83 |
84 | () __check_fift_init() impure method_id {
85 | return __fift_init();
86 | }
87 |
88 | slice job_addr() asm
89 | "b{100} x{00227e0ff3501e47609d3cf76dfb6485252be873632f4b479fb86dc5d65dc3ffb0}"
90 | "|+ PUSHSLICE";
91 | builder build_job_pfx() asm
92 | "x{6}"
93 | "b{100} x{00227e0ff3501e47609d3cf76dfb6485252be873632f4b479fb86dc5d65dc3ffb0}"
94 | "b{00}"
95 | "|+ |+ |+ PUSHSLICE NEWC STSLICE";
96 |
97 | builder build_worker_pfx() asm
98 | "x{6}"
99 | "b{100} x{0077ebf11f623158315f29fdcc52617eb56c0d0567807ae5934de8bf08358fb980}"
100 | "b{00}"
101 | "|+ |+ |+ PUSHSLICE NEWC STSLICE";
102 | builder build_worker_pfx_bounced() asm
103 | "x{7}"
104 | "b{100} x{0077ebf11f623158315f29fdcc52617eb56c0d0567807ae5934de8bf08358fb980}"
105 | "b{00}"
106 | "|+ |+ |+ PUSHSLICE NEWC STSLICE";
107 |
108 | int job_hash() asm "job-hash PUSHINT";
109 | cell init_offer() asm
110 | " ref,"
118 | "20230216 256 u>B priv>pub B,"
119 | "job-hash 160 u,"
120 | "b> PUSHREF";
121 | cell init_offer_locked() asm
122 | " ref,"
130 | "20230216 256 u>B priv>pub B,"
131 | "job-hash 160 u,"
132 | "b> PUSHREF";
133 |
134 | () __test_offer_init_success() {
135 | set_data(init_offer());
136 | invoke_gas(recv_internal, [50000000, build_worker_pfx().end_cell(),
137 | begin_cell().end_cell().begin_parse()]);
138 | throw_unless(110, cell_hash(get_data()) == cell_hash(init_offer()));
139 | }
140 |
141 | () __test_offer_plugin_destroy() {
142 | set_data(init_offer());
143 | invoke_gas(recv_internal, [1, build_worker_pfx().end_cell(),
144 | begin_cell().store_uint(op::destruct, 32).end_cell().begin_parse()]);
145 |
146 | tuple actions = parse_c5();
147 | throw_if(120, is_null(actions));
148 | throw_unless(121, tuple_length(actions) == 1);
149 | throw_unless(122, first(first(actions)) == 0);
150 | throw_unless(123, third(first(actions)) == 128);
151 | }
152 |
153 | () __test_offer_collapse_initiates() {
154 | set_data(init_offer());
155 | invoke_gas(recv_internal, [1, build_job_pfx().end_cell(),
156 | begin_cell()
157 | .store_uint(op::collapse, 32)
158 | .store_uint(job_hash(), 160)
159 | .end_cell()
160 | .begin_parse()]);
161 |
162 | tuple actions = parse_c5();
163 | throw_if(130, is_null(actions));
164 | throw_unless(131, tuple_length(actions) == 1);
165 | throw_unless(132, first(first(actions)) == 0);
166 | throw_unless(133, third(first(actions)) == 64);
167 | throw_unless(134, cell_hash(get_data()) == cell_hash(init_offer_locked()));
168 | }
169 |
170 | () __test_offer_collapse_rejection_handled() {
171 | set_data(init_offer_locked());
172 | invoke_gas(recv_internal, [1, build_worker_pfx_bounced().end_cell(),
173 | begin_cell().store_uint(0xFFFFFFFF, 32).end_cell().begin_parse()]);
174 |
175 | tuple actions = parse_c5();
176 | throw_if(140, is_null(actions));
177 | throw_unless(141, tuple_length(actions) == 1);
178 | throw_unless(142, first(first(actions)) == 0);
179 | throw_unless(143, third(first(actions)) == 64);
180 | throw_unless(144, cell_hash(get_data()) == cell_hash(init_offer()));
181 | }
182 |
183 | () __test_offer_collapse_completion() {
184 | set_data(init_offer_locked());
185 | invoke_gas(recv_internal, [1, build_worker_pfx().end_cell(),
186 | begin_cell().store_uint(op::payment_ok, 32).end_cell().begin_parse()]);
187 |
188 | tuple actions = parse_c5();
189 | throw_if(150, is_null(actions));
190 | throw_unless(151, tuple_length(actions) == 1);
191 | throw_unless(152, first(first(actions)) == 0);
192 | throw_unless(153, third(first(actions)) == 128);
193 | throw_unless(154, cell_hash(get_data()) == cell_hash(init_offer_locked()));
194 | }
195 |
--------------------------------------------------------------------------------
/contracts/toncli.err:
--------------------------------------------------------------------------------
1 | --- Logging error ---
2 | Traceback (most recent call last):
3 | File "C:\Program Files\Python39\lib\logging\__init__.py", line 1082, in emit
4 | stream.write(msg + self.terminator)
5 | File "C:\Program Files\Python39\lib\encodings\cp1251.py", line 19, in encode
6 | return codecs.charmap_encode(input,self.errors,encoding_table)[0]
7 | UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f308' in position 6: character maps to
8 | Call stack:
9 | File "C:\Program Files\Python39\lib\runpy.py", line 197, in _run_module_as_main
10 | return _run_code(code, main_globals, None,
11 | File "C:\Program Files\Python39\lib\runpy.py", line 87, in _run_code
12 | exec(code, run_globals)
13 | File "C:\Program Files\Python39\Scripts\toncli.exe\__main__.py", line 7, in
14 | sys.exit(main())
15 | File "C:\Users\Tigr\AppData\Roaming\Python\Python39\site-packages\toncli\main.py", line 67, in main
16 | CommandsExecuter(command, string_kwargs, parser)
17 | File "C:\Users\Tigr\AppData\Roaming\Python\Python39\site-packages\toncli\modules\utils\commands\commands_executer.py", line 39, in __init__
18 | self.command_mapper[command](self)
19 | File "C:\Users\Tigr\AppData\Roaming\Python\Python39\site-packages\toncli\modules\utils\commands\commands_executer.py", line 49, in run_tests_command
20 | return RunTestsCommand(self.string_kwargs, self.parser)
21 | File "C:\Users\Tigr\AppData\Roaming\Python\Python39\site-packages\toncli\modules\utils\commands\command_classes\run_tests_command.py", line 12, in __init__
22 | test_runner.run(args.contracts.split() if args.contracts else None,
23 | File "C:\Users\Tigr\AppData\Roaming\Python\Python39\site-packages\toncli\modules\utils\test\tests.py", line 26, in run
24 | logger.info(f"\U0001f308 Start tests")
25 | Message: '\U0001f308 Start tests'
26 | Arguments: ()
27 | --- Logging error ---
28 | Traceback (most recent call last):
29 | File "C:\Program Files\Python39\lib\logging\__init__.py", line 1082, in emit
30 | stream.write(msg + self.terminator)
31 | File "C:\Program Files\Python39\lib\encodings\cp1251.py", line 19, in encode
32 | return codecs.charmap_encode(input,self.errors,encoding_table)[0]
33 | UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f94c' in position 6: character maps to
34 | Call stack:
35 | File "C:\Program Files\Python39\lib\runpy.py", line 197, in _run_module_as_main
36 | return _run_code(code, main_globals, None,
37 | File "C:\Program Files\Python39\lib\runpy.py", line 87, in _run_code
38 | exec(code, run_globals)
39 | File "C:\Program Files\Python39\Scripts\toncli.exe\__main__.py", line 7, in
40 | sys.exit(main())
41 | File "C:\Users\Tigr\AppData\Roaming\Python\Python39\site-packages\toncli\main.py", line 67, in main
42 | CommandsExecuter(command, string_kwargs, parser)
43 | File "C:\Users\Tigr\AppData\Roaming\Python\Python39\site-packages\toncli\modules\utils\commands\commands_executer.py", line 39, in __init__
44 | self.command_mapper[command](self)
45 | File "C:\Users\Tigr\AppData\Roaming\Python\Python39\site-packages\toncli\modules\utils\commands\commands_executer.py", line 49, in run_tests_command
46 | return RunTestsCommand(self.string_kwargs, self.parser)
47 | File "C:\Users\Tigr\AppData\Roaming\Python\Python39\site-packages\toncli\modules\utils\commands\command_classes\run_tests_command.py", line 12, in __init__
48 | test_runner.run(args.contracts.split() if args.contracts else None,
49 | File "C:\Users\Tigr\AppData\Roaming\Python\Python39\site-packages\toncli\modules\utils\test\tests.py", line 47, in run
50 | logger.info(f"\U0001f94c Build {gr}successfully{rs}, check out {gr}.{location}{rs}")
51 | Message: '\U0001f94c Build \x1b[32msuccessfully\x1b[0m, check out \x1b[32m.\\build\x1b[0m'
52 | Arguments: ()
53 |
--------------------------------------------------------------------------------
/contracts/toncli.log:
--------------------------------------------------------------------------------
1 | #DEBUG#: s0 = CS{Cell{004077ebf11f623158315f29fdcc52617eb56c0d0567807ae5934de8bf08358fb980} bits: 0..256; refs: 0..0}
2 | [ 3][t 0][2023-07-01 03:30:15.2123331][vm.cpp:586] steps: 14 gas: used=1711, max=1000000000, limit=1000000000, credit=0
3 | [ 3][t 0][2023-07-01 03:30:15.2128946][vm.cpp:586] steps: 100 gas: used=6222, max=1000000000, limit=1000000000, credit=0
4 | [ 3][t 0][2023-07-01 03:30:15.2129829][vm.cpp:586] steps: 221 gas: used=10618, max=1000000000, limit=1000000000, credit=0
5 | [ 3][t 0][2023-07-01 03:30:15.2130583][vm.cpp:586] steps: 281 gas: used=13982, max=1000000000, limit=1000000000, credit=0
6 | [ 3][t 0][2023-07-01 03:30:15.2131100][vm.cpp:586] steps: 126 gas: used=7646, max=1000000000, limit=1000000000, credit=0
7 | [ 3][t 0][2023-07-01 03:30:15.2131640][vm.cpp:586] steps: 169 gas: used=8611, max=1000000000, limit=1000000000, credit=0
8 | [ 3][t 0][2023-07-01 03:30:15.2132158][vm.cpp:586] steps: 169 gas: used=8603, max=1000000000, limit=1000000000, credit=0
9 | [ 3][t 0][2023-07-01 03:30:15.2132711][vm.cpp:586] steps: 191 gas: used=9763, max=1000000000, limit=1000000000, credit=0
10 | [ 3][t 0][2023-07-01 03:30:15.2134929][vm.cpp:586] steps: 412 gas: used=18346, max=1000000000, limit=1000000000, credit=0
11 | INFO: Test [[32m__test_output_address[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m1711[0m]
12 | INFO: Test [[32m__test_job_init_success[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m6222[0m]
13 | INFO: Test [[32m__test_job_revoke_success[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m10618[0m]
14 | INFO: Test [[32m__test_job_lock_success[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m13982[0m]
15 | INFO: Test [[32m__test_job_lock_insufficient_funds[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m7646[0m]
16 | INFO: Test [[32m__test_job_unlock_on_failure[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m8611[0m]
17 | INFO: Test [[32m__test_job_unlock_on_bounce[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m8603[0m]
18 | INFO: Test [[32m__test_job_lockup_worker_stake[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m9763[0m]
19 | INFO: Test [[32m__test_job_work_complete[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m18346[0m]
20 |
21 | [ 3][t 0][2023-07-01 03:30:15.2472293][vm.cpp:586] steps: 107 gas: used=6735, max=1000000000, limit=1000000000, credit=0
22 | [ 3][t 0][2023-07-01 03:30:15.2473602][vm.cpp:586] steps: 236 gas: used=11313, max=1000000000, limit=1000000000, credit=0
23 | [ 3][t 0][2023-07-01 03:30:15.2474368][vm.cpp:586] steps: 291 gas: used=14122, max=1000000000, limit=1000000000, credit=0
24 | [ 3][t 0][2023-07-01 03:30:15.2475034][vm.cpp:586] steps: 269 gas: used=13715, max=1000000000, limit=1000000000, credit=0
25 | [ 3][t 0][2023-07-01 03:30:15.2475700][vm.cpp:586] steps: 282 gas: used=13313, max=1000000000, limit=1000000000, credit=0
26 | INFO: Test [[32m__test_offer_init_success[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m6735[0m]
27 | INFO: Test [[32m__test_offer_plugin_destroy[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m11313[0m]
28 | INFO: Test [[32m__test_offer_collapse_initiates[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m14122[0m]
29 | INFO: Test [[32m__test_offer_collapse_rejection_handled[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m13715[0m]
30 | INFO: Test [[32m__test_offer_collapse_completion[0m] status: [[32mSUCCESS[0m] Test result: [[32m[][0m] Total gas used (including testing code): [[32m13313[0m]
31 |
32 |
--------------------------------------------------------------------------------
/freelance-highlevel.idr:
--------------------------------------------------------------------------------
1 | module Main
2 |
3 | import Data.Fin
4 |
5 | %default covering
6 |
7 |
8 | --------------------------------------------------------------------------------
9 | ---- Std extensions
10 | --------------------------------------------------------------------------------
11 |
12 | lookupBy : (a -> b -> Bool) -> a -> List (b, v) -> Maybe v
13 | lookupBy p e [] = Nothing
14 | lookupBy p e ((l, r) :: xs) =
15 | if p e l then
16 | Just r
17 | else
18 | lookupBy p e xs
19 |
20 | repeat : (n : Nat) -> List a -> List a
21 | repeat Z _ = []
22 | repeat (S n) x = x ++ repeat n x
23 |
24 | --------------------------------------------------------------------------------
25 | ---- Arithmetic types
26 | --------------------------------------------------------------------------------
27 |
28 | data UintSeq : Nat -> Type where
29 | UintSeqVoid : UintSeq Z
30 | UintSeqLow : (bits : Nat) -> UintSeq bits -> UintSeq (S bits)
31 | UintSeqHigh : (bits : Nat) -> UintSeq bits -> UintSeq (S bits)
32 |
33 | using (n : Nat)
34 | Eq (UintSeq n) where
35 | (==) UintSeqVoid UintSeqVoid = True
36 | (==) (UintSeqLow _ a) (UintSeqLow _ b) = a == b
37 | (==) (UintSeqHigh _ _) (UintSeqLow _ _) = False
38 | (==) (UintSeqLow _ _) (UintSeqHigh _ _) = False
39 | (==) (UintSeqHigh _ a) (UintSeqHigh _ b) = a == b
40 |
41 | Ord (UintSeq n) where
42 | compare UintSeqVoid UintSeqVoid = EQ
43 | compare (UintSeqLow _ a) (UintSeqLow _ b) = compare a b
44 | compare (UintSeqHigh _ _) (UintSeqLow _ _) = GT
45 | compare (UintSeqLow _ _) (UintSeqHigh _ _) = LT
46 | compare (UintSeqHigh _ a) (UintSeqHigh _ b) = compare a b
47 |
48 |
49 | seq_to_bits : (n : Nat) -> UintSeq n -> List Nat
50 | seq_to_bits Z UintSeqVoid = Nil
51 | seq_to_bits (S n) (UintSeqLow n low) = 0 :: (seq_to_bits n low)
52 | seq_to_bits (S n) (UintSeqHigh n low) = 1 :: (seq_to_bits n low)
53 |
54 | bits_to_seq : (a : List (Fin 2)) -> UintSeq (length a)
55 | bits_to_seq Nil = UintSeqVoid
56 | bits_to_seq (0 :: next) = UintSeqLow _ $ bits_to_seq next
57 | bits_to_seq (1 :: next) = UintSeqHigh _ $ bits_to_seq next
58 |
59 | bits_to_nat : List Nat -> Nat
60 | bits_to_nat Nil = 0
61 | bits_to_nat (v :: old) = (bits_to_nat old) * 2 + v
62 |
63 | stn : (n : Nat) -> UintSeq n -> Nat
64 | stn n v = bits_to_nat (reverse (seq_to_bits n v))
65 |
66 | build_zero_uint_seq : (n : Nat) -> UintSeq n
67 | build_zero_uint_seq Z = UintSeqVoid
68 | build_zero_uint_seq (S k) = UintSeqLow _ (build_zero_uint_seq k)
69 |
70 | build_high_uint_seq : (n : Nat) -> UintSeq n
71 | build_high_uint_seq Z = UintSeqVoid
72 | build_high_uint_seq (S k) = UintSeqHigh _ (build_high_uint_seq k)
73 |
74 | --------------------------------------------------------------------------------
75 | ---- TON-specific types
76 | --------------------------------------------------------------------------------
77 |
78 | data Anycast : Type where
79 | AnycastCreate : (depth : UintSeq 5) -> (pfx : UintSeq (stn 5 depth)) -> Anycast
80 |
81 | data MessageAddr : Type where
82 | MsgAddressIntStd : (Maybe Anycast) -> (workchain : UintSeq 8) -> (hash_part : UintSeq 256) -> MessageAddr
83 | -- MsgAddressIntVar : (Maybe Anycast) -> (addr_len : UintSeq 9) -> (workchain : UintSeq 32) -> (hash_part : UintSeq (stn 9 addr_len)) -> MessageAddr
84 |
85 | Eq MessageAddr where
86 | (==) (MsgAddressIntStd Nothing wc1 hp1) (MsgAddressIntStd Nothing wc2 hp2) = (wc1 == wc2) && (hp1 == hp2)
87 | (==) _ _ = False
88 |
89 | data TxMessage : Type where
90 | IntMsg : (bounce : Bool) -> (src : MessageAddr) -> (dest : MessageAddr) -> (coins : UintSeq 120) -> (init : Maybe ()) -> (body : JobMsgBody) -> TxMessage
91 | ExtInMsg : (src : MessageAddr) -> (dest : MessageAddr) -> (init : Maybe ()) -> (body : List Nat) -> TxMessage
92 | ExtOutMsg : (src : MessageAddr) -> (dest : MessageAddr) -> (init : Maybe ()) -> (body : List Nat) -> TxMessage
93 |
94 | --------------------------------------------------------------------------------
95 | ---- High-level contracts representation
96 | --------------------------------------------------------------------------------
97 |
98 | mutual
99 | data NextState : Type where
100 | MkNextState : ContractState ncs => ncs -> NextState
101 |
102 | interface ContractState cs where
103 | to_code : cs -> (cs -> TxMessage -> (NextState, List TxMessage))
104 | to_data : cs -> Type
105 |
106 | data Contract : Type where
107 | Uninit : (addr : MessageAddr) -> Contract
108 | Init : ContractState cs => (addr : MessageAddr) -> (st : cs) -> (balance : Nat) -> Contract
109 |
110 | run_tvm : NextState -> TxMessage -> (NextState, List TxMessage)
111 | run_tvm (MkNextState wrapped_state) msg = (to_code wrapped_state) wrapped_state msg
112 |
113 | build_bounce : TxMessage -> List TxMessage
114 | build_bounce (IntMsg True src self coins _ body) = [IntMsg False self src coins Nothing $ Bounce body]
115 | build_bounce _ = Nil
116 |
117 | bounce_typical : ContractState cs => cs -> TxMessage -> (NextState, List TxMessage)
118 | bounce_typical state msg = (MkNextState state, build_bounce msg)
119 |
120 | ----
121 |
122 | -- ContractStorage = (List (MessageAddr, NextState))
123 | lookup_contract : (List (MessageAddr, NextState)) -> MessageAddr -> Maybe NextState
124 | lookup_contract contracts addr = lookupBy f () contracts where
125 | f : () -> MessageAddr -> Bool
126 | f _ some_addr = addr == some_addr
127 |
128 | extract_contract : (List (MessageAddr, NextState)) -> MessageAddr -> (Maybe NextState, (List (MessageAddr, NextState)))
129 | extract_contract Nil addr = (Nothing, Nil)
130 | extract_contract (x::xs) addr = let (gaddr, v) = x in
131 | if gaddr == addr then
132 | (Just v, xs)
133 | else
134 | let (loaded, storage) = extract_contract xs addr in
135 | (loaded, (gaddr,v) :: storage)
136 |
137 | extract_dest : TxMessage -> MessageAddr
138 | extract_dest (IntMsg _ _ dest _ _ _) = dest
139 | extract_dest (ExtInMsg _ dest _ _) = dest
140 | extract_dest (ExtOutMsg _ dest _ _) = dest
141 |
142 | main_loop : List TxMessage -> List (MessageAddr, NextState) -> (List TxMessage, List (MessageAddr, NextState))
143 | main_loop Nil contracts = (Nil, contracts)
144 | main_loop (msg :: later) contracts = let (mb_contract, contracts_ext) = extract_contract contracts $ extract_dest msg in
145 | case mb_contract of
146 | Nothing => main_loop (later ++ (build_bounce msg)) contracts_ext
147 | Just contract => let (upd_contract, send) = run_tvm contract msg in
148 | (later ++ send, (extract_dest msg, upd_contract) :: contracts_ext)
149 |
150 | --------------------------------------------------------------------------------
151 | ---- Job contract
152 | --------------------------------------------------------------------------------
153 |
154 | data PayProposal : Type where
155 | ProposePay : (worker_lower : UintSeq 64) -> (worker_upper : UintSeq 64) -> PayProposal
156 |
157 | data PaySignature : PayProposal -> Type where
158 | SignPay : (proposal : PayProposal) -> (role : UintSeq 2) -> (sig : UintSeq 512) -> PaySignature proposal
159 |
160 | check_distinct_roles : PaySignature -> PaySignature -> Bool
161 | check_distinct_roles (SignPay _ r1 _) (SignPay _ r2 _) = (r1 != r2)
162 |
163 | data JobMsgBody : Type where
164 | JumLock : (offer : MessageAddr) -> JobMsgBody
165 | JlmConfirm : (worker_addr : MessageAddr) -> (worker_desc : Type) -> (worker_key : UintSeq 256) -> JobMsgBody
166 | JwmFinish : (sig_a : PaySignature) -> (sig_b: PaySignature) -> JobMsgBody
167 | Bounce : JobMsgBody -> JobMsgBody
168 | MsgRaw : List Nat -> JobMsgBody
169 |
170 | data JobContractStateDestroyed : Type where
171 | JcsDestroyed : JobContractStateDestroyed
172 |
173 | ContractState JobContractStateDestroyed where
174 | to_code _ = bounce_typical
175 | to_data _ = believe_me ()
176 |
177 | ----
178 |
179 | data JobContractDataUnlocked : Type where
180 | JcdUnlocked : (poster : MessageAddr) -> (desc : Type) -> (value : UintSeq 120) -> (poster_key : UintSeq 256) -> JobContractDataUnlocked
181 |
182 | data JobContractStateUnlocked : Type where
183 | JcsUnlocked : (jccode : (JobContractStateUnlocked -> TxMessage -> (NextState, List TxMessage))) -> (jcdata : JobContractDataUnlocked) -> JobContractStateUnlocked
184 |
185 | ContractState JobContractStateUnlocked where
186 | to_code cur_state = let JcsUnlocked jccode jcdata = cur_state in
187 | jccode
188 | to_data cur_state = let JcsUnlocked jccode jcdata = cur_state in
189 | believe_me jcdata
190 |
191 | ----
192 |
193 | data JobContractDataLockedOn : Type where
194 | JcdLockedOn : (poster : MessageAddr) -> (desc : Type) -> (value : UintSeq 120) -> (poster_key : UintSeq 256) -> (offer : MessageAddr) -> JobContractDataLockedOn
195 |
196 | data JobContractStateLockedOn : Type where
197 | JcsLockedOn : (jccode : (JobContractStateLockedOn -> TxMessage -> (NextState, List TxMessage))) -> (jcdata : JobContractDataLockedOn) -> JobContractStateLockedOn
198 |
199 | ContractState JobContractStateLockedOn where
200 | to_code cur_state = let JcsLockedOn jccode jcdata = cur_state in
201 | jccode
202 | to_data cur_state = let JcsLockedOn jccode jcdata = cur_state in
203 | believe_me jcdata
204 |
205 | ----
206 |
207 | data JobContractDataWorking : Type where
208 | JcdWorking : (poster : MessageAddr) -> (desc : Type) -> (value : UintSeq 120) -> (poster_key : UintSeq 256) -> (worker_key : UintSeq 256) -> JobContractDataWorking
209 |
210 | data JobContractStateWorking : Type where
211 | JcsWorking : (jccode : (JobContractStateWorking -> TxMessage -> (NextState, List TxMessage))) -> (jcdata : JobContractDataWorking) -> JobContractStateWorking
212 |
213 | ContractState JobContractStateWorking where
214 | to_code cur_state = let JcsWorking jccode jcdata = cur_state in
215 | jccode
216 | to_data cur_state = let JcsWorking jccode jcdata = cur_state in
217 | believe_me jcdata
218 |
219 | ----
220 |
221 | job_working : JobContractStateWorking -> TxMessage -> (NextState, List TxMessage)
222 | job_working state (IntMsg bounce src self coins _ body) = case believe_me $ to_data state of
223 | (JcdWorking poster desc value poster_key worker_key) => case body of
224 | Bounce _ => (MkNextState state, Nil)
225 | JwmFinish worker poster_sig worker_sig => case True of -- TODO: check signatures
226 | True => (MkNextState JcsDestroyed, [IntMsg False self worker value Nothing $ MsgRaw []])
227 | False => (MkNextState state, build_bounce (IntMsg bounce src self coins Nothing body))
228 | _ => (MkNextState state, build_bounce (IntMsg bounce src self coins Nothing body))
229 | _ => (MkNextState state, Nil)
230 | job_working state _ = (MkNextState state, Nil)
231 |
232 |
233 | job_locked_on : JobContractStateLockedOn -> TxMessage -> (NextState, List TxMessage)
234 | job_locked_on state (IntMsg bounce src self coins _ body) = case believe_me $ to_data state of
235 | (JcdLockedOn poster desc value poster_key offer) => case body of
236 | Bounce _ => (MkNextState state, Nil)
237 | (JlmConfirm worker_key) => case offer == src of
238 | True => ((MkNextState $ JcsWorking job_working $ believe_me $ JcdWorking poster desc value poster_key worker_key), Nil)
239 | False => (MkNextState state, build_bounce (IntMsg bounce src self coins Nothing body))
240 | _ => (MkNextState state, build_bounce (IntMsg bounce src self coins Nothing body))
241 | _ => (MkNextState state, Nil)
242 | job_locked_on state _ = (MkNextState state, Nil)
243 |
244 |
245 | job_unlocked : JobContractStateUnlocked -> TxMessage -> (NextState, List TxMessage)
246 | job_unlocked state (IntMsg bounce src self coins _ body) = case believe_me $ to_data state of
247 | (JcdUnlocked poster desc value poster_key) => case body of
248 | Bounce _ => (MkNextState state, Nil)
249 | (JumUpdate new_desc new_value new_poster_key) => case poster == src of
250 | True => ((MkNextState $ JcsUnlocked job_unlocked $ believe_me $ JcdUnlocked poster new_desc new_value new_poster_key), Nil)
251 | False => (MkNextState state, build_bounce (IntMsg bounce src self coins Nothing body))
252 | (JumLock offer) => case poster == src of
253 | True => ((MkNextState $ JcsLockedOn job_locked_on $ believe_me $ JcdLockedOn poster desc value poster_key offer), Nil)
254 | False => (MkNextState state, build_bounce (IntMsg bounce src self coins Nothing body))
255 | _ => (MkNextState state, build_bounce (IntMsg bounce src self coins Nothing body))
256 | _ => (MkNextState state, Nil)
257 | job_unlocked state _ = (MkNextState state, Nil)
258 |
259 | ----
260 |
261 | {-
262 | check_invariant : NextState -> TxMessage -> Bool
263 | check_invariant state msg = state == (fst $ run_tvm state msg)
264 |
265 | build_ju : MessageAddr -> Type -> UintSeq 120 -> UintSeq 256 -> NextState
266 | build_ju poster desc value poster_key = MkNextState $ JcsUnlocked job_unlocked $ believe_me $ JcdUnlocked poster desc value poster_key
267 |
268 | theorem_ju_no_extin_processing : (cp : MessageAddr) -> (cd : Type) -> (cv : UintSeq 120) -> (cpk : UintSeq 256)
269 | -> (ExtInMsg ma mb mc md) -> (check_invariant (build_ju cp cd cv cpk) $ ExtInMsg ma mb mc md) = True
270 | theorem_ju_no_extin_processing _ _ _ _ _ = Refl
271 |
272 | theorem_ju_invariant_nonposter : (poster : MessageAddr) -> (any : MessageAddr)
273 | -> (cd : Type) -> (cv : UintSeq 120) -> (cpk : UintSeq 256)
274 | -> (
275 | -> Not (poster == any)
276 | -> (check_invariant (build_ju poster cd cv cpk)
277 | -}
278 |
279 | ----
280 |
281 | main : IO ()
282 | main = do putStrLn "Theorems proved"
283 | putStrLn "Starting transactions loop"
284 | putStrLn "Transactions loop finished"
285 |
--------------------------------------------------------------------------------
/interaction.tlb:
--------------------------------------------------------------------------------
1 | // Common user story = CUS.
2 | // CUS-1. Poster creates a job contract. Message is empty.
3 | job_unlocked$00 poster:MsgAddressInt value:uint64 desc:^Cell poster_key:uint256
4 | = JobContractData;
5 |
6 | // CUS-2. Analytic message indicating address of newly created job contract.
7 | // zlib.crc32(b'notify::contract') & 0x7FFFFFFF
8 | _#130850fc job_contract:MsgAddressInt value:uint64 desc:^Cell poster_key:uint256
9 | = InternalMsgBody;
10 |
11 | // CUS-3. Worker deploys an offer contract as plugin.
12 | offer_unlocked$00 job:MsgAddressInt worker:MsgAddressInt stake:uint64 desc:^Cell
13 | worker_key:uint256 short_job_hash:uint160 = OfferContractData;
14 | _ sig:uint512 sw:uint32 until:uint32 seqno:uint32 [1]:uint8 [0]:uint8 [0.05]:TON
15 | state_init:^OfferContractState body_init:^(()) = InternalMsgBody;
16 |
17 | // CUS-4. Analytic message indicating address of newly created offer contract.
18 | // zlib.crc32(b'notify::offer') & 0x7FFFFFFF
19 | _#18ceb1bf offer_contract:MsgAddressInt stake:uint64 desc:^Cell
20 | worker_key:uint256 short_job_hash:uint160 = InternalMsgBody;
21 |
22 | // CUS-5. Poster chooses an offer.
23 | lock_on_offer#000000AC offer_data_init:^OfferContractData
24 | addr:MsgAddressInt = InternalMsgBody;
25 |
26 | // CUS-6. Job contract locks.
27 | collapse#000000B1 current_short_hash:uint160 = InternalMsgBody;
28 | job_locked$01 offer:MsgAddressInt poster:MsgAddressInt value:uint64 desc:^Cell
29 | poster_key:uint256 = JobContractData;
30 |
31 | // CUS-7. Offer contract requests money and locks.
32 | take_stake#706c7567 query_id:uint64 stake:(VarUInteger 16) ext:hme_empty
33 | = InternalMsgBody;
34 | offer_locked$01 job:MsgAddressInt worker:MsgAddressInt stake:uint64 desc:^Cell
35 | worker_key:uint256 short_job_hash:uint160 = OfferContractData;
36 |
37 | // CUS-7-ERR. Offer contract specified by job poster does not exist.
38 | refuse_collapse#ffffffff [000000B1]:uint32 ... = InternalMsgBody;
39 |
40 | // CUS-8-OK. Wallet returns the money.
41 | give_stake#f06c7567 query_id:uint64 = InternalMsgBody;
42 |
43 | // CUS-9-OK. Offer merges into job contract.
44 | lock_success#000000AD worker:MsgAddressInt desc:^Cell worker_key:uint256
45 | = InternalMsgBody;
46 | unplug#64737472 query_id:uint64 = InternalMsgBody;
47 |
48 | // CUS-8-ERR. Insufficient funds on worker's wallet.
49 | refuse_give#ffffffff [706c7567]:uint32 query_id:uint64 ... = InternalMsgBody;
50 |
51 | // CUS-9-ERR. Offer contract unlocks itself and job contract.
52 | // offer_unlocked
53 | lock_failed#000000D4 = InternalMsgBody;
54 | // job_unlocked
55 |
56 | // CUS-10. Job contract essentially becomes multisig wallet.
57 | job_working$10 poster:MsgAddressInt worker:MsgAddressInt value:uint64
58 | poster_desc:^Cell worker_desc:^Cell
59 | keys:^[poster:uint256 worker:uint256 platform:uint256]
60 | = JobContractData;
61 |
62 | // CUS-11. Single-signed messages.
63 | _ min_nton:uint64 max_nton:uint64 = PayLim;
64 | _ [FFFF726C3A3A6A6F623A3A7630]:bits $00000 job:MsgAddressInt ton_range:PayLim
65 | = Signed;
66 | poster_proposal$00 sig:bits512 worker_ton_range:PayLim = Proposal;
67 | worker_proposal$01 sig:bits512 worker_ton_range:PayLim = Proposal;
68 | ratelance_proposal$10 sig:bits512 worker_ton_range:PayLim = Proposal;
69 | ton_vals_proposal$11 = Proposal;
70 |
71 | // Config parameter ID: zlib.crc32(b'ratelance::decisions') & 0x7FFFFFFF
72 | ton_vals_proposal#_ dec_by_job:(HashmapE MsgAddressInt (uint64,uint64))
73 | = ConfigParam 1652841508;
74 |
75 | // Messages to `multisig_negotiation` address (normally the job itself)
76 | // zlib.crc32(b'op::negotiate_reward') & 0x7FFFFFFF
77 | negotiate_reward#4bed4ee8 proposal:^Proposal = InternalMsgBody;
78 |
79 | // CUS-12. Finishing work.
80 | finish_job#000000BB first_sig:^Proposal second_sig:^Proposal
81 | {first_sig::tag != second_sig::tag}
82 | = InternalMsgBody;
83 |
84 | // CUS-1,2-REV. Cancelling job.
85 | cancel_job#000000EB = InternalMsgBody;
86 |
87 | // CUS-3,4-REV. Cancelling offer.
88 | destruct#64737472 = InternalMsgBody;
89 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | class JobNotifications {
4 | static const LOAD_URL = 'https://dton.io/graphql';
5 | static const LOAD_REQUEST = `
6 | {
7 | transactions(address_friendly: "EQA__RATELANCE_______________________________JvN") {
8 | gen_utime in_msg_src_addr_workchain_id in_msg_src_addr_address_hex in_msg_body
9 | }
10 | }
11 | `;
12 |
13 | static async load() {
14 |
15 | }
16 | }
--------------------------------------------------------------------------------
/web/assets/index-GKpznQ_X.css:
--------------------------------------------------------------------------------
1 | div.svelte-3lybgc{height:1px;border-top:var(--color) 1px solid;margin:var(--margin) 0}.pages.svelte-1w3fjj0{display:flex;border:#cc880040 1px solid;border-radius:8px 8px 4px 4px;padding:0 16px;box-shadow:0 1px 2px #46321429;font-weight:600}input.svelte-1w3fjj0{flex-grow:.1;margin:8px 12px 6px;-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none}input.svelte-1w3fjj0:after{content:var(--text)}input.svelte-1w3fjj0:not(:disabled):hover{color:#80002c}input.svelte-1w3fjj0:checked{color:#b0503c;text-shadow:#a0402c16 1px 1px}.root.svelte-u61idj{min-width:400px;width:50%;min-height:200px;padding:32px;background-color:#fff}input.svelte-u61idj{font-size:16px;width:100%}input[type=radio].svelte-u61idj{-webkit-appearance:none;-moz-appearance:none;appearance:none;font-size:18px}input[type=radio].svelte-u61idj:after{content:var(--text)}.method.svelte-u61idj{display:block;border:none;border-top:#666 1px solid}.method.svelte-u61idj:not(:disabled):not(:checked):hover{color:#80002c}.method.svelte-u61idj:checked{color:#b0503c;text-shadow:#a0402c16 1px 1px}.wallet-versions.svelte-u61idj{display:flex}.wallet-ver.svelte-u61idj:not(:last-child){margin-right:12px}.wallet-ver.svelte-u61idj:not(:checked):hover{color:#2c0080}.wallet-ver.svelte-u61idj:checked{color:#3c50b0;text-shadow:#2c40a016 1px 1px}.show-hover.svelte-u61idj:not(:hover){background-color:#888;color:#888}.show-hover.svelte-u61idj{margin:0 0 8px;font-size:13px}.popup.svelte-f0v46i{position:fixed;left:0;right:0;top:0;bottom:0;background-color:#3338;display:flex;align-items:center;justify-content:center}.popup.svelte-f0v46i:empty{display:none;visibility:hidden}header.svelte-1eyc10s.svelte-1eyc10s{display:flex;margin:0 8px 8px}.icon.svelte-1eyc10s.svelte-1eyc10s{width:40px;height:40px;margin-right:8px;display:inline-block;vertical-align:top}header.svelte-1eyc10s>.svelte-1eyc10s{margin:8px}#logo.svelte-1eyc10s.svelte-1eyc10s{display:flex;align-items:center;color:#232328;height:100%}.fill-space.svelte-1eyc10s.svelte-1eyc10s{flex-grow:1}.ton-connect-v2.svelte-1eyc10s.svelte-1eyc10s{border:#cf988c 2px solid;border-radius:10px;padding:7px 17px;font-size:16px;background-color:#fff;color:#b0503c}span.svelte-t1qxhd.svelte-t1qxhd{font-size:14px;color:#71717a}a.svelte-t1qxhd.svelte-t1qxhd{color:#6b6fb0}.myaddress.svelte-t1qxhd a.svelte-t1qxhd{color:#6bb03f}.monospace.svelte-t1qxhd.svelte-t1qxhd{font-family:monospace}.job-container.svelte-1pkob5b{border:#cc880040 1px solid;border-radius:4px 4px 8px 8px;box-shadow:0 1px 2px #46321429;padding:12px 16px;font-size:17px;margin-bottom:16px}.job-error.svelte-1pkob5b{border:#ff223340 1px solid;border-radius:4px 4px 8px 8px;box-shadow:0 1px 2px #460a1429;background-color:#ff223310;color:#602328;padding:24px;font-size:17px}.job-poster.svelte-1pkob5b{font-size:14px;color:#71717a}.price.svelte-1pkob5b{margin-left:auto;margin-right:24px;margin-top:8px;width:fit-content;font-weight:800;color:#4080c0;border-right:#4080c0 2px solid;padding-right:12px}.job-description.svelte-1pkob5b{border-left:#b0503c 2px solid;padding-left:12px;margin-top:8px}.job-description.svelte-1pkob5b p:first-child:first-line{font-weight:800;font-size:22px}.job-description.svelte-1pkob5b p{margin-top:0}.controls.svelte-1pkob5b{margin-top:4px;border-top:#444 1px solid;padding-top:8px}.article.svelte-1fepyk7{border:#cc880040 1px solid;border-radius:4px 4px 8px 8px;box-shadow:0 1px 2px #46321429;padding:24px;font-size:17px}.job-container.svelte-1tuh96e{border:#cc880040 1px solid;border-radius:4px 4px 8px 8px;box-shadow:0 1px 2px #46321429;padding:12px 16px;font-size:17px;margin-bottom:16px}.job-error.svelte-1tuh96e{border:#ff223340 1px solid;border-radius:4px 4px 8px 8px;box-shadow:0 1px 2px #460a1429;background-color:#ff223310;color:#602328;padding:24px;font-size:17px}.job-poster.svelte-1tuh96e{font-size:14px;color:#71717a}.price.svelte-1tuh96e{margin-left:auto;margin-right:24px;margin-top:8px;width:fit-content;font-weight:800;color:#4080c0;border-right:#4080c0 2px solid;padding-right:12px}.job-description.svelte-1tuh96e{border-left:#b0503c 2px solid;padding-left:12px;margin-top:8px}.job-description.svelte-1tuh96e p:first-child:first-line{font-weight:800;font-size:22px}.job-description.svelte-1tuh96e p{margin-top:0}.width-limit.svelte-1roawt3{max-width:1200px;margin:0 auto}.h-spacer.svelte-1roawt3{height:24px}
2 |
--------------------------------------------------------------------------------
/web/global.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | position: relative;
3 | width: 100%;
4 | height: 100%;
5 | }
6 |
7 | body {
8 | color: #333;
9 | margin: 0;
10 | padding: 16px;
11 | box-sizing: border-box;
12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
13 | min-width: 320px;
14 | min-height: 100vh;
15 | }
16 |
17 | a {
18 | color: rgb(0,100,200);
19 | text-decoration: none;
20 | }
21 |
22 | a:hover {
23 | text-decoration: underline;
24 | }
25 |
26 | a:visited {
27 | color: rgb(0,80,160);
28 | }
29 |
30 | label {
31 | display: block;
32 | }
33 |
34 | input, button, select, textarea {
35 | font-family: inherit;
36 | font-size: inherit;
37 | -webkit-padding: 0.4em 0;
38 | padding: 0.4em;
39 | margin: 0 0 0.5em 0;
40 | box-sizing: border-box;
41 | border: 1px solid #ccc;
42 | border-radius: 2px;
43 | }
44 |
45 | input:disabled {
46 | color: #ccc;
47 | }
48 | :root {
49 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
50 | line-height: 1.5;
51 | font-weight: 400;
52 |
53 | color-scheme: light dark;
54 | color: rgba(255, 255, 255, 0.87);
55 | background-color: #242424;
56 |
57 | font-synthesis: none;
58 | text-rendering: optimizeLegibility;
59 | -webkit-font-smoothing: antialiased;
60 | -moz-osx-font-smoothing: grayscale;
61 | }
62 |
63 | a {
64 | font-weight: 500;
65 | color: #646cff;
66 | text-decoration: inherit;
67 | }
68 | a:hover {
69 | color: #535bf2;
70 | }
71 |
72 | h1 {
73 | font-size: 1.8em;
74 | line-height: 1.1;
75 | }
76 |
77 | button {
78 | border-radius: 8px;
79 | border: 1px solid transparent;
80 | padding: 0.6em 1.2em;
81 | font-size: 1em;
82 | font-weight: 500;
83 | font-family: inherit;
84 | background-color: #1a1a1a;
85 | cursor: pointer;
86 | transition: border-color 0.25s;
87 | }
88 | button:hover {
89 | border-color: #646cff;
90 | }
91 | button:focus,
92 | button:focus-visible {
93 | outline: 4px auto -webkit-focus-ring-color;
94 | }
95 |
96 | @media (prefers-color-scheme: light) {
97 | :root {
98 | color: #213547;
99 | background-color: #ffffff;
100 | }
101 | a:hover {
102 | color: #747bff;
103 | }
104 | button {
105 | background-color: #f9f9f9;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/web/images/cube.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProgramCrafter/ratelance/2de56f4f96acf0f9e1e5b0e60ae0ad846ba50170/web/images/cube.png
--------------------------------------------------------------------------------
/web/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProgramCrafter/ratelance/2de56f4f96acf0f9e1e5b0e60ae0ad846ba50170/web/images/favicon.ico
--------------------------------------------------------------------------------
/web/images/ratelance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ProgramCrafter/ratelance/2de56f4f96acf0f9e1e5b0e60ae0ad846ba50170/web/images/ratelance.png
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Svelte app
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/web/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------