rubysol 0.1.0
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 +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
|
+
|