@quantovainc/qcore 0.2.0 → 0.2.2

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/shared.mjs CHANGED
@@ -63,7 +63,82 @@ const VALIDITY_BLOCKS = 300n;
63
63
  const MAX_PLAUSIBLE_HEAD = 1n << 40n;
64
64
  const HEAD_BLOCKS_PER_SEC = 4n;
65
65
  const HEAD_SLACK_SECS = 60n;
66
- const TRANSFER_METER = 1210n;
66
+ const U64_MAX = 0xffffffffffffffffn;
67
+ const U128_MAX = (1n << 128n) - 1n;
68
+
69
+ function wholeNumber(value, label, max) {
70
+ let n;
71
+ if (typeof value === 'bigint') {
72
+ n = value;
73
+ } else if (typeof value === 'number') {
74
+ if (!Number.isSafeInteger(value)) {
75
+ throw new Error(`the ${label} must be a whole number in the safe integer range, a number outside it silently rounds, pass a decimal string or a BigInt`);
76
+ }
77
+ n = BigInt(value);
78
+ } else if (typeof value === 'string' && /^[0-9]+$/.test(value)) {
79
+ n = BigInt(value);
80
+ } else {
81
+ throw new Error(`the ${label} must be a string of decimal digits, a BigInt, or a safe integer number`);
82
+ }
83
+ if (n < 0n || n > max) {
84
+ throw new Error(`the ${label} must be a whole number from 0 to ${max}`);
85
+ }
86
+ return n;
87
+ }
88
+
89
+ function coreArg(kind, label, value) {
90
+ if (kind === 'text') {
91
+ if (typeof value !== 'string') throw new Error(`the ${label} must be a string`);
92
+ return value;
93
+ }
94
+ if (kind === 'u64') return wholeNumber(value, label, U64_MAX);
95
+ if (kind === 'u64s') return wholeNumber(value, label, U64_MAX).toString();
96
+ if (kind === 'u128s') return wholeNumber(value, label, U128_MAX).toString();
97
+ throw new Error(`unknown argument kind ${kind}`);
98
+ }
99
+
100
+ const CORE_ARGS = {
101
+ address: [['text', 'seed'], ['u64', 'account index']],
102
+ mnemonicFromSeed: [['text', 'seed']],
103
+ seedFromMnemonic: [['text', 'recovery phrase']],
104
+ orderSigner: [['text', 'seed'], ['u64', 'account index']],
105
+ sign_transfer: [['text', 'seed'], ['u64', 'account index'], ['text', 'recipient'], ['u64s', 'amount'], ['u64', 'nonce'], ['u128s', 'fee'], ['u64', 'chain id'], ['u64', 'validity deadline']],
106
+ signRegister: [['text', 'seed'], ['u64', 'account index'], ['u64', 'nonce'], ['u128s', 'fee'], ['u64', 'chain id'], ['u64', 'validity deadline']],
107
+ sign_call: [['text', 'seed'], ['u64', 'account index'], ['text', 'target'], ['text', 'call arguments'], ['u64', 'nonce'], ['u64', 'meter limit'], ['u128s', 'fee'], ['u64', 'chain id'], ['u64', 'validity deadline'], ['u128s', 'transfer fee']],
108
+ signPayableCall: [['text', 'seed'], ['u64', 'account index'], ['text', 'target'], ['text', 'call arguments'], ['u64', 'nonce'], ['u64', 'meter limit'], ['u128s', 'fee'], ['u64s', 'value'], ['u64', 'chain id'], ['u64', 'validity deadline'], ['u128s', 'transfer fee']],
109
+ signAssetCall: [['text', 'seed'], ['u64', 'account index'], ['text', 'target'], ['text', 'call arguments'], ['text', 'asset issuer'], ['u64s', 'amount'], ['u64', 'nonce'], ['u64', 'meter limit'], ['u128s', 'fee'], ['u64', 'chain id'], ['u64', 'validity deadline'], ['u128s', 'transfer fee']],
110
+ buildSignedOrderCall: [['u64', 'chain id'], ['text', 'contract'], ['text', 'selector'], ['u64', 'scheme offset'], ['u64', 'pointer offset'], ['text', 'field offsets'], ['text', 'fields'], ['u64', 'region offset'], ['text', 'owner seed'], ['u64', 'owner index'], ['u64', 'order nonce']],
111
+ buildTypedOrderCall: [['u64', 'chain id'], ['text', 'contract'], ['text', 'selector'], ['u64', 'scheme offset'], ['u64', 'pointer offset'], ['u64', 'region offset'], ['text', 'fields'], ['text', 'owner seed'], ['u64', 'owner index'], ['u64', 'order nonce']],
112
+ contractAddress: [['text', 'deployer'], ['u64', 'nonce']],
113
+ scalarSlotKey: [['u64', 'slot']],
114
+ mapSlotKey: [['u64', 'map domain tag'], ['text', 'key address']],
115
+ mapAddrWordKey: [['u64', 'map domain tag'], ['text', 'key'], ['u64', 'word']],
116
+ unpackSymbol: [['u64', 'symbol word']],
117
+ eventsBody: [['u64', 'height']],
118
+ block_by_height_body: [['u64', 'height']],
119
+ vmCallFee: [['u128s', 'transfer fee'], ['u64', 'meter limit']],
120
+ checkValidUntil: [['u64', 'validity deadline'], ['u64', 'head height']],
121
+ };
122
+
123
+ function wrapCore(raw) {
124
+ const core = Object.assign({}, raw);
125
+ for (const [name, kinds] of Object.entries(CORE_ARGS)) {
126
+ const fn = raw[name];
127
+ if (typeof fn !== 'function') continue;
128
+ core[name] = (...args) => fn(...kinds.map(([kind, label], i) => coreArg(kind, label, args[i])));
129
+ }
130
+ const fee = core.vmCallFee;
131
+ if (typeof fee === 'function') core.vmCallFee = (transferFee, meterLimit) => BigInt(fee(transferFee, meterLimit));
132
+ return Object.freeze(core);
133
+ }
134
+
135
+ function isMainnetChain(name) {
136
+ return name.startsWith('Q-main-net-');
137
+ }
138
+
139
+ function isPublicChain(name) {
140
+ return name.startsWith('Q-test-net-') || isMainnetChain(name);
141
+ }
67
142
 
