@quantovainc/qcore 0.4.2 → 0.5.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/shared.mjs CHANGED
@@ -29,14 +29,16 @@ function accountNonce(nonce) {
29
29
  }
30
30
 
31
31
  const VALIDITY_BLOCKS = 300n;
32
- const ORDER_HOLD_UNTIL = 1n << 62n;
33
32
  const MAX_PLAUSIBLE_HEAD = 1n << 40n;
34
- const HEAD_BLOCKS_PER_SEC = 20n;
35
- const HEAD_SLACK_SECS = 60n;
36
- const GENESIS_FLOOR_SECS = 1735689600n;
37
33
  const U64_MAX = 0xffffffffffffffffn;
38
34
  const U128_MAX = (1n << 128n) - 1n;
39
35
 
36
+ function monotonicSecs() {
37
+ const clock = globalThis.performance;
38
+ const ms = clock && typeof clock.now === 'function' ? clock.now() : Date.now();
39
+ return BigInt(Math.floor(ms / 1000));
40
+ }
41
+
40
42
  function wholeNumber(value, label, max) {
41
43
  let n;
42
44
  if (typeof value === 'bigint') {
@@ -62,12 +64,17 @@ function coreArg(kind, label, value) {
62
64
  if (typeof value !== 'string') throw new Error(`the ${label} must be a string`);
63
65
  return value;
64
66
  }
67
+ if (kind === 'phrase') {
68
+ if (!(value instanceof Uint8Array)) throw new Error(`the ${label} must be a Uint8Array of UTF-8 text`);
69
+ return value;
70
+ }
65
71
  if (kind === 'seed') {
66
72
  if (!(value instanceof Uint8Array)) throw new Error(`the ${label} must be a Uint8Array of 32 bytes; pass a handle you can wipe after use, not a hex string`);
67
73
  if (value.length !== 32) throw new Error(`the ${label} must be 32 bytes`);
68
74
  return value;
69
75
  }
70
76
  if (kind === 'u64') return wholeNumber(value, label, U64_MAX);
77
+ if (kind === 'u64opt') return value == null ? undefined : wholeNumber(value, label, U64_MAX);
71
78
  if (kind === 'u64s') return wholeNumber(value, label, U64_MAX).toString();
72
79
  if (kind === 'u128s') return wholeNumber(value, label, U128_MAX).toString();
73
80
  throw new Error(`unknown argument kind ${kind}`);
@@ -76,15 +83,21 @@ function coreArg(kind, label, value) {
76
83
  const CORE_ARGS = {
77
84
  address: [['seed', 'seed'], ['u64', 'account index']],
78
85
  mnemonicFromSeed: [['seed', 'seed']],
86
+ mnemonicFromSeedText: [['seed', 'seed']],
79
87
  seedFromMnemonic: [['text', 'recovery phrase']],
88
+ seedFromMnemonicBytes: [['phrase', 'recovery phrase']],
89
+ genesisSecs: [['text', 'chain name']],
90
+ headCeiling: [['text', 'chain name'], ['u64', 'wall clock seconds']],
91
+ boundedValidUntil: [['text', 'chain name'], ['u64', 'head height'], ['u64', 'wall clock seconds'], ['u64opt', 'head floor'], ['u64', 'head floor age']],
92
+ checkGatewayJson: [['text', 'gateway response']],
80
93
  orderSigner: [['seed', 'seed'], ['u64', 'account index']],
81
94
  sign_transfer: [['seed', 'seed'], ['u64', 'account index'], ['text', 'recipient'], ['u64s', 'amount'], ['u64', 'nonce'], ['u128s', 'fee'], ['u64', 'chain id'], ['u64', 'validity deadline']],
82
95
  signRegister: [['seed', 'seed'], ['u64', 'account index'], ['u64', 'nonce'], ['u128s', 'fee'], ['u64', 'chain id'], ['u64', 'validity deadline']],
83
96
  sign_call: [['seed', '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']],
84
97
  signPayableCall: [['seed', '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']],
85
98
  signAssetCall: [['seed', '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']],
86
- buildSignedOrderCall: [['u64', 'chain id'], ['text', 'contract'], ['text', 'selector'], ['u64', 'scheme offset'], ['u64', 'pointer offset'], ['text', 'field offsets'], ['text', 'fields'], ['u64', 'region offset'], ['seed', 'owner seed'], ['u64', 'owner index'], ['u64', 'order nonce']],
87
- buildTypedOrderCall: [['u64', 'chain id'], ['text', 'contract'], ['text', 'selector'], ['u64', 'scheme offset'], ['u64', 'pointer offset'], ['u64', 'region offset'], ['text', 'fields'], ['seed', 'owner seed'], ['u64', 'owner index'], ['u64', 'order nonce']],
99
+ buildSignedOrderCall: [['u64', 'chain id'], ['text', 'contract'], ['text', 'selector'], ['u64', 'scheme offset'], ['u64', 'pointer offset'], ['u64', 'expiry offset'], ['text', 'field offsets'], ['text', 'fields'], ['u64', 'region offset'], ['seed', 'owner seed'], ['u64', 'owner index'], ['u64', 'order nonce'], ['u64', 'order expiry']],
100
+ buildTypedOrderCall: [['u64', 'chain id'], ['text', 'contract'], ['text', 'selector'], ['u64', 'scheme offset'], ['u64', 'pointer offset'], ['u64', 'expiry offset'], ['u64', 'region offset'], ['text', 'fields'], ['seed', 'owner seed'], ['u64', 'owner index'], ['u64', 'order nonce'], ['u64', 'order expiry']],
88
101
  contractAddress: [['text', 'deployer'], ['u64', 'nonce']],
89
102
  scalarSlotKey: [['u64', 'slot']],
90
103
  mapSlotKey: [['u64', 'map domain tag'], ['text', 'key address']],
@@ -105,9 +118,48 @@ function wrapCore(raw) {
105
118
  }
106
119
  const fee = core.vmCallFee;
107
120
  if (typeof fee === 'function') core.vmCallFee = (transferFee, meterLimit) => BigInt(fee(transferFee, meterLimit));
121
+ const text = core.seedFromMnemonic;
122
+ const bytes = core.seedFromMnemonicBytes;
123
+ if (typeof text === 'function' && typeof bytes === 'function') {
124
+ core.seedFromMnemonic = (phrase) => (phrase instanceof Uint8Array ? bytes(phrase) : text(phrase));
125
+ }
126
+ bindNetworkDefaults(raw);
108
127
  return Object.freeze(core);
109
128
  }
110
129
 
130
+ const NETWORK_DEFAULTS = { testnetChainName: null, testnetRpcUrl: null };
131
+
132
+ function bindNetworkDefaults(core) {
133
+ if (core && typeof core.testnetRpcUrl === 'function' && typeof core.testnetChainName === 'function') {
134
+ NETWORK_DEFAULTS.testnetRpcUrl = core.testnetRpcUrl();
135
+ NETWORK_DEFAULTS.testnetChainName = core.testnetChainName();
136
+ }
137
+ }
138
+
139
+ function requireNetworkDefaults() {
140
+ if (!NETWORK_DEFAULTS.testnetRpcUrl || !NETWORK_DEFAULTS.testnetChainName) {
141
+ throw new Error('load the core before naming the testnet, its endpoint and chain come from the core');
142
+ }
143
+ return NETWORK_DEFAULTS;
144
+ }
145
+
146
+ function gatewayInteger(value, label) {
147
+ if (value == null) throw new Error(`the gateway did not report a ${label}`);
148
+ if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) return BigInt(value);
149
+ if (typeof value === 'bigint' && value >= 0n && value <= U64_MAX) return value;
150
+ throw new Error(`the gateway reported a ${label} that is not a whole number`);
151
+ }
152
+
153
+ function gatewayDecimal(value, label) {
154
+ if (value == null) throw new Error(`the gateway did not report a ${label}`);
155
+ if (typeof value !== 'string' || !/^[0-9]+$/.test(value)) {
156
+ throw new Error(`the gateway reported a ${label} that is not a string of decimal digits`);
157
+ }
158
+ const n = BigInt(value);
159
+ if (n > U128_MAX) throw new Error(`the gateway reported a ${label} outside the unsigned 128 bit range`);
160
+ return n;
161
+ }
162
+
111
163
  function isMainnetChain(name) {
112
164
  return name.startsWith('Q-main-net-');
113
165
  }
@@ -121,25 +173,23 @@ function meterLimitOf(meterLimit) {
121
173
  }
122
174
 
123
175
  function validUntil(info) {
124
- const head = info && info.head_height;
125
- if (head == null) throw new Error('the gateway did not report a head height to bound the transaction to');
126
- if (typeof head === 'number' && !Number.isSafeInteger(head)) {
127
- throw new Error('the gateway reported a head height outside the safe integer range');
128
- }
129
- let h;
130
- try {
131
- h = BigInt(head);
132
- } catch {
133
- throw new Error('the gateway reported a head height that is not a whole number');
134
- }
135
- if (h < 0n || h > MAX_PLAUSIBLE_HEAD) {
176
+ const h = gatewayInteger(info && info.head_height, 'head height');
177
+ if (h > MAX_PLAUSIBLE_HEAD) {
136
178
  throw new Error('the gateway reported a head height past any height this chain can have reached');
137
179
  }
138
180
  return h + VALIDITY_BLOCKS;
139
181
  }
140
182
 
141
183
  function gatewayFee(fee) {
142
- return wholeNumber(fee, 'fee', U128_MAX);
184
+ return gatewayDecimal(fee, 'transfer fee');
185
+ }
186
+
187
+ function decodeUtf8(bytes) {
188
+ try {
189
+ return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
190
+ } catch {
191
+ throw new Error('the response is not UTF-8');
192
+ }
143
193
  }
144
194
 
145
195
  async function readBounded(res) {
@@ -150,7 +200,9 @@ async function readBounded(res) {
150
200
  throw new Error('the response has no content-length to bound it and cannot be read safely');
151
201
  }
152
202
  if (len > MAX_RESPONSE) throw new Error('the response is too large');
153
- return await res.text();
203
+ const whole = new Uint8Array(await res.arrayBuffer());
204
+ if (whole.byteLength > MAX_RESPONSE) throw new Error('the response is too large');
205
+ return decodeUtf8(whole);
154
206
  }
155
207
  const reader = res.body.getReader();
156
208
  const chunks = [];
@@ -171,7 +223,7 @@ async function readBounded(res) {
171
223
  merged.set(chunk, at);
172
224
  at += chunk.byteLength;
173
225
  }
174
- return new TextDecoder().decode(merged);
226
+ return decodeUtf8(merged);
175
227
  }
176
228
 
177
229
  function isLoopbackHost(hostname) {
@@ -221,10 +273,11 @@ class Network {
221
273
  }
222
274
 
223
275
  static testnet() {
276
+ const defaults = requireNetworkDefaults();
224
277
  return new Network({
225
278
  name: 'testnet',
226
- chainId: 'Q-test-net-1',
227
- rpcUrl: 'https://rpc-testnet.quantova.org',
279
+ chainId: defaults.testnetChainName,
280
+ rpcUrl: defaults.testnetRpcUrl,
228
281
  explorerUrl: 'https://qvmscan.io',
229
282
  isMainnet: false,
230
283
  });
@@ -256,6 +309,7 @@ function generateSeed() {
256
309
  }
257
310
 
258
311
  function makeClient(core) {
312
+ bindNetworkDefaults(core);
259
313
  return class Client {
260
314
  constructor(target, options) {
261
315
  const opts = options || {};
@@ -279,7 +333,10 @@ function makeClient(core) {
279
333
  typeof opts.expectedChainId === 'string' && opts.expectedChainId.length > 0
280
334
  ? opts.expectedChainId
281
335
  : null;
336
+ const book = opts.addressBook == null ? [] : Array.from(opts.addressBook, String);
337
+ this._session = new core.SigningSession(book);
282
338
  this._pinnedChainName = null;
339
+ this._headFloor = null;
283
340
  }
284
341
 
285
342
  _guardMainnet() {
@@ -314,7 +371,7 @@ function makeClient(core) {
314
371
  return BigInt(core.chainIdFromName(name));
315
372
  }
316
373
 
317
- async _call(method, body) {
374
+ async _call(method, body, strict) {
318
375
  const controller = new AbortController();
319
376
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
320
377
  try {
@@ -326,9 +383,10 @@ function makeClient(core) {
326
383
  redirect: 'error',
327
384
  });
328
385
  const text = await readBounded(res);
386
+ if (strict && res.status === 200) core.checkGatewayJson(text);
329
387
  let data;
330
388
  try { data = JSON.parse(text); } catch { throw new Error('the node returned a non JSON response'); }
331
- if (!res.ok) {
389
+ if (res.status !== 200) {
332
390
  const detail = data && typeof data === 'object' ? (data.message || data.error) : null;
333
391
  throw new Error(detail || ('status ' + res.status));
334
392
  }
@@ -338,10 +396,10 @@ function makeClient(core) {
338
396
  }
339
397
  }
340
398
 
341
- nodeInfo() { return this._call('node_info', '{}'); }
399
+ nodeInfo() { return this._call('node_info', '{}', true); }
342
400
  head() { return this._call('head', '{}'); }
343
401
  async account(address) {
344
- const acct = await this._call('get_account', core.account_body(address));
402
+ const acct = await this._call('get_account', core.account_body(address), true);
345
403
  if (!acct || typeof acct.address !== 'string') {
346
404
  throw new Error(`the gateway answered about ${address} without naming the account, refusing to trust it`);
347
405
  }
@@ -350,212 +408,172 @@ function makeClient(core) {
350
408
  }
351
409
  return acct;
352
410
  }
353
- _checkedNonce(reported, expected, key) {
354
- const n = accountNonce(reported);
355
- if (expected != null) {
356
- const e = wholeNumber(expected, 'expected nonce', U64_MAX);
357
- if (e !== n) {
358
- 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`);
359
- }
360
- }
361
- if (!this._nextNonces) this._nextNonces = new Map();
362
- if (key != null) {
363
- const local = this._nextNonces.get(key);
364
- if (local == null || local < n) this._nextNonces.set(key, n);
365
- }
366
- return n;
411
+
412
+ _vet(sender, targets) {
413
+ this._session.rememberAddress(sender);
414
+ for (const target of targets) this._session.checkTarget(target);
367
415
  }
368
416
 
369
- _guardSigned(key, slot, txHex, until, explicit) {
370
- if (key == null) return;
371
- if (!this._signedNonces) this._signedNonces = new Map();
372
- let held = this._signedNonces.get(key);
373
- if (!held) { held = new Map(); this._signedNonces.set(key, held); }
374
- const head = until - VALIDITY_BLOCKS;
375
- for (const [slotKey, entry] of held) {
376
- if (head > entry.until) held.delete(slotKey);
377
- }
378
- const seen = held.get(String(slot));
379
- if (!explicit && seen != null && seen.txHex !== txHex) {
380
- 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`);
381
- }
382
- held.set(String(slot), { txHex, until });
417
+ _rememberTargets(targets) {
418
+ for (const target of targets) this._session.rememberAddress(target);
419
+ }
420
+
421
+ _expected(value, label) {
422
+ return value == null ? undefined : wholeNumber(value, label, U64_MAX);
383
423
  }
384
424
 
385
- _remember(key, used, outcome) {
386
- if (!this._nextNonces) this._nextNonces = new Map();
425
+ async _accountSlot(from, expectedNonce) {
426
+ const acct = await this.account(from);
427
+ const reported = gatewayInteger(acct.nonce, 'nonce');
428
+ return BigInt(this._session.slot(from, reported, this._expected(expectedNonce, 'expected nonce'), false));
429
+ }
430
+
431
+ async _submitHeld(slots, txHex) {
432
+ for (const [key, slot] of slots) this._session.signed(key, slot);
433
+ const outcome = await this.submit(txHex);
387
434
  const verdict = outcome && outcome.verdict;
388
- if (verdict === 'accepted') {
389
- const next = BigInt(used) + 1n;
390
- const local = this._nextNonces.get(key);
391
- if (local == null || local < next) this._nextNonces.set(key, next);
392
- }
435
+ if (verdict !== 'accepted' && verdict !== 'rejected') throw new Error(`unknown verdict ${verdict}`);
436
+ for (const [key, slot] of slots) this._session.outcome(key, slot, verdict === 'accepted');
437
+ return outcome;
438
+ }
439
+
440
+ _transferFee(info) {
441
+ return gatewayFee(info && info.fee && info.fee.transfer_quon);
393
442
  }
394
443
 
395
444
  _validity(info) {
396
- const until = validUntil(info);
397
- const head = until - VALIDITY_BLOCKS;
398
- const now = BigInt(Math.floor(Date.now() / 1000));
399
- if (now > GENESIS_FLOOR_SECS) {
400
- const maxHead = (now - GENESIS_FLOOR_SECS + HEAD_SLACK_SECS) * HEAD_BLOCKS_PER_SEC;
401
- if (head > maxHead) throw new Error(`the gateway reports head ${head} further ahead than wall-clock time allows, refusing to sign`);
402
- }
403
- if (this._headFloor) {
404
- const { height, at } = this._headFloor;
405
- if (head < height) throw new Error(`the gateway reports head ${head} below the ${height} it reported earlier, refusing to sign`);
406
- const elapsed = now > at ? now - at : 0n;
407
- const allowed = (elapsed + HEAD_SLACK_SECS) * HEAD_BLOCKS_PER_SEC;
408
- if (head > height + allowed) throw new Error(`the gateway head leapt from ${height} to ${head} faster than blocks are made, refusing to sign`);
409
- }
410
- core.checkValidUntil(until, head);
411
- if (!this._headFloor || head > this._headFloor.height) this._headFloor = { height: head, at: now };
445
+ const head = gatewayInteger(info && info.head_height, 'head height');
446
+ const chain = info && typeof info.chain_id === 'string' ? info.chain_id : '';
447
+ const wall = BigInt(Math.floor(Date.now() / 1000));
448
+ const tick = monotonicSecs();
449
+ const floor = this._headFloor;
450
+ const age = floor && tick > floor.at ? tick - floor.at : 0n;
451
+ const until = BigInt(core.boundedValidUntil(chain, head, wall, floor ? floor.height : undefined, age));
452
+ if (!floor || head > floor.height) this._headFloor = { height: head, at: tick };
412
453
  return until;
413
454
  }
455
+
456
+ _callFee(info, meterLimit, ceiling, maxFeeQuon) {
457
+ const transferFee = this._transferFee(info);
458
+ const fee = BigInt(core.vmCallFee(String(transferFee), meterLimitOf(meterLimit)));
459
+ if (fee > ceiling) {
460
+ throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
461
+ }
462
+ return { transferFee, fee };
463
+ }
464
+
414
465
  transaction(txId) { return this._call('get_transaction', core.transaction_body(txId)); }
415
- block(height) { return this._call('get_block', core.block_by_height_body(BigInt(height))); }
416
- submit(txHex) { return this._call('submit_transaction', core.submit_body(txHex)); }
466
+ block(height) { return this._call('get_block', core.block_by_height_body(wholeNumber(height, 'height', U64_MAX))); }
467
+ submit(txHex) { return this._call('submit_transaction', core.submit_body(txHex), true); }
417
468
  container(address) { return this._call('get_container', JSON.stringify({ address })); }
418
469
  storage(address) { return this._call('get_storage', JSON.stringify({ address })); }
419
- events(height) { return this._call('get_events', core.eventsBody(BigInt(height))); }
470
+ events(height) { return this._call('get_events', core.eventsBody(wholeNumber(height, 'height', U64_MAX))); }
420
471
 
421
- address(seedHex, index) { return core.address(seedHex, accountIndex(index)); }
472
+ address(seed, index) { return core.address(seed, accountIndex(index)); }
422
473
 
423
- async transfer(seedHex, index, to, amount, maxFeeQuon, expectedNonce) {
474
+ async transfer(seed, index, to, amount, maxFeeQuon, expectedNonce) {
424
475
  if (!core.valid_address(to)) throw new Error('the recipient is not a q1 address');
425
476
  checkAmount(amount);
426
477
  const ceiling = feeCeiling(maxFeeQuon);
478
+ const from = core.address(seed, accountIndex(index));
479
+ this._vet(from, [to]);
427
480
  const info = await this.nodeInfo();
428
481
  this._guardMainnet();
429
482
  const chainId = this._signingChainId(info);
430
- const reported = info && info.fee && info.fee.transfer_quon;
431
- if (reported == null) throw new Error('the gateway did not report a transfer fee');
432
- const fee = gatewayFee(reported);
483
+ const fee = this._transferFee(info);
433
484
  if (fee > ceiling) {
434
485
  throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
435
486
  }
436
487
  const until = this._validity(info);
437
- const from = core.address(seedHex, accountIndex(index));
438
- const acct = await this.account(from);
439
- if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
440
- const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
488
+ const nonce = await this._accountSlot(from, expectedNonce);
441
489
  const signed = JSON.parse(
442
- core.sign_transfer(seedHex, accountIndex(index), to, String(amount), nonce, String(fee), chainId, until)
490
+ core.sign_transfer(seed, accountIndex(index), to, String(amount), nonce, String(fee), chainId, until)
443
491
  );
444
- this._guardSigned(from, nonce, signed.tx_hex, until, expectedNonce != null);
445
- const outcome = await this.submit(signed.tx_hex);
446
- this._remember(from, nonce, outcome);
492
+ this._rememberTargets([to]);
493
+ const outcome = await this._submitHeld([[from, nonce]], signed.tx_hex);
447
494
  return { signed, outcome };
448
495
  }
449
496
 
450
- async register(seedHex, index, maxFeeQuon, expectedNonce) {
497
+ async register(seed, index, maxFeeQuon, expectedNonce) {
451
498
  const ceiling = feeCeiling(maxFeeQuon);
499
+ const from = core.address(seed, accountIndex(index));
500
+ this._vet(from, []);
452
501
  const info = await this.nodeInfo();
453
502
  this._guardMainnet();
454
503
  const chainId = this._signingChainId(info);
455
- const reported = info && info.fee && info.fee.transfer_quon;
456
- if (reported == null) throw new Error('the gateway did not report a transfer fee');
457
- const fee = gatewayFee(reported);
504
+ const fee = this._transferFee(info);
458
505
  if (fee > ceiling) {
459
506
  throw new Error(`the gateway fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
460
507
  }
461
508
  const until = this._validity(info);
462
- const from = core.address(seedHex, accountIndex(index));
463
- const acct = await this.account(from);
464
- if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
465
- const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
466
- const signed = JSON.parse(core.signRegister(seedHex, accountIndex(index), nonce, String(fee), chainId, until));
467
- this._guardSigned(from, nonce, signed.tx_hex, until, expectedNonce != null);
468
- const outcome = await this.submit(signed.tx_hex);
469
- this._remember(from, nonce, outcome);
509
+ const nonce = await this._accountSlot(from, expectedNonce);
510
+ const signed = JSON.parse(core.signRegister(seed, accountIndex(index), nonce, String(fee), chainId, until));
511
+ const outcome = await this._submitHeld([[from, nonce]], signed.tx_hex);
470
512
  return { signed, outcome };
471
513
  }
472
514
 
473
- async call(seedHex, index, target, argsHex, meterLimit, maxFeeQuon, expectedNonce) {
515
+ async call(seed, index, target, argsHex, meterLimit, maxFeeQuon, expectedNonce) {
474
516
  if (!core.valid_address(target)) throw new Error('the target is not a q1 address');
475
517
  const ceiling = feeCeiling(maxFeeQuon);
518
+ const from = core.address(seed, accountIndex(index));
519
+ this._vet(from, [target]);
476
520
  const info = await this.nodeInfo();
477
521
  this._guardMainnet();
478
522
  const chainId = this._signingChainId(info);
479
- const reported = info && info.fee && info.fee.transfer_quon;
480
- if (reported == null) throw new Error('the gateway did not report a transfer fee');
481
- const transferFee = gatewayFee(reported);
482
- const fee = BigInt(core.vmCallFee(String(transferFee), meterLimitOf(meterLimit)));
483
- if (fee > ceiling) {
484
- throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
485
- }
523
+ const { transferFee, fee } = this._callFee(info, meterLimit, ceiling, maxFeeQuon);
486
524
  const until = this._validity(info);
487
- const from = core.address(seedHex, accountIndex(index));
488
- const acct = await this.account(from);
489
- if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
490
- const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
525
+ const nonce = await this._accountSlot(from, expectedNonce);
491
526
  const signed = JSON.parse(
492
- core.sign_call(seedHex, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), chainId, until, String(transferFee))
527
+ core.sign_call(seed, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), chainId, until, String(transferFee))
493
528
  );
494
- this._guardSigned(from, nonce, signed.tx_hex, until, expectedNonce != null);
495
- const outcome = await this.submit(signed.tx_hex);
496
- this._remember(from, nonce, outcome);
529
+ this._rememberTargets([target]);
530
+ const outcome = await this._submitHeld([[from, nonce]], signed.tx_hex);
497
531
  return { signed, outcome };
498
532
  }
499
533
 
500
- async assetCall(seedHex, index, target, argsHex, assetIssuer, amount, meterLimit, maxFeeQuon, expectedNonce) {
534
+ async assetCall(seed, index, target, argsHex, assetIssuer, amount, meterLimit, maxFeeQuon, expectedNonce) {
501
535
  if (!core.valid_address(target)) throw new Error('the target is not a q1 address');
502
536
  if (!core.valid_address(assetIssuer)) throw new Error('the asset issuer is not a q1 address');
503
537
  checkAmount(amount);
504
538
  const ceiling = feeCeiling(maxFeeQuon);
539
+ const from = core.address(seed, accountIndex(index));
540
+ this._vet(from, [target, assetIssuer]);
505
541
  const info = await this.nodeInfo();
506
542
  this._guardMainnet();
507
543
  const chainId = this._signingChainId(info);
508
- const reported = info && info.fee && info.fee.transfer_quon;
509
- if (reported == null) throw new Error('the gateway did not report a transfer fee');
510
- const transferFee = gatewayFee(reported);
511
- const fee = BigInt(core.vmCallFee(String(transferFee), meterLimitOf(meterLimit)));
512
- if (fee > ceiling) {
513
- throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
514
- }
544
+ const { transferFee, fee } = this._callFee(info, meterLimit, ceiling, maxFeeQuon);
515
545
  const until = this._validity(info);
516
- const from = core.address(seedHex, accountIndex(index));
517
- const acct = await this.account(from);
518
- if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
519
- const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
546
+ const nonce = await this._accountSlot(from, expectedNonce);
520
547
  const signed = JSON.parse(
521
- core.signAssetCall(seedHex, accountIndex(index), target, argsHex, assetIssuer, String(amount), nonce, meterLimitOf(meterLimit), String(fee), chainId, until, String(transferFee))
548
+ core.signAssetCall(seed, accountIndex(index), target, argsHex, assetIssuer, String(amount), nonce, meterLimitOf(meterLimit), String(fee), chainId, until, String(transferFee))
522
549
  );
523
- this._guardSigned(from, nonce, signed.tx_hex, until, expectedNonce != null);
524
- const outcome = await this.submit(signed.tx_hex);
525
- this._remember(from, nonce, outcome);
550
+ this._rememberTargets([target, assetIssuer]);
551
+ const outcome = await this._submitHeld([[from, nonce]], signed.tx_hex);
526
552
  return { signed, outcome };
527
553
  }
528
554
 
529
- async payableCall(seedHex, index, target, argsHex, value, meterLimit, maxFeeQuon, expectedNonce) {
555
+ async payableCall(seed, index, target, argsHex, value, meterLimit, maxFeeQuon, expectedNonce) {
530
556
  if (!core.valid_address(target)) throw new Error('the target is not a q1 address');
531
557
  checkAmount(value);
532
558
  const ceiling = feeCeiling(maxFeeQuon);
559
+ const from = core.address(seed, accountIndex(index));
560
+ this._vet(from, [target]);
533
561
  const info = await this.nodeInfo();
534
562
  this._guardMainnet();
535
563
  const chainId = this._signingChainId(info);
536
- const reported = info && info.fee && info.fee.transfer_quon;
537
- if (reported == null) throw new Error('the gateway did not report a transfer fee');
538
- const transferFee = gatewayFee(reported);
539
- const fee = BigInt(core.vmCallFee(String(transferFee), meterLimitOf(meterLimit)));
540
- if (fee > ceiling) {
541
- throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
542
- }
564
+ const { transferFee, fee } = this._callFee(info, meterLimit, ceiling, maxFeeQuon);
543
565
  const until = this._validity(info);
544
- const from = core.address(seedHex, accountIndex(index));
545
- const acct = await this.account(from);
546
- if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
547
- const nonce = this._checkedNonce(acct.nonce, expectedNonce, from);
566
+ const nonce = await this._accountSlot(from, expectedNonce);
548
567
  const signed = JSON.parse(
549
- core.signPayableCall(seedHex, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), String(value), chainId, until, String(transferFee))
568
+ core.signPayableCall(seed, accountIndex(index), target, argsHex, nonce, meterLimitOf(meterLimit), String(fee), String(value), chainId, until, String(transferFee))
550
569
  );
551
- this._guardSigned(from, nonce, signed.tx_hex, until, expectedNonce != null);
552
- const outcome = await this.submit(signed.tx_hex);
553
- this._remember(from, nonce, outcome);
570
+ this._rememberTargets([target]);
571
+ const outcome = await this._submitHeld([[from, nonce]], signed.tx_hex);
554
572
  return { signed, outcome };
555
573
  }
556
574
 
557
575
  async _slotValue(contract, key) {
558
- const resp = await this._call('get_storage_at', JSON.stringify({ address: contract, keys: [key] }));
576
+ const resp = await this._call('get_storage_at', JSON.stringify({ address: contract, keys: [key] }), true);
559
577
  return BigInt(core.storageValue(JSON.stringify(resp), key));
560
578
  }
561
579
 
@@ -564,61 +582,60 @@ function makeClient(core) {
564
582
  }
565
583
 
566
584
  async contractScalar(contract, slot) {
567
- return this._slotValue(contract, core.scalarSlotKey(BigInt(slot)));
585
+ return this._slotValue(contract, core.scalarSlotKey(wholeNumber(slot, 'slot', U64_MAX)));
586
+ }
587
+
588
+ async contractMap(contract, mapDomainTag, keyHex) {
589
+ return this._slotValue(contract, core.mapSlotKey(wholeNumber(mapDomainTag, 'map domain tag', U64_MAX), keyHex));
568
590
  }
569
591
 
570
- async callSignedOrder(callerSeedHex, callerIndex, contract, selectorHex, orderSpec, ownerSeedHex, ownerIndex, meterLimit, maxFeeQuon, expectedOrderNonce, expectedNonce) {
592
+ async assetBalance(issuer, holder) {
593
+ if (!core.valid_address(issuer) || !core.valid_address(holder)) {
594
+ throw new Error('the issuer or holder is not a q1 address');
595
+ }
596
+ const resp = await this._call('get_asset_balance', core.assetBalanceBody(issuer, holder), true);
597
+ return gatewayDecimal(resp && resp.balance, 'balance');
598
+ }
599
+
600
+ async callSignedOrder(callerSeed, callerIndex, contract, selectorHex, orderSpec, ownerSeed, ownerIndex, meterLimit, maxFeeQuon, expectedOrderNonce, expectedNonce) {
571
601
  if (!core.valid_address(contract)) throw new Error('the contract is not a q1 address');
572
602
  const ceiling = feeCeiling(maxFeeQuon);
603
+ const from = core.address(callerSeed, accountIndex(callerIndex));
604
+ this._vet(from, [contract]);
573
605
  const info = await this.nodeInfo();
574
606
  this._guardMainnet();
575
607
  const chainId = this._signingChainId(info);
576
- const reported = info && info.fee && info.fee.transfer_quon;
577
- if (reported == null) throw new Error('the gateway did not report a transfer fee');
578
- const transferFee = gatewayFee(reported);
579
- const fee = BigInt(core.vmCallFee(String(transferFee), meterLimitOf(meterLimit)));
580
- if (fee > ceiling) {
581
- throw new Error(`the fee ${fee} is above the maximum you allowed ${maxFeeQuon}, refusing to sign`);
582
- }
608
+ const { transferFee, fee } = this._callFee(info, meterLimit, ceiling, maxFeeQuon);
583
609
  const until = this._validity(info);
584
- const signer = core.orderSigner(ownerSeedHex, accountIndex(ownerIndex));
610
+ const signer = core.orderSigner(ownerSeed, accountIndex(ownerIndex));
585
611
  const orderKey = contract + '/' + signer;
586
612
  const reportedOrder = await this.contractNonce(contract, signer);
587
- if (expectedOrderNonce != null && BigInt(expectedOrderNonce) !== reportedOrder) {
588
- throw new Error(
589
- `the gateway reported order nonce ${reportedOrder} but you expected ${expectedOrderNonce}, refusing to sign`
590
- );
591
- }
592
- const nonce = this._checkedNonce(reportedOrder, expectedOrderNonce, orderKey);
613
+ const nonce = BigInt(this._session.slot(orderKey, reportedOrder, this._expected(expectedOrderNonce, 'expected order nonce'), true));
593
614
  const order = JSON.parse(core.buildTypedOrderCall(
594
615
  chainId,
595
616
  contract,
596
617
  selectorHex,
597
- BigInt(orderSpec.schemeOff),
598
- BigInt(orderSpec.ptrOff),
599
- BigInt(orderSpec.regionOff || 0),
618
+ wholeNumber(orderSpec.schemeOff, 'scheme offset', U64_MAX),
619
+ wholeNumber(orderSpec.ptrOff, 'pointer offset', U64_MAX),
620
+ wholeNumber(orderSpec.expiryOff, 'expiry offset', U64_MAX),
621
+ orderSpec.regionOff == null ? 0n : wholeNumber(orderSpec.regionOff, 'region offset', U64_MAX),
600
622
  JSON.stringify(orderSpec.fields || [], (k, v) => {
601
623
  if (typeof v !== 'bigint') return v;
602
624
  if (k === 'value') return v.toString();
603
625
  if (v > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`the order field ${k} ${v} is too large to carry`);
604
626
  return Number(v);
605
627
  }),
606
- ownerSeedHex,
628
+ ownerSeed,
607
629
  accountIndex(ownerIndex),
608
630
  nonce,
631
+ wholeNumber(orderSpec.expiry, 'order expiry', U64_MAX),
609
632
  ));
610
- const from = core.address(callerSeedHex, accountIndex(callerIndex));
611
- const acct = await this.account(from);
612
- if (!acct || acct.nonce == null) throw new Error('the gateway did not report a nonce');
613
- const accountNonceUsed = this._checkedNonce(acct.nonce, expectedNonce, from);
633
+ const accountNonceUsed = await this._accountSlot(from, expectedNonce);
614
634
  const signed = JSON.parse(
615
- core.sign_call(callerSeedHex, accountIndex(callerIndex), contract, order.call_args, accountNonceUsed, meterLimitOf(meterLimit), String(fee), chainId, until, String(transferFee))
635
+ core.sign_call(callerSeed, accountIndex(callerIndex), contract, order.call_args, accountNonceUsed, meterLimitOf(meterLimit), String(fee), chainId, until, String(transferFee))
616
636
  );
617
- this._guardSigned(orderKey, nonce, order.call_args, ORDER_HOLD_UNTIL, expectedOrderNonce != null);
618
- this._guardSigned(from, accountNonceUsed, signed.tx_hex, until, expectedNonce != null);
619
- const outcome = await this.submit(signed.tx_hex);
620
- this._remember(from, accountNonceUsed, outcome);
621
- this._remember(orderKey, nonce, outcome);
637
+ this._rememberTargets([contract]);
638
+ const outcome = await this._submitHeld([[orderKey, nonce], [from, accountNonceUsed]], signed.tx_hex);
622
639
  return { order, signed, outcome, orderNonce: nonce };
623
640
  }
624
641
  };