iota-ruby 1.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.
- 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
|