@leocuvee/turtlecoin-utils 0.0.14
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.
- package/.github/workflows/ci.yml +27 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/inspectionProfiles/Project_Default.xml +7 -0
- package/.idea/misc.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/turtlecoin-utils.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/.travis.yml +11 -0
- package/CONTRIBUTING.md +3 -0
- package/LICENSE +674 -0
- package/README.md +203 -0
- package/config.json +7 -0
- package/docs/.nojekyll +0 -0
- package/docs/CNAME +1 -0
- package/docs/assets/css/main.css +2321 -0
- package/docs/assets/images/icons.png +0 -0
- package/docs/assets/images/icons@2x.png +0 -0
- package/docs/assets/images/widgets.png +0 -0
- package/docs/assets/images/widgets@2x.png +0 -0
- package/docs/assets/js/main.js +1 -0
- package/docs/assets/js/search.js +3 -0
- package/docs/classes/address.html +964 -0
- package/docs/classes/addressprefix.html +431 -0
- package/docs/classes/block.html +965 -0
- package/docs/classes/blocktemplate.html +695 -0
- package/docs/classes/cryptonote.html +1137 -0
- package/docs/classes/ed25519.keypair.html +400 -0
- package/docs/classes/ed25519.keys.html +373 -0
- package/docs/classes/extranoncetag.extranoncedata.html +454 -0
- package/docs/classes/extranoncetag.extranoncepaymentid.html +453 -0
- package/docs/classes/extranoncetag.iextranonce.html +347 -0
- package/docs/classes/extratag.extramergedmining.html +494 -0
- package/docs/classes/extratag.extranonce.html +530 -0
- package/docs/classes/extratag.extrapadding.html +456 -0
- package/docs/classes/extratag.extrapublickey.html +460 -0
- package/docs/classes/extratag.iextratag.html +355 -0
- package/docs/classes/levinpacket.html +674 -0
- package/docs/classes/levinpayloads.handshake.html +731 -0
- package/docs/classes/levinpayloads.ilevinpayload.html +318 -0
- package/docs/classes/levinpayloads.liteblock.html +494 -0
- package/docs/classes/levinpayloads.missingtransactions.html +494 -0
- package/docs/classes/levinpayloads.newblock.html +540 -0
- package/docs/classes/levinpayloads.newtransactions.html +402 -0
- package/docs/classes/levinpayloads.peerentry.html +610 -0
- package/docs/classes/levinpayloads.ping.html +450 -0
- package/docs/classes/levinpayloads.rawblock.html +344 -0
- package/docs/classes/levinpayloads.requestchain.html +402 -0
- package/docs/classes/levinpayloads.requestgetobjects.html +448 -0
- package/docs/classes/levinpayloads.requesttxpool.html +402 -0
- package/docs/classes/levinpayloads.responsechain.html +494 -0
- package/docs/classes/levinpayloads.responsegetobjects.html +540 -0
- package/docs/classes/levinpayloads.timedsync.html +540 -0
- package/docs/classes/multisig.html +930 -0
- package/docs/classes/multisigmessage.html +694 -0
- package/docs/classes/parentblock.html +347 -0
- package/docs/classes/transaction.html +925 -0
- package/docs/classes/transactioninputs.coinbaseinput.html +390 -0
- package/docs/classes/transactioninputs.itransactioninput.html +321 -0
- package/docs/classes/transactioninputs.keyinput.html +459 -0
- package/docs/classes/transactionoutputs.itransactionoutput.html +317 -0
- package/docs/classes/transactionoutputs.keyoutput.html +422 -0
- package/docs/enums/extranoncetag.noncetagtype.html +246 -0
- package/docs/enums/extratag.extratagtype.html +280 -0
- package/docs/enums/levinprotocol.commandtype.html +391 -0
- package/docs/enums/transactioninputs.inputtype.html +246 -0
- package/docs/enums/transactionoutputs.outputtype.html +229 -0
- package/docs/globals.html +238 -0
- package/docs/index.html +271 -0
- package/docs/interfaces/interfaces.config.html +590 -0
- package/docs/interfaces/interfaces.daemonblocktemplateresponse.html +323 -0
- package/docs/interfaces/interfaces.generatedinput.html +304 -0
- package/docs/interfaces/interfaces.generatedoutput.html +285 -0
- package/docs/interfaces/interfaces.inputkeys.html +304 -0
- package/docs/interfaces/interfaces.ipreparedtransaction.html +268 -0
- package/docs/interfaces/interfaces.output.html +399 -0
- package/docs/interfaces/interfaces.preparedringsignature.html +377 -0
- package/docs/interfaces/interfaces.preparedtransaction.html +329 -0
- package/docs/interfaces/interfaces.randomoutput.html +285 -0
- package/docs/interfaces/interfaces.transactionrecipient.html +285 -0
- package/docs/interfaces/multisiginterfaces.partialkeyimage.html +277 -0
- package/docs/interfaces/multisiginterfaces.partialsigningkey.html +277 -0
- package/docs/modules/ed25519.html +195 -0
- package/docs/modules/extranoncetag.html +208 -0
- package/docs/modules/extratag.html +216 -0
- package/docs/modules/interfaces.html +231 -0
- package/docs/modules/levinpayloads.html +247 -0
- package/docs/modules/levinprotocol.html +191 -0
- package/docs/modules/multisiginterfaces.html +195 -0
- package/docs/modules/transactioninputs.html +208 -0
- package/docs/modules/transactionoutputs.html +204 -0
- package/index.d.ts +417 -0
- package/index.js +1508 -0
- package/lib/base58.js +220 -0
- package/lib/biginteger.js +1591 -0
- package/lib/blocktemplate.js +408 -0
- package/lib/crypto.js +19698 -0
- package/lib/mnemonic.js +1204 -0
- package/lib/nacl-fast-cn.js +608 -0
- package/lib/ringsigs.js +24262 -0
- package/lib/sha3.js +477 -0
- package/package.json +58 -0
- package/src/Address.ts +433 -0
- package/src/AddressPrefix.ts +117 -0
- package/src/Block.ts +556 -0
- package/src/BlockTemplate.ts +289 -0
- package/src/Common.ts +105 -0
- package/src/Config.ts +66 -0
- package/src/CryptoNote.ts +1072 -0
- package/src/LevinPacket.ts +366 -0
- package/src/Multisig.ts +600 -0
- package/src/MultisigMessage.ts +374 -0
- package/src/ParentBlock.ts +39 -0
- package/src/Transaction.ts +628 -0
- package/src/Types/ED25519.ts +187 -0
- package/src/Types/IExtraNonce.ts +225 -0
- package/src/Types/IExtraTag.ts +507 -0
- package/src/Types/ITransaction.ts +230 -0
- package/src/Types/ITransactionInput.ts +190 -0
- package/src/Types/ITransactionOutput.ts +108 -0
- package/src/Types/LevinPayloads.ts +1576 -0
- package/src/Types/MultisigInterfaces.ts +65 -0
- package/src/Types/PortableStorage.ts +289 -0
- package/src/Types.ts +36 -0
- package/src/index.ts +36 -0
- package/test/template.json +6 -0
- package/test/test.js +1457 -0
- package/tests/blocktemplate.json +6 -0
- package/tests/tests.js +215 -0
- package/tsconfig.json +15 -0
- package/tslint.json +36 -0
- package/typedoc.json +10 -0
- package/webpack.config.js +15 -0
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
// Copyright (c) 2018-2020, The TurtleCoin Developers
|
|
2
|
+
//
|
|
3
|
+
// Please see the included LICENSE file for more information.
|
|
4
|
+
|
|
5
|
+
import {Address} from './Address';
|
|
6
|
+
import {AddressPrefix} from './AddressPrefix';
|
|
7
|
+
import * as ConfigInterface from './Config';
|
|
8
|
+
import {Common} from './Common';
|
|
9
|
+
import {BigInteger, ED25519, TransactionInputs, TransactionOutputs, TurtleCoinCrypto, Interfaces} from './Types';
|
|
10
|
+
import {Transaction} from './Transaction';
|
|
11
|
+
import * as Numeral from 'numeral';
|
|
12
|
+
import Config = ConfigInterface.Interfaces.Config;
|
|
13
|
+
|
|
14
|
+
/** @ignore */
|
|
15
|
+
const Config = require('../config.json');
|
|
16
|
+
|
|
17
|
+
/** @ignore */
|
|
18
|
+
const UINT64_MAX = BigInteger(2).pow(64);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* CryptoNote helper class for constructing transactions and performing
|
|
22
|
+
* various other cryptographic items during the receipt or transfer
|
|
23
|
+
* of funds on the network
|
|
24
|
+
*/
|
|
25
|
+
export class CryptoNote {
|
|
26
|
+
|
|
27
|
+
protected config: Config = require('../config.json');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Constructs a new instance of the object
|
|
31
|
+
* If a configuration is supplied, it is also passed to the underlying
|
|
32
|
+
* cryptographic library
|
|
33
|
+
* @param [config] the base configuration to apply to our helper
|
|
34
|
+
*/
|
|
35
|
+
constructor(config?: Config) {
|
|
36
|
+
if (config) {
|
|
37
|
+
Object.keys(config).forEach((key) => {
|
|
38
|
+
switch (key) {
|
|
39
|
+
case 'coinUnitPlaces':
|
|
40
|
+
this.config.coinUnitPlaces = config[key];
|
|
41
|
+
break;
|
|
42
|
+
case 'addressPrefix':
|
|
43
|
+
this.config.addressPrefix = config[key];
|
|
44
|
+
break;
|
|
45
|
+
case 'keccakIterations':
|
|
46
|
+
this.config.keccakIterations = config[key];
|
|
47
|
+
break;
|
|
48
|
+
case 'defaultNetworkFee':
|
|
49
|
+
this.config.defaultNetworkFee = config[key];
|
|
50
|
+
break;
|
|
51
|
+
case 'fusionMinInputCount':
|
|
52
|
+
this.config.fusionMinInputCount = config[key];
|
|
53
|
+
break;
|
|
54
|
+
case 'fusionMinInOutCountRatio':
|
|
55
|
+
this.config.fusionMinInOutCountRatio = config[key];
|
|
56
|
+
break;
|
|
57
|
+
case 'mmMiningBlockVersion':
|
|
58
|
+
this.config.mmMiningBlockVersion = config[key];
|
|
59
|
+
break;
|
|
60
|
+
case 'maximumOutputAmount':
|
|
61
|
+
this.config.maximumOutputAmount = config[key];
|
|
62
|
+
break;
|
|
63
|
+
case 'maximumOutputsPerTransaction':
|
|
64
|
+
this.config.maximumOutputsPerTransaction = config[key];
|
|
65
|
+
break;
|
|
66
|
+
case 'maximumExtraSize':
|
|
67
|
+
this.config.maximumExtraSize = config[key];
|
|
68
|
+
break;
|
|
69
|
+
case 'activateFeePerByteTransactions':
|
|
70
|
+
this.config.activateFeePerByteTransactions = config[key];
|
|
71
|
+
break;
|
|
72
|
+
case 'feePerByte':
|
|
73
|
+
this.config.feePerByte = config[key];
|
|
74
|
+
break;
|
|
75
|
+
case 'feePerByteChunkSize':
|
|
76
|
+
this.config.feePerByteChunkSize = config[key];
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
TurtleCoinCrypto.userCryptoFunctions = config;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Converts absolute global index offsets to relative ones
|
|
87
|
+
* @param offsets the absolute offsets
|
|
88
|
+
* @returns the relative offsets
|
|
89
|
+
*/
|
|
90
|
+
public absoluteToRelativeOffsets(offsets: BigInteger.BigInteger[] | string[] | number[]): number[] {
|
|
91
|
+
const result: number[] = [];
|
|
92
|
+
|
|
93
|
+
const tmpOffsets = Common.absoluteToRelativeOffsets(offsets);
|
|
94
|
+
|
|
95
|
+
tmpOffsets.forEach((offset) => result.push(offset.toJSNumber()));
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Converts relative global index offsets to absolute offsets
|
|
102
|
+
* @param offsets the relative offsets
|
|
103
|
+
* @returns the absolute offsets
|
|
104
|
+
*/
|
|
105
|
+
public relativeToAbsoluteOffsets(offsets: BigInteger.BigInteger[] | string[] | number[]): number[] {
|
|
106
|
+
const result: number[] = [];
|
|
107
|
+
|
|
108
|
+
const tmpOffsets = Common.relativeToAbsoluteOffsets(offsets);
|
|
109
|
+
|
|
110
|
+
tmpOffsets.forEach((offset) => result.push(offset.toJSNumber()));
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generates a key image from the supplied values
|
|
117
|
+
* @async
|
|
118
|
+
* @param transactionPublicKey the transaction public key
|
|
119
|
+
* @param privateViewKey the private view key
|
|
120
|
+
* @param publicSpendKey the public spend key
|
|
121
|
+
* @param privateSpendKey the private spend key
|
|
122
|
+
* @param outputIndex the index of the output in the transaction
|
|
123
|
+
* @returns the key image
|
|
124
|
+
*/
|
|
125
|
+
public async generateKeyImage(
|
|
126
|
+
transactionPublicKey: string,
|
|
127
|
+
privateViewKey: string,
|
|
128
|
+
publicSpendKey: string,
|
|
129
|
+
privateSpendKey: string,
|
|
130
|
+
outputIndex: number): Promise<string> {
|
|
131
|
+
const derivation = await TurtleCoinCrypto.generateKeyDerivation(transactionPublicKey, privateViewKey);
|
|
132
|
+
|
|
133
|
+
return this.generateKeyImagePrimitive(publicSpendKey, privateSpendKey, outputIndex, derivation);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Primitive method for generating a key image from the supplied values
|
|
138
|
+
* @async
|
|
139
|
+
* @param publicSpendKey the public spend key
|
|
140
|
+
* @param privateSpendKey the private spend key
|
|
141
|
+
* @param outputIndex the index of the output in the transaction
|
|
142
|
+
* @param derivation the key derivation
|
|
143
|
+
* @returns the key image
|
|
144
|
+
*/
|
|
145
|
+
public async generateKeyImagePrimitive(
|
|
146
|
+
publicSpendKey: string,
|
|
147
|
+
privateSpendKey: string,
|
|
148
|
+
outputIndex: number,
|
|
149
|
+
derivation: string): Promise<string> {
|
|
150
|
+
const publicEphemeral = await TurtleCoinCrypto.derivePublicKey(derivation, outputIndex, publicSpendKey);
|
|
151
|
+
|
|
152
|
+
const privateEphemeral = await TurtleCoinCrypto.deriveSecretKey(derivation, outputIndex, privateSpendKey);
|
|
153
|
+
|
|
154
|
+
return TurtleCoinCrypto.generateKeyImage(publicEphemeral, privateEphemeral);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Provides the public key of the supplied private key
|
|
159
|
+
* @async
|
|
160
|
+
* @param privateKey the private key
|
|
161
|
+
* @returns the public key
|
|
162
|
+
*/
|
|
163
|
+
public async privateKeyToPublicKey(privateKey: string): Promise<string> {
|
|
164
|
+
return TurtleCoinCrypto.secretKeyToPublicKey(privateKey);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Scans the provided transaction outputs and returns those outputs which belong to us.
|
|
169
|
+
* If the privateSpendKey is not supplied, the private ephemeral and key image will be undefined
|
|
170
|
+
* @async
|
|
171
|
+
* @param transactionPublicKey the transaction public key
|
|
172
|
+
* @param outputs the transaction outputs
|
|
173
|
+
* @param privateViewKey the private view key
|
|
174
|
+
* @param publicSpendKey the public spend key
|
|
175
|
+
* @param [privateSpendKey] the private spend key
|
|
176
|
+
* @param [generatePartial] whether we should generate partial key images if the output belongs to use
|
|
177
|
+
* @returns an list of outputs that belong to us
|
|
178
|
+
*/
|
|
179
|
+
public async scanTransactionOutputs(
|
|
180
|
+
transactionPublicKey: string,
|
|
181
|
+
outputs: Interfaces.Output[],
|
|
182
|
+
privateViewKey: string,
|
|
183
|
+
publicSpendKey: string,
|
|
184
|
+
privateSpendKey?: string,
|
|
185
|
+
generatePartial?: boolean,
|
|
186
|
+
): Promise<Interfaces.Output[]> {
|
|
187
|
+
const promises = [];
|
|
188
|
+
|
|
189
|
+
for (const output of outputs) {
|
|
190
|
+
promises.push(
|
|
191
|
+
this.isOurTransactionOutput(
|
|
192
|
+
transactionPublicKey,
|
|
193
|
+
output,
|
|
194
|
+
privateViewKey,
|
|
195
|
+
publicSpendKey,
|
|
196
|
+
privateSpendKey,
|
|
197
|
+
generatePartial).catch(),
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const results = await Promise.all(promises);
|
|
202
|
+
|
|
203
|
+
const ourOutputs: Interfaces.Output[] = [];
|
|
204
|
+
|
|
205
|
+
for (const result of results) {
|
|
206
|
+
if (result) {
|
|
207
|
+
ourOutputs.push(result);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return ourOutputs;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Scans the given transaction output to determine if it belongs to us, if so, we return the output
|
|
216
|
+
* with the private ephemeral and key image if the privateSpendKey was supplied
|
|
217
|
+
* @async
|
|
218
|
+
* @param transactionPublicKey the transaction public key
|
|
219
|
+
* @param output the transaction output
|
|
220
|
+
* @param privateViewKey the private view key
|
|
221
|
+
* @param publicSpendKey the public spend key
|
|
222
|
+
* @param [privateSpendKey] the private spend key
|
|
223
|
+
* @param [generatePartial] whether we should generate partial key images
|
|
224
|
+
* @returns the output if it belongs to us
|
|
225
|
+
*/
|
|
226
|
+
public async isOurTransactionOutput(
|
|
227
|
+
transactionPublicKey: string,
|
|
228
|
+
output: Interfaces.Output,
|
|
229
|
+
privateViewKey: string,
|
|
230
|
+
publicSpendKey: string,
|
|
231
|
+
privateSpendKey?: string,
|
|
232
|
+
generatePartial?: boolean,
|
|
233
|
+
): Promise<Interfaces.Output> {
|
|
234
|
+
const derivedKey = await TurtleCoinCrypto.generateKeyDerivation(transactionPublicKey, privateViewKey);
|
|
235
|
+
|
|
236
|
+
const publicEphemeral = await TurtleCoinCrypto.derivePublicKey(derivedKey, output.index, publicSpendKey);
|
|
237
|
+
|
|
238
|
+
if (publicEphemeral === output.key) {
|
|
239
|
+
output.input = {
|
|
240
|
+
publicEphemeral,
|
|
241
|
+
transactionKeys: {
|
|
242
|
+
publicKey: transactionPublicKey,
|
|
243
|
+
derivedKey,
|
|
244
|
+
outputIndex: output.index,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
if (privateSpendKey) {
|
|
249
|
+
/* If we are forcing the generation of a partial key image then we
|
|
250
|
+
use the supplied private spend key in the key generation instead of
|
|
251
|
+
the privateEphemeral that we don't have
|
|
252
|
+
*/
|
|
253
|
+
const privateEphemeral = (generatePartial) ?
|
|
254
|
+
privateSpendKey :
|
|
255
|
+
await TurtleCoinCrypto.deriveSecretKey(
|
|
256
|
+
derivedKey, output.index, privateSpendKey);
|
|
257
|
+
|
|
258
|
+
const derivedPublicEphemeral = await TurtleCoinCrypto.secretKeyToPublicKey(privateEphemeral);
|
|
259
|
+
|
|
260
|
+
if (derivedPublicEphemeral !== publicEphemeral && !generatePartial) {
|
|
261
|
+
throw new Error('Incorrect private spend key supplied');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const keyImage = await TurtleCoinCrypto.generateKeyImage(publicEphemeral, privateEphemeral);
|
|
265
|
+
|
|
266
|
+
output.input.privateEphemeral = privateEphemeral;
|
|
267
|
+
|
|
268
|
+
output.keyImage = keyImage;
|
|
269
|
+
|
|
270
|
+
output.isPartialKeyImage = (generatePartial) ? generatePartial : false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return output;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
throw new Error('Not our output');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Calculates the minimum transaction fee given the transaction size (bytes)
|
|
281
|
+
* @param txSize the transaction size in bytes
|
|
282
|
+
* @returns the minimum transaction fee
|
|
283
|
+
*/
|
|
284
|
+
public calculateMinimumTransactionFee(txSize: number) {
|
|
285
|
+
const chunks = Math.ceil(
|
|
286
|
+
txSize /
|
|
287
|
+
(this.config.feePerByteChunkSize || Config.feePerByteChunkSize));
|
|
288
|
+
|
|
289
|
+
return chunks *
|
|
290
|
+
(this.config.feePerByteChunkSize || Config.feePerByteChunkSize) *
|
|
291
|
+
(this.config.feePerByte || Config.feePerByte);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Creates an integrated address using the supplied values
|
|
296
|
+
* @param address the wallet address
|
|
297
|
+
* @param paymentId the payment ID
|
|
298
|
+
* @param [prefix] the address prefix
|
|
299
|
+
* @returns the integrated address
|
|
300
|
+
*/
|
|
301
|
+
public createIntegratedAddress(address: string, paymentId: string, prefix?: AddressPrefix | number): string {
|
|
302
|
+
if (typeof prefix === 'number') {
|
|
303
|
+
prefix = new AddressPrefix(prefix);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!prefix) {
|
|
307
|
+
prefix = new AddressPrefix(this.config.addressPrefix || Config.addressPrefix);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const addr = Address.fromAddress(address);
|
|
311
|
+
|
|
312
|
+
addr.paymentId = paymentId;
|
|
313
|
+
|
|
314
|
+
if (prefix) {
|
|
315
|
+
addr.prefix = prefix;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return addr.toString();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Formats atomic units into human readable units
|
|
323
|
+
* @param amount the amount in atomic units
|
|
324
|
+
* @returns the amount in human readable units
|
|
325
|
+
*/
|
|
326
|
+
public formatMoney(amount: BigInteger.BigInteger | number): string {
|
|
327
|
+
let places = '';
|
|
328
|
+
|
|
329
|
+
for (let i = 0; i < (this.config.coinUnitPlaces || Config.coinUnitPlaces); i++) {
|
|
330
|
+
places += '0';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (typeof amount !== 'number') {
|
|
334
|
+
amount = amount.toJSNumber();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return Numeral(
|
|
338
|
+
amount / Math.pow(10, this.config.coinUnitPlaces || Config.coinUnitPlaces),
|
|
339
|
+
).format('0,0.' + places);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Generates an array of transaction outputs (new destinations) for the given address
|
|
344
|
+
* and the given amount within the allowed rules of the network
|
|
345
|
+
* @param address the destination wallet address
|
|
346
|
+
* @param amount the amount to send
|
|
347
|
+
* @returns a list of transaction outputs
|
|
348
|
+
*/
|
|
349
|
+
public generateTransactionOutputs(address: string, amount: number): Interfaces.GeneratedOutput[] {
|
|
350
|
+
if (amount < 0) {
|
|
351
|
+
throw new RangeError('Amount must be a positive value');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const result: Interfaces.GeneratedOutput[] = [];
|
|
355
|
+
|
|
356
|
+
const destination = Address.fromAddress(address);
|
|
357
|
+
|
|
358
|
+
const amountChars = amount.toString().split('').reverse();
|
|
359
|
+
|
|
360
|
+
for (let i = 0; i < amountChars.length; i++) {
|
|
361
|
+
const amt = parseInt(amountChars[i], 10) * Math.pow(10, i);
|
|
362
|
+
|
|
363
|
+
if (amt > (this.config.maximumOutputAmount || Config.maximumOutputAmount)) {
|
|
364
|
+
let splitAmt = amt;
|
|
365
|
+
|
|
366
|
+
while (splitAmt >= (this.config.maximumOutputAmount || Config.maximumOutputAmount)) {
|
|
367
|
+
result.push({
|
|
368
|
+
amount: this.config.maximumOutputAmount || Config.maximumOutputAmount,
|
|
369
|
+
destination: destination,
|
|
370
|
+
});
|
|
371
|
+
splitAmt -= this.config.maximumOutputAmount || Config.maximumOutputAmount;
|
|
372
|
+
}
|
|
373
|
+
} else if (amt !== 0) {
|
|
374
|
+
result.push({
|
|
375
|
+
amount: amt,
|
|
376
|
+
destination: destination,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Signs an arbitrary message using the supplied private key
|
|
386
|
+
* @async
|
|
387
|
+
* @param message the arbitrary message to sign
|
|
388
|
+
* @param privateKey the private key to sign with
|
|
389
|
+
* @returns the signature
|
|
390
|
+
*/
|
|
391
|
+
public async signMessage(message: any, privateKey: string): Promise<string> {
|
|
392
|
+
if (typeof message !== 'string') {
|
|
393
|
+
message = JSON.stringify(message);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const publicKey = await TurtleCoinCrypto.secretKeyToPublicKey(privateKey);
|
|
397
|
+
|
|
398
|
+
const hex = Buffer.from(message);
|
|
399
|
+
|
|
400
|
+
const hash = await TurtleCoinCrypto.cn_fast_hash(hex.toString('hex'));
|
|
401
|
+
|
|
402
|
+
return TurtleCoinCrypto.generateSignature(hash, publicKey, privateKey);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Verifies the signature of an arbitrary message using the signature and the supplied public key
|
|
407
|
+
* @async
|
|
408
|
+
* @param message the arbitrary message that was signed
|
|
409
|
+
* @param publicKey the public key of the private key that was used to sign
|
|
410
|
+
* @param signature the signature
|
|
411
|
+
* @returns whether the signature is valid
|
|
412
|
+
*/
|
|
413
|
+
public async verifyMessageSignature(message: any, publicKey: string, signature: string): Promise<void> {
|
|
414
|
+
if (typeof message !== 'string') {
|
|
415
|
+
message = JSON.stringify(message);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const hex = Buffer.from(message);
|
|
419
|
+
|
|
420
|
+
const hash = await TurtleCoinCrypto.cn_fast_hash(hex.toString('hex'));
|
|
421
|
+
|
|
422
|
+
const valid = await TurtleCoinCrypto.checkSignature(hash, publicKey, signature);
|
|
423
|
+
|
|
424
|
+
if (!valid) {
|
|
425
|
+
throw new Error('Invalid signature');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Constructs a new Transaction using the supplied values.
|
|
431
|
+
* The resulting transaction can be broadcasted to the TurtleCoin network
|
|
432
|
+
* @async
|
|
433
|
+
* @param outputs the new outputs for the transaction (TO)
|
|
434
|
+
* @param inputs outputs we will be spending (FROM)
|
|
435
|
+
* @param randomOutputs the random outputs to use for mixing
|
|
436
|
+
* @param mixin the number of mixins to use
|
|
437
|
+
* @param [feeAmount] the transaction fee amount to pay
|
|
438
|
+
* @param [paymentId] the payment ID to use in the transaction,
|
|
439
|
+
* @param [unlockTime] the unlock time or block height for the transaction
|
|
440
|
+
* @param [extraData] arbitrary extra data to include in the transaction extra field
|
|
441
|
+
* @returns the newly created transaction object
|
|
442
|
+
*/
|
|
443
|
+
public async createTransaction(
|
|
444
|
+
outputs: Interfaces.GeneratedOutput[],
|
|
445
|
+
inputs: Interfaces.Output[],
|
|
446
|
+
randomOutputs: Interfaces.RandomOutput[][],
|
|
447
|
+
mixin: number,
|
|
448
|
+
feeAmount?: number,
|
|
449
|
+
paymentId?: string,
|
|
450
|
+
unlockTime?: number,
|
|
451
|
+
extraData?: any,
|
|
452
|
+
): Promise<Transaction> {
|
|
453
|
+
const feePerByte =
|
|
454
|
+
this.config.activateFeePerByteTransactions || Config.activateFeePerByteTransactions || false;
|
|
455
|
+
|
|
456
|
+
const prepared = await this.createTransactionStructure(
|
|
457
|
+
outputs, inputs, randomOutputs, mixin, feeAmount, paymentId, unlockTime, extraData,
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
const txPrefixHash = prepared.transaction.prefixHash;
|
|
461
|
+
|
|
462
|
+
const promises = [];
|
|
463
|
+
|
|
464
|
+
for (let i = 0; i < prepared.inputs.length; i++) {
|
|
465
|
+
const input = prepared.inputs[i];
|
|
466
|
+
const srcKeys: string[] = [];
|
|
467
|
+
|
|
468
|
+
if (!input.input.privateEphemeral) {
|
|
469
|
+
throw new Error('private ephemeral missing from input');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
input.outputs.forEach((out) => srcKeys.push(out.key));
|
|
473
|
+
|
|
474
|
+
promises.push(
|
|
475
|
+
generateRingSignatures(
|
|
476
|
+
txPrefixHash,
|
|
477
|
+
input.keyImage,
|
|
478
|
+
srcKeys,
|
|
479
|
+
input.input.privateEphemeral,
|
|
480
|
+
input.realOutputIndex,
|
|
481
|
+
i));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const tmpSignatures = await Promise.all(promises);
|
|
485
|
+
|
|
486
|
+
tmpSignatures.sort((a, b) => (a.index > b.index) ? 1 : -1);
|
|
487
|
+
|
|
488
|
+
const signatures: string[][] = [];
|
|
489
|
+
|
|
490
|
+
tmpSignatures.forEach((sigs) => {
|
|
491
|
+
const sigSet: string[] = [];
|
|
492
|
+
sigs.signatures.forEach((sig) => sigSet.push(sig));
|
|
493
|
+
signatures.push(sigSet);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
prepared.transaction.signatures = signatures;
|
|
497
|
+
|
|
498
|
+
const minimumFee = this.calculateMinimumTransactionFee(prepared.transaction.size);
|
|
499
|
+
|
|
500
|
+
if (feeAmount && feeAmount !== 0 && feePerByte && feeAmount < minimumFee) {
|
|
501
|
+
throw new Error('Transaction fee [' + prepared.transaction.fee +
|
|
502
|
+
'] is not enough for network transmission: ' + minimumFee);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return prepared.transaction;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Constructs a new Transaction using the supplied values.
|
|
510
|
+
* Note: Does not sign the transaction
|
|
511
|
+
* @async
|
|
512
|
+
* @param outputs the new outputs for the transaction (TO)
|
|
513
|
+
* @param inputs outputs we will be spending (FROM)
|
|
514
|
+
* @param randomOutputs the random outputs to use for mixing
|
|
515
|
+
* @param mixin the number of mixins to use
|
|
516
|
+
* @param [feeAmount] the transaction fee amount to pay
|
|
517
|
+
* @param [paymentId] the payment ID to use in the transaction,
|
|
518
|
+
* @param [unlockTime] the unlock time or block height for the transaction
|
|
519
|
+
* @param [extraData] arbitrary extra data to include in the transaction extra field
|
|
520
|
+
* @returns the newly created transaction object and it's input data
|
|
521
|
+
*/
|
|
522
|
+
public async createTransactionStructure(
|
|
523
|
+
outputs: Interfaces.GeneratedOutput[],
|
|
524
|
+
inputs: Interfaces.Output[],
|
|
525
|
+
randomOutputs: Interfaces.RandomOutput[][],
|
|
526
|
+
mixin: number,
|
|
527
|
+
feeAmount?: number,
|
|
528
|
+
paymentId?: string,
|
|
529
|
+
unlockTime?: number,
|
|
530
|
+
extraData?: any,
|
|
531
|
+
): Promise<Interfaces.IPreparedTransaction> {
|
|
532
|
+
if (typeof feeAmount === 'undefined') {
|
|
533
|
+
feeAmount = this.config.defaultNetworkFee || Config.defaultNetworkFee;
|
|
534
|
+
}
|
|
535
|
+
unlockTime = unlockTime || 0;
|
|
536
|
+
|
|
537
|
+
const feePerByte =
|
|
538
|
+
this.config.activateFeePerByteTransactions || Config.activateFeePerByteTransactions || false;
|
|
539
|
+
|
|
540
|
+
if (randomOutputs.length !== inputs.length && mixin !== 0) {
|
|
541
|
+
throw new Error('The sets of random outputs supplied does not match the number of inputs supplied');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
for (const randomOutput of randomOutputs) {
|
|
545
|
+
if (randomOutput.length < mixin) {
|
|
546
|
+
throw new Error('There are not enough random outputs to mix with');
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const neededMoney = BigInteger.zero;
|
|
551
|
+
let integratedPaymentId: string | undefined;
|
|
552
|
+
|
|
553
|
+
for (const output of outputs) {
|
|
554
|
+
if (output.amount <= 0) {
|
|
555
|
+
throw new RangeError('Cannot create an output with an amount <= 0');
|
|
556
|
+
}
|
|
557
|
+
if (output.amount > (this.config.maximumOutputAmount || Config.maximumOutputAmount)) {
|
|
558
|
+
throw new RangeError('Cannot create an output with an amount > ' +
|
|
559
|
+
(this.config.maximumOutputAmount || Config.maximumOutputAmount));
|
|
560
|
+
}
|
|
561
|
+
neededMoney.add(output.amount);
|
|
562
|
+
if (neededMoney.greater(UINT64_MAX)) {
|
|
563
|
+
throw new RangeError('Total output amount exceeds UINT64_MAX');
|
|
564
|
+
}
|
|
565
|
+
/* Check to see if our destination contains differeing payment IDs via integrated addresses */
|
|
566
|
+
if (output.destination.paymentId) {
|
|
567
|
+
if (!integratedPaymentId) {
|
|
568
|
+
integratedPaymentId = output.destination.paymentId;
|
|
569
|
+
} else if (integratedPaymentId && integratedPaymentId !== output.destination.paymentId) {
|
|
570
|
+
throw new Error('Cannot perform multiple transfers with differing integrated addresses');
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/* If we found an integrated payment ID in the destinations and we supplied a payment ID
|
|
576
|
+
in our call to this method and they do not match, this will result in a failure */
|
|
577
|
+
if (integratedPaymentId && paymentId && integratedPaymentId !== paymentId) {
|
|
578
|
+
throw new Error('Transfer destinations contains an integrated payment ID that does not match the payment' +
|
|
579
|
+
'ID supplied to this method');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const foundMoney = BigInteger.zero;
|
|
583
|
+
|
|
584
|
+
for (const input of inputs) {
|
|
585
|
+
if (input.amount <= 0) {
|
|
586
|
+
throw new RangeError('Cannot spend outputs with an amount <= 0');
|
|
587
|
+
}
|
|
588
|
+
foundMoney.add(input.amount);
|
|
589
|
+
if (foundMoney.greater(UINT64_MAX)) {
|
|
590
|
+
throw new RangeError('Total input amount exceeds UINT64_MAX');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (neededMoney.greater(foundMoney)) {
|
|
595
|
+
throw new Error('We need more funds than was currently supplied for the transaction');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const change = foundMoney.subtract(neededMoney);
|
|
599
|
+
|
|
600
|
+
if (!feePerByte && feeAmount && change.lesser(feeAmount)) {
|
|
601
|
+
throw new Error('We have not spent all of what we sent in');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const transactionInputs = await prepareTransactionInputs(inputs, randomOutputs, mixin);
|
|
605
|
+
|
|
606
|
+
const transactionOutputs = await prepareTransactionOutputs(outputs);
|
|
607
|
+
|
|
608
|
+
if (transactionOutputs.outputs.length >
|
|
609
|
+
(this.config.maximumOutputsPerTransaction || Config.maximumOutputsPerTransaction)) {
|
|
610
|
+
throw new RangeError('Tried to create a transaction with more outputs than permitted');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (feeAmount === 0) {
|
|
614
|
+
if (transactionInputs.length < 12) {
|
|
615
|
+
throw new Error('Sending a [0] fee transaction (fusion) requires a minimum of ['
|
|
616
|
+
+ (this.config.fusionMinInputCount || Config.fusionMinInputCount) + '] inputs');
|
|
617
|
+
}
|
|
618
|
+
const ratio = this.config.fusionMinInOutCountRatio || Config.fusionMinInOutCountRatio;
|
|
619
|
+
if ((transactionInputs.length / transactionOutputs.outputs.length) < ratio) {
|
|
620
|
+
throw new Error('Sending a [0] fee transaction (fusion) requires the ' +
|
|
621
|
+
'correct input:output ratio be met');
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const tx = new Transaction();
|
|
626
|
+
tx.unlockTime = BigInteger(unlockTime);
|
|
627
|
+
tx.addPublicKey(transactionOutputs.transactionKeys.publicKey);
|
|
628
|
+
tx.transactionKeys = transactionOutputs.transactionKeys;
|
|
629
|
+
|
|
630
|
+
if (integratedPaymentId) {
|
|
631
|
+
tx.addPaymentId(integratedPaymentId);
|
|
632
|
+
} else if (paymentId) {
|
|
633
|
+
tx.addPaymentId(paymentId);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (extraData) {
|
|
637
|
+
if (!(extraData instanceof Buffer)) {
|
|
638
|
+
extraData = (typeof extraData === 'string') ?
|
|
639
|
+
Buffer.from(extraData) : Buffer.from(JSON.stringify(extraData));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
tx.addData(extraData);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
transactionInputs.sort((a, b) => {
|
|
646
|
+
return (BigInteger(a.keyImage, 16).compare(BigInteger(b.keyImage, 16)) * -1);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
for (const input of transactionInputs) {
|
|
650
|
+
let offsets: BigInteger.BigInteger[] = [];
|
|
651
|
+
|
|
652
|
+
input.outputs.forEach((output) => offsets.push(BigInteger(output.index)));
|
|
653
|
+
|
|
654
|
+
offsets = Common.absoluteToRelativeOffsets(offsets);
|
|
655
|
+
|
|
656
|
+
tx.inputs.push(new TransactionInputs.KeyInput(input.amount, offsets, input.keyImage));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
for (const output of transactionOutputs.outputs) {
|
|
660
|
+
tx.outputs.push(new TransactionOutputs.KeyOutput(output.amount, output.key));
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (tx.extra.length > (this.config.maximumExtraSize || Config.maximumExtraSize)) {
|
|
664
|
+
throw new Error('Transaction extra exceeds the limit of [' +
|
|
665
|
+
(this.config.maximumExtraSize || Config.maximumExtraSize) + '] bytes');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
transaction: tx,
|
|
670
|
+
inputs: transactionInputs,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Constructs a new Transaction using the supplied values.
|
|
676
|
+
* The resulting transaction can be broadcasted to the TurtleCoin network
|
|
677
|
+
* @async
|
|
678
|
+
* @param outputs the new outputs for the transaction (TO)
|
|
679
|
+
* @param inputs outputs we will be spending (FROM)
|
|
680
|
+
* @param randomOutputs the random outputs to use for mixing
|
|
681
|
+
* @param mixin the number of mixins to use
|
|
682
|
+
* @param [feeAmount] the transaction fee amount to pay
|
|
683
|
+
* @param [paymentId] the payment ID to use in the transaction,
|
|
684
|
+
* @param [unlockTime] the unlock time or block height for the transaction
|
|
685
|
+
* @param [extraData] arbitrary extra data to include in the transaction extra field
|
|
686
|
+
* @returns the newly created transaction object with prepared signatures
|
|
687
|
+
*/
|
|
688
|
+
public async prepareTransaction(
|
|
689
|
+
outputs: Interfaces.GeneratedOutput[],
|
|
690
|
+
inputs: Interfaces.Output[],
|
|
691
|
+
randomOutputs: Interfaces.RandomOutput[][],
|
|
692
|
+
mixin: number,
|
|
693
|
+
feeAmount?: number,
|
|
694
|
+
paymentId?: string,
|
|
695
|
+
unlockTime?: number,
|
|
696
|
+
extraData?: any,
|
|
697
|
+
): Promise<Interfaces.PreparedTransaction> {
|
|
698
|
+
const feePerByte =
|
|
699
|
+
this.config.activateFeePerByteTransactions || Config.activateFeePerByteTransactions || false;
|
|
700
|
+
|
|
701
|
+
const prepared = await this.createTransactionStructure(
|
|
702
|
+
outputs, inputs, randomOutputs, mixin, feeAmount, paymentId, unlockTime, extraData,
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
const recipients: Interfaces.TransactionRecipient[] = [];
|
|
706
|
+
|
|
707
|
+
for (const output of outputs) {
|
|
708
|
+
recipients.push({
|
|
709
|
+
address: output.destination.address,
|
|
710
|
+
amount: output.amount,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const txPrefixHash = prepared.transaction.prefixHash;
|
|
715
|
+
|
|
716
|
+
const promises = [];
|
|
717
|
+
|
|
718
|
+
for (let i = 0; i < prepared.inputs.length; i++) {
|
|
719
|
+
const input = prepared.inputs[i];
|
|
720
|
+
const srcKeys: string[] = [];
|
|
721
|
+
|
|
722
|
+
input.outputs.forEach((out) => srcKeys.push(out.key));
|
|
723
|
+
|
|
724
|
+
promises.push(
|
|
725
|
+
prepareRingSignatures(
|
|
726
|
+
txPrefixHash,
|
|
727
|
+
input.keyImage,
|
|
728
|
+
srcKeys,
|
|
729
|
+
input.realOutputIndex,
|
|
730
|
+
input.input.transactionKeys.derivedKey,
|
|
731
|
+
input.input.transactionKeys.outputIndex,
|
|
732
|
+
i));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const results = await Promise.all(promises);
|
|
736
|
+
|
|
737
|
+
results.sort((a, b) => (a.index > b.index) ? 1 : -1);
|
|
738
|
+
|
|
739
|
+
const signatures: string[][] = [];
|
|
740
|
+
|
|
741
|
+
const signatureMeta: Interfaces.PreparedRingSignature[] = [];
|
|
742
|
+
|
|
743
|
+
for (const result of results) {
|
|
744
|
+
const sigSet: string[] = [];
|
|
745
|
+
if (!result.signatures) {
|
|
746
|
+
throw new Error('Prepared signatures are incomplete');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
result.signatures.forEach((sig) => sigSet.push(sig));
|
|
750
|
+
signatures.push(sigSet);
|
|
751
|
+
|
|
752
|
+
const meta: Interfaces.PreparedRingSignature = {
|
|
753
|
+
index: result.index,
|
|
754
|
+
realOutputIndex: result.realOutputIndex,
|
|
755
|
+
key: result.key,
|
|
756
|
+
inputKeys: result.inputKeys,
|
|
757
|
+
input: {
|
|
758
|
+
derivation: prepared.inputs[result.index].input.transactionKeys.derivedKey,
|
|
759
|
+
outputIndex: prepared.inputs[result.index].input.transactionKeys.outputIndex,
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
signatureMeta.push(meta);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
prepared.transaction.signatures = signatures;
|
|
767
|
+
|
|
768
|
+
const minimumFee = this.calculateMinimumTransactionFee(prepared.transaction.size);
|
|
769
|
+
|
|
770
|
+
if (feeAmount && feeAmount !== 0 && feePerByte && feeAmount < minimumFee) {
|
|
771
|
+
throw new Error('Transaction fee [' + prepared.transaction.fee +
|
|
772
|
+
'] is not enough for network transmission: ' + minimumFee);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return {
|
|
776
|
+
transaction: prepared.transaction,
|
|
777
|
+
transactionRecipients: recipients,
|
|
778
|
+
transactionPrivateKey: prepared.transaction.transactionKeys.privateKey,
|
|
779
|
+
signatureMeta: signatureMeta,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Completes a prepared transaction using the supplied private ephemeral
|
|
785
|
+
* The resulting transaction can be broadcast to the network. Please note that the PreparedTransaction
|
|
786
|
+
* signatures meta data must be updated to include the proper private ephemeral
|
|
787
|
+
* @param preparedTransaction the prepared transaction
|
|
788
|
+
* @param privateSpendKey the private spend key of the wallet that contains the funds
|
|
789
|
+
* @returns the completed transaction
|
|
790
|
+
*/
|
|
791
|
+
public async completeTransaction(
|
|
792
|
+
preparedTransaction: Interfaces.PreparedTransaction,
|
|
793
|
+
privateSpendKey: string,
|
|
794
|
+
): Promise<Transaction> {
|
|
795
|
+
const promises = [];
|
|
796
|
+
|
|
797
|
+
const tx = preparedTransaction.transaction;
|
|
798
|
+
|
|
799
|
+
if (!preparedTransaction.signatureMeta) {
|
|
800
|
+
throw new Error('No transaction signature meta data supplied');
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
for (const meta of preparedTransaction.signatureMeta) {
|
|
804
|
+
if (!meta.input || !meta.input.derivation || !meta.input.outputIndex) {
|
|
805
|
+
throw new Error('Meta data is missing critical information');
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
promises.push(completeRingSignatures(
|
|
809
|
+
privateSpendKey,
|
|
810
|
+
meta.input.derivation,
|
|
811
|
+
meta.input.outputIndex,
|
|
812
|
+
meta.realOutputIndex,
|
|
813
|
+
meta.key,
|
|
814
|
+
tx.signatures[meta.index],
|
|
815
|
+
meta.index,
|
|
816
|
+
));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const results = await Promise.all(promises);
|
|
820
|
+
|
|
821
|
+
for (const result of results) {
|
|
822
|
+
tx.signatures[result.index] = result.signatures;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const prefixHash = tx.prefixHash;
|
|
826
|
+
|
|
827
|
+
const checkPromises = [];
|
|
828
|
+
|
|
829
|
+
for (let i = 0; i < tx.inputs.length; i++) {
|
|
830
|
+
checkPromises.push(checkRingSignatures(
|
|
831
|
+
prefixHash,
|
|
832
|
+
(tx.inputs[i] as TransactionInputs.KeyInput).keyImage,
|
|
833
|
+
getInputKeys(preparedTransaction.signatureMeta, i),
|
|
834
|
+
tx.signatures[i],
|
|
835
|
+
));
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const validSigs = await Promise.all(checkPromises);
|
|
839
|
+
|
|
840
|
+
for (const valid of validSigs) {
|
|
841
|
+
if (!valid) {
|
|
842
|
+
throw new Error('Could not complete ring signatures');
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return preparedTransaction.transaction;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** @ignore */
|
|
851
|
+
async function checkRingSignatures(
|
|
852
|
+
hash: string,
|
|
853
|
+
keyImage: string,
|
|
854
|
+
publicKeys: string[],
|
|
855
|
+
signatures: string[],
|
|
856
|
+
): Promise<boolean> {
|
|
857
|
+
return TurtleCoinCrypto.checkRingSignatures(hash, keyImage, publicKeys, signatures);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** @ignore */
|
|
861
|
+
async function generateRingSignatures(
|
|
862
|
+
hash: string,
|
|
863
|
+
keyImage: string,
|
|
864
|
+
publicKeys: string[],
|
|
865
|
+
privateKey: string,
|
|
866
|
+
realOutputIndex: number,
|
|
867
|
+
index: number,
|
|
868
|
+
): Promise<Interfaces.GeneratedRingSignatures> {
|
|
869
|
+
const signatures = await TurtleCoinCrypto.generateRingSignatures(
|
|
870
|
+
hash,
|
|
871
|
+
keyImage,
|
|
872
|
+
publicKeys,
|
|
873
|
+
privateKey,
|
|
874
|
+
realOutputIndex);
|
|
875
|
+
|
|
876
|
+
const valid = await checkRingSignatures(hash, keyImage, publicKeys, signatures);
|
|
877
|
+
|
|
878
|
+
if (!valid) {
|
|
879
|
+
throw new Error('Could not generate ring signatures');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return {signatures, index};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** @ignore */
|
|
886
|
+
async function prepareRingSignatures(
|
|
887
|
+
hash: string,
|
|
888
|
+
keyImage: string,
|
|
889
|
+
publicKeys: string[],
|
|
890
|
+
realOutputIndex: number,
|
|
891
|
+
derivation: string,
|
|
892
|
+
outputIndex: number,
|
|
893
|
+
index: number,
|
|
894
|
+
): Promise<Interfaces.PreparedRingSignature> {
|
|
895
|
+
const prepped = await TurtleCoinCrypto.prepareRingSignatures(hash, keyImage, publicKeys, realOutputIndex);
|
|
896
|
+
|
|
897
|
+
return {
|
|
898
|
+
index: index,
|
|
899
|
+
realOutputIndex: realOutputIndex,
|
|
900
|
+
key: prepped.key,
|
|
901
|
+
signatures: prepped.signatures,
|
|
902
|
+
inputKeys: publicKeys,
|
|
903
|
+
input: {
|
|
904
|
+
derivation,
|
|
905
|
+
outputIndex,
|
|
906
|
+
},
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** @ignore */
|
|
911
|
+
async function completeRingSignatures(
|
|
912
|
+
privateSpendKey: string,
|
|
913
|
+
derivation: string,
|
|
914
|
+
outputIndex: number,
|
|
915
|
+
realOutputIndex: number,
|
|
916
|
+
key: string,
|
|
917
|
+
sigs: string[],
|
|
918
|
+
index: number,
|
|
919
|
+
): Promise<Interfaces.GeneratedRingSignatures> {
|
|
920
|
+
const privateEphemeral = await TurtleCoinCrypto.deriveSecretKey(derivation, outputIndex, privateSpendKey);
|
|
921
|
+
|
|
922
|
+
const signatures = await TurtleCoinCrypto.completeRingSignatures(privateEphemeral, realOutputIndex, key, sigs);
|
|
923
|
+
|
|
924
|
+
return {signatures, index};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** @ignore */
|
|
928
|
+
function prepareTransactionInputs(
|
|
929
|
+
inputs: Interfaces.Output[],
|
|
930
|
+
randomOutputs: Interfaces.RandomOutput[][],
|
|
931
|
+
mixin: number): Interfaces.PreparedInput[] {
|
|
932
|
+
if (inputs.length !== randomOutputs.length && mixin !== 0) {
|
|
933
|
+
throw new Error('There are not enough random output sets to mix with the real outputs');
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
for (const randomOutput of randomOutputs) {
|
|
937
|
+
if (randomOutput.length < mixin) {
|
|
938
|
+
throw new Error('There are not enough random outputs to mix with');
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const mixedInputs: Interfaces.PreparedInput[] = [];
|
|
943
|
+
|
|
944
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
945
|
+
const mixedOutputs: Interfaces.PreparedInputOutputs[] = [];
|
|
946
|
+
const realOutput = inputs[i];
|
|
947
|
+
|
|
948
|
+
if (!realOutput.keyImage) {
|
|
949
|
+
throw new Error('input is missing its key image');
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (!realOutput.input) {
|
|
953
|
+
throw new Error('input is missing mandatory data fields');
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (realOutput.amount <= 0) {
|
|
957
|
+
throw new RangeError('Real inputs cannot have an amount <= 0');
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (mixin !== 0) {
|
|
961
|
+
const fakeOutputs = randomOutputs[i];
|
|
962
|
+
|
|
963
|
+
fakeOutputs.sort((a, b) => {
|
|
964
|
+
return BigInteger(a.globalIndex).compare(b.globalIndex);
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
for (const fakeOutput of fakeOutputs) {
|
|
968
|
+
if (mixedOutputs.length === mixin) {
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (fakeOutput.globalIndex === realOutput.globalIndex) {
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
mixedOutputs.push({
|
|
977
|
+
key: fakeOutput.key,
|
|
978
|
+
index: fakeOutput.globalIndex,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (mixedOutputs.length < mixin) {
|
|
983
|
+
throw new Error('It is impossible to mix with yourself. Find some more random outputs and try again.');
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
mixedOutputs.push({
|
|
988
|
+
key: realOutput.key,
|
|
989
|
+
index: realOutput.globalIndex,
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
mixedOutputs.sort((a, b) => {
|
|
993
|
+
return BigInteger(a.index).compare(b.index);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
const input = {
|
|
997
|
+
amount: realOutput.amount,
|
|
998
|
+
realOutputIndex: 0,
|
|
999
|
+
keyImage: realOutput.keyImage,
|
|
1000
|
+
input: realOutput.input,
|
|
1001
|
+
outputs: mixedOutputs,
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
for (let j = 0; j < mixedOutputs.length; j++) {
|
|
1005
|
+
if (mixedOutputs[j].index === realOutput.globalIndex) {
|
|
1006
|
+
input.realOutputIndex = j;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
mixedInputs.push(input);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return mixedInputs;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** @ignore */
|
|
1017
|
+
async function prepareTransactionOutputs(outputs: Interfaces.GeneratedOutput[]): Promise<Interfaces.PreparedOutputs> {
|
|
1018
|
+
async function prepareOutput(
|
|
1019
|
+
destination: Address,
|
|
1020
|
+
amount: number,
|
|
1021
|
+
index: number,
|
|
1022
|
+
privateKey: string): Promise<Interfaces.PreparedOutput> {
|
|
1023
|
+
const outDerivation = await TurtleCoinCrypto.generateKeyDerivation(destination.view.publicKey, privateKey);
|
|
1024
|
+
|
|
1025
|
+
const outPublicEphemeral = await TurtleCoinCrypto.derivePublicKey(
|
|
1026
|
+
outDerivation,
|
|
1027
|
+
index,
|
|
1028
|
+
destination.spend.publicKey);
|
|
1029
|
+
|
|
1030
|
+
return {
|
|
1031
|
+
amount,
|
|
1032
|
+
key: outPublicEphemeral,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const keys = await TurtleCoinCrypto.generateKeys();
|
|
1037
|
+
|
|
1038
|
+
const transactionKeys: ED25519.KeyPair = new ED25519.KeyPair(keys.publicKey, keys.privateKey);
|
|
1039
|
+
|
|
1040
|
+
outputs.sort((a, b) => (a.amount > b.amount) ? 1 : ((b.amount > a.amount) ? -1 : 0));
|
|
1041
|
+
|
|
1042
|
+
const promises = [];
|
|
1043
|
+
|
|
1044
|
+
for (let i = 0; i < outputs.length; i++) {
|
|
1045
|
+
const output = outputs[i];
|
|
1046
|
+
if (output.amount <= 0) {
|
|
1047
|
+
throw new RangeError('Amount cannot be <= 0');
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
promises.push(prepareOutput(output.destination, output.amount, i, transactionKeys.privateKey));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const preparedOutputs = await Promise.all(promises);
|
|
1054
|
+
|
|
1055
|
+
return {
|
|
1056
|
+
transactionKeys,
|
|
1057
|
+
outputs: preparedOutputs,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/** @ignore */
|
|
1062
|
+
function getInputKeys(preparedSignatures: Interfaces.PreparedRingSignature[], index: number): string[] {
|
|
1063
|
+
for (const meta of preparedSignatures) {
|
|
1064
|
+
if (meta.index === index) {
|
|
1065
|
+
if (meta.inputKeys) {
|
|
1066
|
+
return meta.inputKeys;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
throw new Error('Could not locate input keys in the prepared signatures');
|
|
1072
|
+
}
|