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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +8 -0
  3. data/.gitignore +15 -0
  4. data/.travis.yml +24 -0
  5. data/.yardopts +7 -0
  6. data/CHANGELOG.md +18 -0
  7. data/Gemfile +3 -0
  8. data/LICENSE +21 -0
  9. data/README.md +121 -0
  10. data/Rakefile +36 -0
  11. data/bin/iota-console +15 -0
  12. data/examples/multisig.rb +69 -0
  13. data/ext/ccurl/ccurl.c +134 -0
  14. data/ext/ccurl/extconf.rb +22 -0
  15. data/ext/jcurl/JCurl.java +126 -0
  16. data/ext/jcurl/JCurlService.java +36 -0
  17. data/ext/pow/ccurl-0.3.0.dll +0 -0
  18. data/ext/pow/libccurl-0.3.0.dylib +0 -0
  19. data/ext/pow/libccurl-0.3.0.so +0 -0
  20. data/iota-ruby.gemspec +37 -0
  21. data/lib/iota.rb +76 -0
  22. data/lib/iota/api/api.rb +251 -0
  23. data/lib/iota/api/commands.rb +113 -0
  24. data/lib/iota/api/transport.rb +43 -0
  25. data/lib/iota/api/wrappers.rb +429 -0
  26. data/lib/iota/crypto/bundle.rb +163 -0
  27. data/lib/iota/crypto/converter.rb +244 -0
  28. data/lib/iota/crypto/curl.rb +18 -0
  29. data/lib/iota/crypto/curl_c.rb +17 -0
  30. data/lib/iota/crypto/curl_java.rb +18 -0
  31. data/lib/iota/crypto/curl_ruby.rb +70 -0
  32. data/lib/iota/crypto/hmac.rb +27 -0
  33. data/lib/iota/crypto/kerl.rb +82 -0
  34. data/lib/iota/crypto/pow_provider.rb +27 -0
  35. data/lib/iota/crypto/private_key.rb +80 -0
  36. data/lib/iota/crypto/sha3_ruby.rb +122 -0
  37. data/lib/iota/crypto/signing.rb +97 -0
  38. data/lib/iota/models/account.rb +489 -0
  39. data/lib/iota/models/base.rb +13 -0
  40. data/lib/iota/models/bundle.rb +87 -0
  41. data/lib/iota/models/input.rb +38 -0
  42. data/lib/iota/models/seed.rb +33 -0
  43. data/lib/iota/models/transaction.rb +52 -0
  44. data/lib/iota/models/transfer.rb +44 -0
  45. data/lib/iota/multisig/address.rb +41 -0
  46. data/lib/iota/multisig/multisig.rb +244 -0
  47. data/lib/iota/utils/ascii.rb +50 -0
  48. data/lib/iota/utils/broker.rb +124 -0
  49. data/lib/iota/utils/input_validator.rb +149 -0
  50. data/lib/iota/utils/object_validator.rb +34 -0
  51. data/lib/iota/utils/utils.rb +324 -0
  52. data/lib/iota/version.rb +3 -0
  53. data/lib/jcurl.jar +0 -0
  54. data/lib/patch.rb +17 -0
  55. data/test/ascii_test.rb +114 -0
  56. data/test/curl_c_test.rb +31 -0
  57. data/test/curl_java_test.rb +31 -0
  58. data/test/curl_ruby_test.rb +27 -0
  59. data/test/kerl_test.rb +52 -0
  60. data/test/pow_provider_test.rb +36 -0
  61. data/test/sha3_test.rb +71 -0
  62. data/test/test_helper.rb +4 -0
  63. data/test/utils_test.rb +179 -0
  64. metadata +183 -0
@@ -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
@@ -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 && 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[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