@quantovainc/qcore 0.1.30 → 0.1.32

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.js CHANGED
@@ -51,6 +51,53 @@ function accountNonce(nonce) {
51
51
  return n;
52
52
  }
53
53
 
54
+ const VALIDITY_BLOCKS = 300n;
55
+ const MAX_PLAUSIBLE_HEAD = 1n << 40n;
56
+ const HEAD_BLOCKS_PER_SEC = 4n;
57
+ const HEAD_SLACK_SECS = 60n;
58
+ const TRANSFER_METER = 1210n;
59
+
60
+ function meterLimitOf(meterLimit) {
61
+ if (typeof meterLimit === 'number' && !Number.isSafeInteger(meterLimit)) {
62
+ throw new Error('the meter limit must be a whole number in the safe integer range');
63
+ }
64
+ let meter;
65
+ try {
66
+ meter = BigInt(meterLimit);
67
+ } catch {
68
+ throw new Error('the meter limit must be a whole number');
69
+ }
70
+ if (meter < 0n || meter > 0xffffffffffffffffn) {
71
+ throw new Error('the meter limit must fit in an unsigned 64 bit integer');
72
+ }
73
+ return meter;
74
+ }
75
+
76
+ function vmCallFee(transferFee, meterLimit) {
77
+ const meter = meterLimitOf(meterLimit);
78
+ let units = (meter + TRANSFER_METER - 1n) / TRANSFER_METER;
79
+ if (units < 1n) units = 1n;
80
+ return BigInt(transferFee) * units;
81
+ }
82
+
83
+ function validUntil(info) {
84
+ const head = info && info.head_height;
85
+ if (head == null) throw new Error('the gateway did not report a head height to bound the transaction to');
86
+ if (typeof head === 'number' && !Number.isSafeInteger(head)) {
87
+ throw new Error('the gateway reported a head height outside the safe integer range');
88
+ }
89
+ let h;
90
+ try {
91
+ h = BigInt(head);
92
+ } catch {
93
+ throw new Error('the gateway reported a head height that is not a whole number');
94
+ }
95
+ if (h < 0n || h > MAX_PLAUSIBLE_HEAD) {
96
+ throw new Error('the gateway reported a head height past any height this chain can have reached');
97
+ }
98
+ return h + VALIDITY_BLOCKS;
99
+ }
100
+
54
101
  function gatewayFee(fee) {
55
102
  if (typeof fee === 'number' && !Number.isSafeInteger(fee)) {
56
103
  throw new Error('the gateway reported a fee outside the safe integer range, a number that large silently rounds and would sign a different fee');
@@ -83,7 +130,7 @@ async function readBounded(res) {
83
130
  if (done) break;
84
131
  total += value.byteLength;
85
132
  if (total > MAX_RESPONSE) {
86
- try { await reader.cancel(); } catch { /* already closing */ }
133
+ try { await reader.cancel(); } catch {}
87
134
  throw new Error('the response is too large');
88
135
  }
89
136
  chunks.push(value);
@@ -198,12 +245,6 @@ function makeClient(core) {
198
245
  this.network = opts.network instanceof Network ? opts.network : Network.forUrl(base);
199
246
  }
200
247
  this.base = requireSafeTransport(base).replace(/\/$/, '');
201
- // The chain name the gateway reports is hashed into the signature preimage of
202
- // every signing path, so whoever names it decides which network a signature is
203
- // valid on. `Network.forUrl` leaves chainId null, which is the default for
204
- // `new Client(url)`, and the configured check is skipped when it is null, so by
205
- // default the endpoint chose. Bind it with expectedChainId, or rely on the pin,
206
- // which at least refuses a gateway that changes its mind mid session.
207
248
  this.expectedChainId =
208
249
  typeof opts.expectedChainId === 'string' && opts.expectedChainId.length > 0
209
250
  ? opts.expectedChainId
@@ -231,12 +272,6 @@ function makeClient(core) {
231
272
  if (!this.acknowledgeMainnet && this._isMainnetId(id)) {
232
273
  throw new Error(`the gateway reports the mainnet chain ${name}; refusing to sign a mainnet transaction without acknowledgeMainnet`);
233
274
  }
234
- // Pin on first ACCEPTED use, after every refusal above. With nothing configured
235
- // the endpoint names the chain, and the least this can do is refuse one that
236
- // answers differently later in the same session, which is how a signature ends
237
- // up bound to a network the caller never chose while every earlier call looked
238
- // entirely normal. Pinning a name we refused to sign for would be wrong: it was
239
- // never a chain this client accepted.
240
275
  if (this._pinnedChainName === null) {
241
276
  this._pinnedChainName = name;
242
277
  } else if (this._pinnedChainName !== name) {
@@ -275,7 +310,45 @@ function makeClient(core) {
275
310
 
276
311
  nodeInfo() { return this._call('node_info', '{}'); }
277
312
  head() { return this._call('head', '{}'); }
278
- account(address) { return this._call('get_account', core.account_body(address)); }
313
+ async account(address) {
314
+ const acct = await this._call('get_account', core.account_body(address));
315
+ if (!acct || typeof acct.address !== 'string') {
316
+ throw new Error(`the gateway answered about ${address} without naming the account, refusing to trust it`);
317
+ }
318
+ if (acct.address !== address) {
319
+ throw new Error(`the gateway answered for ${acct.address} when asked about ${address}, refusing to trust it`);
320
+ }
321
+ return acct;
322
+ }
323
+ _checkedNonce(reported, expected, key) {
324
+ const n = accountNonce(reported);
325
+ if (!this._nextNonces) this._nextNonces = new Map();
326
+ const local = key != null ? this._nextNonces.get(key) : undefined;
327
+ 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;
331
+ }
332
+
333
+ _remember(key, used, outcome) {
334
+ if (!this._nextNonces) this._nextNonces = new Map();
335
+ if (outcome && outcome.verdict === 'accepted') this._nextNonces.set(key, BigInt(used) + 1n);
336
+ }
337
+
338
+ _validity(info) {
339
+ const until = validUntil(info);
340
+ const head = until - VALIDITY_BLOCKS;
341
+ const now = BigInt(Math.floor(Date.now() / 1000));
342
+ if (this._headFloor) {
343
+ const { height, at } = this._headFloor;
344
+ 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;
346
+ if (head > height + allowed) throw new Error(`the gateway head leapt from ${height} to ${head} faster than blocks are made, refusing to sign`);
347
+ } else {
348
+ this._headFloor = { height: head, at: now };
349
+ }
350
+ return until;
351
+ }
279
352
  transaction(txId) { return this._call('get_transaction', core.transaction_body(txId)); }
280
353
  block(height) { return this._call('get_block', core.block_by_height_body(BigInt(height))); }
281
354
  submit(txHex) { return this._call('submit_transaction', core.submit_body(txHex)); }
@@ -285,7 +358,7 @@ function makeClient(core) {
285
358
 
286
359
  address(seedHex, index) { return core.address(seedHex, accountIndex(index)); }
287
360
 
288
- async transfer(seedHex, index, to, amount, maxFeeQuon) {
361
+ async transfer(seedHex, index, to, amount, maxFeeQuon, expectedNonce) {
289
362
  if (!core.valid_address(to)) throw new Error('the recipient is not a q1 address');
290
363
  checkAmount(amount);
291
364
  const ceiling = feeCeiling(maxFeeQuon);
@@ -300,14 +373,16 @@ function makeClient(core) {
300
373
  const from = core.address(seedHex, accountIndex(index));
301
374
  const acct = await this.account(from);
302
375
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
376
+ const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
303
377
  const signed = JSON.parse(
304
- core.sign_transfer(seedHex, accountIndex(index), to, String(amount), accountNonce(acct.nonce), String(fee), chainId)
378
+ core.sign_transfer(seedHex, accountIndex(index), to, String(amount), nonce, String(fee), chainId, this._validity(info))
305
379
  );
306
380
  const outcome = await this.submit(signed.tx_hex);
381
+ this._remember(from, nonce, outcome);
307
382
  return { signed, outcome };
308
383
  }
309
384
 
310
- async register(seedHex, index, maxFeeQuon) {
385
+ async register(seedHex, index, maxFeeQuon, expectedNonce) {
311
386
  const ceiling = feeCeiling(maxFeeQuon);
312
387
  const info = await this.nodeInfo();
313
388
  this._guardMainnet();
@@ -320,33 +395,38 @@ function makeClient(core) {
320
395
  const from = core.address(seedHex, accountIndex(index));
321
396
  const acct = await this.account(from);
322
397
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
323
- const signed = JSON.parse(core.signRegister(seedHex, accountIndex(index), accountNonce(acct.nonce), String(fee), chainId));
398
+ const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
399
+ const signed = JSON.parse(core.signRegister(seedHex, accountIndex(index), nonce, String(fee), chainId, this._validity(info)));
324
400
  const outcome = await this.submit(signed.tx_hex);
401
+ this._remember(from, nonce, outcome);
325
402
  return { signed, outcome };
326
403
  }
327
404
 
328
- async call(seedHex, index, target, argsHex, meterLimit, maxFeeQuon) {
405
+ async call(seedHex, index, target, argsHex, meterLimit, maxFeeQuon, expectedNonce) {
329
406
  if (!core.valid_address(target)) throw new Error('the target is not a q1 address');
330
407
  const ceiling = feeCeiling(maxFeeQuon);
331
408
  const info = await this.nodeInfo();
332
409
  this._guardMainnet();
333
410
  const chainId = this._signingChainId(info);
334
- const fee = info && info.fee && info.fee.transfer_quon;
335
- if (fee == null) throw new Error('the gateway did not report a transfer fee');
336
- if (gatewayFee(fee) > ceiling) {
337
- throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
411
+ const reported = info && info.fee && info.fee.transfer_quon;
412
+ if (reported == null) throw new Error('the gateway did not report a transfer fee');
413
+ const fee = vmCallFee(gatewayFee(reported), meterLimit);
414
+ if (fee > ceiling) {
415
+ throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
338
416
  }
339
417
  const from = core.address(seedHex, accountIndex(index));
340
418
  const acct = await this.account(from);
341
419
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
420
+ const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
342
421
  const signed = JSON.parse(
343
- core.sign_call(seedHex, accountIndex(index), target, argsHex, accountNonce(acct.nonce), BigInt(meterLimit), String(fee), chainId)
422
+ core.sign_call(seedHex, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), chainId, this._validity(info))
344
423
  );
345
424
  const outcome = await this.submit(signed.tx_hex);
425
+ this._remember(from, nonce, outcome);
346
426
  return { signed, outcome };
347
427
  }
348
428
 
349
- async assetCall(seedHex, index, target, argsHex, assetIssuer, amount, meterLimit, maxFeeQuon) {
429
+ async assetCall(seedHex, index, target, argsHex, assetIssuer, amount, meterLimit, maxFeeQuon, expectedNonce) {
350
430
  if (!core.valid_address(target)) throw new Error('the target is not a q1 address');
351
431
  if (!core.valid_address(assetIssuer)) throw new Error('the asset issuer is not a q1 address');
352
432
  checkAmount(amount);
@@ -354,77 +434,83 @@ function makeClient(core) {
354
434
  const info = await this.nodeInfo();
355
435
  this._guardMainnet();
356
436
  const chainId = this._signingChainId(info);
357
- const fee = info && info.fee && info.fee.transfer_quon;
358
- if (fee == null) throw new Error('the gateway did not report a transfer fee');
359
- if (gatewayFee(fee) > ceiling) {
360
- throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
437
+ const reported = info && info.fee && info.fee.transfer_quon;
438
+ if (reported == null) throw new Error('the gateway did not report a transfer fee');
439
+ const fee = vmCallFee(gatewayFee(reported), meterLimit);
440
+ if (fee > ceiling) {
441
+ throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
361
442
  }
362
443
  const from = core.address(seedHex, accountIndex(index));
363
444
  const acct = await this.account(from);
364
445
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
446
+ const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
365
447
  const signed = JSON.parse(
366
- core.signAssetCall(seedHex, accountIndex(index), target, argsHex, assetIssuer, String(amount), accountNonce(acct.nonce), BigInt(meterLimit), String(fee), chainId)
448
+ core.signAssetCall(seedHex, accountIndex(index), target, argsHex, assetIssuer, String(amount), nonce, meterLimitOf(meterLimit), String(fee), chainId, this._validity(info))
367
449
  );
368
450
  const outcome = await this.submit(signed.tx_hex);
451
+ this._remember(from, nonce, outcome);
369
452
  return { signed, outcome };
370
453
  }
371
454
 
372
- async payableCall(seedHex, index, target, argsHex, value, meterLimit, maxFeeQuon) {
455
+ async payableCall(seedHex, index, target, argsHex, value, meterLimit, maxFeeQuon, expectedNonce) {
373
456
  if (!core.valid_address(target)) throw new Error('the target is not a q1 address');
374
457
  checkAmount(value);
375
458
  const ceiling = feeCeiling(maxFeeQuon);
376
459
  const info = await this.nodeInfo();
377
460
  this._guardMainnet();
378
461
  const chainId = this._signingChainId(info);
379
- const fee = info && info.fee && info.fee.transfer_quon;
380
- if (fee == null) throw new Error('the gateway did not report a transfer fee');
381
- if (gatewayFee(fee) > ceiling) {
382
- throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
462
+ const reported = info && info.fee && info.fee.transfer_quon;
463
+ if (reported == null) throw new Error('the gateway did not report a transfer fee');
464
+ const fee = vmCallFee(gatewayFee(reported), meterLimit);
465
+ if (fee > ceiling) {
466
+ throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
383
467
  }
384
468
  const from = core.address(seedHex, accountIndex(index));
385
469
  const acct = await this.account(from);
386
470
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
471
+ const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
387
472
  const signed = JSON.parse(
388
- core.signPayableCall(seedHex, accountIndex(index), target, argsHex, accountNonce(acct.nonce), BigInt(meterLimit), String(fee), String(value), chainId)
473
+ core.signPayableCall(seedHex, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), String(value), chainId, this._validity(info))
389
474
  );
390
475
  const outcome = await this.submit(signed.tx_hex);
476
+ this._remember(from, nonce, outcome);
391
477
  return { signed, outcome };
392
478
  }
393
479
 
480
+ async _slotValue(contract, key) {
481
+ const resp = await this._call('get_storage_at', JSON.stringify({ address: contract, keys: [key] }));
482
+ return BigInt(core.storageValue(JSON.stringify(resp), key));
483
+ }
484
+
394
485
  async contractNonce(contract, signerHex) {
395
- const resp = await this._call('get_storage', core.storageBody(contract));
396
- return BigInt(core.storageValue(JSON.stringify(resp), core.nonceSlotKey(signerHex)));
486
+ return this._slotValue(contract, core.nonceSlotKey(signerHex));
397
487
  }
398
488
 
399
489
  async contractScalar(contract, slot) {
400
- const resp = await this._call('get_storage', core.storageBody(contract));
401
- return BigInt(core.storageValue(JSON.stringify(resp), core.scalarSlotKey(BigInt(slot))));
490
+ return this._slotValue(contract, core.scalarSlotKey(BigInt(slot)));
402
491
  }
403
492
 
404
- async callSignedOrder(callerSeedHex, callerIndex, contract, selectorHex, orderSpec, ownerSeedHex, ownerIndex, meterLimit, maxFeeQuon, expectedOrderNonce) {
493
+ async callSignedOrder(callerSeedHex, callerIndex, contract, selectorHex, orderSpec, ownerSeedHex, ownerIndex, meterLimit, maxFeeQuon, expectedOrderNonce, expectedNonce) {
405
494
  if (!core.valid_address(contract)) throw new Error('the contract is not a q1 address');
406
495
  const ceiling = feeCeiling(maxFeeQuon);
407
496
  const info = await this.nodeInfo();
408
497
  this._guardMainnet();
409
498
  const chainId = this._signingChainId(info);
410
- const fee = info && info.fee && info.fee.transfer_quon;
411
- if (fee == null) throw new Error('the gateway did not report a transfer fee');
412
- if (gatewayFee(fee) > ceiling) {
413
- throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
499
+ const reported = info && info.fee && info.fee.transfer_quon;
500
+ if (reported == null) throw new Error('the gateway did not report a transfer fee');
501
+ const fee = vmCallFee(gatewayFee(reported), meterLimit);
502
+ if (fee > ceiling) {
503
+ throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
414
504
  }
415
505
  const signer = core.orderSigner(ownerSeedHex, accountIndex(ownerIndex));
416
- // The owner authorisation carries no deadline, so this nonce is the only thing
417
- // that stops it being replayed. Reading it from the gateway hands the party the
418
- // signature protects against the one term that expires it: answer with a future
419
- // nonce and the reply is a valid order banked until the contract counter reaches
420
- // it. Pass expectedOrderNonce to bind what you meant to sign; without it you are
421
- // trusting the endpoint with the owner key.
422
- const nonce = await this.contractNonce(contract, signer);
423
- if (expectedOrderNonce != null && BigInt(expectedOrderNonce) !== nonce) {
506
+ const orderKey = contract + '/' + signer;
507
+ const reportedOrder = await this.contractNonce(contract, signer);
508
+ if (expectedOrderNonce != null && BigInt(expectedOrderNonce) !== reportedOrder) {
424
509
  throw new Error(
425
- `the gateway reported order nonce ${nonce} but you expected ${expectedOrderNonce}, refusing to sign`
510
+ `the gateway reported order nonce ${reportedOrder} but you expected ${expectedOrderNonce}, refusing to sign`
426
511
  );
427
512
  }
513
+ const nonce = this._checkedNonce(reportedOrder, expectedOrderNonce, orderKey);
428
514
  const order = JSON.parse(core.buildTypedOrderCall(
429
515
  chainId,
430
516
  contract,
@@ -432,7 +518,12 @@ function makeClient(core) {
432
518
  BigInt(orderSpec.schemeOff),
433
519
  BigInt(orderSpec.ptrOff),
434
520
  BigInt(orderSpec.regionOff || 0),
435
- JSON.stringify(orderSpec.fields || []),
521
+ JSON.stringify(orderSpec.fields || [], (k, v) => {
522
+ if (typeof v !== 'bigint') return v;
523
+ if (k === 'value') return v.toString();
524
+ if (v > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`the order field ${k} ${v} is too large to carry`);
525
+ return Number(v);
526
+ }),
436
527
  ownerSeedHex,
437
528
  accountIndex(ownerIndex),
438
529
  nonce,
@@ -440,13 +531,16 @@ function makeClient(core) {
440
531
  const from = core.address(callerSeedHex, accountIndex(callerIndex));
441
532
  const acct = await this.account(from);
442
533
  if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
534
+ const accountNonceUsed = this._checkedNonce(acct.nonce, expectedNonce, from);
443
535
  const signed = JSON.parse(
444
- core.sign_call(callerSeedHex, accountIndex(callerIndex), contract, order.call_args, accountNonce(acct.nonce), BigInt(meterLimit), String(fee), chainId)
536
+ core.sign_call(callerSeedHex, accountIndex(callerIndex), contract, order.call_args, accountNonceUsed, meterLimitOf(meterLimit), String(fee), chainId, this._validity(info))
445
537
  );
446
538
  const outcome = await this.submit(signed.tx_hex);
539
+ this._remember(from, accountNonceUsed, outcome);
540
+ this._remember(orderKey, nonce, outcome);
447
541
  return { order, signed, outcome, orderNonce: nonce };
448
542
  }
449
543
  };
450
544
  }
451
545
 
452
- module.exports = { makeClient, feeCeiling, checkAmount, generateSeed, readBounded, requireSafeTransport, Network };
546
+ module.exports = { makeClient, feeCeiling, checkAmount, generateSeed, readBounded, requireSafeTransport, validUntil, vmCallFee, VALIDITY_BLOCKS, Network };