68
143
  function meterLimitOf(meterLimit) {
69
144
  if (typeof meterLimit === 'number' && !Number.isSafeInteger(meterLimit)) {
@@ -81,13 +156,6 @@ function meterLimitOf(meterLimit) {
81
156
  return meter;
82
157
  }
83
158
 
84
- function vmCallFee(transferFee, meterLimit) {
85
- const meter = meterLimitOf(meterLimit);
86
- let units = (meter + TRANSFER_METER - 1n) / TRANSFER_METER;
87
- if (units < 1n) units = 1n;
88
- return BigInt(transferFee) * units;
89
- }
90
-
91
159
  function validUntil(info) {
92
160
  const head = info && info.head_height;
93
161
  if (head == null) throw new Error('the gateway did not report a head height to bound the transaction to');
@@ -201,7 +269,7 @@ class Network {
201
269
  static testnet() {
202
270
  return new Network({
203
271
  name: 'testnet',
204
- chainId: 'Q-test-net-3',
272
+ chainId: 'Q-test-net-1',
205
273
  rpcUrl: 'https://rpc-testnet.quantova.org',
206
274
  explorerUrl: 'https://qvmscan.io',
207
275
  isMainnet: false,
@@ -271,13 +339,17 @@ function makeClient(core) {
271
339
  const name = info && info.chain_id;
272
340
  if (!name) throw new Error('the gateway did not report a chain id to bind the signature to');
273
341
  if (typeof name !== 'string') throw new Error('the gateway reported a chain id that is not a string, refusing to bind a signature to it');
274
- const id = BigInt(core.chainIdFromName(name));
275
342
  const configured =
276
343
  this.expectedChainId || (this.network && this.network.chainId) || null;
277
- if (configured && name !== configured) {
278
- throw new Error(`the gateway reports chain ${name} but this client is configured for ${configured}; refusing to sign a transaction that would be valid on a network you did not choose`);
344
+ const mainnet = isMainnetChain(name);
345
+ if (configured) {
346
+ if (name !== configured) {
347
+ throw new Error(`the gateway reports chain ${name} but this client is configured for ${configured}; refusing to sign a transaction that would be valid on a network you did not choose`);
348
+ }
349
+ } else if (isPublicChain(name) && !(mainnet && this.acknowledgeMainnet)) {
350
+ throw new Error(`the gateway reports the public chain ${name} but this client was opened for an unnamed network; configure the testnet or mainnet network before signing for it`);
279
351
  }
280
- if (!this.acknowledgeMainnet && this._isMainnetId(id)) {
352
+ if (mainnet && !this.acknowledgeMainnet) {
281
353
  throw new Error(`the gateway reports the mainnet chain ${name}; refusing to sign a mainnet transaction without acknowledgeMainnet`);
282
354
  }
283
355
  if (this._pinnedChainName === null) {
@@ -285,11 +357,7 @@ function makeClient(core) {
285
357
  } else if (this._pinnedChainName !== name) {
286
358
  throw new Error(`the gateway reported chain ${this._pinnedChainName} earlier and now reports ${name}; refusing to sign, the endpoint is not naming one network`);
287
359
  }
288
- return id;
289
- }
290
-
291
- _isMainnetId(id) {
292
- return id === BigInt(core.mainnetChainId());
360
+ return BigInt(core.chainIdFromName(name));
293
361
  }
294
362
 
295
363
  async _call(method, body) {
@@ -330,40 +398,47 @@ function makeClient(core) {
330
398
  }
331
399
  _checkedNonce(reported, expected, key) {
332
400
  const n = accountNonce(reported);
401
+ if (expected != null) {
402
+ const e = wholeNumber(expected, 'expected nonce', U64_MAX);
403
+ if (e !== n) {
404
+ throw new Error(`the gateway reported nonce ${n} but you expected ${e}; the chain admits only the nonce the account has reached, refusing to sign`);
405
+ }
406
+ }
333
407
  if (!this._nextNonces) this._nextNonces = new Map();
334
- if (!this._signedNonces) this._signedNonces = new Map();
335
- const local = key != null ? this._nextNonces.get(key) : undefined;
336
- const e = expected != null ? BigInt(expected) : local;
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;
408
+ if (key != null) {
409
+ const local = this._nextNonces.get(key);
410
+ if (local == null || local < n) this._nextNonces.set(key, n);
343
411
  }
344
- return slot;
412
+ return n;
345
413
  }
346
414
 
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) {
415
+ _guardSigned(key, slot, txHex, until, explicit) {
353
416
  if (key == null) return;
354
417
  if (!this._signedNonces) this._signedNonces = new Map();
355
418
  let held = this._signedNonces.get(key);
356
419
  if (!held) { held = new Map(); this._signedNonces.set(key, held); }
420
+ const head = until - VALIDITY_BLOCKS;
421
+ for (const [slotKey, entry] of held) {
422
+ if (head > entry.until) held.delete(slotKey);
423
+ }
357
424
  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`);
425
+ if (!explicit && seen != null && seen.txHex !== txHex) {
426
+ throw new Error(`a different transaction was already signed for nonce ${slot} in this session and was neither rejected nor expired; one nonce carries one signature, pass the nonce explicitly to replace one that never landed`);
360
427
  }
361
- held.set(String(slot), txHex);
428
+ held.set(String(slot), { txHex, until });
362
429
  }
363
430
 
364
431
  _remember(key, used, outcome) {
365
432
  if (!this._nextNonces) this._nextNonces = new Map();
366
- if (outcome && outcome.verdict === 'accepted') this._nextNonces.set(key, BigInt(used) + 1n);
433
+ const verdict = outcome && outcome.verdict;
434
+ if (verdict === 'accepted') {
435
+ const next = BigInt(used) + 1n;
436
+ const local = this._nextNonces.get(key);
437
+ if (local == null || local < next) this._nextNonces.set(key, next);
438
+ } else if (verdict === 'rejected') {
439
+ const held = this._signedNonces && this._signedNonces.get(key);
440
+ if (held) held.delete(String(used));
441
+ }
367
442
  }
368
443
 
369
444
  _validity(info) {
@@ -376,9 +451,9 @@ function makeClient(core) {
376
451
  const elapsed = now > at ? now - at : 0n;
377
452
  const allowed = (elapsed + HEAD_SLACK_SECS) * HEAD_BLOCKS_PER_SEC;
378
453
  if (head > height + allowed) throw new Error(`the gateway head leapt from ${height} to ${head} faster than blocks are made, refusing to sign`);
379
- } else {
380
- this._headFloor = { height: head, at: now };
381
454
  }
455
+ core.checkValidUntil(until, head);
456
+ if (!this._headFloor || head > this._headFloor.height) this._headFloor = { height: head, at: now };
382
457
  return until;
383
458
  }
384
459
  transaction(txId) { return this._call('get_transaction', core.transaction_body(txId)); }
@@ -403,14 +478,15 @@ function makeClient(core) {
403
478
  if (fee > ceiling) {
404
479
  throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
405
480
  }
481
+ const until = this._validity(info);
406
482
  const from = core.address(seedHex, accountIndex(index));
407
483
  const acct = await this.account(from);
408
484
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
409
485
  const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
410
486
  const signed = JSON.parse(
411
- core.sign_transfer(seedHex, accountIndex(index), to, String(amount), nonce, String(fee), chainId, this._validity(info))
487
+ core.sign_transfer(seedHex, accountIndex(index), to, String(amount), nonce, String(fee), chainId, until)
412
488
  );
413
- this._guardSigned(from, nonce, signed.tx_hex);
489
+ this._guardSigned(from, nonce, signed.tx_hex, until, expectedNonce != null);
414
490
  const outcome = await this.submit(signed.tx_hex);
415
491
  this._remember(from, nonce, outcome);
416
492
  return { signed, outcome };
@@ -427,12 +503,13 @@ function makeClient(core) {
427
503
  if (fee > ceiling) {
428
504
  throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
429
505
  }
506
+ const until = this._validity(info);
430
507
  const from = core.address(seedHex, accountIndex(index));
431
508
  const acct = await this.account(from);
432
509
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
433
510
  const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
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);
511
+ const signed = JSON.parse(core.signRegister(seedHex, accountIndex(index), nonce, String(fee), chainId, until));
512
+ this._guardSigned(from, nonce, signed.tx_hex, until, expectedNonce != null);
436
513
  const outcome = await this.submit(signed.tx_hex);
437
514
  this._remember(from, nonce, outcome);
438
515
  return { signed, outcome };
@@ -446,18 +523,20 @@ function makeClient(core) {
446
523
  const chainId = this._signingChainId(info);
447
524
  const reported = info && info.fee && info.fee.transfer_quon;
448
525
  if (reported == null) throw new Error('the gateway did not report a transfer fee');
449
- const fee = vmCallFee(gatewayFee(reported), meterLimit);
526
+ const transferFee = gatewayFee(reported);
527
+ const fee = BigInt(core.vmCallFee(String(transferFee), meterLimitOf(meterLimit)));
450
528
  if (fee > ceiling) {
451
529
  throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
452
530
  }
531
+ const until = this._validity(info);
453
532
  const from = core.address(seedHex, accountIndex(index));
454
533
  const acct = await this.account(from);
455
534
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
456
535
  const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
457
536
  const signed = JSON.parse(
458
- core.sign_call(seedHex, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), chainId, this._validity(info))
537
+ core.sign_call(seedHex, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), chainId, until, String(transferFee))
459
538
  );
460
- this._guardSigned(from, nonce, signed.tx_hex);
539
+ this._guardSigned(from, nonce, signed.tx_hex, until, expectedNonce != null);
461
540
  const outcome = await this.submit(signed.tx_hex);
462
541
  this._remember(from, nonce, outcome);
463
542
  return { signed, outcome };
@@ -473,18 +552,20 @@ function makeClient(core) {
473
552
  const chainId = this._signingChainId(info);
474
553
  const reported = info && info.fee && info.fee.transfer_quon;
475
554
  if (reported == null) throw new Error('the gateway did not report a transfer fee');
476
- const fee = vmCallFee(gatewayFee(reported), meterLimit);
555
+ const transferFee = gatewayFee(reported);
556
+ const fee = BigInt(core.vmCallFee(String(transferFee), meterLimitOf(meterLimit)));
477
557
  if (fee > ceiling) {
478
558
  throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
479
559
  }
560
+ const until = this._validity(info);
480
561
  const from = core.address(seedHex, accountIndex(index));
481
562
  const acct = await this.account(from);
482
563
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
483
564
  const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
484
565
  const signed = JSON.parse(
485
- core.signAssetCall(seedHex, accountIndex(index), target, argsHex, assetIssuer, String(amount), nonce, meterLimitOf(meterLimit), String(fee), chainId, this._validity(info))
566
+ core.signAssetCall(seedHex, accountIndex(index), target, argsHex, assetIssuer, String(amount), nonce, meterLimitOf(meterLimit), String(fee), chainId, until, String(transferFee))
486
567
  );
487
- this._guardSigned(from, nonce, signed.tx_hex);
568
+ this._guardSigned(from, nonce, signed.tx_hex, until, expectedNonce != null);
488
569
  const outcome = await this.submit(signed.tx_hex);
489
570
  this._remember(from, nonce, outcome);
490
571
  return { signed, outcome };
@@ -499,18 +580,20 @@ function makeClient(core) {
499
580
  const chainId = this._signingChainId(info);
500
581
  const reported = info && info.fee && info.fee.transfer_quon;
501
582
  if (reported == null) throw new Error('the gateway did not report a transfer fee');
502
- const fee = vmCallFee(gatewayFee(reported), meterLimit);
583
+ const transferFee = gatewayFee(reported);
584
+ const fee = BigInt(core.vmCallFee(String(transferFee), meterLimitOf(meterLimit)));
503
585
  if (fee > ceiling) {
504
586
  throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
505
587
  }
588
+ const until = this._validity(info);
506
589
  const from = core.address(seedHex, accountIndex(index));
507
590
  const acct = await this.account(from);
508
591
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
509
592
  const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
510
593
  const signed = JSON.parse(
511
- core.signPayableCall(seedHex, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), String(value), chainId, this._validity(info))
594
+ core.signPayableCall(seedHex, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), String(value), chainId, until, String(transferFee))
512
595
  );
513
- this._guardSigned(from, nonce, signed.tx_hex);
596
+ this._guardSigned(from, nonce, signed.tx_hex, until, expectedNonce != null);
514
597
  const outcome = await this.submit(signed.tx_hex);
515
598
  this._remember(from, nonce, outcome);
516
599
  return { signed, outcome };
@@ -537,10 +620,12 @@ function makeClient(core) {
537
620
  const chainId = this._signingChainId(info);
538
621
  const reported = info && info.fee && info.fee.transfer_quon;
539
622
  if (reported == null) throw new Error('the gateway did not report a transfer fee');
540
- const fee = vmCallFee(gatewayFee(reported), meterLimit);
623
+ const transferFee = gatewayFee(reported);
624
+ const fee = BigInt(core.vmCallFee(String(transferFee), meterLimitOf(meterLimit)));
541
625
  if (fee > ceiling) {
542
626
  throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
543
627
  }
628
+ const until = this._validity(info);
544
629
  const signer = core.orderSigner(ownerSeedHex, accountIndex(ownerIndex));
545
630
  const orderKey = contract + '/' + signer;
546
631
  const reportedOrder = await this.contractNonce(contract, signer);
@@ -572,9 +657,9 @@ function makeClient(core) {
572
657
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
573
658
  const accountNonceUsed = this._checkedNonce(acct.nonce, expectedNonce, from);
574
659
  const signed = JSON.parse(
575
- core.sign_call(callerSeedHex, accountIndex(callerIndex), contract, order.call_args, accountNonceUsed, meterLimitOf(meterLimit), String(fee), chainId, this._validity(info))
660
+ core.sign_call(callerSeedHex, accountIndex(callerIndex), contract, order.call_args, accountNonceUsed, meterLimitOf(meterLimit), String(fee), chainId, until, String(transferFee))
576
661
  );
577
- this._guardSigned(from, nonce, signed.tx_hex);
662
+ this._guardSigned(from, accountNonceUsed, signed.tx_hex, until, expectedNonce != null);
578
663
  const outcome = await this.submit(signed.tx_hex);
579
664
  this._remember(from, accountNonceUsed, outcome);
580
665
  this._remember(orderKey, nonce, outcome);
@@ -583,4 +668,4 @@ function makeClient(core) {
583
668
  };
584
669
  }
585
670
 
586
- export { makeClient, feeCeiling, checkAmount, generateSeed, readBounded, requireSafeTransport, validUntil, vmCallFee, VALIDITY_BLOCKS, Network };
671
+ export { makeClient, wrapCore, feeCeiling, checkAmount, generateSeed, readBounded, requireSafeTransport, validUntil, VALIDITY_BLOCKS, Network };