@quantovainc/qcore 0.1.32 → 0.2.0

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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@quantovainc/qcore",
3
- "version": "0.1.32",
3
+ "version": "0.2.0",
4
4
  "description": "Quantova's post quantum client core for JavaScript. Signs with ML-DSA-65 and builds Q1 addresses.",
5
5
  "scripts": {
6
6
  "build": "bash scripts/build.sh",
7
- "test": "node test-types.js && node test-conformance.js && node test-order-nonce.js && node test-chain-binding.js && node test-sign-validate.js && node test-transport.js && node test-fee-cap.js && node test-network.js && node test-typed-order.js && node test-asset-guards.js"
7
+ "test": "node test-entry-parity.mjs && node test-browser.mjs && node test-types.js && node test-conformance.js && node test-order-nonce.js && node test-chain-binding.js && node test-sign-validate.js && node test-transport.js && node test-fee-cap.js && node test-network.js && node test-typed-order.js && node test-asset-guards.js && node test-account-index.js"
8
8
  },
9
9
  "keywords": [
10
10
  "post-quantum",
package/pkg/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "qcore-js",
3
3
  "type": "module",
4
4
  "description": "The JavaScript binding for the Quantova client core, generated over QCore with wasm-bindgen. The core does the key derivation, the post quantum signing, and the wire, the page does the fetch.",
5
- "version": "0.1.31",
5
+ "version": "0.2.0",
6
6
  "license": "Apache-2.0 OR MIT",
7
7
  "repository": {
8
8
  "type": "git",
Binary file
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qcore-js",
3
3
  "description": "The JavaScript binding for the Quantova client core, generated over QCore with wasm-bindgen. The core does the key derivation, the post quantum signing, and the wire, the page does the fetch.",
4
- "version": "0.1.31",
4
+ "version": "0.2.0",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "repository": {
7
7
  "type": "git",
Binary file
package/shared.js CHANGED
@@ -28,7 +28,15 @@ function checkAmount(amount) {
28
28
  }
29
29
 
30
30
  function accountIndex(index) {
31
- const i = BigInt(index);
31
+ if (typeof index === 'number' && !Number.isSafeInteger(index)) {
32
+ throw new Error('the account index must be a whole number in the safe integer range, a number that large silently rounds and would sign with a different account key');
33
+ }
34
+ let i;
35
+ try {
36
+ i = BigInt(index);
37
+ } catch {
38
+ throw new Error('the account index must be a whole number');
39
+ }
32
40
  if (i < 0n || i > 0xffffffffffffffffn) {
33
41
  throw new Error('the account index must fit in an unsigned 64 bit integer');
34
42
  }
@@ -323,11 +331,34 @@ function makeClient(core) {
323
331
  _checkedNonce(reported, expected, key) {
324
332
  const n = accountNonce(reported);
325
333
  if (!this._nextNonces) this._nextNonces = new Map();
334
+ if (!this._signedNonces) this._signedNonces = new Map();
326
335
  const local = key != null ? this._nextNonces.get(key) : undefined;
327
336
  const e = expected != null ? BigInt(expected) : local;
328
- if (e == null) return n;
329
- if (n > e) throw new Error(`the gateway reported nonce ${n} above the expected ${e}; refusing so a signature cannot be banked for a nonce the account has not reached`);
330
- return expected != null ? e : n;
337
+ let slot;
338
+ if (e == null) {
339
+ slot = n;
340
+ } else {
341
+ if (n > e) throw new Error(`the gateway reported nonce ${n} above the expected ${e}; refusing so a signature cannot be banked for a nonce the account has not reached`);
342
+ slot = expected != null ? e : n;
343
+ }
344
+ return slot;
345
+ }
346
+
347
+ // Refuse a second, different transaction at a nonce this session already
348
+ // signed at. A gateway can broadcast a transaction and still report it
349
+ // rejected, or walk the nonce it reports back to a slot already used.
350
+ // Either way two valid signatures for one slot let it choose which the
351
+ // chain runs. An identical retry is harmless and still allowed.
352
+ _guardSigned(key, slot, txHex) {
353
+ if (key == null) return;
354
+ if (!this._signedNonces) this._signedNonces = new Map();
355
+ let held = this._signedNonces.get(key);
356
+ if (!held) { held = new Map(); this._signedNonces.set(key, held); }
357
+ const seen = held.get(String(slot));
358
+ if (seen != null && seen !== txHex) {
359
+ throw new Error(`a different transaction was already signed for nonce ${slot} in this session; one nonce carries one signature`);
360
+ }
361
+ held.set(String(slot), txHex);
331
362
  }
332
363
 
333
364
  _remember(key, used, outcome) {
@@ -342,7 +373,8 @@ function makeClient(core) {
342
373
  if (this._headFloor) {
343
374
  const { height, at } = this._headFloor;
344
375
  if (head < height) throw new Error(`the gateway reports head ${head} below the ${height} it reported earlier, refusing to sign`);
345
- const allowed = (now - at + HEAD_SLACK_SECS) * HEAD_BLOCKS_PER_SEC;
376
+ const elapsed = now > at ? now - at : 0n;
377
+ const allowed = (elapsed + HEAD_SLACK_SECS) * HEAD_BLOCKS_PER_SEC;
346
378
  if (head > height + allowed) throw new Error(`the gateway head leapt from ${height} to ${head} faster than blocks are made, refusing to sign`);
347
379
  } else {
348
380
  this._headFloor = { height: head, at: now };
@@ -365,9 +397,10 @@ function makeClient(core) {
365
397
  const info = await this.nodeInfo();
366
398
  this._guardMainnet();
367
399
  const chainId = this._signingChainId(info);
368
- const fee = info && info.fee && info.fee.transfer_quon;
369
- if (fee == null) throw new Error('the gateway did not report a transfer fee');
370
- if (gatewayFee(fee) > ceiling) {
400
+ const reported = info && info.fee && info.fee.transfer_quon;
401
+ if (reported == null) throw new Error('the gateway did not report a transfer fee');
402
+ const fee = gatewayFee(reported);
403
+ if (fee > ceiling) {
371
404
  throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
372
405
  }
373
406
  const from = core.address(seedHex, accountIndex(index));
@@ -377,6 +410,7 @@ function makeClient(core) {
377
410
  const signed = JSON.parse(
378
411
  core.sign_transfer(seedHex, accountIndex(index), to, String(amount), nonce, String(fee), chainId, this._validity(info))
379
412
  );
413
+ this._guardSigned(from, nonce, signed.tx_hex);
380
414
  const outcome = await this.submit(signed.tx_hex);
381
415
  this._remember(from, nonce, outcome);
382
416
  return { signed, outcome };
@@ -387,9 +421,10 @@ function makeClient(core) {
387
421
  const info = await this.nodeInfo();
388
422
  this._guardMainnet();
389
423
  const chainId = this._signingChainId(info);
390
- const fee = info && info.fee && info.fee.transfer_quon;
391
- if (fee == null) throw new Error('the gateway did not report a transfer fee');
392
- if (gatewayFee(fee) > ceiling) {
424
+ const reported = info && info.fee && info.fee.transfer_quon;
425
+ if (reported == null) throw new Error('the gateway did not report a transfer fee');
426
+ const fee = gatewayFee(reported);
427
+ if (fee > ceiling) {
393
428
  throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
394
429
  }
395
430
  const from = core.address(seedHex, accountIndex(index));
@@ -397,6 +432,7 @@ function makeClient(core) {
397
432
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
398
433
  const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
399
434
  const signed = JSON.parse(core.signRegister(seedHex, accountIndex(index), nonce, String(fee), chainId, this._validity(info)));
435
+ this._guardSigned(from, nonce, signed.tx_hex);
400
436
  const outcome = await this.submit(signed.tx_hex);
401
437
  this._remember(from, nonce, outcome);
402
438
  return { signed, outcome };
@@ -421,6 +457,7 @@ function makeClient(core) {
421
457
  const signed = JSON.parse(
422
458
  core.sign_call(seedHex, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), chainId, this._validity(info))
423
459
  );
460
+ this._guardSigned(from, nonce, signed.tx_hex);
424
461
  const outcome = await this.submit(signed.tx_hex);
425
462
  this._remember(from, nonce, outcome);
426
463
  return { signed, outcome };
@@ -447,6 +484,7 @@ function makeClient(core) {
447
484
  const signed = JSON.parse(
448
485
  core.signAssetCall(seedHex, accountIndex(index), target, argsHex, assetIssuer, String(amount), nonce, meterLimitOf(meterLimit), String(fee), chainId, this._validity(info))
449
486
  );
487
+ this._guardSigned(from, nonce, signed.tx_hex);
450
488
  const outcome = await this.submit(signed.tx_hex);
451
489
  this._remember(from, nonce, outcome);
452
490
  return { signed, outcome };
@@ -472,6 +510,7 @@ function makeClient(core) {
472
510
  const signed = JSON.parse(
473
511
  core.signPayableCall(seedHex, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), String(value), chainId, this._validity(info))
474
512
  );
513
+ this._guardSigned(from, nonce, signed.tx_hex);
475
514
  const outcome = await this.submit(signed.tx_hex);
476
515
  this._remember(from, nonce, outcome);
477
516
  return { signed, outcome };
@@ -535,6 +574,7 @@ function makeClient(core) {
535
574
  const signed = JSON.parse(
536
575
  core.sign_call(callerSeedHex, accountIndex(callerIndex), contract, order.call_args, accountNonceUsed, meterLimitOf(meterLimit), String(fee), chainId, this._validity(info))
537
576
  );
577
+ this._guardSigned(from, nonce, signed.tx_hex);
538
578
  const outcome = await this.submit(signed.tx_hex);
539
579
  this._remember(from, accountNonceUsed, outcome);
540
580
  this._remember(orderKey, nonce, outcome);
package/shared.mjs CHANGED
@@ -28,7 +28,15 @@ function checkAmount(amount) {
28
28
  }
29
29
 
30
30
  function accountIndex(index) {
31
- const i = BigInt(index);
31
+ if (typeof index === 'number' && !Number.isSafeInteger(index)) {
32
+ throw new Error('the account index must be a whole number in the safe integer range, a number that large silently rounds and would sign with a different account key');
33
+ }
34
+ let i;
35
+ try {
36
+ i = BigInt(index);
37
+ } catch {
38
+ throw new Error('the account index must be a whole number');
39
+ }
32
40
  if (i < 0n || i > 0xffffffffffffffffn) {
33
41
  throw new Error('the account index must fit in an unsigned 64 bit integer');
34
42
  }
@@ -323,11 +331,34 @@ function makeClient(core) {
323
331
  _checkedNonce(reported, expected, key) {
324
332
  const n = accountNonce(reported);
325
333
  if (!this._nextNonces) this._nextNonces = new Map();
334
+ if (!this._signedNonces) this._signedNonces = new Map();
326
335
  const local = key != null ? this._nextNonces.get(key) : undefined;
327
336
  const e = expected != null ? BigInt(expected) : local;
328
- if (e == null) return n;
329
- if (n > e) throw new Error(`the gateway reported nonce ${n} above the expected ${e}; refusing so a signature cannot be banked for a nonce the account has not reached`);
330
- return expected != null ? e : n;
337
+ let slot;
338
+ if (e == null) {
339
+ slot = n;
340
+ } else {
341
+ if (n > e) throw new Error(`the gateway reported nonce ${n} above the expected ${e}; refusing so a signature cannot be banked for a nonce the account has not reached`);
342
+ slot = expected != null ? e : n;
343
+ }
344
+ return slot;
345
+ }
346
+
347
+ // Refuse a second, different transaction at a nonce this session already
348
+ // signed at. A gateway can broadcast a transaction and still report it
349
+ // rejected, or walk the nonce it reports back to a slot already used.
350
+ // Either way two valid signatures for one slot let it choose which the
351
+ // chain runs. An identical retry is harmless and still allowed.
352
+ _guardSigned(key, slot, txHex) {
353
+ if (key == null) return;
354
+ if (!this._signedNonces) this._signedNonces = new Map();
355
+ let held = this._signedNonces.get(key);
356
+ if (!held) { held = new Map(); this._signedNonces.set(key, held); }
357
+ const seen = held.get(String(slot));
358
+ if (seen != null && seen !== txHex) {
359
+ throw new Error(`a different transaction was already signed for nonce ${slot} in this session; one nonce carries one signature`);
360
+ }
361
+ held.set(String(slot), txHex);
331
362
  }
332
363
 
333
364
  _remember(key, used, outcome) {
@@ -342,7 +373,8 @@ function makeClient(core) {
342
373
  if (this._headFloor) {
343
374
  const { height, at } = this._headFloor;
344
375
  if (head < height) throw new Error(`the gateway reports head ${head} below the ${height} it reported earlier, refusing to sign`);
345
- const allowed = (now - at + HEAD_SLACK_SECS) * HEAD_BLOCKS_PER_SEC;
376
+ const elapsed = now > at ? now - at : 0n;
377
+ const allowed = (elapsed + HEAD_SLACK_SECS) * HEAD_BLOCKS_PER_SEC;
346
378
  if (head > height + allowed) throw new Error(`the gateway head leapt from ${height} to ${head} faster than blocks are made, refusing to sign`);
347
379
  } else {
348
380
  this._headFloor = { height: head, at: now };
@@ -365,9 +397,10 @@ function makeClient(core) {
365
397
  const info = await this.nodeInfo();
366
398
  this._guardMainnet();
367
399
  const chainId = this._signingChainId(info);
368
- const fee = info && info.fee && info.fee.transfer_quon;
369
- if (fee == null) throw new Error('the gateway did not report a transfer fee');
370
- if (gatewayFee(fee) > ceiling) {
400
+ const reported = info && info.fee && info.fee.transfer_quon;
401
+ if (reported == null) throw new Error('the gateway did not report a transfer fee');
402
+ const fee = gatewayFee(reported);
403
+ if (fee > ceiling) {
371
404
  throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
372
405
  }
373
406
  const from = core.address(seedHex, accountIndex(index));
@@ -377,6 +410,7 @@ function makeClient(core) {
377
410
  const signed = JSON.parse(
378
411
  core.sign_transfer(seedHex, accountIndex(index), to, String(amount), nonce, String(fee), chainId, this._validity(info))
379
412
  );
413
+ this._guardSigned(from, nonce, signed.tx_hex);
380
414
  const outcome = await this.submit(signed.tx_hex);
381
415
  this._remember(from, nonce, outcome);
382
416
  return { signed, outcome };
@@ -387,9 +421,10 @@ function makeClient(core) {
387
421
  const info = await this.nodeInfo();
388
422
  this._guardMainnet();
389
423
  const chainId = this._signingChainId(info);
390
- const fee = info && info.fee && info.fee.transfer_quon;
391
- if (fee == null) throw new Error('the gateway did not report a transfer fee');
392
- if (gatewayFee(fee) > ceiling) {
424
+ const reported = info && info.fee && info.fee.transfer_quon;
425
+ if (reported == null) throw new Error('the gateway did not report a transfer fee');
426
+ const fee = gatewayFee(reported);
427
+ if (fee > ceiling) {
393
428
  throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
394
429
  }
395
430
  const from = core.address(seedHex, accountIndex(index));
@@ -397,6 +432,7 @@ function makeClient(core) {
397
432
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
398
433
  const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
399
434
  const signed = JSON.parse(core.signRegister(seedHex, accountIndex(index), nonce, String(fee), chainId, this._validity(info)));
435
+ this._guardSigned(from, nonce, signed.tx_hex);
400
436
  const outcome = await this.submit(signed.tx_hex);
401
437
  this._remember(from, nonce, outcome);
402
438
  return { signed, outcome };
@@ -421,6 +457,7 @@ function makeClient(core) {
421
457
  const signed = JSON.parse(
422
458
  core.sign_call(seedHex, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), chainId, this._validity(info))
423
459
  );
460
+ this._guardSigned(from, nonce, signed.tx_hex);
424
461
  const outcome = await this.submit(signed.tx_hex);
425
462
  this._remember(from, nonce, outcome);
426
463
  return { signed, outcome };
@@ -447,6 +484,7 @@ function makeClient(core) {
447
484
  const signed = JSON.parse(
448
485
  core.signAssetCall(seedHex, accountIndex(index), target, argsHex, assetIssuer, String(amount), nonce, meterLimitOf(meterLimit), String(fee), chainId, this._validity(info))
449
486
  );
487
+ this._guardSigned(from, nonce, signed.tx_hex);
450
488
  const outcome = await this.submit(signed.tx_hex);
451
489
  this._remember(from, nonce, outcome);
452
490
  return { signed, outcome };
@@ -472,6 +510,7 @@ function makeClient(core) {
472
510
  const signed = JSON.parse(
473
511
  core.signPayableCall(seedHex, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), String(value), chainId, this._validity(info))
474
512
  );
513
+ this._guardSigned(from, nonce, signed.tx_hex);
475
514
  const outcome = await this.submit(signed.tx_hex);
476
515
  this._remember(from, nonce, outcome);
477
516
  return { signed, outcome };
@@ -535,6 +574,7 @@ function makeClient(core) {
535
574
  const signed = JSON.parse(
536
575
  core.sign_call(callerSeedHex, accountIndex(callerIndex), contract, order.call_args, accountNonceUsed, meterLimitOf(meterLimit), String(fee), chainId, this._validity(info))
537
576
  );
577
+ this._guardSigned(from, nonce, signed.tx_hex);
538
578
  const outcome = await this.submit(signed.tx_hex);
539
579
  this._remember(from, accountNonceUsed, outcome);
540
580
  this._remember(orderKey, nonce, outcome);