iota-ruby 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +8 -0
- data/.gitignore +12 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +88 -0
- data/Rakefile +10 -0
- data/bin/iota-console +15 -0
- data/examples/multisig.rb +57 -0
- data/iota-ruby.gemspec +25 -0
- data/lib/iota/api/api.rb +194 -0
- data/lib/iota/api/commands.rb +99 -0
- data/lib/iota/api/wrappers.rb +402 -0
- data/lib/iota/crypto/bundle.rb +163 -0
- data/lib/iota/crypto/converter.rb +241 -0
- data/lib/iota/crypto/curl.rb +74 -0
- data/lib/iota/crypto/hmac.rb +27 -0
- data/lib/iota/crypto/kerl.rb +78 -0
- data/lib/iota/crypto/private_key.rb +80 -0
- data/lib/iota/crypto/signing.rb +97 -0
- data/lib/iota/models/account.rb +470 -0
- data/lib/iota/models/base.rb +13 -0
- data/lib/iota/models/bundle.rb +87 -0
- data/lib/iota/models/input.rb +38 -0
- data/lib/iota/models/seed.rb +33 -0
- data/lib/iota/models/transaction.rb +52 -0
- data/lib/iota/models/transfer.rb +44 -0
- data/lib/iota/multisig/address.rb +41 -0
- data/lib/iota/multisig/multisig.rb +244 -0
- data/lib/iota/utils/ascii.rb +50 -0
- data/lib/iota/utils/broker.rb +117 -0
- data/lib/iota/utils/input_validator.rb +149 -0
- data/lib/iota/utils/object_validator.rb +34 -0
- data/lib/iota/utils/utils.rb +326 -0
- data/lib/iota/version.rb +3 -0
- data/lib/iota.rb +73 -0
- data/test/ascii_test.rb +114 -0
- data/test/kerl_test.rb +64 -0
- data/test/test_helper.rb +4 -0
- data/test/utils_test.rb +173 -0
- metadata +144 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
module IOTA
|
2
|
+
module Models
|
3
|
+
class Transfer < Base
|
4
|
+
attr_accessor :address, :message, :obsoleteTag, :value, :hmacKey
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@utils = IOTA::Utils::Utils.new
|
8
|
+
|
9
|
+
options = symbolize_keys(options)
|
10
|
+
@address = options[:address] || nil
|
11
|
+
if @address.nil?
|
12
|
+
raise StandardError, "address not provided for transfer"
|
13
|
+
end
|
14
|
+
|
15
|
+
if @address.length == 90 && !@utils.isValidChecksum(@address)
|
16
|
+
raise StandardError, "Invalid checksum: #{thisTransfer[:address]}"
|
17
|
+
end
|
18
|
+
|
19
|
+
@address = @utils.noChecksum(@address)
|
20
|
+
|
21
|
+
@message = options[:message] || ''
|
22
|
+
@obsoleteTag = options[:tag] || options[:obsoleteTag] || ''
|
23
|
+
@value = options[:value]
|
24
|
+
@hmacKey = options[:hmacKey] || nil
|
25
|
+
|
26
|
+
if @hmacKey
|
27
|
+
@message = ('9'*244) + @message
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid?
|
32
|
+
keysToValidate = [
|
33
|
+
{ key: 'address', validator: :isAddress, args: nil },
|
34
|
+
{ key: 'value', validator: :isValue, args: nil },
|
35
|
+
{ key: 'message', validator: :isTrytes, args: nil },
|
36
|
+
{ key: 'obsoleteTag', validator: :isTrytes, args: '0,27' }
|
37
|
+
]
|
38
|
+
|
39
|
+
validator = IOTA::Utils::ObjectValidator.new(keysToValidate)
|
40
|
+
validator.valid?(self)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module IOTA
|
2
|
+
module Multisig
|
3
|
+
class Address
|
4
|
+
def initialize(digests = nil)
|
5
|
+
# Initialize kerl instance
|
6
|
+
@kerl = IOTA::Crypto::Kerl.new
|
7
|
+
|
8
|
+
# Add digests if passed
|
9
|
+
absorb(digests) if digests
|
10
|
+
end
|
11
|
+
|
12
|
+
def absorb(digest)
|
13
|
+
# Construct array
|
14
|
+
digests = digest.class == Array ? digest : [digest]
|
15
|
+
|
16
|
+
# Add digests
|
17
|
+
digests.each do |d|
|
18
|
+
# Get trits of digest
|
19
|
+
digestTrits = IOTA::Crypto::Converter.trits(d)
|
20
|
+
|
21
|
+
# Absorb
|
22
|
+
@kerl.absorb(digestTrits, 0, digestTrits.length)
|
23
|
+
end
|
24
|
+
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def finalize(digest = nil)
|
29
|
+
# Absorb last digest if passed
|
30
|
+
absorb(digest) if digest
|
31
|
+
|
32
|
+
# Squeeze the address trits
|
33
|
+
addressTrits = []
|
34
|
+
@kerl.squeeze(addressTrits, 0, IOTA::Crypto::Kerl::HASH_LENGTH)
|
35
|
+
|
36
|
+
# Convert trits into trytes and return the address
|
37
|
+
IOTA::Crypto::Converter.trytes(addressTrits)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
module IOTA
|
2
|
+
module Multisig
|
3
|
+
class Multisig
|
4
|
+
def initialize(client)
|
5
|
+
@api = client.api
|
6
|
+
@utils = client.utils
|
7
|
+
@validator = client.validator
|
8
|
+
@converter = IOTA::Crypto::Converter
|
9
|
+
@signing = IOTA::Crypto::Signing
|
10
|
+
end
|
11
|
+
|
12
|
+
def getKey(seed, index, security)
|
13
|
+
pk = getPrivateKey(seed, index, security)
|
14
|
+
@converter.trytes(pk.key)
|
15
|
+
end
|
16
|
+
|
17
|
+
def getDigest(seed, index, security)
|
18
|
+
pk = getPrivateKey(seed, index, security)
|
19
|
+
@converter.trytes(pk.digests)
|
20
|
+
end
|
21
|
+
|
22
|
+
def validateAddress(multisigAddress, digests)
|
23
|
+
kerl = IOTA::Crypto::Kerl.new
|
24
|
+
|
25
|
+
# Absorb all key digests
|
26
|
+
digests.each do |digest|
|
27
|
+
trits = @converter.trits(digest)
|
28
|
+
kerl.absorb(@converter.trits(digest), 0, trits.length)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Squeeze address trits
|
32
|
+
addressTrits = []
|
33
|
+
kerl.squeeze(addressTrits, 0, IOTA::Crypto::Kerl::HASH_LENGTH)
|
34
|
+
|
35
|
+
# Convert trits into trytes and return the address
|
36
|
+
@converter.trytes(addressTrits) === multisigAddress
|
37
|
+
end
|
38
|
+
|
39
|
+
def initiateTransfer(input, remainderAddress = nil, transfers)
|
40
|
+
input = IOTA::Models::Input.new(input) if input.class != IOTA::Models::Input
|
41
|
+
|
42
|
+
# If message or tag is not supplied, provide it
|
43
|
+
# Also remove the checksum of the address if it's there
|
44
|
+
(0...transfers.length).step(1) do |i|
|
45
|
+
transfers[i] = IOTA::Models::Transfer.new(transfers[i]) if transfers[i].class != IOTA::Models::Transfer
|
46
|
+
end
|
47
|
+
|
48
|
+
# Input validation of transfers object
|
49
|
+
raise StandardError, "Invalid transfers provided" if !@validator.isTransfersArray(transfers)
|
50
|
+
|
51
|
+
# check if int
|
52
|
+
raise StandardError, "Invalid inputs provided" if !@validator.isValue(input.security)
|
53
|
+
|
54
|
+
# validate input address
|
55
|
+
raise StandardError, "Invalid input address provided" if !@validator.isAddress(input.address)
|
56
|
+
|
57
|
+
# validate remainder address
|
58
|
+
raise StandardError, "Invalid remainder address provided" if remainderAddress && !@validator.isAddress(remainderAddress)
|
59
|
+
|
60
|
+
remainderAddress = @utils.noChecksum(remainderAddress) if remainderAddress.length == 90
|
61
|
+
|
62
|
+
# Create a new bundle
|
63
|
+
bundle = IOTA::Crypto::Bundle.new
|
64
|
+
|
65
|
+
totalValue = 0
|
66
|
+
signatureFragments = []
|
67
|
+
tag = nil
|
68
|
+
|
69
|
+
# Iterate over all transfers, get totalValue and prepare the signatureFragments, message and tag
|
70
|
+
(0...transfers.length).step(1) do |i|
|
71
|
+
signatureMessageLength = 1
|
72
|
+
|
73
|
+
# If message longer than 2187 trytes, increase signatureMessageLength (add multiple transactions)
|
74
|
+
if transfers[i].message.length > 2187
|
75
|
+
# Get total length, message / maxLength (2187 trytes)
|
76
|
+
signatureMessageLength += (transfers[i].message.length / 2187).floor
|
77
|
+
|
78
|
+
msgCopy = transfers[i].message
|
79
|
+
|
80
|
+
# While there is still a message, copy it
|
81
|
+
while msgCopy
|
82
|
+
fragment = msgCopy.slice(0, 2187)
|
83
|
+
msgCopy = msgCopy.slice(2187, msgCopy.length)
|
84
|
+
|
85
|
+
# Pad remainder of fragment
|
86
|
+
fragment += (['9']*(2187-fragment.length)).join('') if fragment.length < 2187
|
87
|
+
|
88
|
+
signatureFragments.push(fragment)
|
89
|
+
end
|
90
|
+
else
|
91
|
+
# Else, get single fragment with 2187 of 9's trytes
|
92
|
+
fragment = ''
|
93
|
+
|
94
|
+
fragment = transfers[i].message.slice(0, 2187) if transfers[i].message
|
95
|
+
|
96
|
+
# Pad remainder of fragment
|
97
|
+
fragment += (['9']*(2187-fragment.length)).join('') if fragment.length < 2187
|
98
|
+
|
99
|
+
signatureFragments.push(fragment)
|
100
|
+
end
|
101
|
+
|
102
|
+
# get current timestamp in seconds
|
103
|
+
timestamp = Time.now.utc.to_i
|
104
|
+
|
105
|
+
# If no tag defined, get 27 tryte tag.
|
106
|
+
tag = transfers[i].obsoleteTag ? transfers[i].obsoleteTag : (['9']*27).join('')
|
107
|
+
|
108
|
+
# Pad for required 27 tryte length
|
109
|
+
tag += (['9']*(27-tag.length)).join('') if tag.length < 27
|
110
|
+
|
111
|
+
# Add first entries to the bundle
|
112
|
+
# Slice the address in case the user provided a checksummed one
|
113
|
+
bundle.addEntry(signatureMessageLength, transfers[i].address.slice(0, 81), transfers[i].value, tag, timestamp)
|
114
|
+
|
115
|
+
# Sum up total value
|
116
|
+
totalValue += transfers[i].value.to_i
|
117
|
+
end
|
118
|
+
|
119
|
+
# Get inputs if we are sending tokens
|
120
|
+
if totalValue > 0
|
121
|
+
if input.balance
|
122
|
+
return createBundle(input.balance, totalValue, bundle, input, remainderAddress, tag, signatureFragments)
|
123
|
+
else
|
124
|
+
@api.getBalances([input.address], 100) do |st1, balances|
|
125
|
+
if !st1
|
126
|
+
raise StandardError, "Error fetching balances: #{balances}"
|
127
|
+
else
|
128
|
+
return createBundle(balances['balances'][0].to_i, totalValue, bundle, input, remainderAddress, tag, signatureFragments)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
else
|
133
|
+
raise StandardError, "Invalid value transfer: the transfer does not require a signature."
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def addSignature(bundleToSign, inputAddress, key)
|
138
|
+
bundleToSign = [bundleToSign] if bundleToSign.class != Array
|
139
|
+
bundle = IOTA::Crypto::Bundle.new(bundleToSign)
|
140
|
+
|
141
|
+
# Get the security used for the private key
|
142
|
+
# 1 security level = 2187 trytes
|
143
|
+
security = (key.length / 2187).to_i
|
144
|
+
|
145
|
+
# convert private key trytes into trits
|
146
|
+
key = @converter.trits(key)
|
147
|
+
|
148
|
+
# First get the total number of already signed transactions
|
149
|
+
# use that for the bundle hash calculation as well as knowing
|
150
|
+
# where to add the signature
|
151
|
+
numSignedTxs = 0
|
152
|
+
|
153
|
+
(0...bundle.bundle.length).step(1) do |i|
|
154
|
+
bundle.bundle[i] = IOTA::Models::Transaction.new(bundle.bundle[i]) if bundle.bundle[i].class != IOTA::Models::Transaction
|
155
|
+
if bundle.bundle[i].address === inputAddress
|
156
|
+
# If transaction is already signed, increase counter
|
157
|
+
if !@validator.isAllNine(bundle.bundle[i].signatureMessageFragment)
|
158
|
+
numSignedTxs += 1
|
159
|
+
else
|
160
|
+
# sign the transactions
|
161
|
+
bundleHash = bundle.bundle[i].bundle
|
162
|
+
|
163
|
+
# First 6561 trits for the firstFragment
|
164
|
+
firstFragment = key.slice(0, 6561)
|
165
|
+
|
166
|
+
# Get the normalized bundle hash
|
167
|
+
normalizedBundleHash = bundle.normalizedBundle(bundleHash)
|
168
|
+
normalizedBundleFragments = []
|
169
|
+
|
170
|
+
# Split hash into 3 fragments
|
171
|
+
(0...3).step(1) do |k|
|
172
|
+
normalizedBundleFragments[k] = normalizedBundleHash.slice(k * 27, 27)
|
173
|
+
end
|
174
|
+
|
175
|
+
# First bundle fragment uses 27 trytes
|
176
|
+
firstBundleFragment = normalizedBundleFragments[numSignedTxs % 3]
|
177
|
+
|
178
|
+
# Calculate the new signatureFragment with the first bundle fragment
|
179
|
+
firstSignedFragment = @signing.signatureFragment(firstBundleFragment, firstFragment)
|
180
|
+
|
181
|
+
# Convert signature to trytes and assign the new signatureFragment
|
182
|
+
bundle.bundle[i].signatureMessageFragment = @converter.trytes(firstSignedFragment)
|
183
|
+
|
184
|
+
(1...security).step(1) do |j|
|
185
|
+
# Next 6561 trits for the firstFragment
|
186
|
+
nextFragment = key.slice(6561 * j, 6561)
|
187
|
+
|
188
|
+
|
189
|
+
# Use the next 27 trytes
|
190
|
+
nextBundleFragment = normalizedBundleFragments[(numSignedTxs + j) % 3]
|
191
|
+
|
192
|
+
# Calculate the new signatureFragment with the first bundle fragment
|
193
|
+
nextSignedFragment = @signing.signatureFragment(nextBundleFragment, nextFragment)
|
194
|
+
|
195
|
+
# Convert signature to trytes and add new bundle entry at i + j position
|
196
|
+
# Assign the signature fragment
|
197
|
+
bundle.bundle[i + j].signatureMessageFragment = @converter.trytes(nextSignedFragment)
|
198
|
+
end
|
199
|
+
|
200
|
+
break
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
bundle.bundle
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
def getPrivateKey(seed, index, security)
|
210
|
+
seed = IOTA::Models::Seed.new(seed) if seed.class != IOTA::Models::Seed
|
211
|
+
IOTA::Crypto::PrivateKey.new(seed.as_trits, index, security)
|
212
|
+
end
|
213
|
+
|
214
|
+
def createBundle(totalBalance, totalValue, bundle, input, remainderAddress, tag, signatureFragments)
|
215
|
+
if totalBalance > 0
|
216
|
+
toSubtract = 0 - totalBalance
|
217
|
+
timestamp = Time.now.utc.to_i
|
218
|
+
|
219
|
+
# Add input as bundle entry
|
220
|
+
# Only a single entry, signatures will be added later
|
221
|
+
bundle.addEntry(input.security, input.address, toSubtract, tag, timestamp)
|
222
|
+
end
|
223
|
+
|
224
|
+
raise StandardError, "Not enough balance" if totalValue > totalBalance
|
225
|
+
|
226
|
+
# If there is a remainder value
|
227
|
+
# Add extra output to send remaining funds to
|
228
|
+
if totalBalance > totalValue
|
229
|
+
remainder = totalBalance - totalValue
|
230
|
+
|
231
|
+
# Remainder bundle entry if necessary
|
232
|
+
return StandardError, "No remainder address defined" if !remainderAddress
|
233
|
+
|
234
|
+
bundle.addEntry(1, remainderAddress, remainder, tag, timestamp)
|
235
|
+
end
|
236
|
+
|
237
|
+
bundle.finalize()
|
238
|
+
bundle.addTrytes(signatureFragments)
|
239
|
+
|
240
|
+
bundle.bundle
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module IOTA
|
2
|
+
module Utils
|
3
|
+
module Ascii
|
4
|
+
TRYTE_VALUES = "9ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
5
|
+
|
6
|
+
def toTrytes(input)
|
7
|
+
# If input is not a string, return nil
|
8
|
+
return nil if !@validator.isString(input)
|
9
|
+
trytes = ""
|
10
|
+
|
11
|
+
(0...input.length).step(1) do |i|
|
12
|
+
char = input[i]
|
13
|
+
asciiValue = char.bytes.sum
|
14
|
+
|
15
|
+
# If not recognizable ASCII character, return null
|
16
|
+
return nil if asciiValue > 255
|
17
|
+
|
18
|
+
firstValue = asciiValue % 27
|
19
|
+
secondValue = (asciiValue - firstValue) / 27
|
20
|
+
|
21
|
+
trytesValue = TRYTE_VALUES[firstValue] + TRYTE_VALUES[secondValue]
|
22
|
+
|
23
|
+
trytes += trytesValue
|
24
|
+
end
|
25
|
+
|
26
|
+
trytes
|
27
|
+
end
|
28
|
+
|
29
|
+
def fromTrytes(input)
|
30
|
+
# If input is invalid trytes or input length is odd
|
31
|
+
return nil if !@validator.isTrytes(input) || input.length % 2 != 0
|
32
|
+
|
33
|
+
outputString = ""
|
34
|
+
|
35
|
+
(0...input.length).step(2) do |i|
|
36
|
+
trytes = input[i] + input[i + 1]
|
37
|
+
|
38
|
+
firstValue = TRYTE_VALUES.index(trytes[0])
|
39
|
+
secondValue = TRYTE_VALUES.index(trytes[1])
|
40
|
+
|
41
|
+
decimalValue = firstValue + secondValue * 27
|
42
|
+
|
43
|
+
outputString += decimalValue.chr
|
44
|
+
end
|
45
|
+
|
46
|
+
outputString
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module IOTA
|
6
|
+
module Utils
|
7
|
+
class Broker
|
8
|
+
def initialize(provider, token, timeout = 120)
|
9
|
+
@provider, @token = provider, token
|
10
|
+
@timeout = timeout if timeout.to_i > 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def send(command, &callback)
|
14
|
+
uri, request, req_options = prepareRequest(command)
|
15
|
+
response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
|
16
|
+
http.request(request)
|
17
|
+
end
|
18
|
+
|
19
|
+
success = true
|
20
|
+
begin
|
21
|
+
data = JSON.parse(response.body)
|
22
|
+
if data.key?('error')
|
23
|
+
data = data['error']
|
24
|
+
success = false
|
25
|
+
else
|
26
|
+
data = prepareResponse(data, command[:command])
|
27
|
+
end
|
28
|
+
rescue JSON::ParserError
|
29
|
+
success = false
|
30
|
+
data = "Invalid response"
|
31
|
+
end
|
32
|
+
callback ? callback.call(success, data) : [success, data]
|
33
|
+
end
|
34
|
+
|
35
|
+
def sandboxSend(uri, &callback)
|
36
|
+
processed = false
|
37
|
+
success = false
|
38
|
+
retValue = nil
|
39
|
+
|
40
|
+
loop do
|
41
|
+
uri, request, req_options = prepareRequest(nil, uri, Net::HTTP::Get)
|
42
|
+
|
43
|
+
response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
|
44
|
+
http.request(request)
|
45
|
+
end
|
46
|
+
|
47
|
+
begin
|
48
|
+
data = JSON.parse(response.body)
|
49
|
+
if data['status'] == 'FINISHED'
|
50
|
+
processed = true
|
51
|
+
success = true
|
52
|
+
retValue = data['attachToTangleResponse']['trytes']
|
53
|
+
elsif data['status'] == 'FAILED'
|
54
|
+
processed = true
|
55
|
+
success = false
|
56
|
+
retValue = "Sandbox transaction processing failed. Please retry."
|
57
|
+
end
|
58
|
+
rescue JSON::ParserError
|
59
|
+
processed = true
|
60
|
+
success = false
|
61
|
+
retValue = "Invalid response"
|
62
|
+
end
|
63
|
+
|
64
|
+
break if processed
|
65
|
+
|
66
|
+
# Sleep for 15 seconds before making another request
|
67
|
+
sleep 15
|
68
|
+
end
|
69
|
+
|
70
|
+
callback ? callback.call(success, retValue) : [success, retValue]
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def prepareRequest(command = nil, uri = @provider, requestMethod = Net::HTTP::Post)
|
75
|
+
uri = URI.parse(uri)
|
76
|
+
request = requestMethod.new(uri)
|
77
|
+
request.content_type = "application/json"
|
78
|
+
request['X-IOTA-API-Version'] = '1'
|
79
|
+
request.body = JSON.dump(command) if !command.nil?
|
80
|
+
|
81
|
+
req_options = {
|
82
|
+
use_ssl: uri.scheme == "https",
|
83
|
+
read_timeout: @timeout || 120
|
84
|
+
}
|
85
|
+
|
86
|
+
request["Authorization"] = "token #{@token}" if @token
|
87
|
+
|
88
|
+
[uri, request, req_options]
|
89
|
+
end
|
90
|
+
|
91
|
+
def prepareResponse(result, command)
|
92
|
+
resultMap = {
|
93
|
+
'getNeighbors' => 'neighbors',
|
94
|
+
'addNeighbors' => 'addedNeighbors',
|
95
|
+
'removeNeighbors' => 'removedNeighbors',
|
96
|
+
'getTips' => 'hashes',
|
97
|
+
'findTransactions' => 'hashes',
|
98
|
+
'getTrytes' => 'trytes',
|
99
|
+
'getInclusionStates' => 'states',
|
100
|
+
'attachToTangle' => 'trytes'
|
101
|
+
}
|
102
|
+
|
103
|
+
# If correct result and we want to prepare the result
|
104
|
+
if result && resultMap.key?(command)
|
105
|
+
# If the response is from the sandbox, don't prepare the result
|
106
|
+
if command === 'attachToTangle' && result.key?('id')
|
107
|
+
result = result
|
108
|
+
else
|
109
|
+
result = result[resultMap[command]]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
result
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module IOTA
|
2
|
+
module Utils
|
3
|
+
class InputValidator
|
4
|
+
def isAllNine(input)
|
5
|
+
/^[9]+$/.match?(input)
|
6
|
+
end
|
7
|
+
|
8
|
+
def isValue(input)
|
9
|
+
input && input.is_a?(Integer)
|
10
|
+
end
|
11
|
+
|
12
|
+
def isNum(input)
|
13
|
+
/^(\d+\.?\d{0,15}|\.\d{0,15})$/.match?(input.to_s)
|
14
|
+
end
|
15
|
+
|
16
|
+
def isString(input)
|
17
|
+
input.class == String
|
18
|
+
end
|
19
|
+
|
20
|
+
def isArray(input)
|
21
|
+
input.class == Array
|
22
|
+
end
|
23
|
+
|
24
|
+
def isObject(input)
|
25
|
+
input.class == Hash
|
26
|
+
end
|
27
|
+
|
28
|
+
def isHash(input)
|
29
|
+
isTrytes(input, 81)
|
30
|
+
end
|
31
|
+
|
32
|
+
def isAddress(input)
|
33
|
+
if input.length == 90
|
34
|
+
if !isTrytes(input, 90)
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
else
|
38
|
+
if !isTrytes(input, 81)
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def isTrytes(input, length = "0,")
|
47
|
+
isString(input) && /^[9A-Z]{#{length}}$/.match?(input)
|
48
|
+
end
|
49
|
+
|
50
|
+
def isArrayOfTrytes(trytesArray)
|
51
|
+
return false if !isArray(trytesArray)
|
52
|
+
|
53
|
+
trytesArray.each do |tryte|
|
54
|
+
return false if !isTrytes(tryte, 2673)
|
55
|
+
end
|
56
|
+
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
def isArrayOfHashes(input)
|
61
|
+
return false if !isArray(input)
|
62
|
+
|
63
|
+
input.each do |entry|
|
64
|
+
return false if !isAddress(entry)
|
65
|
+
end
|
66
|
+
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
def isArrayOfAttachedTrytes(trytes)
|
71
|
+
return false if !isArray(trytes)
|
72
|
+
|
73
|
+
(0...trytes.length).step(1) do |i|
|
74
|
+
tryte = trytes[i]
|
75
|
+
return false if !isTrytes(tryte, 2673)
|
76
|
+
|
77
|
+
lastTrytes = tryte.slice(2673 - (3 * 81), trytes.length)
|
78
|
+
return false if isAllNine(lastTrytes)
|
79
|
+
end
|
80
|
+
|
81
|
+
true
|
82
|
+
end
|
83
|
+
|
84
|
+
def isArrayOfTxObjects(bundle)
|
85
|
+
bundle = bundle.transactions if bundle.class == IOTA::Models::Bundle
|
86
|
+
|
87
|
+
return false if !isArray(bundle) || bundle.length == 0
|
88
|
+
|
89
|
+
bundle.each do |txObject|
|
90
|
+
if txObject.class != IOTA::Models::Transaction
|
91
|
+
txObject = IOTA::Models::Transaction.new(txObject)
|
92
|
+
end
|
93
|
+
|
94
|
+
return false if !txObject.valid?
|
95
|
+
end
|
96
|
+
|
97
|
+
true
|
98
|
+
end
|
99
|
+
|
100
|
+
def isTransfersArray(transfersArray)
|
101
|
+
return false if !isArray(transfersArray)
|
102
|
+
|
103
|
+
transfersArray.each do |transfer|
|
104
|
+
if transfer.class != IOTA::Models::Transfer
|
105
|
+
transfer = IOTA::Models::Transfer.new(transfer)
|
106
|
+
end
|
107
|
+
|
108
|
+
return false if !transfer.valid?
|
109
|
+
end
|
110
|
+
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
114
|
+
def isInputs(inputs)
|
115
|
+
return false if !isArray(inputs)
|
116
|
+
|
117
|
+
inputs.each do |input|
|
118
|
+
if input.class != IOTA::Models::Input
|
119
|
+
input = IOTA::Models::Input.new(input)
|
120
|
+
end
|
121
|
+
|
122
|
+
return false if !input.valid?
|
123
|
+
end
|
124
|
+
|
125
|
+
true
|
126
|
+
end
|
127
|
+
|
128
|
+
# Checks that a given uri is valid
|
129
|
+
# Valid Examples:
|
130
|
+
# udp://[2001:db8:a0b:12f0::1]:14265
|
131
|
+
# udp://[2001:db8:a0b:12f0::1]
|
132
|
+
# udp://8.8.8.8:14265
|
133
|
+
# udp://domain.com
|
134
|
+
# udp://domain2.com:14265
|
135
|
+
def isUri(node)
|
136
|
+
getInside = /^(udp|tcp):\/\/([\[][^\]\.]*[\]]|[^\[\]:]*)[:]{0,1}([0-9]{1,}$|$)/i
|
137
|
+
|
138
|
+
stripBrackets = /[\[]{0,1}([^\[\]]*)[\]]{0,1}/
|
139
|
+
|
140
|
+
uriTest = /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))|(^\s*((?=.{1,255}$)(?=.*[A-Za-z].*)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*)\s*$)/
|
141
|
+
|
142
|
+
match = getInside.match(node)
|
143
|
+
return false if match.nil? || match[2].nil?
|
144
|
+
|
145
|
+
uriTest.match?(stripBrackets.match(match[2])[1])
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module IOTA
|
2
|
+
module Utils
|
3
|
+
class ObjectValidator
|
4
|
+
def initialize(keysToValidate)
|
5
|
+
@validator = InputValidator.new
|
6
|
+
@keysToValidate = keysToValidate
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid?(object)
|
10
|
+
valid = true
|
11
|
+
@keysToValidate.each do |keyToValidate|
|
12
|
+
key = keyToValidate[:key]
|
13
|
+
func = keyToValidate[:validator]
|
14
|
+
args = keyToValidate[:args]
|
15
|
+
|
16
|
+
# If input does not have keyIndex and address, return false
|
17
|
+
if !object.respond_to?(key)
|
18
|
+
valid = false
|
19
|
+
break
|
20
|
+
end
|
21
|
+
|
22
|
+
# If input function does not return true, exit
|
23
|
+
method_args = [object.send(key)]
|
24
|
+
method_args << args if !args.nil?
|
25
|
+
if !@validator.method(func).call(*method_args)
|
26
|
+
valid = false
|
27
|
+
break
|
28
|
+
end
|
29
|
+
end
|
30
|
+
valid
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|