iota-ruby 1.1.8-java
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 +15 -0
- data/.travis.yml +24 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +121 -0
- data/Rakefile +36 -0
- data/bin/iota-console +15 -0
- data/examples/multisig.rb +69 -0
- data/ext/ccurl/ccurl.c +134 -0
- data/ext/ccurl/extconf.rb +22 -0
- data/ext/jcurl/JCurl.java +126 -0
- data/ext/jcurl/JCurlService.java +36 -0
- data/ext/pow/ccurl-0.3.0.dll +0 -0
- data/ext/pow/libccurl-0.3.0.dylib +0 -0
- data/ext/pow/libccurl-0.3.0.so +0 -0
- data/iota-ruby.gemspec +37 -0
- data/lib/iota.rb +76 -0
- data/lib/iota/api/api.rb +251 -0
- data/lib/iota/api/commands.rb +113 -0
- data/lib/iota/api/transport.rb +43 -0
- data/lib/iota/api/wrappers.rb +429 -0
- data/lib/iota/crypto/bundle.rb +163 -0
- data/lib/iota/crypto/converter.rb +244 -0
- data/lib/iota/crypto/curl.rb +18 -0
- data/lib/iota/crypto/curl_c.rb +17 -0
- data/lib/iota/crypto/curl_java.rb +18 -0
- data/lib/iota/crypto/curl_ruby.rb +70 -0
- data/lib/iota/crypto/hmac.rb +27 -0
- data/lib/iota/crypto/kerl.rb +82 -0
- data/lib/iota/crypto/pow_provider.rb +27 -0
- data/lib/iota/crypto/private_key.rb +80 -0
- data/lib/iota/crypto/sha3_ruby.rb +122 -0
- data/lib/iota/crypto/signing.rb +97 -0
- data/lib/iota/models/account.rb +489 -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 +124 -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 +324 -0
- data/lib/iota/version.rb +3 -0
- data/lib/jcurl.jar +0 -0
- data/lib/patch.rb +17 -0
- data/test/ascii_test.rb +114 -0
- data/test/curl_c_test.rb +31 -0
- data/test/curl_java_test.rb +31 -0
- data/test/curl_ruby_test.rb +27 -0
- data/test/kerl_test.rb +52 -0
- data/test/pow_provider_test.rb +36 -0
- data/test/sha3_test.rb +71 -0
- data/test/test_helper.rb +4 -0
- data/test/utils_test.rb +179 -0
- metadata +183 -0
@@ -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,124 @@
|
|
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, user: nil, password: nil)
|
9
|
+
@provider, @token, @user, @password = provider, token, user, password
|
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
|
+
if @token
|
87
|
+
request["Authorization"] = "token #{@token}"
|
88
|
+
elsif @user && @password
|
89
|
+
request.basic_auth @user, @password
|
90
|
+
end
|
91
|
+
|
92
|
+
[uri, request, req_options]
|
93
|
+
end
|
94
|
+
|
95
|
+
def prepareResponse(result, command)
|
96
|
+
resultMap = {
|
97
|
+
'getNeighbors' => 'neighbors',
|
98
|
+
'addNeighbors' => 'addedNeighbors',
|
99
|
+
'removeNeighbors' => 'removedNeighbors',
|
100
|
+
'getTips' => 'hashes',
|
101
|
+
'findTransactions' => 'hashes',
|
102
|
+
'getTrytes' => 'trytes',
|
103
|
+
'getInclusionStates' => 'states',
|
104
|
+
'attachToTangle' => 'trytes',
|
105
|
+
'checkConsistency' => 'state',
|
106
|
+
'getBalances' => 'balances',
|
107
|
+
'wereAddressesSpentFrom'=> 'states'
|
108
|
+
}
|
109
|
+
|
110
|
+
# If correct result and we want to prepare the result
|
111
|
+
if result && resultMap.key?(command)
|
112
|
+
# If the response is from the sandbox, don't prepare the result
|
113
|
+
if command === 'attachToTangle' && result.key?('id')
|
114
|
+
result = result
|
115
|
+
else
|
116
|
+
result = result[resultMap[command]]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
result
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
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
|
@@ -0,0 +1,324 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
3
|
+
module IOTA
|
4
|
+
module Utils
|
5
|
+
class Utils
|
6
|
+
include Ascii
|
7
|
+
|
8
|
+
attr_reader :validator
|
9
|
+
|
10
|
+
UNIT_MAP = {
|
11
|
+
'i' => 1,
|
12
|
+
'Ki' => 1_000,
|
13
|
+
'Mi' => 1_000_000,
|
14
|
+
'Gi' => 1_000_000_000,
|
15
|
+
'Ti' => 1_000_000_000_000,
|
16
|
+
'Pi' => 1_000_000_000_000_000
|
17
|
+
}
|
18
|
+
|
19
|
+
UNIT_MAP_ORDER = ['Pi', 'Ti', 'Gi', 'Mi', 'Ki', 'i']
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@validator = InputValidator.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def convertUnits(value, fromUnit, toUnit)
|
26
|
+
# Check if wrong unit provided
|
27
|
+
if !UNIT_MAP[fromUnit] || !UNIT_MAP[toUnit]
|
28
|
+
raise ArgumentError, "Invalid unit provided"
|
29
|
+
end
|
30
|
+
|
31
|
+
# If not valid value, throw error
|
32
|
+
if !@validator.isNum(value)
|
33
|
+
raise ArgumentError, "Invalid value"
|
34
|
+
end
|
35
|
+
|
36
|
+
converted = (BigDecimal(value.to_s) * UNIT_MAP[fromUnit]) / UNIT_MAP[toUnit]
|
37
|
+
converted.to_f
|
38
|
+
end
|
39
|
+
|
40
|
+
def noChecksum(address)
|
41
|
+
isSingleAddress = @validator.isString(address)
|
42
|
+
|
43
|
+
return address if isSingleAddress && address.length == 81
|
44
|
+
|
45
|
+
# If only single address, turn it into an array
|
46
|
+
if isSingleAddress
|
47
|
+
address = [address]
|
48
|
+
end
|
49
|
+
|
50
|
+
addressesWithChecksum = []
|
51
|
+
|
52
|
+
address.each do |addr|
|
53
|
+
addressesWithChecksum << addr.slice(0, 81)
|
54
|
+
end
|
55
|
+
|
56
|
+
# return either string or the list
|
57
|
+
if isSingleAddress
|
58
|
+
return addressesWithChecksum.first
|
59
|
+
else
|
60
|
+
return addressesWithChecksum
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def transactionObject(trytes)
|
65
|
+
return nil if !trytes
|
66
|
+
|
67
|
+
# validity check
|
68
|
+
(2279...2295).step(1) do |i|
|
69
|
+
return nil if trytes[i] != "9"
|
70
|
+
end
|
71
|
+
|
72
|
+
trx = {}
|
73
|
+
transactionTrits = IOTA::Crypto::Converter.trits(trytes)
|
74
|
+
hash = []
|
75
|
+
|
76
|
+
# generate the correct transaction hash
|
77
|
+
curl = IOTA::Crypto::Curl.new
|
78
|
+
curl.absorb(transactionTrits)
|
79
|
+
curl.squeeze(hash)
|
80
|
+
|
81
|
+
trx['hash'] = IOTA::Crypto::Converter.trytes(hash)
|
82
|
+
trx['signatureMessageFragment'] = trytes.slice(0, 2187)
|
83
|
+
trx['address'] = trytes.slice(2187, 81)
|
84
|
+
trx['value'] = IOTA::Crypto::Converter.value(transactionTrits.slice(6804, 33))
|
85
|
+
trx['obsoleteTag'] = trytes.slice(2295, 27)
|
86
|
+
trx['timestamp'] = IOTA::Crypto::Converter.value(transactionTrits.slice(6966, 27))
|
87
|
+
trx['currentIndex'] = IOTA::Crypto::Converter.value(transactionTrits.slice(6993, 27))
|
88
|
+
trx['lastIndex'] = IOTA::Crypto::Converter.value(transactionTrits.slice(7020, 27))
|
89
|
+
trx['bundle'] = trytes.slice(2349, 81)
|
90
|
+
trx['trunkTransaction'] = trytes.slice(2430, 81)
|
91
|
+
trx['branchTransaction'] = trytes.slice(2511, 81)
|
92
|
+
|
93
|
+
trx['tag'] = trytes.slice(2592, 27)
|
94
|
+
trx['attachmentTimestamp'] = IOTA::Crypto::Converter.value(transactionTrits.slice(7857, 27))
|
95
|
+
trx['attachmentTimestampLowerBound'] = IOTA::Crypto::Converter.value(transactionTrits.slice(7884, 27))
|
96
|
+
trx['attachmentTimestampUpperBound'] = IOTA::Crypto::Converter.value(transactionTrits.slice(7911, 27))
|
97
|
+
trx['nonce'] = trytes.slice(2646, 27)
|
98
|
+
|
99
|
+
IOTA::Models::Transaction.new(trx)
|
100
|
+
end
|
101
|
+
|
102
|
+
def transactionTrytes(transaction)
|
103
|
+
valueTrits = IOTA::Crypto::Converter.trits(transaction.value)
|
104
|
+
valueTrits = valueTrits.concat([0]*(81-valueTrits.length)) if valueTrits.length < 81
|
105
|
+
|
106
|
+
timestampTrits = IOTA::Crypto::Converter.trits(transaction.timestamp)
|
107
|
+
timestampTrits = timestampTrits.concat([0]*(27-timestampTrits.length)) if timestampTrits.length < 27
|
108
|
+
|
109
|
+
currentIndexTrits = IOTA::Crypto::Converter.trits(transaction.currentIndex)
|
110
|
+
currentIndexTrits = currentIndexTrits.concat([0]*(27-currentIndexTrits.length)) if currentIndexTrits.length < 27
|
111
|
+
|
112
|
+
lastIndexTrits = IOTA::Crypto::Converter.trits(transaction.lastIndex)
|
113
|
+
lastIndexTrits = lastIndexTrits.concat([0]*(27-lastIndexTrits.length)) if lastIndexTrits.length < 27
|
114
|
+
|
115
|
+
attachmentTimestampTrits = IOTA::Crypto::Converter.trits(transaction.attachmentTimestamp || 0);
|
116
|
+
attachmentTimestampTrits = attachmentTimestampTrits.concat([0]*(27-attachmentTimestampTrits.length)) if attachmentTimestampTrits.length < 27
|
117
|
+
|
118
|
+
attachmentTimestampLowerBoundTrits = IOTA::Crypto::Converter.trits(transaction.attachmentTimestampLowerBound || 0);
|
119
|
+
attachmentTimestampLowerBoundTrits = attachmentTimestampLowerBoundTrits.concat([0]*(27-attachmentTimestampLowerBoundTrits.length)) if attachmentTimestampLowerBoundTrits.length < 27
|
120
|
+
|
121
|
+
attachmentTimestampUpperBoundTrits = IOTA::Crypto::Converter.trits(transaction.attachmentTimestampUpperBound || 0);
|
122
|
+
attachmentTimestampUpperBoundTrits = attachmentTimestampUpperBoundTrits.concat([0]*(27-attachmentTimestampUpperBoundTrits.length)) if attachmentTimestampUpperBoundTrits.length < 27
|
123
|
+
|
124
|
+
tag = transaction.tag || transaction.obsoleteTag
|
125
|
+
|
126
|
+
return (
|
127
|
+
transaction.signatureMessageFragment +
|
128
|
+
transaction.address +
|
129
|
+
IOTA::Crypto::Converter.trytes(valueTrits) +
|
130
|
+
transaction.obsoleteTag +
|
131
|
+
IOTA::Crypto::Converter.trytes(timestampTrits) +
|
132
|
+
IOTA::Crypto::Converter.trytes(currentIndexTrits) +
|
133
|
+
IOTA::Crypto::Converter.trytes(lastIndexTrits) +
|
134
|
+
transaction.bundle +
|
135
|
+
transaction.trunkTransaction +
|
136
|
+
transaction.branchTransaction +
|
137
|
+
tag +
|
138
|
+
IOTA::Crypto::Converter.trytes(attachmentTimestampTrits) +
|
139
|
+
IOTA::Crypto::Converter.trytes(attachmentTimestampLowerBoundTrits) +
|
140
|
+
IOTA::Crypto::Converter.trytes(attachmentTimestampUpperBoundTrits) +
|
141
|
+
transaction.nonce
|
142
|
+
)
|
143
|
+
end
|
144
|
+
|
145
|
+
def addChecksum(input, checksumLength = 9, isAddress = true)
|
146
|
+
# the length of the trytes to be validated
|
147
|
+
validationLength = isAddress ? 81 : nil
|
148
|
+
|
149
|
+
isSingleInput = @validator.isString(input)
|
150
|
+
|
151
|
+
# If only single address, turn it into an array
|
152
|
+
input = [input] if isSingleInput
|
153
|
+
|
154
|
+
inputsWithChecksum = input.map do |inputValue|
|
155
|
+
# check if correct trytes
|
156
|
+
if !@validator.isTrytes(inputValue, validationLength)
|
157
|
+
raise ArgumentError, "Invalid input provided"
|
158
|
+
end
|
159
|
+
|
160
|
+
kerl = IOTA::Crypto::Kerl.new
|
161
|
+
|
162
|
+
# Address trits
|
163
|
+
addressTrits = IOTA::Crypto::Converter.trits(inputValue)
|
164
|
+
|
165
|
+
# Checksum trits
|
166
|
+
checksumTrits = []
|
167
|
+
|
168
|
+
# Absorb address trits
|
169
|
+
kerl.absorb(addressTrits, 0, addressTrits.length)
|
170
|
+
|
171
|
+
# Squeeze checksum trits
|
172
|
+
kerl.squeeze(checksumTrits, 0, IOTA::Crypto::Curl::HASH_LENGTH)
|
173
|
+
|
174
|
+
# First 9 trytes as checksum
|
175
|
+
checksum = IOTA::Crypto::Converter.trytes(checksumTrits)[81-checksumLength...81]
|
176
|
+
inputValue + checksum
|
177
|
+
end
|
178
|
+
|
179
|
+
isSingleInput ? inputsWithChecksum[0] : inputsWithChecksum
|
180
|
+
end
|
181
|
+
|
182
|
+
def isBundle(bundle)
|
183
|
+
# If not correct bundle
|
184
|
+
return false if !@validator.isArrayOfTxObjects(bundle)
|
185
|
+
|
186
|
+
bundle = bundle.transactions if bundle.class == IOTA::Models::Bundle
|
187
|
+
|
188
|
+
totalSum = 0
|
189
|
+
bundleHash = bundle[0].bundle
|
190
|
+
|
191
|
+
# Prepare to absorb txs and get bundleHash
|
192
|
+
bundleFromTxs = []
|
193
|
+
|
194
|
+
kerl = IOTA::Crypto::Kerl.new
|
195
|
+
|
196
|
+
# Prepare for signature validation
|
197
|
+
signaturesToValidate = []
|
198
|
+
|
199
|
+
bundle.each_with_index do |bundleTx, index|
|
200
|
+
totalSum += bundleTx.value
|
201
|
+
|
202
|
+
# currentIndex has to be equal to the index in the array
|
203
|
+
return false if bundleTx.currentIndex != index
|
204
|
+
|
205
|
+
# Get the transaction trytes
|
206
|
+
trytes = transactionTrytes(bundleTx)
|
207
|
+
|
208
|
+
# Absorb bundle hash + value + timestamp + lastIndex + currentIndex trytes.
|
209
|
+
trits = IOTA::Crypto::Converter.trits(trytes.slice(2187, 162))
|
210
|
+
kerl.absorb(trits, 0, trits.length)
|
211
|
+
|
212
|
+
# Check if input transaction
|
213
|
+
if bundleTx.value < 0
|
214
|
+
address = bundleTx.address
|
215
|
+
|
216
|
+
newSignatureToValidate = {
|
217
|
+
address: address,
|
218
|
+
signatureFragments: [bundleTx.signatureMessageFragment]
|
219
|
+
}
|
220
|
+
|
221
|
+
# Find the subsequent txs with the remaining signature fragment
|
222
|
+
(index...bundle.length-1).step(1) do |i|
|
223
|
+
newBundleTx = bundle[i+1]
|
224
|
+
|
225
|
+
if newBundleTx.address == address && newBundleTx.value == 0
|
226
|
+
newSignatureToValidate[:signatureFragments] << newBundleTx.signatureMessageFragment
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
signaturesToValidate << newSignatureToValidate
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Check for total sum, if not equal 0 return error
|
235
|
+
return false if totalSum != 0
|
236
|
+
|
237
|
+
# get the bundle hash from the bundle transactions
|
238
|
+
kerl.squeeze(bundleFromTxs, 0, IOTA::Crypto::Kerl::HASH_LENGTH)
|
239
|
+
bundleFromTxs = IOTA::Crypto::Converter.trytes(bundleFromTxs)
|
240
|
+
|
241
|
+
# Check if bundle hash is the same as returned by tx object
|
242
|
+
return false if bundleFromTxs != bundleHash
|
243
|
+
|
244
|
+
# Last tx in the bundle should have currentIndex === lastIndex
|
245
|
+
return false if bundle[bundle.length - 1].currentIndex != bundle[bundle.length - 1].lastIndex
|
246
|
+
|
247
|
+
# Validate the signatures
|
248
|
+
(0...signaturesToValidate.length).step(1) do |i|
|
249
|
+
return false if !IOTA::Crypto::Signing.validateSignatures(signaturesToValidate[i][:address], signaturesToValidate[i][:signatureFragments], bundleHash)
|
250
|
+
end
|
251
|
+
|
252
|
+
true
|
253
|
+
end
|
254
|
+
|
255
|
+
def isValidChecksum(addressWithChecksum)
|
256
|
+
withoutChecksum = noChecksum(addressWithChecksum)
|
257
|
+
newWithCheckcum = addChecksum(withoutChecksum)
|
258
|
+
newWithCheckcum == addressWithChecksum
|
259
|
+
end
|
260
|
+
|
261
|
+
def validateSignatures(signedBundle, inputAddress)
|
262
|
+
bundleHash = nil
|
263
|
+
signatureFragments = []
|
264
|
+
|
265
|
+
signedBundle = signedBundle.transactions if signedBundle.class == IOTA::Models::Bundle
|
266
|
+
|
267
|
+
(0...signedBundle.length).step(1) do |i|
|
268
|
+
if signedBundle[i].address === inputAddress
|
269
|
+
bundleHash = signedBundle[i].bundle
|
270
|
+
|
271
|
+
signature = signedBundle[i].signatureMessageFragment
|
272
|
+
|
273
|
+
# if we reached remainder bundle
|
274
|
+
break if @validator.isString(signature) && @validator.isAllNine(signature)
|
275
|
+
|
276
|
+
signatureFragments << signature
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
return false if bundleHash.nil?
|
281
|
+
|
282
|
+
IOTA::Crypto::Signing.validateSignatures(inputAddress, signatureFragments, bundleHash)
|
283
|
+
end
|
284
|
+
|
285
|
+
def categorizeTransfers(transfers, addresses)
|
286
|
+
categorized = {
|
287
|
+
sent: [],
|
288
|
+
received: []
|
289
|
+
}
|
290
|
+
|
291
|
+
addresses = addresses.map { |a| a[0...81] }
|
292
|
+
|
293
|
+
# Iterate over all bundles and sort them between incoming and outgoing transfers
|
294
|
+
transfers.each do |bundle|
|
295
|
+
spentAlreadyAdded = false
|
296
|
+
|
297
|
+
bundle = IOTA::Models::Bundle.new(bundle) if bundle.class != IOTA::Models::Bundle
|
298
|
+
|
299
|
+
# Iterate over every bundle entry
|
300
|
+
bundle.transactions.each_with_index do |bundleEntry, bundleIndex|
|
301
|
+
address = bundleEntry.address[0...81]
|
302
|
+
if !addresses.index(address).nil?
|
303
|
+
# Check if it's a remainder address
|
304
|
+
isRemainder = (bundleEntry.currentIndex == bundleEntry.lastIndex) && bundleEntry.lastIndex != 0
|
305
|
+
|
306
|
+
# check if sent transaction
|
307
|
+
if bundleEntry.value < 0 && !spentAlreadyAdded && !isRemainder
|
308
|
+
categorized[:sent] << bundle
|
309
|
+
|
310
|
+
# too make sure we do not add transactions twice
|
311
|
+
spentAlreadyAdded = true
|
312
|
+
elsif bundleEntry.value >= 0 && !spentAlreadyAdded && !isRemainder
|
313
|
+
# check if received transaction, or 0 value (message)
|
314
|
+
# also make sure that this is not a 2nd tx for spent inputs
|
315
|
+
categorized[:received] << bundle
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
categorized
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|