tron.rb 1.1.5 → 1.1.6
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.
- checksums.yaml +4 -4
- data/lib/tron/abi.rb +38 -0
- data/lib/tron/services/contract.rb +5 -6
- data/lib/tron/version.rb +1 -1
- metadata +1 -2
- data/lib/tron/utils/abi.rb +0 -321
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f59c819dcc9f4459d1f1dfb4ac7fb574641f0328db9e9ddbfcedd8b0fb4d0d61
|
|
4
|
+
data.tar.gz: 78791543da62e51939da9740f996c5f58da444f701215816f1bb126f830b7e0d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e83539da13849989f51dac5885fdf516eab9a06931b8ea898485523e35d20f924b40ac28d8d0136163810f282cd70f7ae6f37ea4584d7f08da49b40b29eeb9da
|
|
7
|
+
data.tar.gz: 5e5fadf2581f2c5c505afc929252878a8f121bd9e1e87c5af74a5c73f22752cb77181a963242e84aa699847f649d35ce57d38fa1c5e869df6f050fe8d913bf03
|
data/lib/tron/abi.rb
CHANGED
|
@@ -77,6 +77,44 @@ module Tron
|
|
|
77
77
|
Util.bin_to_hex(result_binary)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
# Encode a function call with signature and parameters
|
|
81
|
+
#
|
|
82
|
+
# @param signature [String] function signature like "transfer(address,uint256)"
|
|
83
|
+
# @param parameters [Array] parameter values
|
|
84
|
+
# @return [String] hex-encoded function call (selector + params)
|
|
85
|
+
def self.encode_function_call(signature, parameters = [])
|
|
86
|
+
# Parse function signature to get parameter types
|
|
87
|
+
match = signature.match(/^(\w+)\((.*)\)$/)
|
|
88
|
+
raise ArgumentError, "Invalid function signature: #{signature}" unless match
|
|
89
|
+
|
|
90
|
+
function_name = match[1]
|
|
91
|
+
param_types_str = match[2]
|
|
92
|
+
param_types = param_types_str.empty? ? [] : param_types_str.split(',').map(&:strip)
|
|
93
|
+
|
|
94
|
+
# Get function selector (first 4 bytes of keccak256 hash)
|
|
95
|
+
require_relative 'utils/crypto'
|
|
96
|
+
hash = Utils::Crypto.keccak256(signature)
|
|
97
|
+
selector = Utils::Crypto.bin_to_hex(hash[0, 4])
|
|
98
|
+
|
|
99
|
+
# Encode parameters if any
|
|
100
|
+
if parameters.empty?
|
|
101
|
+
return selector
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
encoded_params = encode(param_types, parameters)
|
|
105
|
+
selector + encoded_params
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Decode output from a contract call
|
|
109
|
+
#
|
|
110
|
+
# @param type_str [String] the output type
|
|
111
|
+
# @param hex_data [String] hex-encoded output data
|
|
112
|
+
# @return [Object] decoded value
|
|
113
|
+
def self.decode_output(type_str, hex_data)
|
|
114
|
+
decoded = decode([type_str], hex_data)
|
|
115
|
+
decoded.first
|
|
116
|
+
end
|
|
117
|
+
|
|
80
118
|
# Convenience method for decoding
|
|
81
119
|
def self.decode(types, hex_data)
|
|
82
120
|
# Convert hex to binary at the boundary
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
require_relative '../utils/http'
|
|
2
2
|
require_relative '../utils/address'
|
|
3
|
-
require_relative '../utils/abi'
|
|
4
3
|
require_relative '../abi'
|
|
5
4
|
require_relative 'transaction'
|
|
6
5
|
|
|
@@ -86,7 +85,7 @@ module Tron
|
|
|
86
85
|
endpoint = "#{@base_url}/wallet/triggerconstantcontract"
|
|
87
86
|
|
|
88
87
|
# Encode function call
|
|
89
|
-
encoded_data =
|
|
88
|
+
encoded_data = Abi.encode_function_call(function, parameters)
|
|
90
89
|
|
|
91
90
|
# Use 'data' field instead of 'function_selector' (same as trigger_contract)
|
|
92
91
|
payload = {
|
|
@@ -118,7 +117,7 @@ module Tron
|
|
|
118
117
|
parameters: [operator_address, payment_id]
|
|
119
118
|
)
|
|
120
119
|
|
|
121
|
-
|
|
120
|
+
Abi.decode_output('bool', result)
|
|
122
121
|
end
|
|
123
122
|
|
|
124
123
|
# Gets the fee destination (example helper)
|
|
@@ -133,7 +132,7 @@ module Tron
|
|
|
133
132
|
parameters: [operator_address]
|
|
134
133
|
)
|
|
135
134
|
|
|
136
|
-
|
|
135
|
+
Abi.decode_output('address', result)
|
|
137
136
|
end
|
|
138
137
|
|
|
139
138
|
# Checks if an operator is registered (example helper)
|
|
@@ -148,7 +147,7 @@ module Tron
|
|
|
148
147
|
parameters: [operator_address]
|
|
149
148
|
)
|
|
150
149
|
|
|
151
|
-
|
|
150
|
+
Abi.decode_output('bool', result)
|
|
152
151
|
end
|
|
153
152
|
|
|
154
153
|
private
|
|
@@ -173,7 +172,7 @@ module Tron
|
|
|
173
172
|
endpoint = "#{@base_url}/wallet/triggersmartcontract"
|
|
174
173
|
|
|
175
174
|
# Encode function call (selector + parameters)
|
|
176
|
-
encoded_data =
|
|
175
|
+
encoded_data = Abi.encode_function_call(function, parameters)
|
|
177
176
|
|
|
178
177
|
# IMPORTANT: Use 'data' field, not 'function_selector'
|
|
179
178
|
# When function_selector is provided, the API expects it to be the signature string
|
data/lib/tron/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tron.rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bolo Michelin
|
|
@@ -156,7 +156,6 @@ files:
|
|
|
156
156
|
- lib/tron/services/resources.rb
|
|
157
157
|
- lib/tron/services/transaction.rb
|
|
158
158
|
- lib/tron/signature.rb
|
|
159
|
-
- lib/tron/utils/abi.rb
|
|
160
159
|
- lib/tron/utils/address.rb
|
|
161
160
|
- lib/tron/utils/cache.rb
|
|
162
161
|
- lib/tron/utils/crypto.rb
|
data/lib/tron/utils/abi.rb
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
require 'digest'
|
|
2
|
-
|
|
3
|
-
module Tron
|
|
4
|
-
module Utils
|
|
5
|
-
# Utility class for handling TRON contract ABIs
|
|
6
|
-
class ABI
|
|
7
|
-
# Type mappings for Solidity to Ruby
|
|
8
|
-
SOLIDITY_TYPES = {
|
|
9
|
-
'address' => :address,
|
|
10
|
-
'uint256' => :uint256,
|
|
11
|
-
'uint' => :uint256,
|
|
12
|
-
'bool' => :bool,
|
|
13
|
-
'bytes16' => :bytes16,
|
|
14
|
-
'bytes32' => :bytes32,
|
|
15
|
-
'string' => :string
|
|
16
|
-
}.freeze
|
|
17
|
-
|
|
18
|
-
# Parse function signature
|
|
19
|
-
#
|
|
20
|
-
# @param signature [String] the function signature to parse
|
|
21
|
-
# @return [Hash] a hash with :name and :params keys
|
|
22
|
-
# @raise [ArgumentError] if the signature is invalid
|
|
23
|
-
def self.parse_signature(signature)
|
|
24
|
-
match = signature.match(/^(\w+)\((.*)\)$/)
|
|
25
|
-
raise ArgumentError, "Invalid function signature: #{signature}" unless match
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
name: match[1],
|
|
29
|
-
params: match[2].empty? ? [] : match[2].split(',').map(&:strip)
|
|
30
|
-
}
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Encode function call
|
|
34
|
-
#
|
|
35
|
-
# @param signature [String] the function signature
|
|
36
|
-
# @param parameters [Array] the parameter values
|
|
37
|
-
# @return [String] the encoded function call
|
|
38
|
-
def self.encode_function_call(signature, parameters = [])
|
|
39
|
-
parsed = parse_signature(signature)
|
|
40
|
-
|
|
41
|
-
# Get function selector (first 4 bytes of keccak256 hash)
|
|
42
|
-
selector = function_selector(signature)
|
|
43
|
-
|
|
44
|
-
# Encode parameters
|
|
45
|
-
encoded_params = encode_parameters(parsed[:params], parameters)
|
|
46
|
-
|
|
47
|
-
selector + encoded_params
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Function selector (4-byte hash)
|
|
51
|
-
#
|
|
52
|
-
# @param signature [String] the function signature
|
|
53
|
-
# @return [String] the 4-byte function selector as hex
|
|
54
|
-
def self.function_selector(signature)
|
|
55
|
-
require_relative 'crypto'
|
|
56
|
-
# Note: TRON uses same ABI as Ethereum
|
|
57
|
-
# Using Keccak256 for hash
|
|
58
|
-
hash = Crypto.keccak256(signature)
|
|
59
|
-
# Take only the first 4 bytes (8 hex chars)
|
|
60
|
-
Crypto.bin_to_hex(hash[0, 4])
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Encode parameters
|
|
64
|
-
#
|
|
65
|
-
# @param types [Array<String>] the parameter types
|
|
66
|
-
# @param values [Array] the parameter values
|
|
67
|
-
# @return [String] the encoded parameters as hex
|
|
68
|
-
# @raise [ArgumentError] if there's a mismatch between types and values
|
|
69
|
-
def self.encode_parameters(types, values)
|
|
70
|
-
raise ArgumentError, "Types and values length mismatch" if types.length != values.length
|
|
71
|
-
|
|
72
|
-
encoded_parts = []
|
|
73
|
-
dynamic_params = []
|
|
74
|
-
dynamic_offset = types.length * 32 # Each static param takes 32 bytes (64 hex chars)
|
|
75
|
-
|
|
76
|
-
types.each_with_index do |type, index|
|
|
77
|
-
value = values[index]
|
|
78
|
-
|
|
79
|
-
case type
|
|
80
|
-
when 'address'
|
|
81
|
-
# Address is padded to 32 bytes (64 hex chars)
|
|
82
|
-
padded_address = encode_address(value)
|
|
83
|
-
encoded_parts << padded_address
|
|
84
|
-
when 'uint256', 'uint'
|
|
85
|
-
# Convert to hex and pad to 32 bytes (64 hex chars)
|
|
86
|
-
hex_value = encode_uint256(value)
|
|
87
|
-
encoded_parts << hex_value
|
|
88
|
-
when 'bool'
|
|
89
|
-
# Boolean to uint256 (1 for true, 0 for false)
|
|
90
|
-
encoded_parts << encode_bool(value)
|
|
91
|
-
when /^bytes(\d+)$/
|
|
92
|
-
# Static, fixed-size bytes array, pad to 32 bytes
|
|
93
|
-
encoded_parts << encode_bytes($1.to_i, value)
|
|
94
|
-
when 'string', 'bytes'
|
|
95
|
-
# For dynamic types, add offset and store actual data separately
|
|
96
|
-
encoded_parts << dynamic_offset.to_s(16).rjust(64, '0') # offset
|
|
97
|
-
# Calculate the actual data for later encoding
|
|
98
|
-
dynamic_data = encode_dynamic_parameter(type, value)
|
|
99
|
-
dynamic_params << dynamic_data
|
|
100
|
-
dynamic_offset += (dynamic_data.length / 2.0).ceil # Increase offset by byte length, rounded up to nearest whole byte
|
|
101
|
-
else
|
|
102
|
-
raise ArgumentError, "Unsupported ABI type: #{type}"
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Encode dynamic parameters
|
|
107
|
-
dynamic_parts = []
|
|
108
|
-
dynamic_params.each do |data|
|
|
109
|
-
dynamic_parts << data
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
(encoded_parts + dynamic_parts).join
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Encode a single value
|
|
116
|
-
#
|
|
117
|
-
# @param type [String] the type to encode
|
|
118
|
-
# @param value [Object] the value to encode
|
|
119
|
-
# @return [String] the encoded value as hex
|
|
120
|
-
# @raise [ArgumentError] if the type is unsupported
|
|
121
|
-
def self.encode_value(type, value)
|
|
122
|
-
case type
|
|
123
|
-
when 'address'
|
|
124
|
-
encode_address(value)
|
|
125
|
-
when 'uint256', 'uint'
|
|
126
|
-
encode_uint256(value)
|
|
127
|
-
when 'bool'
|
|
128
|
-
encode_bool(value)
|
|
129
|
-
when /^bytes(\d+)$/
|
|
130
|
-
encode_bytes($1.to_i, value)
|
|
131
|
-
when 'string'
|
|
132
|
-
encode_string(value)
|
|
133
|
-
else
|
|
134
|
-
raise ArgumentError, "Unsupported type: #{type}"
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Encode TRON address (T address to hex)
|
|
139
|
-
#
|
|
140
|
-
# @param address [String] the TRON address to encode
|
|
141
|
-
# @return [String] the encoded address as hex
|
|
142
|
-
def self.encode_address(address)
|
|
143
|
-
# Convert TRON T-address to hex address
|
|
144
|
-
# Remove 'T' prefix, convert base58 to hex, pad to 32 bytes
|
|
145
|
-
hex = Address.to_hex(address)
|
|
146
|
-
# Strip the 41 prefix for ABI encoding (only use the 20-byte address)
|
|
147
|
-
hex = hex[2..-1] if hex.start_with?('41')
|
|
148
|
-
hex.rjust(64, '0') # Pad to 64 hex chars (32 bytes)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Encode uint256
|
|
152
|
-
#
|
|
153
|
-
# @param value [Integer, String] the value to encode
|
|
154
|
-
# @return [String] the encoded uint256 as hex
|
|
155
|
-
def self.encode_uint256(value)
|
|
156
|
-
value.to_i.to_s(16).rjust(64, '0')
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# Encode bool
|
|
160
|
-
#
|
|
161
|
-
# @param value [Boolean] the boolean value to encode
|
|
162
|
-
# @return [String] the encoded boolean as hex
|
|
163
|
-
def self.encode_bool(value)
|
|
164
|
-
value ? '1'.rjust(64, '0') : '0'.rjust(64, '0')
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Encode bytes
|
|
168
|
-
#
|
|
169
|
-
# @param size [Integer] the size of the bytes type
|
|
170
|
-
# @param value [String] the value to encode
|
|
171
|
-
# @return [String] the encoded bytes as hex
|
|
172
|
-
def self.encode_bytes(size, value)
|
|
173
|
-
hex = value.is_a?(String) ? value.unpack1('H*') : value.to_s
|
|
174
|
-
# Limit to the size of the bytes type and pad to 32 bytes
|
|
175
|
-
hex_part = hex[0...(size * 2)] # Each byte is 2 hex chars
|
|
176
|
-
hex_part.ljust(64, '0')
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Encode string
|
|
180
|
-
#
|
|
181
|
-
# @param value [String] the string to encode
|
|
182
|
-
# @raise [NotImplementedError] since string encoding is handled separately
|
|
183
|
-
def self.encode_string(value)
|
|
184
|
-
# For dynamic types like string, return the length and data separately
|
|
185
|
-
# This will be handled by the main encode_parameters method
|
|
186
|
-
raise NotImplementedError, "String encoding is handled via encode_dynamic_parameter"
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Encode bytes (dynamic type)
|
|
190
|
-
#
|
|
191
|
-
# @param value [String] the bytes value to encode
|
|
192
|
-
# @return [String] the encoded dynamic bytes as hex
|
|
193
|
-
def self.encode_bytes_dynamic(value)
|
|
194
|
-
# For dynamic bytes, encode length and data
|
|
195
|
-
bytes_data = value.is_a?(String) ? value : value.to_s
|
|
196
|
-
bytes_array = bytes_data.start_with?('0x') ? bytes_data[2..-1].scan(/../) : bytes_data.scan(/../)
|
|
197
|
-
length = bytes_array.length.to_s(16).rjust(64, '0')
|
|
198
|
-
data = bytes_array.map { |b| b.rjust(2, '0') }.join
|
|
199
|
-
# Pad data to 32-byte boundaries (64 hex chars)
|
|
200
|
-
padded_length = ((data.length / 64.0).ceil * 64).to_i
|
|
201
|
-
padded_data = data.ljust(padded_length, '0')
|
|
202
|
-
|
|
203
|
-
length + padded_data
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# Decode output
|
|
207
|
-
#
|
|
208
|
-
# @param type [String] the type to decode
|
|
209
|
-
# @param hex_data [String] the hex data to decode
|
|
210
|
-
# @return [Object] the decoded value
|
|
211
|
-
def self.decode_output(type, hex_data)
|
|
212
|
-
case type
|
|
213
|
-
when 'bool'
|
|
214
|
-
hex_data.to_i(16) != 0
|
|
215
|
-
when 'address'
|
|
216
|
-
Address.from_hex(hex_data)
|
|
217
|
-
when 'uint256', 'uint'
|
|
218
|
-
hex_data.to_i(16)
|
|
219
|
-
else
|
|
220
|
-
hex_data
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
# Decodes parameters based on ABI types
|
|
225
|
-
#
|
|
226
|
-
# @param types [Array<String>] the types to decode
|
|
227
|
-
# @param data [String] the hex data to decode
|
|
228
|
-
# @return [Array] the decoded values
|
|
229
|
-
# @raise [ArgumentError] if the data is invalid or type is unsupported
|
|
230
|
-
def self.decode_parameters(types, data)
|
|
231
|
-
# Remove '0x' prefix if present
|
|
232
|
-
raw_data = data.start_with?('0x') ? data[2..-1] : data
|
|
233
|
-
|
|
234
|
-
# Ensure the data length is valid
|
|
235
|
-
if raw_data.length % 64 != 0
|
|
236
|
-
raise ArgumentError, "Invalid data length: must be multiple of 64 hex chars"
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
values = []
|
|
240
|
-
pos = 0
|
|
241
|
-
|
|
242
|
-
types.each do |type|
|
|
243
|
-
# Each parameter is 32 bytes (64 hex chars)
|
|
244
|
-
param_hex = raw_data[pos...(pos + 64)]
|
|
245
|
-
pos += 64
|
|
246
|
-
|
|
247
|
-
case type
|
|
248
|
-
when /address/
|
|
249
|
-
# Extract the address (last 40 hex chars)
|
|
250
|
-
addr_hex = param_hex[-40..-1]
|
|
251
|
-
# Add TRON prefix
|
|
252
|
-
values << "41#{addr_hex}"
|
|
253
|
-
when /uint/, /int/
|
|
254
|
-
# Convert hex to integer
|
|
255
|
-
values << param_hex.to_i(16)
|
|
256
|
-
when /bool/
|
|
257
|
-
# Boolean is 0 or 1
|
|
258
|
-
values << (param_hex.to_i(16) != 0)
|
|
259
|
-
when /string|bytes/
|
|
260
|
-
# Dynamic type - get offset first
|
|
261
|
-
offset = param_hex.to_i(16) * 2 # Convert to byte position in hex string
|
|
262
|
-
|
|
263
|
-
# Extract length of data
|
|
264
|
-
length_hex = raw_data[offset...(offset + 64)]
|
|
265
|
-
length = length_hex.to_i(16) * 2 # Each byte is 2 hex chars
|
|
266
|
-
|
|
267
|
-
# Extract actual data
|
|
268
|
-
actual_data = raw_data[(offset + 64)...(offset + 64 + length)]
|
|
269
|
-
|
|
270
|
-
if type.start_with?('string')
|
|
271
|
-
# Convert hex to string
|
|
272
|
-
values << [actual_data].pack('H*')
|
|
273
|
-
else
|
|
274
|
-
# Return hex string for bytes
|
|
275
|
-
values << actual_data
|
|
276
|
-
end
|
|
277
|
-
else
|
|
278
|
-
raise ArgumentError, "Unsupported ABI type for decoding: #{type}"
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
values
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
private
|
|
286
|
-
|
|
287
|
-
# Helper method to encode dynamic parameters
|
|
288
|
-
#
|
|
289
|
-
# @param type [String] the dynamic type ('string' or 'bytes')
|
|
290
|
-
# @param value [Object] the value to encode
|
|
291
|
-
# @return [String] the encoded dynamic parameter as hex
|
|
292
|
-
# @raise [ArgumentError] if the type is unsupported
|
|
293
|
-
def self.encode_dynamic_parameter(type, value)
|
|
294
|
-
if type.start_with?('string')
|
|
295
|
-
# For strings, we need to encode length and data
|
|
296
|
-
str_bytes = value.bytes
|
|
297
|
-
length = str_bytes.length.to_s(16).rjust(64, '0')
|
|
298
|
-
data = str_bytes.map { |b| b.to_s(16).rjust(2, '0') }.join
|
|
299
|
-
# Pad data to 32-byte boundaries (64 hex chars)
|
|
300
|
-
padded_length = ((data.length / 64.0).ceil * 64).to_i
|
|
301
|
-
padded_data = data.ljust(padded_length, '0')
|
|
302
|
-
|
|
303
|
-
length + padded_data
|
|
304
|
-
elsif type.start_with?('bytes')
|
|
305
|
-
# For bytes, similar to string
|
|
306
|
-
bytes_data = value.is_a?(String) ? value : value.to_s
|
|
307
|
-
bytes_array = bytes_data.start_with?('0x') ? bytes_data[2..-1].scan(/../) : bytes_data.scan(/../)
|
|
308
|
-
length = bytes_array.length.to_s(16).rjust(64, '0')
|
|
309
|
-
data = bytes_array.map { |b| b.rjust(2, '0') }.join
|
|
310
|
-
# Pad data to 32-byte boundaries (64 hex chars)
|
|
311
|
-
padded_length = ((data.length / 64.0).ceil * 64).to_i
|
|
312
|
-
padded_data = data.ljust(padded_length, '0')
|
|
313
|
-
|
|
314
|
-
length + padded_data
|
|
315
|
-
else
|
|
316
|
-
raise ArgumentError, "Unsupported dynamic type: #{type}"
|
|
317
|
-
end
|
|
318
|
-
end
|
|
319
|
-
end
|
|
320
|
-
end
|
|
321
|
-
end
|