rubysol 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/Manifest.txt +13 -0
- data/README.md +170 -0
- data/Rakefile +33 -0
- data/lib/rubysol/abi_proxy.rb +206 -0
- data/lib/rubysol/contract/crypto.rb +27 -0
- data/lib/rubysol/contract/runtime.rb +56 -0
- data/lib/rubysol/contract.rb +400 -0
- data/lib/rubysol/generator.rb +394 -0
- data/lib/rubysol/library.rb +126 -0
- data/lib/rubysol/runtime.rb +82 -0
- data/lib/rubysol/version.rb +23 -0
- data/lib/rubysol.rb +117 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 43d35a08ccf1d5b5dff18647a35cd0080da617acb43e299b3c4a0f477b366dae
|
4
|
+
data.tar.gz: 200f1cfe5afb3eb2e90be4c8eb34d31a8c71143d29a1c8d7662be90f9c8d4257
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7f59e4b03ee479d4992aaf8fe49e00a4004574fcfffad96da7bad39d5017a6fd19070a060c981304961ba7c2b34592c6e20792c679a34e5865b9d016eefdc485
|
7
|
+
data.tar.gz: e977a4a7df706617c3891e4ec2291c96797e8a4154c37a9c3b514905597f49254faa41119e30473c003e483845319f08a75f273571443608cffd909968587e0e
|
data/CHANGELOG.md
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
CHANGELOG.md
|
2
|
+
Manifest.txt
|
3
|
+
README.md
|
4
|
+
Rakefile
|
5
|
+
lib/rubysol.rb
|
6
|
+
lib/rubysol/abi_proxy.rb
|
7
|
+
lib/rubysol/contract.rb
|
8
|
+
lib/rubysol/contract/crypto.rb
|
9
|
+
lib/rubysol/contract/runtime.rb
|
10
|
+
lib/rubysol/generator.rb
|
11
|
+
lib/rubysol/library.rb
|
12
|
+
lib/rubysol/runtime.rb
|
13
|
+
lib/rubysol/version.rb
|
data/README.md
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
# Rubysol
|
2
|
+
|
3
|
+
rubysol - ruby for (blockchain) layer 1 (l1) contracts / protocols with "off-chain" indexer; 100% compatible with solidity datatypes and abis
|
4
|
+
|
5
|
+
|
6
|
+
* home :: [github.com/s6ruby/rubidity](https://github.com/s6ruby/rubidity)
|
7
|
+
* bugs :: [github.com/s6ruby/rubidity/issues](https://github.com/s6ruby/rubidity/issues)
|
8
|
+
* gem :: [rubygems.org/gems/rubysol](https://rubygems.org/gems/rubysol)
|
9
|
+
* rdoc :: [rubydoc.info/gems/rubysol](http://rubydoc.info/gems/rubysol)
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
## What's Solidity?! What's Rubidity?! What's Rubysol?!
|
14
|
+
|
15
|
+
See [**Solidity - Contract Application Binary Interface (ABI) Specification** »](https://docs.soliditylang.org/en/latest/abi-spec.html)
|
16
|
+
|
17
|
+
See [**Rubidity - Ruby for Layer 1 (L1) Contracts / Protocols with "Off-Chain" Indexer** »](https://github.com/s6ruby/rubidity)
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
### Token Contract - Rubysol "Off-Chain" Example
|
25
|
+
|
26
|
+
Let's try an "off-chain" token contract with
|
27
|
+
the "core" rubysol language.
|
28
|
+
|
29
|
+
|
30
|
+
``` ruby
|
31
|
+
require 'rubysol'
|
32
|
+
|
33
|
+
|
34
|
+
class TestToken < Contract
|
35
|
+
|
36
|
+
event :Transfer, from: Address,
|
37
|
+
to: Address,
|
38
|
+
amount: UInt
|
39
|
+
|
40
|
+
storage name: String,
|
41
|
+
symbol: String,
|
42
|
+
decimals: UInt,
|
43
|
+
totalSupply: UInt,
|
44
|
+
balanceOf: mapping( Address, UInt )
|
45
|
+
|
46
|
+
|
47
|
+
sig [String, String, UInt, UInt]
|
48
|
+
def constructor(name:,
|
49
|
+
symbol:,
|
50
|
+
decimals:,
|
51
|
+
totalSupply:)
|
52
|
+
@name = name
|
53
|
+
@symbol = symbol
|
54
|
+
@decimals = decimals
|
55
|
+
@totalSupply = totalSupply
|
56
|
+
|
57
|
+
@balanceOf[msg.sender] = totalSupply
|
58
|
+
end
|
59
|
+
|
60
|
+
sig [Address, UInt], returns: Bool
|
61
|
+
def transfer( to:, amount: )
|
62
|
+
assert @balanceOf[msg.sender] >= amount, 'Insufficient balance'
|
63
|
+
|
64
|
+
@balanceOf[msg.sender] -= amount
|
65
|
+
@balanceOf[to] += amount
|
66
|
+
|
67
|
+
log Transfer, from: msg.sender, to: to, amount: amount
|
68
|
+
true
|
69
|
+
end
|
70
|
+
end # class TestToken
|
71
|
+
```
|
72
|
+
|
73
|
+
|
74
|
+
and let's test run the contract ....
|
75
|
+
|
76
|
+
``` ruby
|
77
|
+
pp TestToken.state_variable_definitions
|
78
|
+
pp TestToken.events
|
79
|
+
pp TestToken.is_abstract_contract
|
80
|
+
|
81
|
+
abi = TestToken.abi
|
82
|
+
|
83
|
+
pp TestToken.public_abi
|
84
|
+
|
85
|
+
|
86
|
+
contract = TestToken.new
|
87
|
+
pp contract
|
88
|
+
|
89
|
+
|
90
|
+
## test globals (context)
|
91
|
+
pp contract.msg
|
92
|
+
pp contract.msg.sender
|
93
|
+
contract.msg.sender = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' # a(lice)
|
94
|
+
pp contract.msg.sender
|
95
|
+
|
96
|
+
|
97
|
+
pp contract.serialize
|
98
|
+
#=> {:name=>"",
|
99
|
+
# :symbol=>"",
|
100
|
+
# :decimals=>0,
|
101
|
+
# :totalSupply=>0,
|
102
|
+
# :balanceOf=>{}}
|
103
|
+
|
104
|
+
|
105
|
+
contract.constructor(
|
106
|
+
'My Fun Token',
|
107
|
+
'FUN',
|
108
|
+
18,
|
109
|
+
21000000 )
|
110
|
+
|
111
|
+
pp contract.serialize
|
112
|
+
#=> {:name=>"My Fun Token",
|
113
|
+
# :symbol=>"FUN",
|
114
|
+
# :decimals=>18,
|
115
|
+
# :totalSupply=>21000000,
|
116
|
+
# :balanceOf=>{'0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"=>21000000}}
|
117
|
+
|
118
|
+
pp contract.name
|
119
|
+
#=> "My Fun Token"
|
120
|
+
pp contract.symbol
|
121
|
+
#=> "FUN"
|
122
|
+
pp contract.decimals
|
123
|
+
#=> 18
|
124
|
+
pp contract.totalSupply
|
125
|
+
#=> 21000000
|
126
|
+
|
127
|
+
pp contract.balanceOf( '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
|
128
|
+
#=> 21000000
|
129
|
+
|
130
|
+
|
131
|
+
pp contract.transfer( '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
132
|
+
10000 )
|
133
|
+
|
134
|
+
pp contract.serialize
|
135
|
+
#=> {:name=>"My Fun Token",
|
136
|
+
# :symbol=>"FUN",
|
137
|
+
# :decimals=>18,
|
138
|
+
# :totalSupply=>21000000,
|
139
|
+
# :balanceOf=>
|
140
|
+
# {"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"=>20990000,
|
141
|
+
# "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"=>10000}}
|
142
|
+
|
143
|
+
pp contract.balanceOf( '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
|
144
|
+
#=> 20990000
|
145
|
+
pp contract.balanceOf( '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb')
|
146
|
+
#=> 10000
|
147
|
+
```
|
148
|
+
|
149
|
+
|
150
|
+
And so on. To be continued ...
|
151
|
+
|
152
|
+
|
153
|
+
|
154
|
+
## Bonus - More Blockchain (Crypto) Tools, Libraries & Scripts In Ruby
|
155
|
+
|
156
|
+
See [**/blockchain**](https://github.com/rubycocos/blockchain)
|
157
|
+
at the ruby code commons (rubycocos) org.
|
158
|
+
|
159
|
+
|
160
|
+
|
161
|
+
|
162
|
+
## Questions? Comments?
|
163
|
+
|
164
|
+
Join us in the [Rubidity & Rubysol (community) discord (chat server)](https://discord.gg/3JRnDUap6y). Yes you can.
|
165
|
+
Your questions and commentary welcome.
|
166
|
+
|
167
|
+
Or post them over at the [Help & Support](https://github.com/geraldb/help) page. Thanks.
|
168
|
+
|
169
|
+
|
170
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'hoe'
|
2
|
+
require './lib/rubysol/version.rb'
|
3
|
+
|
4
|
+
|
5
|
+
Hoe.spec 'rubysol' do
|
6
|
+
|
7
|
+
self.version = Rubysol::Module::Lang::VERSION
|
8
|
+
|
9
|
+
self.summary = 'rubysol - ruby for (blockchain) layer 1 (l1) contracts / protocols with "off-chain" indexer; 100% compatible with solidity datatypes and abis'
|
10
|
+
self.description = summary
|
11
|
+
|
12
|
+
self.urls = { home: 'https://github.com/s6ruby/rubidity' }
|
13
|
+
|
14
|
+
self.author = 'Gerald Bauer'
|
15
|
+
self.email = 'gerald.bauer@gmail.com'
|
16
|
+
|
17
|
+
# switch extension to .markdown for gihub formatting
|
18
|
+
self.readme_file = 'README.md'
|
19
|
+
self.history_file = 'CHANGELOG.md'
|
20
|
+
|
21
|
+
self.extra_deps = [
|
22
|
+
['solidity-typed', '>= 0.2.0'],
|
23
|
+
['digest-lite'], ## pulls in keccak256
|
24
|
+
['hexutils'], ## pulls in hex/to_hex (decode/encode_hex)
|
25
|
+
]
|
26
|
+
|
27
|
+
|
28
|
+
self.licenses = ['Public Domain']
|
29
|
+
|
30
|
+
self.spec_extras = {
|
31
|
+
required_ruby_version: '>= 2.3'
|
32
|
+
}
|
33
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class AbiProxy
|
4
|
+
|
5
|
+
## todo: make data private!!!
|
6
|
+
## make read access available via #each !!!
|
7
|
+
attr_accessor :contract_class
|
8
|
+
|
9
|
+
def initialize(contract_class)
|
10
|
+
@contract_class = contract_class
|
11
|
+
@generated = false
|
12
|
+
|
13
|
+
parents = contract_class.linearized_parents
|
14
|
+
if parents.empty?
|
15
|
+
## do nothing
|
16
|
+
else
|
17
|
+
_merge_state_variables( parents)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
###
|
22
|
+
## keep a list of generated classes (only needed/possible to generatae once)
|
23
|
+
def self.contract_classes() @classes ||= []; end
|
24
|
+
def _contract_classes() self.class.contract_classes; end
|
25
|
+
|
26
|
+
|
27
|
+
## generated? - use flag to keep track of code generation (only one time needed/required)
|
28
|
+
def generated?() @generated; end
|
29
|
+
def generate_functions
|
30
|
+
@generated = true
|
31
|
+
|
32
|
+
puts "==> generate (typed) functions for #{@contract_class.name}"
|
33
|
+
|
34
|
+
parents = @contract_class.linearized_parents
|
35
|
+
## revesee order - why? why not?
|
36
|
+
parents.each do |parent|
|
37
|
+
_generate_functions( parent )
|
38
|
+
end
|
39
|
+
_generate_functions( @contract_class )
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def _generate_functions( contract_class )
|
44
|
+
## start with simple (no parents) for now
|
45
|
+
|
46
|
+
sigs = contract_class.sigs
|
47
|
+
puts "#{sigs.size} function signatures in #{contract_class.name}:"
|
48
|
+
pp sigs
|
49
|
+
|
50
|
+
## note: only generate once for now!!!!
|
51
|
+
## maybe check later if new sigs?? possible? why? why not?
|
52
|
+
if _contract_classes.include?( contract_class )
|
53
|
+
puts " already generated!"
|
54
|
+
else
|
55
|
+
## generate global function (e.g. ERC20() or such)
|
56
|
+
Generator.global_function( contract_class )
|
57
|
+
|
58
|
+
# sigs.each do |name, definition|
|
59
|
+
# Generator.typed_function( contract_class, name,
|
60
|
+
# inputs: definition[:inputs] )
|
61
|
+
# end
|
62
|
+
_contract_classes << contract_class
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
### todo/check -- where used? check for parent_contracts
|
68
|
+
# def parent_contracts
|
69
|
+
# contract_class.parent_contracts
|
70
|
+
# end
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
def _merge_state_variables( parents )
|
75
|
+
puts "[debug] AbiProxy#merge_parent_state_variables - #{contract_class}"
|
76
|
+
parent_state_variables = parents.map(&:state_variable_definitions).reverse
|
77
|
+
vars = parent_state_variables.reduce( {} ) { |mem,h| mem.merge(h) }
|
78
|
+
.merge( contract_class.state_variable_definitions)
|
79
|
+
puts "[debug] merged state_variables:"
|
80
|
+
pp vars
|
81
|
+
contract_class.state_variable_definitions = vars
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
|
86
|
+
def public_abi
|
87
|
+
### note: calculate for now on-the-fly - why? why not?
|
88
|
+
## cache results - why? why not?
|
89
|
+
contracts = [@contract_class] + @contract_class.linearized_parents
|
90
|
+
## note: use reverse order -
|
91
|
+
## most concreate comes last (for override)
|
92
|
+
contracts = contracts.reverse
|
93
|
+
|
94
|
+
|
95
|
+
## todo/fix: add (contract) klass for source to data??? - why? why not?
|
96
|
+
data = {}
|
97
|
+
|
98
|
+
contracts.each do |klass|
|
99
|
+
klass.sigs.each do |name, definition|
|
100
|
+
if definition[:options].include?( :public )
|
101
|
+
## check for override
|
102
|
+
## and issue info for now
|
103
|
+
if data.has_key?( name )
|
104
|
+
## Solidity lets developers change how a function in the parent contract is implemented
|
105
|
+
## in the derived class. This is known as function overriding.
|
106
|
+
## The function in the parent contract needs to be declared with
|
107
|
+
## the keyword virtual to indicate that it can be overridden
|
108
|
+
## in the deriving contract.
|
109
|
+
##
|
110
|
+
## todo: check for virtual keyword - why? why not?
|
111
|
+
if name == :constructor
|
112
|
+
puts " overriding constructor in #{klass.name}"
|
113
|
+
else
|
114
|
+
puts " overriding function #{name} in #{klass.name}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
data[name] = definition
|
118
|
+
else
|
119
|
+
puts " skip non-public sig - #{name} #{definition.inspect}"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
data
|
125
|
+
end
|
126
|
+
alias_method :public_api, :public_abi ## only use public_abi (not api) - why? why not?
|
127
|
+
|
128
|
+
|
129
|
+
## rename to to_abi_json or export_abi_json or solidity_abi_json or ??
|
130
|
+
def public_abi_as_json
|
131
|
+
##
|
132
|
+
## todo/fix: add events too!!!
|
133
|
+
## todo/fix: add input parameter names too!!!!
|
134
|
+
|
135
|
+
# json format:
|
136
|
+
# Type - defines the nature of the function (receive, fallback, constructor)
|
137
|
+
# Name - defines the name of the function
|
138
|
+
# Inputs - array of objects with name, type, components
|
139
|
+
# Outputs - array of objects similar to inputs
|
140
|
+
# stateMutability - defines the mutability of the function (pure, view, non-payable or payable)
|
141
|
+
#
|
142
|
+
# "type": "constructor",
|
143
|
+
# "inputs": [
|
144
|
+
# { "type": "string", "name": "symbol" },
|
145
|
+
# { "type": "string", "name": "name" }
|
146
|
+
# ]
|
147
|
+
#
|
148
|
+
# "type": "function",
|
149
|
+
# "name": "balanceOf",
|
150
|
+
# "stateMutability": "view",
|
151
|
+
# "inputs": [
|
152
|
+
# { "type": "address", "name": "owner"}
|
153
|
+
# ],
|
154
|
+
# "outputs": [
|
155
|
+
# { "type": "uint256"}
|
156
|
+
# ]
|
157
|
+
|
158
|
+
data = []
|
159
|
+
|
160
|
+
public_abi.each do |name, definition|
|
161
|
+
inputs = definition[:inputs].map do |input|
|
162
|
+
## todo: use a _arg1, _arg2,
|
163
|
+
## count or such - why?
|
164
|
+
{ 'type' => input.to_s,
|
165
|
+
'name' => '_'
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
state_mutability = 'nonpayable' ## default is nonpayable (double check)
|
170
|
+
state_mutability = 'view' if definition[:options].include?( :view )
|
171
|
+
|
172
|
+
if name == :constructor
|
173
|
+
data << {
|
174
|
+
'type' => 'constructor',
|
175
|
+
'stateMutability' => state_mutability,
|
176
|
+
'inputs' => inputs
|
177
|
+
}
|
178
|
+
else
|
179
|
+
## check if outputs is a single entry or array or nil or hash?
|
180
|
+
outputs = definition[:outputs].is_a?( Array ) ?
|
181
|
+
definition[:outputs] : [definition[:outputs]]
|
182
|
+
outputs = outputs.map do |output|
|
183
|
+
## todo: use a _arg1, _arg2,
|
184
|
+
## count or such - why?
|
185
|
+
{ 'type' => output.to_s,
|
186
|
+
'name' => '_'
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
190
|
+
data << {
|
191
|
+
'type' => 'function',
|
192
|
+
'name' => name.to_s,
|
193
|
+
'stateMutability' => state_mutability,
|
194
|
+
'inputs' => inputs,
|
195
|
+
'outputs' => outputs,
|
196
|
+
}
|
197
|
+
end
|
198
|
+
end
|
199
|
+
data
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
def public_abi_to_json
|
204
|
+
JSON.pretty_generate( public_abi_as_json )
|
205
|
+
end
|
206
|
+
end # class AbiProxy
|
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
### use a differet name? why? why not?
|
4
|
+
## e.g CryptoFunctons or ...
|
5
|
+
|
6
|
+
module CryptoHelper
|
7
|
+
###
|
8
|
+
# Digest::KeccakLite.new( 256 ).hexdigest( 'abc' ) # or
|
9
|
+
# Digest::KeccakLite.hexdigest( 'abc', 256 )
|
10
|
+
# #=> "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45"
|
11
|
+
|
12
|
+
|
13
|
+
## fix-fix-fix - return a bytes32 type!!!!!!
|
14
|
+
def keccak256( input )
|
15
|
+
## todo/fix: check if input is binary string
|
16
|
+
## (convert to bytes - why? why not?)
|
17
|
+
## should really always use hex_to_bin !!!
|
18
|
+
## and convert the result in the end only - why? why not??
|
19
|
+
|
20
|
+
str = Types::String.new( input )
|
21
|
+
|
22
|
+
## fix: convert hexdigest to binary
|
23
|
+
'0x' + Digest::KeccakLite.hexdigest( str.as_data, 256 )
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
end # module CryptoHelper
|
@@ -0,0 +1,56 @@
|
|
1
|
+
### use a differet name? why? why not?
|
2
|
+
### Runtime__ or Runtime ___ ???
|
3
|
+
|
4
|
+
|
5
|
+
module RuntimeHelper
|
6
|
+
|
7
|
+
### keep assert here - why? why not?
|
8
|
+
## use AssertHelper or ErrorHelper or ...
|
9
|
+
##
|
10
|
+
## note: change from require to assert
|
11
|
+
## to avoid confusion with ruby require - why? why not?
|
12
|
+
def assert(condition, message='no message')
|
13
|
+
unless condition
|
14
|
+
# caller_location = caller_locations.detect { |l| l.path.include?('/app/models/contracts') }
|
15
|
+
# file = caller_location.path.gsub(%r{.*app/models/contracts/}, '')
|
16
|
+
# line = caller_location.lineno
|
17
|
+
|
18
|
+
puts "!! ASSERT FAILED - #{message}"
|
19
|
+
|
20
|
+
error_message = "#{message}" ##. (#{file}:#{line})"
|
21
|
+
## todo/fix: change to (built-in) ???Error, ....
|
22
|
+
## check for error to raise for assertion fail??
|
23
|
+
raise error_message
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
## note: for now this is just the solidity alias/used name
|
29
|
+
## for ruby's self - anything missing - why? why not?
|
30
|
+
## - to get the address - use address( this )
|
31
|
+
def this() self; end
|
32
|
+
|
33
|
+
|
34
|
+
## todo/check: change current_transaction to tx - why? why not?
|
35
|
+
def current_transaction() Runtime.current_transaction; end
|
36
|
+
|
37
|
+
def msg() Runtime.msg; end
|
38
|
+
def block() Runtime.block; end
|
39
|
+
|
40
|
+
def log( event_klass, *args, **kwargs )
|
41
|
+
|
42
|
+
raise "event class expected; got: >#{event_klass.inspect}<; sorry" unless event_klass.ancestors.include?( Types::Event)
|
43
|
+
|
44
|
+
rec = if kwargs.size > 0
|
45
|
+
event_klass.new( **kwargs )
|
46
|
+
else
|
47
|
+
event_klass.new( *args )
|
48
|
+
end
|
49
|
+
data = rec.as_data ## "serialize" to "plain" types
|
50
|
+
|
51
|
+
current_transaction.log_event( { event: event_klass.name,
|
52
|
+
data: data })
|
53
|
+
end
|
54
|
+
|
55
|
+
end # module RuntimeHelper
|
56
|
+
|