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.
@@ -0,0 +1,470 @@
1
+ module IOTA
2
+ module Models
3
+ class Account < Base
4
+ attr_reader :client, :seed, :latestAddress, :addresses, :transfers, :inputs, :balance
5
+
6
+ def initialize(client, seed)
7
+ @api = client.api
8
+ @validator = client.validator
9
+ @utils = client.utils
10
+ @seed = seed.class == String ? Seed.new(seed) : seed
11
+
12
+ reset
13
+ end
14
+
15
+ def reset
16
+ @latestAddress = nil
17
+ @addresses = []
18
+ @transfers = []
19
+ @inputs = []
20
+ @balance = 0.0
21
+ end
22
+
23
+ def getAccountDetails(options = {}, fetchInputs = true, fetchTransfers = true)
24
+ reset
25
+ options = symbolize_keys(options)
26
+ startIndex = options[:start] || 0
27
+ endIndex = options[:end] || nil
28
+ security = options[:security] || 2
29
+
30
+ # If start value bigger than end, return error or if difference between end and start is bigger than 1000 keys
31
+ if endIndex && (startIndex > endIndex || endIndex > (startIndex + 1000))
32
+ raise StandardError, "Invalid inputs provided"
33
+ end
34
+
35
+ addressOptions = {
36
+ index: startIndex,
37
+ total: endIndex ? endIndex - startIndex : nil,
38
+ returnAll: true,
39
+ security: security,
40
+ checksum: true
41
+ }
42
+
43
+ # Get a list of all addresses associated with the users seed
44
+ addresses = getNewAddress(addressOptions)
45
+
46
+ # assign the last address as the latest address
47
+ # since it has no transactions associated with it
48
+ @latestAddress = addresses.last
49
+
50
+ # Add all returned addresses to the lsit of addresses
51
+ # remove the last element as that is the most recent address
52
+ @addresses = addresses.slice(0, addresses.length)
53
+
54
+ if fetchInputs
55
+ # Get the correct balance count of all addresses
56
+ @api.getBalances(addresses, 100) do |status2, balancesData|
57
+ if !status2
58
+ raise StandardError, balancesData
59
+ end
60
+
61
+ balancesData['balances'].each_with_index do |balance, index|
62
+ balance = balance.to_i
63
+ @balance += balance
64
+
65
+ if balance > 0
66
+ @inputs << IOTA::Models::Input.new(
67
+ address: @addresses[index],
68
+ keyIndex: index,
69
+ security: security,
70
+ balance: balance
71
+ )
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ if fetchTransfers
78
+ # get all bundles from a list of addresses
79
+ @api.bundlesFromAddresses(addresses, true) do |status1, bundlesData|
80
+ if !status1
81
+ raise StandardError, bundlesData
82
+ end
83
+
84
+ # add all transfers
85
+ @transfers = bundlesData
86
+ end
87
+ end
88
+
89
+ self
90
+ end
91
+
92
+ def getInputs(options = {})
93
+ self.getAccountDetails(options, true, false)
94
+ @inputs
95
+ end
96
+
97
+ def getTransfers(options = {})
98
+ self.getAccountDetails(options, false, true)
99
+ @transfers
100
+ end
101
+
102
+ def prepareTransfers(transfers, options = {})
103
+ options = symbolize_keys(options)
104
+ hmacKey = options[:hmacKey] || nil
105
+ if !hmacKey.nil?
106
+ raise StandardError, "Invalid trytes provided: #{hmacKey}" if !@validator.isTrytes(hmacKey)
107
+ end
108
+
109
+ # If message or tag is not supplied, provide it
110
+ # Also remove the checksum of the address if it's there after validating it
111
+ (0...transfers.length).step(1) do |index|
112
+ if transfers[index].class != IOTA::Models::Transfer
113
+ transfers[index] = IOTA::Models::Transfer.new(transfers[index].merge({ hmacKey: hmacKey }))
114
+ end
115
+ end
116
+
117
+ # Input validation of transfers object
118
+ raise StandardError, "Invalid transfers provided" if !@validator.isTransfersArray(transfers)
119
+
120
+ # If inputs provided, validate the format
121
+ chosenInputs = options[:inputs] || nil
122
+ raise StandardError, "Invalid inputs provided" if chosenInputs && !@validator.isInputs(chosenInputs)
123
+
124
+ remainderAddress = options[:address] || nil
125
+ chosenInputs = chosenInputs || []
126
+ security = options[:security] || 2
127
+
128
+ remainderAddress = @utils.noChecksum(remainderAddress) if remainderAddress && remainderAddress.length == 90
129
+
130
+ # Create a new bundle
131
+ bundle = IOTA::Crypto::Bundle.new
132
+
133
+ totalValue = 0
134
+ signatureFragments = []
135
+ tag = nil
136
+
137
+ # Iterate over all transfers, get totalValue and prepare the signatureFragments, message and tag
138
+ (0...transfers.length).step(1) do |i|
139
+ signatureMessageLength = 1
140
+
141
+ # If message longer than 2187 trytes, increase signatureMessageLength (add 2nd transaction)
142
+ if transfers[i].message.length > 2187
143
+ # Get total length, message / maxLength (2187 trytes)
144
+ signatureMessageLength += (transfers[i].message.length / 2187).floor
145
+ msgCopy = transfers[i].message
146
+
147
+ # While there is still a message, copy it
148
+ while msgCopy
149
+ fragment = msgCopy[0...2187]
150
+ msgCopy = msgCopy[2187...msgCopy.length]
151
+
152
+ # Pad remainder of fragment
153
+ while fragment.length < 2187
154
+ fragment += '9'
155
+ end
156
+
157
+ signatureFragments << fragment
158
+ end
159
+ else
160
+ # Else, get single fragment with 2187 of 9's trytes
161
+ fragment = ''
162
+
163
+ fragment = transfers[i].message[0...2187] if transfers[i].message
164
+
165
+ while fragment.length < 2187
166
+ fragment += '9'
167
+ end
168
+
169
+ signatureFragments << fragment
170
+ end
171
+
172
+ # get current timestamp in seconds
173
+ timestamp = Time.now.utc.to_i
174
+
175
+ # If no tag defined, get 27 tryte tag.
176
+ tag = transfers[i].obsoleteTag || ''
177
+
178
+ # Pad for required 27 tryte length
179
+ while tag.length < 27
180
+ tag += '9'
181
+ end
182
+
183
+ # Add first entries to the bundle
184
+ # Slice the address in case the user provided a checksummed one
185
+ bundle.addEntry(signatureMessageLength, transfers[i].address, transfers[i].value, tag, timestamp)
186
+ # Sum up total value
187
+ totalValue += transfers[i].value.to_i
188
+ end
189
+
190
+ # Get inputs if we are sending tokens
191
+ if totalValue
192
+ # Case 1: user provided inputs
193
+ #
194
+ # Validate the inputs by calling getBalances
195
+ if chosenInputs && chosenInputs.length > 0
196
+ # Get list if addresses of the provided inputs
197
+ inputsAddresses = [];
198
+ (0...chosenInputs.length).step(1) do |i|
199
+ if chosenInputs[i].class != IOTA::Models::Input
200
+ chosenInputs[i] = IOTA::Models::Input.new(chosenInputs[i])
201
+ end
202
+ inputsAddresses << chosenInputs[i].address
203
+ end
204
+
205
+ @api.getBalances(inputsAddresses, 100) do |status, balances|
206
+ raise StandardError, balances if !status
207
+
208
+ confirmedInputs = []
209
+ totalBalance = 0
210
+
211
+ balances['balances'].each_with_index do |balance, i|
212
+ # If input has balance, add it to confirmedInputs
213
+ balance = balance.to_i
214
+
215
+ if balance > 0
216
+ totalBalance += balance
217
+
218
+ input = chosenInputs[i]
219
+ input.balance = balance
220
+
221
+ confirmedInputs << input
222
+
223
+ # if we've already reached the intended input value, break out of loop
224
+ if totalBalance >= totalValue
225
+ break
226
+ end
227
+ end
228
+ end
229
+
230
+ # Return not enough balance error
231
+ raise StandardError, "Not enough balance" if totalValue > totalBalance
232
+
233
+ return addRemainder(confirmedInputs, totalValue, bundle, tag, security, signatureFragments, remainderAddress, hmacKey)
234
+ end
235
+ else
236
+ # Case 2: Get inputs deterministically
237
+ # If no inputs provided, derive the addresses from the seed and
238
+ # confirm that the inputs exceed the threshold
239
+ self.getInputs(security: security)
240
+
241
+ raise StandardError, "Not enough balance" if totalValue > @balance
242
+
243
+ return addRemainder(@inputs, totalValue, bundle, tag, security, signatureFragments, remainderAddress, hmacKey)
244
+ end
245
+ else
246
+ # If no input required, don't sign and simply finalize the bundle
247
+ bundle.finalize()
248
+ bundle.addTrytes(signatureFragments)
249
+
250
+ bundleTrytes = []
251
+ bundle.bundle.each do |bndl|
252
+ bundleTrytes << @utils.transactionTrytes(bndl)
253
+ end
254
+
255
+ return bundleTrytes.reverse
256
+ end
257
+ end
258
+
259
+ def sendTransfer(depth, minWeightMagnitude, transfers, options = {})
260
+ # Check if correct depth and minWeightMagnitude
261
+ if !@validator.isValue(depth) || !@validator.isValue(minWeightMagnitude)
262
+ raise StandardError, "Invalid inputs provided"
263
+ end
264
+
265
+ trytes = prepareTransfers(transfers, options)
266
+ @api.sendTrytes(trytes, depth, minWeightMagnitude) do |status, data|
267
+ if !status
268
+ raise StandardError, data
269
+ else
270
+ return data
271
+ end
272
+ end
273
+ end
274
+
275
+ def getNewAddress(options = {})
276
+ # default index value
277
+ options = symbolize_keys(options)
278
+ index = options[:index] || 0
279
+
280
+ # validate the index option
281
+ if !@validator.isValue(index) || index < 0
282
+ raise StandardError, "Invalid index provided: #{index}"
283
+ end
284
+
285
+ checksum = options[:checksum] || false
286
+ total = options[:total] || nil
287
+ return_all = options[:returnAll] || false
288
+
289
+ # If no user defined security, use the standard value of 2
290
+ security = options[:security] || 2
291
+
292
+ # validate the security option
293
+ if !@validator.isValue(security) || security < 1 || security > 3
294
+ raise StandardError, "Invalid security provided: #{index}"
295
+ end
296
+
297
+ allAddresses = []
298
+
299
+ # Case 1: total
300
+ #
301
+ # If total number of addresses to generate is supplied, simply generate
302
+ # and return the list of all addresses
303
+ if total
304
+ # Increase index with each iteration
305
+ (0...total).step(1) do |i|
306
+ address = @seed.getAddress(index, security, checksum)
307
+ allAddresses << address
308
+ index += 1
309
+ end
310
+
311
+ return allAddresses
312
+ else
313
+ # Case 2: no total provided
314
+ #
315
+ # Continue calling findTransactions to see if address was already created
316
+ # if null, return list of addresses
317
+ loop do
318
+ newAddress = @seed.getAddress(index, security, checksum)
319
+
320
+ @api.findTransactions(addresses: [newAddress]) do |status, trx_hashes|
321
+ if !status
322
+ raise StandardError, trx_hashes
323
+ end
324
+
325
+ if return_all
326
+ allAddresses << newAddress
327
+ end
328
+
329
+ index += 1
330
+
331
+ return (return_all ? allAddresses : newAddress) if trx_hashes.length == 0
332
+ end
333
+ end
334
+ end
335
+ end
336
+
337
+ private
338
+ def addRemainder(inputs, totalValue, bundle, tag, security, signatureFragments, remainderAddress, hmacKey)
339
+ totalTransferValue = totalValue
340
+ inputs.each do |input|
341
+ balance = input.balance
342
+ timestamp = Time.now.utc.to_i
343
+
344
+ # Add input as bundle entry
345
+ bundle.addEntry(input.security, input.address, -balance, tag, timestamp)
346
+
347
+ # If there is a remainder value
348
+ # Add extra output to send remaining funds to
349
+ if balance >= totalTransferValue
350
+ remainder = balance - totalTransferValue
351
+
352
+ # If user has provided remainder address
353
+ # Use it to send remaining funds to
354
+ if remainder > 0 && remainderAddress
355
+ # Remainder bundle entry
356
+ bundle.addEntry(1, remainderAddress, remainder, tag, timestamp)
357
+
358
+ # Final function for signing inputs
359
+ return signInputs(inputs, bundle, signatureFragments, hmacKey)
360
+ elsif remainder > 0
361
+ # Generate a new Address by calling getNewAddress
362
+ address = getNewAddress({security: security})
363
+ timestamp = Time.now.utc.to_i
364
+
365
+ # Remainder bundle entry
366
+ bundle.addEntry(1, address, remainder, tag, timestamp)
367
+
368
+ # Final function for signing inputs
369
+ return signInputs(inputs, bundle, signatureFragments, hmacKey)
370
+ else
371
+ # If there is no remainder, do not add transaction to bundle simply sign and return
372
+ return signInputs(inputs, bundle, signatureFragments, hmacKey)
373
+ end
374
+ else
375
+ # If multiple inputs provided, subtract the totalTransferValue by the inputs balance
376
+ totalTransferValue -= balance
377
+ end
378
+ end
379
+ end
380
+
381
+ def signInputs(inputs, bundle, signatureFragments, hmacKey)
382
+ bundle.finalize()
383
+ bundle.addTrytes(signatureFragments)
384
+
385
+ # SIGNING OF INPUTS
386
+ # Here we do the actual signing of the inputs
387
+ # Iterate over all bundle transactions, find the inputs
388
+ # Get the corresponding private key and calculate the signatureFragment
389
+
390
+ (0...bundle.bundle.length).step(1) do |i|
391
+ trx = bundle.bundle[i]
392
+
393
+ if trx.value < 0
394
+ address = trx.address
395
+
396
+ # Get the corresponding keyIndex and security of the address
397
+ keyIndex = nil
398
+ keySecurity = nil
399
+ inputs.each do |input|
400
+ if input.address == address
401
+ keyIndex = input.keyIndex
402
+ keySecurity = input.security ? input.security : security
403
+ break
404
+ end
405
+ end
406
+
407
+ # Get corresponding private key of address
408
+ privateKey = IOTA::Crypto::PrivateKey.new(@seed.as_trits, keyIndex, keySecurity)
409
+ key = privateKey.key
410
+
411
+ # Get the normalized bundle hash
412
+ normalizedBundleHash = bundle.normalizedBundle(trx.bundle)
413
+ normalizedBundleFragments = []
414
+
415
+ # Split hash into 3 fragments
416
+ (0...3).step(1) do |l|
417
+ normalizedBundleFragments[l] = normalizedBundleHash.slice(l * 27, 27)
418
+ end
419
+
420
+ # First 6561 trits for the firstFragment
421
+ firstFragment = key[0...6561]
422
+
423
+ # First bundle fragment uses the first 27 trytes
424
+ firstBundleFragment = normalizedBundleFragments[0]
425
+
426
+ # Calculate the new signatureFragment with the first bundle fragment
427
+ firstSignedFragment = IOTA::Crypto::Signing.signatureFragment(firstBundleFragment, firstFragment)
428
+
429
+ # Convert signature to trytes and assign the new signatureFragment
430
+ trx.signatureMessageFragment = IOTA::Crypto::Converter.trytes(firstSignedFragment)
431
+
432
+ # if user chooses higher than 27-tryte security
433
+ # for each security level, add an additional signature
434
+ (1...keySecurity).step(1) do |j|
435
+ # Because the signature is > 2187 trytes, we need to
436
+ # find the subsequent transaction to add the remainder of the signature
437
+ # Same address as well as value = 0 (as we already spent the input)
438
+ if bundle.bundle[i + j].address == address && bundle.bundle[i + j].value == 0
439
+ # Use the next 6561 trits
440
+ nextFragment = key.slice(6561 * j, 6561)
441
+
442
+ nextBundleFragment = normalizedBundleFragments[j]
443
+
444
+ # Calculate the new signature
445
+ nextSignedFragment = IOTA::Crypto::Signing.signatureFragment(nextBundleFragment, nextFragment)
446
+
447
+ # Convert signature to trytes and assign it again to this bundle entry
448
+ bundle.bundle[i + j].signatureMessageFragment = IOTA::Crypto::Converter.trytes(nextSignedFragment)
449
+ end
450
+ end
451
+ end
452
+ end
453
+
454
+ if hmacKey
455
+ hmac = IOTA::Crypto::Hmac(hmacKey)
456
+ hmac.addHMAC(bundle)
457
+ end
458
+
459
+ bundleTrytes = []
460
+
461
+ # Convert all bundle entries into trytes
462
+ bundle.bundle.each do |bndl|
463
+ bundleTrytes << @utils.transactionTrytes(bndl)
464
+ end
465
+
466
+ bundleTrytes.reverse
467
+ end
468
+ end
469
+ end
470
+ end
@@ -0,0 +1,13 @@
1
+ module IOTA
2
+ module Models
3
+ class Base
4
+ def inspect
5
+ self.to_s
6
+ end
7
+
8
+ def symbolize_keys(hash)
9
+ hash.inject({}){ |h,(k,v)| h[k.to_sym] = v; h }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,87 @@
1
+ module IOTA
2
+ module Models
3
+ class Bundle < Base
4
+ attr_reader :transactions, :persistence, :attachmentTimestamp
5
+
6
+ def initialize(transactions)
7
+ if transactions.class != Array
8
+ raise StandardError, "Invalid transactions array"
9
+ end
10
+
11
+ @transactions = []
12
+ transactions.each do |trx|
13
+ trx = Transaction.new(trx) if trx.class != IOTA::Models::Transaction
14
+ @transactions << trx
15
+ end
16
+
17
+ @persistence = @transactions.first.persistence
18
+ @attachmentTimestamp = @transactions.first.attachmentTimestamp
19
+ end
20
+
21
+ def extractJSON
22
+ utils = IOTA::Utils::Utils.new
23
+
24
+ # Sanity check: if the first tryte pair is not opening bracket, it's not a message
25
+ firstTrytePair = transactions[0].signatureMessageFragment[0] + transactions[0].signatureMessageFragment[1]
26
+
27
+ return nil if firstTrytePair != "OD"
28
+
29
+ index = 0
30
+ notEnded = true
31
+ trytesChunk = ''
32
+ trytesChecked = 0
33
+ preliminaryStop = false
34
+ finalJson = ''
35
+
36
+ while index < transactions.length && notEnded
37
+ messageChunk = transactions[index].signatureMessageFragment
38
+
39
+ # We iterate over the message chunk, reading 9 trytes at a time
40
+ (0...messageChunk.length).step(9) do |i|
41
+ # get 9 trytes
42
+ trytes = messageChunk.slice(i, 9)
43
+ trytesChunk += trytes
44
+
45
+ # Get the upper limit of the tytes that need to be checked
46
+ # because we only check 2 trytes at a time, there is sometimes a leftover
47
+ upperLimit = trytesChunk.length - trytesChunk.length % 2
48
+
49
+ trytesToCheck = trytesChunk[trytesChecked...upperLimit]
50
+
51
+ # We read 2 trytes at a time and check if it equals the closing bracket character
52
+ (0...trytesToCheck.length).step(2) do |j|
53
+ trytePair = trytesToCheck[j] + trytesToCheck[j + 1]
54
+
55
+ # If closing bracket char was found, and there are only trailing 9's
56
+ # we quit and remove the 9's from the trytesChunk.
57
+ if preliminaryStop && trytePair == '99'
58
+ notEnded = false
59
+ break
60
+ end
61
+
62
+ finalJson += utils.fromTrytes(trytePair)
63
+
64
+ # If tryte pair equals closing bracket char, we set a preliminary stop
65
+ # the preliminaryStop is useful when we have a nested JSON object
66
+ if trytePair === "QD"
67
+ preliminaryStop = true
68
+ end
69
+ end
70
+
71
+ break if !notEnded
72
+
73
+ trytesChecked += trytesToCheck.length;
74
+ end
75
+
76
+ # If we have not reached the end of the message yet, we continue with the next transaction in the bundle
77
+ index += 1
78
+ end
79
+
80
+ # If we did not find any JSON, return nil
81
+ return nil if notEnded
82
+
83
+ finalJson
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,38 @@
1
+ module IOTA
2
+ module Models
3
+ class Input < Base
4
+ attr_accessor :address, :keyIndex, :security, :balance
5
+
6
+ def initialize(options)
7
+ utils = IOTA::Utils::Utils.new
8
+ options = symbolize_keys(options)
9
+
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) if @address.length == 90
20
+
21
+ @keyIndex = options[:keyIndex]
22
+ @security = options[:security]
23
+ @balance = options[:balance]
24
+ end
25
+
26
+ def valid?
27
+ keysToValidate = [
28
+ { key: 'address', validator: :isAddress, args: nil },
29
+ { key: 'security', validator: :isValue, args: nil },
30
+ { key: 'keyIndex', validator: :isValue, args: nil }
31
+ ]
32
+
33
+ validator = IOTA::Utils::ObjectValidator.new(keysToValidate)
34
+ validator.valid?(self)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ module IOTA
2
+ module Models
3
+ class Seed < Base
4
+ def initialize(seed)
5
+ @utils = IOTA::Utils::Utils.new
6
+
7
+ # Check if correct seed
8
+ if seed.class == String && !@utils.validator.isTrytes(seed)
9
+ raise StandardError, "Invalid seed provided"
10
+ end
11
+
12
+ seed += "9" * (81 - seed.length) if seed.length < 81
13
+
14
+ @seed = seed
15
+ end
16
+
17
+ def getAddress(index, security, checksum)
18
+ pk = IOTA::Crypto::PrivateKey.new(self.as_trits, index, security)
19
+ address_trits = IOTA::Crypto::Signing.address(pk.digests)
20
+ address = IOTA::Crypto::Converter.trytes(address_trits)
21
+
22
+ address = @utils.addChecksum(address) if checksum
23
+
24
+ address
25
+ end
26
+
27
+ # Converter methods
28
+ def as_trits
29
+ IOTA::Crypto::Converter.trits(@seed)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,52 @@
1
+ module IOTA
2
+ module Models
3
+ class Transaction < Base
4
+ attr_accessor :hash, :signatureMessageFragment, :address, :value, :obsoleteTag, :timestamp, :currentIndex, :lastIndex, :bundle, :trunkTransaction, :branchTransaction, :tag, :attachmentTimestamp, :attachmentTimestampLowerBound, :attachmentTimestampUpperBound, :nonce, :persistence
5
+
6
+ def initialize(options)
7
+ options = symbolize_keys(options)
8
+ @hash = options[:hash]
9
+ @signatureMessageFragment = options[:signatureMessageFragment]
10
+ @address = options[:address]
11
+ @value = options[:value]
12
+ @obsoleteTag = options[:obsoleteTag]
13
+ @timestamp = options[:timestamp]
14
+ @currentIndex = options[:currentIndex]
15
+ @lastIndex = options[:lastIndex]
16
+ @bundle = options[:bundle]
17
+ @trunkTransaction = options[:trunkTransaction]
18
+ @branchTransaction = options[:branchTransaction]
19
+ @tag = options[:tag]
20
+ @attachmentTimestamp = options[:attachmentTimestamp]
21
+ @attachmentTimestampLowerBound = options[:attachmentTimestampLowerBound]
22
+ @attachmentTimestampUpperBound = options[:attachmentTimestampUpperBound]
23
+ @nonce = options[:nonce]
24
+ @persistence = nil
25
+ end
26
+
27
+ def valid?
28
+ keysToValidate = [
29
+ { key: 'hash', validator: :isHash, args: nil},
30
+ { key: 'signatureMessageFragment', validator: :isTrytes, args: 2187 },
31
+ { key: 'address', validator: :isHash, args: nil },
32
+ { key: 'value', validator: :isValue, args: nil },
33
+ { key: 'obsoleteTag', validator: :isTrytes, args: 27 },
34
+ { key: 'timestamp', validator: :isValue, args: nil },
35
+ { key: 'currentIndex', validator: :isValue, args: nil },
36
+ { key: 'lastIndex', validator: :isValue, args: nil },
37
+ { key: 'bundle', validator: :isHash, args: nil },
38
+ { key: 'trunkTransaction', validator: :isHash, args: nil },
39
+ { key: 'branchTransaction', validator: :isHash, args: nil },
40
+ { key: 'tag', validator: :isTrytes, args: 27 },
41
+ { key: 'attachmentTimestamp', validator: :isValue, args: nil },
42
+ { key: 'attachmentTimestampLowerBound', validator: :isValue, args: nil },
43
+ { key: 'attachmentTimestampUpperBound', validator: :isValue, args: nil },
44
+ { key: 'nonce', validator: :isTrytes, args: 27 }
45
+ ]
46
+
47
+ validator = IOTA::Utils::ObjectValidator.new(keysToValidate)
48
+ validator.valid?(self)
49
+ end
50
+ end
51
+ end
52
+ end