ton-sdk-ruby 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ module TonSdkRuby
2
+ class Provider
3
+ attr_accessor :provider
4
+
5
+ def initialize(provider)
6
+ @provider = provider
7
+ end
8
+
9
+ def get_address_information(address)
10
+ provider.get_address_information(address)
11
+ end
12
+
13
+ def get_extended_address_information(address)
14
+ provider.get_extended_address_information(address)
15
+ end
16
+
17
+ def get_address_balance(address)
18
+ provider.get_address_balance(address)
19
+ end
20
+
21
+ def get_token_data(address)
22
+ provider.get_token_data(address)
23
+ end
24
+
25
+ def get_transactions(address, archival, limit, lt, hash, to_lt)
26
+ provider.get_transactions(address, archival, limit, lt, hash, to_lt)
27
+ end
28
+
29
+ def run_get_method(address, method, stack = [])
30
+ provider.run_get_method(address, method, stack)
31
+ end
32
+
33
+ def send_boc(boc)
34
+ provider.send_boc(boc)
35
+ end
36
+
37
+ def estimate_fee(address, body, init_code, init_data, ignore_chksig = false)
38
+ provider.estimate_fee(address, body, init_code, init_data, ignore_chksig)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,71 @@
1
+ module TonSdkRuby
2
+
3
+ class TonCenter
4
+ URL = 'https://toncenter.com/api/v2/jsonRPC'
5
+ attr_accessor :api_key, :url
6
+
7
+ def initialize(url = nil, api_key)
8
+ @url = url || URL
9
+ @api_key = api_key
10
+ end
11
+
12
+ def send_request(metod, params)
13
+ headers = {
14
+ "X-API-Key": api_key
15
+ }
16
+ body = jrpc_wrap(metod, params)
17
+ read_post_json_from_link(url, body, headers)
18
+ end
19
+
20
+ def get_address_information(address)
21
+ send_request('getAddressInformation', {address: address})
22
+ end
23
+
24
+ def get_address_balance(address)
25
+ send_request('getAddressBalance', {address: address})
26
+ end
27
+
28
+ def get_address_state(address)
29
+ send_request('getAddressState', {address: address})
30
+ end
31
+
32
+ def get_token_data(address)
33
+ send_request('getTokenData', {address: address})
34
+ end
35
+
36
+ def get_transactions(address, archival, limit, lt, hash, to_lt)
37
+ params = {
38
+ address: address, archival: archival, limit: limit, lt: lt, hash: hash, to_lt: to_lt
39
+ }
40
+ send_request('getTransactions', params)
41
+ end
42
+
43
+ def run_get_method(address, method, stack = [])
44
+ params = {
45
+ address: address, method: method, stack: stack
46
+ }
47
+ send_request('runGetMethod', params)
48
+ end
49
+
50
+ def send_boc(boc)
51
+ send_request('sendBoc', {boc: boc})
52
+ end
53
+
54
+ def estimate_fee(address, body, init_code, init_data, ignore_chksig = false)
55
+ params = {
56
+ address: address, body: body, init_code: init_code, init_data: init_data, ignore_chksig: ignore_chksig
57
+ }
58
+ send_request('estimateFee', params)
59
+ end
60
+
61
+ private
62
+ def jrpc_wrap(method, params = {})
63
+ {
64
+ id: '1',
65
+ jsonrpc: '2.0',
66
+ method: method,
67
+ params: params.compact
68
+ }
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,203 @@
1
+ require 'base64'
2
+
3
+ module TonSdkRuby
4
+
5
+ FLAG_BOUNCEABLE = 0x11
6
+ FLAG_NON_BOUNCEABLE = 0x51
7
+ FLAG_TEST_ONLY = 0x80
8
+
9
+ class Address
10
+ NONE = nil
11
+
12
+ class Type
13
+ BASE64 = 'base64'
14
+ RAW = 'raw'
15
+ end
16
+
17
+ attr_reader :hash, :workchain, :bounceable, :test_only, :type
18
+
19
+ def hash
20
+ Array.new(@hash)
21
+ end
22
+
23
+ def initialize(address, options = {})
24
+ options = {workchain: 0, bounceable: false, test_only: false}.merge(options)
25
+ is_address = Address.is_address?(address)
26
+ is_encoded = Address.is_encoded?(address)
27
+ is_raw = Address.is_raw?(address)
28
+
29
+ case true
30
+ when is_address
31
+ result = Address.parse_address(address)
32
+ when is_encoded
33
+ result = Address.parse_encoded(address)
34
+ when is_raw
35
+ result = Address.parse_raw(address)
36
+ else
37
+ raise 'Address: can\'t parse address. Unknown type.'
38
+ end
39
+
40
+ if result.nil?
41
+ raise 'Address: can\'t parse address. Unknown type.'
42
+ end
43
+
44
+ @workchain = options[:workchain] || result[:workchain]
45
+ @bounceable = options[:bounceable] || result[:bounceable]
46
+ @test_only = options[:test_only] || result[:test_only]
47
+ @hash = result[:hash]
48
+ end
49
+
50
+ def self.encode_tag(options)
51
+ bounceable = options[:bounceable]
52
+ test_only = options[:test_only]
53
+ tag = bounceable ? FLAG_BOUNCEABLE : FLAG_NON_BOUNCEABLE
54
+
55
+ test_only ? (tag | FLAG_TEST_ONLY) : tag
56
+ end
57
+
58
+ def self.decode_tag(tag)
59
+ data = tag
60
+ test_only = (data & FLAG_TEST_ONLY) != 0
61
+
62
+ if test_only
63
+ data ^= FLAG_TEST_ONLY
64
+ end
65
+
66
+ if ![FLAG_BOUNCEABLE, FLAG_NON_BOUNCEABLE].include?(data)
67
+ raise 'Address: bad address tag.'
68
+ end
69
+
70
+ bounceable = data == FLAG_BOUNCEABLE
71
+
72
+ {
73
+ bounceable: bounceable,
74
+ test_only: test_only
75
+ }
76
+ end
77
+
78
+ def eq(address)
79
+ address == self ||
80
+ (bytes_compare(hash, address.hash) && workchain == address.workchain)
81
+ end
82
+
83
+ def to_s(options = {})
84
+ type = options[:type] || Type::BASE64
85
+ workchain = options[:workchain] || self.workchain
86
+ bounceable = options[:bounceable] || self.bounceable
87
+ test_only = options[:test_only] || self.test_only
88
+ url_safe = options.key?(:url_safe) ? options[:url_safe] : true
89
+
90
+ raise 'Address: workchain must be int8.' unless workchain.is_a?(Numeric) && workchain >= -128 && workchain < 128
91
+ raise 'Address: bounceable flag must be a boolean.' unless [true, false].include?(bounceable)
92
+ raise 'Address: testOnly flag must be a boolean.' unless [true, false].include?(test_only)
93
+ raise 'Address: urlSafe flag must be a boolean.' unless [true, false].include?(url_safe)
94
+
95
+ if type == Type::RAW
96
+ "#{workchain}:#{bytes_to_hex(hash)}"
97
+ else
98
+ tag = Address.encode_tag(bounceable: bounceable, test_only: test_only)
99
+ address = [tag, workchain] + hash
100
+ checksum = crc16_bytes_be(address)
101
+ base64 = bytes_to_base64(address + checksum)
102
+
103
+ if url_safe
104
+ base64 = base64.tr('/', '_').tr('+', '-')
105
+ else
106
+ base64 = base64.tr('_', '/').tr('-', '+')
107
+ end
108
+
109
+ base64
110
+ end
111
+ end
112
+
113
+
114
+
115
+ private
116
+
117
+ def self.is_encoded?(address)
118
+ re = /^([a-zA-Z0-9_-]{48}|[a-zA-Z0-9\/\+]{48})$/
119
+ address.is_a?(String) && re.match?(address)
120
+ end
121
+
122
+ def self.is_raw?(address)
123
+ re = /^-?[0-9]:[a-zA-Z0-9]{64}$/
124
+ address.is_a?(String) && re.match?(address)
125
+ end
126
+
127
+ def self.parse_encoded(value)
128
+ base64 = value.tr('-', '+').tr('_', '/')
129
+ bytes = base64_to_bytes(base64)
130
+ data = Array.new(bytes)
131
+ address = data.shift(34)
132
+ checksum = data.shift(2)
133
+ crc = crc16_bytes_be(address)
134
+
135
+ raise 'Address: can\'t parse address. Wrong checksum.' unless bytes_compare(crc, checksum)
136
+
137
+ buffer = address.shift(2).pack('C2')
138
+
139
+ tag_and_wc = buffer.unpack('CS>')
140
+ tag = tag_and_wc.first
141
+ workchain = tag_and_wc.last.to_i
142
+
143
+ hash = address.shift(32)
144
+
145
+ decoded_tag = decode_tag(tag)
146
+ bounceable = decoded_tag[:bounceable]
147
+ test_only = decoded_tag[:test_only]
148
+
149
+ {
150
+ bounceable: bounceable,
151
+ test_only: test_only,
152
+ workchain: workchain,
153
+ hash: hash,
154
+ type: Type::BASE64
155
+ }
156
+ end
157
+
158
+ def self.parse_address(value)
159
+ workchain = value.workchain
160
+ bounceable = value.bounceable
161
+ test_only = value.test_only
162
+ hash = value.hash.clone
163
+ type = value.type
164
+
165
+ {
166
+ bounceable: bounceable,
167
+ test_only: test_only,
168
+ workchain: workchain,
169
+ hash: hash,
170
+ type: type
171
+ }
172
+ end
173
+
174
+ def self.parse_raw(value)
175
+ data = value.split(':')
176
+ workchain = data[0].to_i
177
+ hash = hex_to_bytes(data[1])
178
+ bounceable = false
179
+ test_only = false
180
+
181
+ {
182
+ bounceable: bounceable,
183
+ test_only: test_only,
184
+ workchain: workchain,
185
+ hash: hash,
186
+ type: Type::RAW
187
+ }
188
+ end
189
+
190
+ def self.is_address?(address)
191
+ address.is_a?(Address)
192
+ end
193
+
194
+ def self.is_valid?(address)
195
+ begin
196
+ new(address)
197
+ true
198
+ rescue
199
+ false
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,388 @@
1
+ module TonSdkRuby
2
+ class TickTockOptions
3
+ attr_accessor :tick, :tock
4
+
5
+ def initialize(tick, tock)
6
+ @tick = tick
7
+ @tock = tock
8
+ end
9
+ end
10
+
11
+ class SimpleLibOptions
12
+ attr_accessor :public, :root
13
+
14
+ def initialize(public_value, root_value)
15
+ @public = public_value
16
+ @root = root_value
17
+ end
18
+ end
19
+
20
+ class TickTock
21
+ attr_reader :data, :cell
22
+
23
+ def initialize(options)
24
+ @data = options
25
+ @cell = Builder.new
26
+ .store_bit(options[:tick])
27
+ .store_bit(options[:tock])
28
+ .cell
29
+ end
30
+
31
+ def self.parse(cs)
32
+ tick = cs.load_bit == 1
33
+ tock = cs.load_bit == 1
34
+ options = { tick: tick, tock: tock }
35
+ new(options)
36
+ end
37
+ end
38
+
39
+ class SimpleLib
40
+ attr_reader :data, :cell
41
+
42
+ def initialize(options)
43
+ @data = options
44
+ @cell = Builder.new
45
+ .store_bit(options[:public])
46
+ .store_ref(options[:root])
47
+ .cell
48
+ end
49
+
50
+ def self.parse(cs)
51
+ simple_lib = SimpleLib.new(
52
+ public: cs.load_bit == 1,
53
+ root: cs.load_ref
54
+ )
55
+ simple_lib
56
+ end
57
+ end
58
+
59
+ class StateInitOptions
60
+ attr_accessor :split_depth, :special, :code, :data, :library
61
+
62
+ def initialize(options = {})
63
+ @split_depth = options[:split_depth]
64
+ @special = options[:special]
65
+ @code = options[:code]
66
+ @data = options[:data]
67
+ @library = options[:library]
68
+ end
69
+ end
70
+
71
+ class StateInit
72
+ attr_reader :data, :cell
73
+
74
+ def initialize(state_init_options)
75
+ @data = state_init_options
76
+
77
+ b = Builder.new
78
+
79
+ # split_depth
80
+ if data.split_depth
81
+ b.store_uint(data.split_depth, 5)
82
+ else
83
+ b.store_bit(0)
84
+ end
85
+
86
+ # special
87
+ b.store_maybe_ref(data.special&.cell)
88
+ # code
89
+ b.store_maybe_ref(data.code)
90
+ b.store_maybe_ref(data.data)
91
+ b.store_dict(data.library)
92
+
93
+ @cell = b.cell
94
+ end
95
+
96
+ def self.parse(cs)
97
+ options = StateInitOptions.new()
98
+
99
+ options.split_depth = cs.load_bit.nonzero? ? cs.load_uint(5) : nil
100
+ options.special = TickTock.parse(cs) if cs.load_bit.nonzero?
101
+ options.code = cs.load_ref if cs.load_bit.nonzero?
102
+ options.data = cs.load_ref if cs.load_bit.nonzero?
103
+
104
+ deserializers = {
105
+ key: ->(k) { k },
106
+ value: ->(v) { SimpleLib.parse(Slice.parse(v.parse)) }
107
+ }
108
+
109
+ options.library = HashmapE.parse(256, cs, deserializers)
110
+
111
+ new(options)
112
+ end
113
+ end
114
+
115
+ class IntMsgInfo
116
+ attr_accessor :tag, :ihr_disabled, :bounce, :bounced, :src, :dest, :value,
117
+ :ihr_fee, :fwd_fee, :created_lt, :created_at
118
+
119
+ def initialize(options = {})
120
+ @tag = 'int_msg_info'
121
+ @ihr_disabled = options[:ihr_disabled]
122
+ @bounce = options[:bounce]
123
+ @bounced = options[:bounced]
124
+ @src = options[:src]
125
+ @dest = options[:dest]
126
+ @value = options[:value]
127
+ @ihr_fee = options[:ihr_fee]
128
+ @fwd_fee = options[:fwd_fee]
129
+ @created_lt = options[:created_lt]
130
+ @created_at = options[:created_at]
131
+ end
132
+ end
133
+
134
+ class ExtInMsgInfo
135
+ attr_accessor :tag, :src, :dest, :import_fee
136
+
137
+ def initialize(options = {})
138
+ @tag = 'ext_in_msg_info'
139
+ @src = options[:src]
140
+ @dest = options[:dest]
141
+ @import_fee = options[:import_fee]
142
+ end
143
+ end
144
+
145
+ class CommonMsgInfo
146
+ attr_reader :data, :cell
147
+
148
+ def initialize(data)
149
+ case data.tag
150
+ when 'int_msg_info'
151
+ int_msg_info(data)
152
+ when 'ext_in_msg_info'
153
+ ext_in_msg_info(data)
154
+ else
155
+ raise 'OutAction: unexpected tag'
156
+ end
157
+ end
158
+
159
+ private
160
+
161
+ def int_msg_info(data)
162
+ b = Builder.new
163
+ .store_bits([0]) # int_msg_info$0
164
+ .store_bit(data.ihr_disabled || false) # ihr_disabled:Bool
165
+ .store_bit(data.bounce) # bounce:Bool
166
+ .store_bit(data.bounced || false) # bounced:Bool
167
+ .store_address(data.src || Address::NONE) # src:MsgAddressInt
168
+ .store_address(data.dest) # dest:MsgAddressInt
169
+ .store_coins(data.value) # value: -> grams:Grams
170
+ .store_bit(0) # value: -> other:ExtraCurrencyCollection
171
+ .store_coins(data.ihr_fee || Coins.new(0)) # ihr_fee:Grams
172
+ .store_coins(data.fwd_fee || Coins.new(0)) # fwd_fee:Grams
173
+ .store_uint(data.created_lt || 0, 64) # created_lt:uint64
174
+ .store_uint(data.created_at || 0, 32) # created_at:uint32
175
+
176
+ @data = data
177
+ @cell = b.cell
178
+ end
179
+
180
+ def ext_in_msg_info(data)
181
+ b = Builder.new
182
+ .store_bits([1, 0]) # ext_in_msg_info$10
183
+ .store_address(data.src || Address::NONE) # src:MsgAddress
184
+ .store_address(data.dest) # dest:MsgAddressExt
185
+ .store_coins(data.import_fee || Coins.new(0)) # import_fee:Grams
186
+
187
+ @data = data
188
+ @cell = b.cell
189
+ end
190
+
191
+ public
192
+
193
+ def self.parse(cs)
194
+ frst = cs.load_bit
195
+
196
+ if frst == 1
197
+ scnd = cs.load_bit
198
+ raise 'CommonMsgInfo: ext_out_msg_info unimplemented' if scnd == 1
199
+
200
+ return new(ExtInMsgInfo.new(
201
+ tag: 'ext_in_msg_info',
202
+ src: cs.load_address,
203
+ dest: cs.load_address,
204
+ import_fee: cs.load_coins
205
+ ))
206
+ end
207
+
208
+ if frst == 0
209
+ data = IntMsgInfo.new({
210
+ tag: 'int_msg_info',
211
+ ihr_disabled: cs.load_bit == 1,
212
+ bounce: cs.load_bit == 1,
213
+ bounced: cs.load_bit == 1,
214
+ src: cs.load_address,
215
+ dest: cs.load_address,
216
+ value: cs.load_coins
217
+ })
218
+
219
+ # TODO: support with ExtraCurrencyCollection
220
+ cs.skip_bits(1)
221
+
222
+ data.ihr_fee = cs.load_coins
223
+ data.fwd_fee = cs.load_coins
224
+ data.created_lt = cs.load_uint(64)
225
+ data.created_at = cs.load_uint(32)
226
+
227
+ return new(data)
228
+ end
229
+
230
+ raise 'CommonMsgInfo: invalid tag'
231
+ end
232
+ end
233
+
234
+ class MessageOptions
235
+ attr_accessor :info, :init, :body
236
+
237
+ def initialize(options = {})
238
+ @info = options[:info]
239
+ @init = options[:init]
240
+ @body = options[:body]
241
+ end
242
+ end
243
+
244
+ class Message
245
+ attr_reader :data, :cell
246
+
247
+ def initialize(options)
248
+ @data = options
249
+ b = Builder.new
250
+ b.store_slice(data.info.cell.parse) # info:CommonMsgInfo
251
+
252
+ # init:(Maybe (Either StateInit ^StateInit))
253
+ if data.init
254
+ b.store_bits([1, 0])
255
+ b.store_slice(data.init.cell.parse)
256
+ else
257
+ b.store_bit(0)
258
+ end
259
+
260
+ # body:(Either X ^X)
261
+ if data.body
262
+ if (b.bits.length + data.body.bits.length + 1 <= 1023) &&
263
+ (b.refs.length + data.body.refs.length <= 4)
264
+ b.store_bit(0)
265
+ b.store_slice(data.body.parse)
266
+ else
267
+ b.store_bit(1)
268
+ b.store_ref(data.body)
269
+ end
270
+ else
271
+ b.store_bit(0)
272
+ end
273
+
274
+ @cell = b.cell
275
+ end
276
+
277
+ def self.parse(cs)
278
+ data = {}
279
+ data.info = CommonMsgInfo.parse(cs)
280
+
281
+ if cs.load_bit
282
+ init = cs.load_bit ? cs.load_ref.parse : cs
283
+ data.init = StateInit.parse(init)
284
+ end
285
+
286
+ if cs.load_bit
287
+ data.body = cs.load_ref
288
+ else
289
+ data.body = Builder.new.store_slice(cs).cell
290
+ end
291
+
292
+ new(data)
293
+ end
294
+ end
295
+
296
+ class ActionSendMsg
297
+ attr_reader :tag, :mode, :out_msg
298
+
299
+ def initialize(options)
300
+ @tag = 'action_send_msg'
301
+ @mode = options[:mode]
302
+ @out_msg = options[:out_msg]
303
+ end
304
+ end
305
+
306
+ class ActionSetCode
307
+ attr_reader :tag, :new_code
308
+
309
+ def initialize(options)
310
+ @tag = 'action_set_code'
311
+ @new_code = options[:new_code]
312
+ end
313
+ end
314
+
315
+ class OutAction
316
+ def initialize(data)
317
+ case data.tag
318
+ when 'action_send_msg' then action_send_msg(data)
319
+ when 'action_set_code' then action_set_code(data)
320
+ else
321
+ raise 'OutAction: unexpected tag'
322
+ end
323
+ end
324
+
325
+ private def action_send_msg(data)
326
+ b = Builder.new
327
+ b.store_uint(0x0ec3c86d, 32)
328
+ b.store_uint(data.mode, 8)
329
+ b.store_ref(data.out_msg.cell)
330
+ @data = data
331
+ @cell = b.cell
332
+ end
333
+
334
+ private def action_set_code(data)
335
+ b = Builder.new
336
+ b.store_uint(0xad4de08e, 32)
337
+ b.store_ref(data.new_code)
338
+ @data = data
339
+ @cell = b.cell
340
+ end
341
+
342
+ def self.parse(cs)
343
+ tag = cs.load_uint(32)
344
+ data = {}
345
+
346
+ case tag
347
+ when 0x0ec3c86d # action_send_msg
348
+ mode = cs.load_uint(8)
349
+ out_msg = cs.load_ref.parse
350
+ data = ActionSendMsg.new({ tag: 'action_send_msg', mode: mode, out_msg: Message.parse(out_msg) })
351
+ when 0xad4de08e # action_set_code
352
+ data = ActionSetCode.new({ tag: 'action_set_code', new_code: cs.load_ref })
353
+ else
354
+ raise 'OutAction: unexpected tag'
355
+ end
356
+
357
+ OutAction.new(data)
358
+ end
359
+
360
+ attr_reader :data, :cell
361
+ end
362
+
363
+ class OutListOptions
364
+ attr_reader :actions
365
+
366
+ def initialize(options)
367
+ @actions = options[:actions]
368
+ end
369
+ end
370
+
371
+ class OutList
372
+
373
+ attr_reader :cell, :data
374
+ def initialize(action)
375
+ @data = action
376
+ cur = Builder.new.cell
377
+
378
+ data.actions.each do |a|
379
+ cur = Builder.new
380
+ .store_ref(cur)
381
+ .store_slice(a.cell.parse)
382
+ .cell
383
+ end
384
+
385
+ @cell = cur
386
+ end
387
+ end
388
+ end